Skip to content

Commit

Permalink
cisco_telemetry_mdt enhancement (influxdata#8661)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsx1123 authored Mar 16, 2021
1 parent 8a47d6f commit f555294
Show file tree
Hide file tree
Showing 4 changed files with 1,269 additions and 7 deletions.
64 changes: 62 additions & 2 deletions plugins/inputs/cisco_telemetry_mdt/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Cisco Model-Driven Telemetry (MDT) Input Plugin
# Cisco model-driven telemetry (MDT)

Cisco model-driven telemetry (MDT) is an input plugin that consumes
telemetry data from Cisco IOS XR, IOS XE and NX-OS platforms. It supports TCP & GRPC dialout transports.
GRPC-based transport can utilize TLS for authentication and encryption.
RPC-based transport can utilize TLS for authentication and encryption.
Telemetry data is expected to be GPB-KV (self-describing-gpb) encoded.

The GRPC dialout transport is supported on various IOS XR (64-bit) 6.1.x and later, IOS XE 16.10 and later, as well as NX-OS 7.x and later platforms.
Expand All @@ -21,6 +21,9 @@ The TCP dialout transport is supported on IOS XR (32-bit and 64-bit) 6.1.x and l
## Address and port to host telemetry listener
service_address = ":57000"

## Grpc Maximum Message Size, default is 4MB, increase the size.
max_msg_size = 20000000

## Enable TLS; grpc transport only.
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
Expand All @@ -35,10 +38,67 @@ The TCP dialout transport is supported on IOS XR (32-bit and 64-bit) 6.1.x and l
## Define aliases to map telemetry encoding paths to simple measurement names
[inputs.cisco_telemetry_mdt.aliases]
ifstats = "ietf-interfaces:interfaces-state/interface/statistics"
[inputs.cisco_telemetry_mdt.dmes]
# Global Property Xformation.
# prop1 = "uint64 to int"
# prop2 = "uint64 to string"
# prop3 = "string to uint64"
# prop4 = "string to int64"
# prop5 = "string to float64"
# auto-prop-xfrom = "auto-float-xfrom" #Xform any property which is string, and has float number to type float64
# Per Path property xformation, Name is telemetry configuration under sensor-group, path configuration "WORD Distinguished Name"
# Per Path configuration is better as it avoid property collision issue of types.
# dnpath = '{"Name": "show ip route summary","prop": [{"Key": "routes","Value": "string"}, {"Key": "best-paths","Value": "string"}]}'
# dnpath2 = '{"Name": "show processes cpu","prop": [{"Key": "kernel_percent","Value": "float"}, {"Key": "idle_percent","Value": "float"}, {"Key": "process","Value": "string"}, {"Key": "user_percent","Value": "float"}, {"Key": "onesec","Value": "float"}]}'
# dnpath3 = '{"Name": "show processes memory physical","prop": [{"Key": "processname","Value": "string"}]}'
```

### Example Output:
```
ifstats,path=ietf-interfaces:interfaces-state/interface/statistics,host=linux,name=GigabitEthernet2,source=csr1kv,subscription=101 in-unicast-pkts=27i,in-multicast-pkts=0i,discontinuity-time="2019-05-23T07:40:23.000362+00:00",in-octets=5233i,in-errors=0i,out-multicast-pkts=0i,out-discards=0i,in-broadcast-pkts=0i,in-discards=0i,in-unknown-protos=0i,out-unicast-pkts=0i,out-broadcast-pkts=0i,out-octets=0i,out-errors=0i 1559150462624000000
ifstats,path=ietf-interfaces:interfaces-state/interface/statistics,host=linux,name=GigabitEthernet1,source=csr1kv,subscription=101 in-octets=3394770806i,in-broadcast-pkts=0i,in-multicast-pkts=0i,out-broadcast-pkts=0i,in-unknown-protos=0i,out-octets=350212i,in-unicast-pkts=9477273i,in-discards=0i,out-unicast-pkts=2726i,out-discards=0i,discontinuity-time="2019-05-23T07:40:23.000363+00:00",in-errors=30i,out-multicast-pkts=0i,out-errors=0i 1559150462624000000
```

### NX-OS Configuration Example:
```
Requirement DATA-SOURCE Configuration
-----------------------------------------
Environment DME path sys/ch query-condition query-target=subtree&target-subtree-class=eqptPsuSlot,eqptFtSlot,eqptSupCSlot,eqptPsu,eqptFt,eqptSensor,eqptLCSlot
DME path sys/ch depth 5 (Another configuration option)
Environment NXAPI show environment power
NXAPI show environment fan
NXAPI show environment temperature
Interface Stats DME path sys/intf query-condition query-target=subtree&target-subtree-class=rmonIfIn,rmonIfOut,rmonIfHCIn,rmonIfHCOut,rmonEtherStats
Interface State DME path sys/intf depth unbounded query-condition query-target=subtree&target-subtree-class=l1PhysIf,pcAggrIf,l3EncRtdIf,l3LbRtdIf,ethpmPhysIf
VPC DME path sys/vpc query-condition query-target=subtree&target-subtree-class=vpcDom,vpcIf
Resources cpu DME path sys/procsys query-condition query-target=subtree&target-subtree-class=procSystem,procSysCore,procSysCpuSummary,procSysCpu,procIdle,procIrq,procKernel,procNice,procSoftirq,procTotal,procUser,procWait,procSysCpuHistory,procSysLoad
Resources Mem DME path sys/procsys/sysmem/sysmemused
path sys/procsys/sysmem/sysmemusage
path sys/procsys/sysmem/sysmemfree
Per Process cpu DME path sys/proc depth unbounded query-condition rsp-foreign-subtree=ephemeral
vxlan(svi stats) DME path sys/bd query-condition query-target=subtree&target-subtree-class=l2VlanStats
BGP DME path sys/bgp query-condition query-target=subtree&target-subtree-class=bgpDom,bgpPeer,bgpPeerAf,bgpDomAf,bgpPeerAfEntry,bgpOperRtctrlL3,bgpOperRttP,bgpOperRttEntry,bgpOperAfCtrl
mac dynamic DME path sys/mac query-condition query-target=subtree&target-subtree-class=l2MacAddressTable
bfd DME path sys/bfd/inst depth unbounded
lldp DME path sys/lldp depth unbounded
urib DME path sys/urib depth unbounded query-condition rsp-foreign-subtree=ephemeral
u6rib DME path sys/u6rib depth unbounded query-condition rsp-foreign-subtree=ephemeral
multicast flow DME path sys/mca/show/flows depth unbounded
multicast stats DME path sys/mca/show/stats depth unbounded
multicast igmp NXAPI show ip igmp groups vrf all
multicast igmp NXAPI show ip igmp interface vrf all
multicast igmp NXAPI show ip igmp snooping
multicast igmp NXAPI show ip igmp snooping groups
multicast igmp NXAPI show ip igmp snooping groups detail
multicast igmp NXAPI show ip igmp snooping groups summary
multicast igmp NXAPI show ip igmp snooping mrouter
multicast igmp NXAPI show ip igmp snooping statistics
multicast pim NXAPI show ip pim interface vrf all
multicast pim NXAPI show ip pim neighbor vrf all
multicast pim NXAPI show ip pim route vrf all
multicast pim NXAPI show ip pim rp vrf all
multicast pim NXAPI show ip pim statistics vrf all
multicast pim NXAPI show ip pim vrf all
```
159 changes: 156 additions & 3 deletions plugins/inputs/cisco_telemetry_mdt/cisco_telemetry_mdt.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cisco_telemetry_mdt
import (
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"net"
Expand Down Expand Up @@ -37,6 +38,7 @@ type CiscoTelemetryMDT struct {
ServiceAddress string `toml:"service_address"`
MaxMsgSize int `toml:"max_msg_size"`
Aliases map[string]string `toml:"aliases"`
Dmes map[string]string `toml:"dmes"`
EmbeddedTags []string `toml:"embedded_tags"`

Log telegraf.Logger
Expand All @@ -50,13 +52,24 @@ type CiscoTelemetryMDT struct {

// Internal state
aliases map[string]string
dmesFuncs map[string]string
warned map[string]struct{}
extraTags map[string]map[string]struct{}
nxpathMap map[string]map[string]string //per path map
propMap map[string]func(field *telemetry.TelemetryField, value interface{}) interface{}
mutex sync.Mutex
acc telegraf.Accumulator
wg sync.WaitGroup
}

type NxPayloadXfromStructure struct {
Name string `json:"Name"`
Prop []struct {
Key string `json:"Key"`
Value string `json:"Value"`
} `json:"prop"`
}

// Start the Cisco MDT service
func (c *CiscoTelemetryMDT) Start(acc telegraf.Accumulator) error {
var err error
Expand All @@ -66,12 +79,55 @@ func (c *CiscoTelemetryMDT) Start(acc telegraf.Accumulator) error {
return err
}

c.propMap = make(map[string]func(field *telemetry.TelemetryField, value interface{}) interface{}, 100)
c.propMap["test"] = nxosValueXformUint64Toint64
c.propMap["asn"] = nxosValueXformUint64ToString //uint64 to string.
c.propMap["subscriptionId"] = nxosValueXformUint64ToString //uint64 to string.
c.propMap["operState"] = nxosValueXformUint64ToString //uint64 to string.

// Invert aliases list
c.warned = make(map[string]struct{})
c.aliases = make(map[string]string, len(c.Aliases))
for alias, path := range c.Aliases {
c.aliases[path] = alias
}
c.initDb()

c.dmesFuncs = make(map[string]string, len(c.Dmes))
for dme, path := range c.Dmes {
c.dmesFuncs[path] = dme
switch path {
case "uint64 to int":
c.propMap[dme] = nxosValueXformUint64Toint64
case "uint64 to string":
c.propMap[dme] = nxosValueXformUint64ToString
case "string to float64":
c.propMap[dme] = nxosValueXformStringTofloat
case "string to uint64":
c.propMap[dme] = nxosValueXformStringToUint64
case "string to int64":
c.propMap[dme] = nxosValueXformStringToInt64
case "auto-float-xfrom":
c.propMap[dme] = nxosValueAutoXformFloatProp
default:
if !strings.HasPrefix(dme, "dnpath") { // not path based property map
continue
}

var jsStruct NxPayloadXfromStructure
err := json.Unmarshal([]byte(path), &jsStruct)
if err != nil {
continue
}

// Build 2 level Hash nxpathMap Key = jsStruct.Name, Value = map of jsStruct.Prop
// It will override the default of code if same path is provided in configuration.
c.nxpathMap[jsStruct.Name] = make(map[string]string, len(jsStruct.Prop))
for _, prop := range jsStruct.Prop {
c.nxpathMap[jsStruct.Name][prop.Key] = prop.Value
}
}
}

// Fill extra tags
c.extraTags = make(map[string]map[string]struct{})
Expand Down Expand Up @@ -296,7 +352,9 @@ func (c *CiscoTelemetryMDT) handleTelemetry(data []byte) {
// Parse keys
tags = make(map[string]string, len(keys.Fields)+3)
tags["source"] = msg.GetNodeIdStr()
tags["subscription"] = msg.GetSubscriptionIdStr()
if msgID := msg.GetSubscriptionIdStr(); msgID != "" {
tags["subscription"] = msgID
}
tags["path"] = msg.GetEncodingPath()

for _, subfield := range keys.Fields {
Expand Down Expand Up @@ -391,9 +449,72 @@ func (c *CiscoTelemetryMDT) parseKeyField(tags map[string]string, field *telemet
}
}

func (c *CiscoTelemetryMDT) parseRib(grouper *metric.SeriesGrouper, field *telemetry.TelemetryField, prefix string, path string, tags map[string]string, timestamp time.Time) {
// RIB
measurement := path
for _, subfield := range field.Fields {
//For Every table fill the keys which are vrfName, address and masklen
switch subfield.Name {
case "vrfName", "address", "maskLen":
tags[subfield.Name] = decodeTag(subfield)
}
if value := decodeValue(subfield); value != nil {
grouper.Add(measurement, tags, timestamp, subfield.Name, value)
}
if subfield.Name != "nextHop" {
continue
}
//For next hop table fill the keys in the tag - which is address and vrfname
for _, subf := range subfield.Fields {
for _, ff := range subf.Fields {
switch ff.Name {
case "address", "vrfName":
key := "nextHop/" + ff.Name
tags[key] = decodeTag(ff)
}
if value := decodeValue(ff); value != nil {
name := "nextHop/" + ff.Name
grouper.Add(measurement, tags, timestamp, name, value)
}
}
}
}
}

func (c *CiscoTelemetryMDT) parseClassAttributeField(grouper *metric.SeriesGrouper, field *telemetry.TelemetryField, prefix string, path string, tags map[string]string, timestamp time.Time) {
// DME structure: https://developer.cisco.com/site/nxapi-dme-model-reference-api/
var nxAttributes *telemetry.TelemetryField
isDme := strings.Contains(path, "sys/")
if path == "rib" {
//handle native data path rib
c.parseRib(grouper, field, prefix, path, tags, timestamp)
return
}
if field == nil || !isDme || len(field.Fields) == 0 || len(field.Fields[0].Fields) == 0 || len(field.Fields[0].Fields[0].Fields) == 0 {
return
}

if field.Fields[0] != nil && field.Fields[0].Fields != nil && field.Fields[0].Fields[0] != nil && field.Fields[0].Fields[0].Fields[0].Name != "attributes" {
return
}
nxAttributes = field.Fields[0].Fields[0].Fields[0].Fields[0]

for _, subfield := range nxAttributes.Fields {
if subfield.Name == "dn" {
tags["dn"] = decodeTag(subfield)
} else {
c.parseContentField(grouper, subfield, "", path, tags, timestamp)
}
}
}

func (c *CiscoTelemetryMDT) parseContentField(grouper *metric.SeriesGrouper, field *telemetry.TelemetryField, prefix string,
path string, tags map[string]string, timestamp time.Time) {
name := strings.Replace(field.Name, "-", "_", -1)

if (name == "modTs" || name == "createTs") && decodeValue(field) == "never" {
return
}
if len(name) == 0 {
name = prefix
} else if len(prefix) > 0 {
Expand All @@ -416,7 +537,11 @@ func (c *CiscoTelemetryMDT) parseContentField(grouper *metric.SeriesGrouper, fie
c.mutex.Unlock()
}

grouper.Add(measurement, tags, timestamp, name, value)
if val := c.nxosValueXform(field, value, path); val != nil {
grouper.Add(measurement, tags, timestamp, name, val)
} else {
grouper.Add(measurement, tags, timestamp, name, value)
}
return
}

Expand All @@ -430,11 +555,28 @@ func (c *CiscoTelemetryMDT) parseContentField(grouper *metric.SeriesGrouper, fie

var nxAttributes, nxChildren, nxRows *telemetry.TelemetryField
isNXOS := !strings.ContainsRune(path, ':') // IOS-XR and IOS-XE have a colon in their encoding path, NX-OS does not
isEVENT := isNXOS && strings.Contains(path, "EVENT-LIST")
nxChildren = nil
nxAttributes = nil
for _, subfield := range field.Fields {
if isNXOS && subfield.Name == "attributes" && len(subfield.Fields) > 0 {
nxAttributes = subfield.Fields[0]
} else if isNXOS && subfield.Name == "children" && len(subfield.Fields) > 0 {
nxChildren = subfield
if !isEVENT {
nxChildren = subfield
} else {
sub := subfield.Fields
if len(sub) > 0 && sub[0] != nil && sub[0].Fields[0].Name == "subscriptionId" && len(sub[0].Fields) >= 2 {
nxAttributes = sub[0].Fields[1].Fields[0].Fields[0].Fields[0].Fields[0].Fields[0]
}
}
//if nxAttributes == NULL then class based query.
if nxAttributes == nil {
//call function walking over walking list.
for _, sub := range subfield.Fields {
c.parseClassAttributeField(grouper, sub, name, path, tags, timestamp)
}
}
} else if isNXOS && strings.HasPrefix(subfield.Name, "ROW_") {
nxRows = subfield
} else if _, isExtraTag := extraTags[subfield.Name]; !isExtraTag { // Regular telemetry decoding
Expand All @@ -450,9 +592,16 @@ func (c *CiscoTelemetryMDT) parseContentField(grouper *metric.SeriesGrouper, fie
for i, subfield := range row.Fields {
if i == 0 { // First subfield contains the index, promote it from value to tag
tags[prefix] = decodeTag(subfield)
//We can have subfield so recursively handle it.
if len(row.Fields) == 1 {
tags["row_number"] = strconv.FormatInt(int64(i), 10)
c.parseContentField(grouper, subfield, "", path, tags, timestamp)
}
} else {
c.parseContentField(grouper, subfield, "", path, tags, timestamp)
}
// Nxapi we can't identify keys always from prefix
tags["row_number"] = strconv.FormatInt(int64(i), 10)
}
delete(tags, prefix)
}
Expand Down Expand Up @@ -531,6 +680,10 @@ const sampleConfig = `
## Define aliases to map telemetry encoding paths to simple measurement names
[inputs.cisco_telemetry_mdt.aliases]
ifstats = "ietf-interfaces:interfaces-state/interface/statistics"
##Define Property Xformation, please refer README and https://pubhub.devnetcloud.com/media/dme-docs-9-3-3/docs/appendix/ for Model details.
[inputs.cisco_telemetry_mdt.dmes]
ModTs = "ignore"
CreateTs = "ignore"
`

// SampleConfig of plugin
Expand Down
Loading

0 comments on commit f555294

Please sign in to comment.