Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SNMP receiver #110

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/Release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ jobs:
Release:
runs-on: ubuntu-latest
# We need the RPMs, so add dependency
needs: [AlmaLinux-RPM-build, UBI-8-RPM-build, Ubuntu-focal-build]
needs: [AlmaLinux-RPM-build, UBI-8-RPM-build, Ubuntu-jammy-build]

steps:
# See: https://github.com/actions/download-artifact
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/PaesslerAG/gval v1.2.2
github.com/fsnotify/fsnotify v1.6.0
github.com/gorilla/mux v1.8.0
github.com/gosnmp/gosnmp v1.37.0
github.com/influxdata/influxdb-client-go/v2 v2.12.3
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf
github.com/influxdata/line-protocol/v2 v2.2.1
Expand Down
38 changes: 37 additions & 1 deletion go.sum

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions receivers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ This allows to specify
- [`http`](./httpReceiver.md): Listen for HTTP Post requests transporting metrics in InfluxDB line protocol
- [`ipmi`](./ipmiReceiver.md): Read IPMI sensor readings
- [`redfish`](redfishReceiver.md) Use the Redfish (specification) to query thermal and power metrics
- [`snmp`](./snmpReceiver.md) Query SNMP endpoints in the network

## Contributing own receivers

Expand Down
1 change: 1 addition & 0 deletions receivers/receiveManager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
294 changes: 294 additions & 0 deletions receivers/snmpReceiver.go
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
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
}
60 changes: 60 additions & 0 deletions receivers/snmpReceiver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# 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`.

## Testing

For testing an SNMP endpoint and OIDs, you can use [`scripts/snmpReceiverTest`](../scripts/snmpReceiverTest)
Loading