mirror of
https://github.com/ClusterCockpit/cc-metric-collector.git
synced 2025-07-30 16:36:07 +02:00
Add SNMP receiver
This commit is contained in:
@@ -15,6 +15,7 @@ var AvailableReceivers = map[string]func(name string, config json.RawMessage) (R
|
||||
"ipmi": NewIPMIReceiver,
|
||||
"nats": NewNatsReceiver,
|
||||
"redfish": NewRedfishReceiver,
|
||||
"snmp": NewSNMPReceiver,
|
||||
}
|
||||
|
||||
type receiveManager struct {
|
||||
|
294
receivers/snmpReceiver.go
Normal file
294
receivers/snmpReceiver.go
Normal file
@@ -0,0 +1,294 @@
|
||||
package receivers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
"github.com/gosnmp/gosnmp"
|
||||
)
|
||||
|
||||
type SNMPReceiverTargetConfig struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Community string `json:"community,omitempty"`
|
||||
Timeout string `json:"timeout,omitempty"`
|
||||
timeout time.Duration
|
||||
Version string `json:"version,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
TypeId string `json:"type-id,omitempty"`
|
||||
SubType string `json:"subtype,omitempty"`
|
||||
SubTypeId string `json:"subtype-id,omitempty"`
|
||||
}
|
||||
|
||||
type SNMPReceiverMetricConfig struct {
|
||||
Name string `json:"name"`
|
||||
OID string `json:"oid"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
}
|
||||
|
||||
// SNMPReceiver configuration: receiver type, listen address, port
|
||||
type SNMPReceiverConfig struct {
|
||||
Type string `json:"type"`
|
||||
Targets []SNMPReceiverTargetConfig `json:"targets"`
|
||||
Metrics []SNMPReceiverMetricConfig `json:"metrics"`
|
||||
ReadInterval string `json:"read_interval,omitempty"`
|
||||
}
|
||||
|
||||
type SNMPReceiver struct {
|
||||
receiver
|
||||
config SNMPReceiverConfig
|
||||
|
||||
// Storage for static information
|
||||
meta map[string]string
|
||||
tags map[string]string
|
||||
// Use in case of own go routine
|
||||
done chan bool
|
||||
wg sync.WaitGroup
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
func validOid(oid string) bool {
|
||||
// Regex from https://github.com/BornToBeRoot/NETworkManager/blob/6805740762bf19b95051c7eaa73cf2b4727733c3/Source/NETworkManager.Utilities/RegexHelper.cs#L88
|
||||
// Match on leading dot added by Thomas Gruber <thomas.gruber@fau.de>
|
||||
match, err := regexp.MatchString(`^[\.]?[012]\.(?:[0-9]|[1-3][0-9])(\.\d+)*$`, oid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
func (r *SNMPReceiver) readTarget(target SNMPReceiverTargetConfig, output chan lp.CCMetric) {
|
||||
port := uint16(161)
|
||||
comm := "public"
|
||||
timeout := time.Duration(1) * time.Second
|
||||
version := gosnmp.Version2c
|
||||
timestamp := time.Now()
|
||||
if target.Port > 0 {
|
||||
port = uint16(target.Port)
|
||||
}
|
||||
if len(target.Community) > 0 {
|
||||
comm = target.Community
|
||||
}
|
||||
if target.timeout > 0 {
|
||||
timeout = target.timeout
|
||||
}
|
||||
if len(target.Version) > 0 {
|
||||
switch target.Version {
|
||||
case "1":
|
||||
version = gosnmp.Version1
|
||||
case "2c":
|
||||
version = gosnmp.Version2c
|
||||
case "3":
|
||||
version = gosnmp.Version3
|
||||
default:
|
||||
cclog.ComponentError(r.name, "Invalid SNMP version ", target.Version)
|
||||
return
|
||||
}
|
||||
}
|
||||
params := &gosnmp.GoSNMP{
|
||||
Target: target.Hostname,
|
||||
Port: port,
|
||||
Community: comm,
|
||||
Version: version,
|
||||
Timeout: timeout,
|
||||
}
|
||||
err := params.Connect()
|
||||
if err != nil {
|
||||
cclog.ComponentError(r.name, err.Error())
|
||||
return
|
||||
}
|
||||
for _, metric := range r.config.Metrics {
|
||||
if !validOid(metric.OID) {
|
||||
cclog.ComponentDebug(r.name, "Skipping ", metric.Name, ", not valid OID: ", metric.OID)
|
||||
continue
|
||||
}
|
||||
oids := make([]string, 0)
|
||||
name := gosnmp.SnmpPDU{
|
||||
Value: metric.Name,
|
||||
Name: metric.Name,
|
||||
}
|
||||
nameidx := -1
|
||||
value := gosnmp.SnmpPDU{
|
||||
Value: nil,
|
||||
Name: metric.OID,
|
||||
}
|
||||
valueidx := -1
|
||||
unit := gosnmp.SnmpPDU{
|
||||
Value: metric.Unit,
|
||||
Name: metric.Unit,
|
||||
}
|
||||
unitidx := -1
|
||||
idx := 0
|
||||
if validOid(metric.Name) {
|
||||
oids = append(oids, metric.Name)
|
||||
nameidx = idx
|
||||
idx = idx + 1
|
||||
}
|
||||
if validOid(metric.OID) {
|
||||
oids = append(oids, metric.OID)
|
||||
valueidx = idx
|
||||
idx = idx + 1
|
||||
}
|
||||
if len(metric.Unit) > 0 && validOid(metric.Unit) {
|
||||
oids = append(oids, metric.Unit)
|
||||
unitidx = idx
|
||||
}
|
||||
//cclog.ComponentDebug(r.name, len(oids), oids)
|
||||
result, err := params.Get(oids)
|
||||
if err != nil {
|
||||
cclog.ComponentError(r.name, "failed to get data for OIDs ", strings.Join(oids, ","), ": ", err.Error())
|
||||
continue
|
||||
}
|
||||
if nameidx >= 0 && len(result.Variables) > nameidx {
|
||||
name = result.Variables[nameidx]
|
||||
}
|
||||
if valueidx >= 0 && len(result.Variables) > valueidx {
|
||||
value = result.Variables[valueidx]
|
||||
}
|
||||
if unitidx >= 0 && len(result.Variables) > unitidx {
|
||||
unit = result.Variables[unitidx]
|
||||
}
|
||||
tags := r.tags
|
||||
if len(target.Type) > 0 {
|
||||
tags["type"] = target.Type
|
||||
}
|
||||
if len(target.TypeId) > 0 {
|
||||
tags["type-id"] = target.TypeId
|
||||
}
|
||||
if len(target.SubType) > 0 {
|
||||
tags["stype"] = target.SubType
|
||||
}
|
||||
if len(target.SubTypeId) > 0 {
|
||||
tags["stype-id"] = target.SubTypeId
|
||||
}
|
||||
if value.Value != nil {
|
||||
y, err := lp.New(name.Value.(string), tags, r.meta, map[string]interface{}{"value": value.Value}, timestamp)
|
||||
if err == nil {
|
||||
if len(unit.Name) > 0 && unit.Value != nil {
|
||||
y.AddMeta("unit", unit.Value.(string))
|
||||
}
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
params.Conn.Close()
|
||||
}
|
||||
|
||||
// Implement functions required for Receiver interface
|
||||
// Start(), Close()
|
||||
// See: metricReceiver.go
|
||||
|
||||
func (r *SNMPReceiver) Start() {
|
||||
cclog.ComponentDebug(r.name, "START")
|
||||
|
||||
r.done = make(chan bool)
|
||||
r.wg.Add(1)
|
||||
go func() {
|
||||
defer r.wg.Done()
|
||||
|
||||
// Create ticker
|
||||
ticker := time.NewTicker(r.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// process ticker event -> continue
|
||||
if r.sink != nil {
|
||||
for _, t := range r.config.Targets {
|
||||
select {
|
||||
case <-r.done:
|
||||
return
|
||||
default:
|
||||
r.readTarget(t, r.sink)
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
case <-r.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Close receiver: close network connection, close files, close libraries, ...
|
||||
func (r *SNMPReceiver) Close() {
|
||||
cclog.ComponentDebug(r.name, "CLOSE")
|
||||
|
||||
r.done <- true
|
||||
r.wg.Wait()
|
||||
}
|
||||
|
||||
// New function to create a new instance of the receiver
|
||||
// Initialize the receiver by giving it a name and reading in the config JSON
|
||||
func NewSNMPReceiver(name string, config json.RawMessage) (Receiver, error) {
|
||||
var err error = nil
|
||||
r := new(SNMPReceiver)
|
||||
|
||||
// Set name of SNMPReceiver
|
||||
// The name should be chosen in such a way that different instances of SNMPReceiver can be distinguished
|
||||
r.name = fmt.Sprintf("SNMPReceiver(%s)", name)
|
||||
|
||||
// Set static information
|
||||
r.meta = map[string]string{"source": r.name, "group": "SNMP"}
|
||||
r.tags = map[string]string{"type": "node"}
|
||||
|
||||
// Set defaults in r.config
|
||||
r.interval = time.Duration(30) * time.Second
|
||||
|
||||
// Read the sample receiver specific JSON config
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &r.config)
|
||||
if err != nil {
|
||||
cclog.ComponentError(r.name, "Error reading config:", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check that all required fields in the configuration are set
|
||||
if len(r.config.Targets) == 0 {
|
||||
err = fmt.Errorf("no targets configured, exiting")
|
||||
cclog.ComponentError(r.name, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(r.config.Metrics) == 0 {
|
||||
err = fmt.Errorf("no metrics configured, exiting")
|
||||
cclog.ComponentError(r.name, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(r.config.ReadInterval) > 0 {
|
||||
d, err := time.ParseDuration(r.config.ReadInterval)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to parse read interval, exiting")
|
||||
cclog.ComponentError(r.name, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
r.interval = d
|
||||
}
|
||||
newtargets := make([]SNMPReceiverTargetConfig, 0)
|
||||
for _, t := range r.config.Targets {
|
||||
t.timeout = time.Duration(1) * time.Second
|
||||
if len(t.Timeout) > 0 {
|
||||
d, err := time.ParseDuration(t.Timeout)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to parse interval for target %s", t.Hostname)
|
||||
cclog.ComponentError(r.name, err.Error())
|
||||
continue
|
||||
}
|
||||
t.timeout = d
|
||||
}
|
||||
newtargets = append(newtargets, t)
|
||||
}
|
||||
r.config.Targets = newtargets
|
||||
|
||||
return r, nil
|
||||
}
|
56
receivers/snmpReceiver.md
Normal file
56
receivers/snmpReceiver.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# SNMP Receiver
|
||||
|
||||
```json
|
||||
"<name>": {
|
||||
"type": "snmp",
|
||||
"read_interval": "30s",
|
||||
"targets" : [{
|
||||
"hostname" : "host1.example.com",
|
||||
"port" : 161,
|
||||
"community": "public",
|
||||
"timeout" : 1,
|
||||
}],
|
||||
"metrics" : [
|
||||
{
|
||||
"name": "sensor1",
|
||||
"value": "1.3.6.1.2.1.1.4.0",
|
||||
"unit": "1.3.6.1.2.1.1.7.0",
|
||||
},
|
||||
{
|
||||
"name": "1.3.6.1.2.1.1.2.0",
|
||||
"value": "1.3.6.1.2.1.1.4.0",
|
||||
"unit": "mb/s",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `snmp` receiver uses [gosnmp](https://github.com/gosnmp/gosnmp) to read metrics from network-attached devices.
|
||||
|
||||
The configuration of SNMP is quite extensive due to it's flexibility.
|
||||
|
||||
## Configuration
|
||||
|
||||
- `type` has to be `snmp`
|
||||
- `read_interval` as duration like '1s' or '20s' (default '30s')
|
||||
|
||||
For the receiver, the configuration is split in two parts:
|
||||
### Target configuration
|
||||
|
||||
Each network-attached device that should be queried. A target consits of
|
||||
- `hostname`
|
||||
- `port` (default 161)
|
||||
- `community` (default `public`)
|
||||
- `timeout` as duration like '1s' or '20s' (default '1s')
|
||||
- `version` SNMP version `X` (`X` in `1`, `2c`, `3`) (default `2c`)
|
||||
- `type` to specify `type` tag for the target (default `node`)
|
||||
- `type-id` to specify `type-id` tag for the target
|
||||
- `stype` to specify `stype` tag (sub type) for the target
|
||||
- `stype-id` to specify `stype-id` tag for the target
|
||||
|
||||
### Metric configuration
|
||||
- `name` can be an OID or a user-given string
|
||||
- `value` has to be an OID
|
||||
- `unit` can be empty, an OID or a user-given string
|
||||
|
||||
If a OID is used for `name` or `unit`, the receiver will use the returned values to create the output metric. If there are any issues with the returned values, it uses the `OID`.
|
Reference in New Issue
Block a user