Fix gNMI telemetry: OpenConfig paths, json_ietf encoding, SSH config

- Switch Telegraf from native IOS-XR YANG paths to OpenConfig
  (openconfig-interfaces:interfaces/interface/state/counters)
- Use json_ietf encoding instead of proto (IOS-XR 24.3.1 compat)
- Target only CORE-01/CORE-02 (R9K routers blocked by CML mgmt net)
- Update all 3 Grafana dashboard queries to match OpenConfig field
  names (in-octets, out-octets, in-pkts, out-pkts, in-errors, etc.)
- Rewrite gnmi_grpc_config.py to use SSH/CLI via paramiko instead of
  NETCONF (IOS-XR 24.3.1 rejects NETCONF gRPC edit-config)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sam 2026-03-06 16:19:16 -07:00
parent 6b45f124f0
commit c28c9b2527
5 changed files with 100 additions and 83 deletions

View File

@ -8,7 +8,9 @@ telemetry data.
What this script applies per router: What this script applies per router:
- gRPC server on port 57400 with TLS disabled - gRPC server on port 57400 with TLS disabled
- YANG model: Cisco-IOS-XR-man-ems-cfg
Uses SSH/CLI (paramiko) instead of NETCONF because IOS-XR 24.3.1
rejects the NETCONF edit-config for gRPC with "Need to enable GRPC first".
Router targets: Router targets:
CORE-01 (10.100.0.100) CORE-01 (10.100.0.100)
@ -16,11 +18,10 @@ Router targets:
R9K-01 (10.100.0.1) through R9K-07 (10.100.0.7) R9K-01 (10.100.0.1) through R9K-07 (10.100.0.7)
""" """
from ncclient import manager import paramiko
import time
import sys import sys
GRPC_NS = 'http://cisco.com/ns/yang/Cisco-IOS-XR-man-ems-cfg'
ROUTERS = [ ROUTERS = [
('10.100.0.100', 'CORE-01'), ('10.100.0.100', 'CORE-01'),
('10.100.0.200', 'CORE-02'), ('10.100.0.200', 'CORE-02'),
@ -33,38 +34,47 @@ ROUTERS = [
('10.100.0.7', 'R9K-07'), ('10.100.0.7', 'R9K-07'),
] ]
GRPC_CONFIG_XML = """ USERNAME = 'webui'
<config> PASSWORD = 'cisco'
<grpc xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-man-ems-cfg"> GRPC_PORT = 57400
<port>57400</port>
<no-tls/> CONFIG_COMMANDS = [
</grpc> 'configure terminal',
</config> 'grpc',
""" f'port {GRPC_PORT}',
'no-tls',
'commit',
'end',
]
def configure_router(mgmt_ip, label): def configure_router(mgmt_ip, label):
"""Apply gRPC configuration via NETCONF edit-config + commit.""" """Apply gRPC configuration via SSH CLI."""
print(f"\n{''*60}") print(f"\n{''*60}")
print(f" Configuring {label} ({mgmt_ip})") print(f" Configuring {label} ({mgmt_ip})")
print(f"{''*60}") print(f"{''*60}")
print(f" Applying: gRPC port=57400 no-tls") print(f" Applying: gRPC port={GRPC_PORT} no-tls")
try: try:
with manager.connect( client = paramiko.SSHClient()
host=mgmt_ip, client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
port=830, client.connect(mgmt_ip, username=USERNAME, password=PASSWORD, timeout=10)
username='webui',
password='cisco', shell = client.invoke_shell()
hostkey_verify=False, time.sleep(1)
device_params={'name': 'iosxr'}, shell.recv(65535) # clear banner
timeout=20,
) as m: for cmd in CONFIG_COMMANDS:
print(" → Applying gRPC configuration...") shell.send(cmd + '\n')
m.edit_config(target='candidate', config=GRPC_CONFIG_XML) time.sleep(1.5)
output = shell.recv(65535).decode()
client.close()
if 'error' in output.lower() or 'fail' in output.lower():
print(f" ✗ ERROR on {label}: {output.strip()}")
return False
print(" → Committing...")
m.commit()
print(f"{label} done.") print(f"{label} done.")
return True return True
@ -74,30 +84,33 @@ def configure_router(mgmt_ip, label):
def verify_router(mgmt_ip, label): def verify_router(mgmt_ip, label):
"""Re-read running config to confirm the grpc block is present.""" """Verify gRPC configuration via SSH."""
try: try:
with manager.connect( client = paramiko.SSHClient()
host=mgmt_ip, port=830, username='webui', password='cisco', client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
hostkey_verify=False, device_params={'name': 'iosxr'}, timeout=10 client.connect(mgmt_ip, username=USERNAME, password=PASSWORD, timeout=10)
) as m:
filt_grpc = """<filter>
<grpc xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-man-ems-cfg">
<port/>
<no-tls/>
</grpc>
</filter>"""
r_grpc = m.get_config(source='running', filter=filt_grpc)
has_grpc = '<port>57400</port>' in str(r_grpc) shell = client.invoke_shell()
has_notls = '<no-tls' in str(r_grpc) time.sleep(1)
shell.recv(65535)
g = '' if has_grpc else '' shell.send('show running-config grpc\n')
time.sleep(3)
output = shell.recv(65535).decode()
client.close()
has_port = f'port {GRPC_PORT}' in output
has_notls = 'no-tls' in output
p = '' if has_port else ''
t = '' if has_notls else '' t = '' if has_notls else ''
status = 'OK' if (has_grpc and has_notls) else 'INCOMPLETE' status = 'OK' if (has_port and has_notls) else 'INCOMPLETE'
print(f" {label:8s} grpc-port={g} no-tls={t} [{status}]") print(f" {label:8s} grpc-port={p} no-tls={t} [{status}]")
return has_port and has_notls
except Exception as e: except Exception as e:
print(f" {label:8s} verify error: {e}") print(f" {label:8s} verify error: {e}")
return False
def main(): def main():
@ -111,27 +124,34 @@ def main():
ok = configure_router(mgmt_ip, label) ok = configure_router(mgmt_ip, label)
results.append((mgmt_ip, label, ok)) results.append((mgmt_ip, label, ok))
# Summary # Verification pass
print(f"\n{'='*60}") print(f"\n{'='*60}")
print("Post-apply verification") print("Post-apply verification")
print('='*60) print('='*60)
print(f" {'Router':8s} {'gRPC Port':9s} {'No-TLS':6s} Status") print(f" {'Router':8s} {'gRPC Port':9s} {'No-TLS':6s} Status")
for mgmt_ip, label, ok in results:
if ok: all_ok = True
verify_router(mgmt_ip, label) for mgmt_ip, label, applied_ok in results:
if applied_ok:
if not verify_router(mgmt_ip, label):
all_ok = False
else: else:
print(f" {label:8s} skipped (apply failed)") print(f" {label:8s} skipped (apply failed)")
all_ok = False
failed = [label for _, label, ok in results if not ok] failed = [label for _, label, ok in results if not ok]
print() print()
if failed: if failed:
print(f"FAILED: {', '.join(failed)}") print(f"FAILED: {', '.join(failed)}")
sys.exit(1) sys.exit(1)
else: elif all_ok:
print("All routers configured successfully.") print("All routers configured successfully.")
print() print()
print("gRPC is now listening on port 57400 (no TLS) on all routers.") print(f"gRPC is now listening on port {GRPC_PORT} (no TLS) on all routers.")
print("Next: start Telegraf with gNMI input plugin to begin collecting telemetry.") print("Next: start Telegraf with gNMI input plugin to begin collecting telemetry.")
else:
print("Some routers may have incomplete configuration. Check output above.")
sys.exit(1)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -74,7 +74,7 @@
"targets": [ "targets": [
{ {
"datasource": {"type": "influxdb","uid": "obmp_influxdb"}, "datasource": {"type": "influxdb","uid": "obmp_influxdb"},
"query": "from(bucket: \"telemetry\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"interface_counters\")\n |> filter(fn: (r) => r.source =~ /${router:regex}/)\n |> filter(fn: (r) => r._field == \"bytes_received\" or r._field == \"bytes_sent\")\n |> derivative(unit: 1s, nonNegative: true)\n |> map(fn: (r) => ({r with _value: if r._value < 0.0 then 0.0 else r._value}))", "query": "from(bucket: \"telemetry\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"interface_counters\")\n |> filter(fn: (r) => r.source =~ /${router:regex}/)\n |> filter(fn: (r) => r._field == \"in-octets\" or r._field == \"out-octets\")\n |> derivative(unit: 1s, nonNegative: true)\n |> map(fn: (r) => ({r with _value: if r._value < 0.0 then 0.0 else r._value}))",
"refId": "A" "refId": "A"
} }
], ],

View File

@ -59,7 +59,7 @@
"targets": [ "targets": [
{ {
"datasource": {"type": "influxdb","uid": "obmp_influxdb"}, "datasource": {"type": "influxdb","uid": "obmp_influxdb"},
"query": "from(bucket: \"telemetry\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"interface_counters\")\n |> filter(fn: (r) => r.source =~ /${router:regex}/)\n |> filter(fn: (r) => r.name =~ /${interface:regex}/)\n |> filter(fn: (r) => r._field == \"input_errors\" or r._field == \"output_errors\" or r._field == \"crc_errors\")\n |> derivative(unit: 1s, nonNegative: true)", "query": "from(bucket: \"telemetry\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"interface_counters\")\n |> filter(fn: (r) => r.source =~ /${router:regex}/)\n |> filter(fn: (r) => r.name =~ /${interface:regex}/)\n |> filter(fn: (r) => r._field == \"in-errors\" or r._field == \"out-errors\" or r._field == \"in-fcs-errors\")\n |> derivative(unit: 1s, nonNegative: true)",
"refId": "A" "refId": "A"
} }
], ],
@ -84,7 +84,7 @@
"targets": [ "targets": [
{ {
"datasource": {"type": "influxdb","uid": "obmp_influxdb"}, "datasource": {"type": "influxdb","uid": "obmp_influxdb"},
"query": "from(bucket: \"telemetry\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"interface_counters\")\n |> filter(fn: (r) => r.source =~ /${router:regex}/)\n |> filter(fn: (r) => r.name =~ /${interface:regex}/)\n |> filter(fn: (r) => r._field == \"input_drops\" or r._field == \"output_drops\")\n |> derivative(unit: 1s, nonNegative: true)", "query": "from(bucket: \"telemetry\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"interface_counters\")\n |> filter(fn: (r) => r.source =~ /${router:regex}/)\n |> filter(fn: (r) => r.name =~ /${interface:regex}/)\n |> filter(fn: (r) => r._field == \"in-discards\" or r._field == \"out-discards\")\n |> derivative(unit: 1s, nonNegative: true)",
"refId": "A" "refId": "A"
} }
], ],
@ -102,19 +102,19 @@
"thresholds": {"mode": "absolute","steps": [{"color": "green","value": null},{"color": "yellow","value": 1},{"color": "red","value": 100}]} "thresholds": {"mode": "absolute","steps": [{"color": "green","value": null},{"color": "yellow","value": 1},{"color": "red","value": 100}]}
}, },
"overrides": [ "overrides": [
{"matcher": {"id": "byName","options": "input_errors"},"properties": [{"id": "custom.displayMode","value": "color-background-solid"},{"id": "thresholds","value": {"mode": "absolute","steps": [{"color": "green","value": null},{"color": "yellow","value": 1},{"color": "red","value": 100}]}}]}, {"matcher": {"id": "byName","options": "in-errors"},"properties": [{"id": "custom.displayMode","value": "color-background-solid"},{"id": "thresholds","value": {"mode": "absolute","steps": [{"color": "green","value": null},{"color": "yellow","value": 1},{"color": "red","value": 100}]}}]},
{"matcher": {"id": "byName","options": "output_errors"},"properties": [{"id": "custom.displayMode","value": "color-background-solid"},{"id": "thresholds","value": {"mode": "absolute","steps": [{"color": "green","value": null},{"color": "yellow","value": 1},{"color": "red","value": 100}]}}]}, {"matcher": {"id": "byName","options": "out-errors"},"properties": [{"id": "custom.displayMode","value": "color-background-solid"},{"id": "thresholds","value": {"mode": "absolute","steps": [{"color": "green","value": null},{"color": "yellow","value": 1},{"color": "red","value": 100}]}}]},
{"matcher": {"id": "byName","options": "input_drops"},"properties": [{"id": "custom.displayMode","value": "color-background-solid"},{"id": "thresholds","value": {"mode": "absolute","steps": [{"color": "green","value": null},{"color": "yellow","value": 1},{"color": "red","value": 100}]}}]}, {"matcher": {"id": "byName","options": "in-discards"},"properties": [{"id": "custom.displayMode","value": "color-background-solid"},{"id": "thresholds","value": {"mode": "absolute","steps": [{"color": "green","value": null},{"color": "yellow","value": 1},{"color": "red","value": 100}]}}]},
{"matcher": {"id": "byName","options": "output_drops"},"properties": [{"id": "custom.displayMode","value": "color-background-solid"},{"id": "thresholds","value": {"mode": "absolute","steps": [{"color": "green","value": null},{"color": "yellow","value": 1},{"color": "red","value": 100}]}}]} {"matcher": {"id": "byName","options": "out-discards"},"properties": [{"id": "custom.displayMode","value": "color-background-solid"},{"id": "thresholds","value": {"mode": "absolute","steps": [{"color": "green","value": null},{"color": "yellow","value": 1},{"color": "red","value": 100}]}}]}
] ]
}, },
"gridPos": {"h": 12,"w": 24,"x": 0,"y": 20}, "gridPos": {"h": 12,"w": 24,"x": 0,"y": 20},
"id": 3, "id": 3,
"options": {"footer": {"fields": "","reducer": ["sum"],"show": false},"showHeader": true,"sortBy": [{"desc": true,"displayName": "input_errors"}]}, "options": {"footer": {"fields": "","reducer": ["sum"],"show": false},"showHeader": true,"sortBy": [{"desc": true,"displayName": "in-errors"}]},
"targets": [ "targets": [
{ {
"datasource": {"type": "influxdb","uid": "obmp_influxdb"}, "datasource": {"type": "influxdb","uid": "obmp_influxdb"},
"query": "from(bucket: \"telemetry\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"interface_counters\")\n |> filter(fn: (r) => r.source =~ /${router:regex}/)\n |> filter(fn: (r) => r.name =~ /${interface:regex}/)\n |> filter(fn: (r) => r._field == \"input_errors\" or r._field == \"output_errors\" or r._field == \"crc_errors\" or r._field == \"input_drops\" or r._field == \"output_drops\")\n |> last()\n |> pivot(rowKey: [\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\")\n |> keep(columns: [\"source\", \"name\", \"input_errors\", \"output_errors\", \"crc_errors\", \"input_drops\", \"output_drops\"])\n |> sort(columns: [\"input_errors\"], desc: true)", "query": "from(bucket: \"telemetry\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"interface_counters\")\n |> filter(fn: (r) => r.source =~ /${router:regex}/)\n |> filter(fn: (r) => r.name =~ /${interface:regex}/)\n |> filter(fn: (r) => r._field == \"in-errors\" or r._field == \"out-errors\" or r._field == \"in-fcs-errors\" or r._field == \"in-discards\" or r._field == \"out-discards\")\n |> last()\n |> pivot(rowKey: [\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\")\n |> keep(columns: [\"source\", \"name\", \"in-errors\", \"out-errors\", \"in-fcs-errors\", \"in-discards\", \"out-discards\"])\n |> sort(columns: [\"in-errors\"], desc: true)",
"refId": "A" "refId": "A"
} }
], ],

View File

@ -59,7 +59,7 @@
"targets": [ "targets": [
{ {
"datasource": {"type": "influxdb","uid": "obmp_influxdb"}, "datasource": {"type": "influxdb","uid": "obmp_influxdb"},
"query": "from(bucket: \"telemetry\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"interface_counters\")\n |> filter(fn: (r) => r.source =~ /${router:regex}/)\n |> filter(fn: (r) => r.name =~ /${interface:regex}/)\n |> filter(fn: (r) => r._field == \"bytes_received\" or r._field == \"bytes_sent\")\n |> derivative(unit: 1s, nonNegative: true)\n |> map(fn: (r) => ({r with _value: if r._value < 0.0 then 0.0 else r._value}))", "query": "from(bucket: \"telemetry\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"interface_counters\")\n |> filter(fn: (r) => r.source =~ /${router:regex}/)\n |> filter(fn: (r) => r.name =~ /${interface:regex}/)\n |> filter(fn: (r) => r._field == \"in-octets\" or r._field == \"out-octets\")\n |> derivative(unit: 1s, nonNegative: true)\n |> map(fn: (r) => ({r with _value: if r._value < 0.0 then 0.0 else r._value}))",
"refId": "A" "refId": "A"
} }
], ],
@ -84,7 +84,7 @@
"targets": [ "targets": [
{ {
"datasource": {"type": "influxdb","uid": "obmp_influxdb"}, "datasource": {"type": "influxdb","uid": "obmp_influxdb"},
"query": "from(bucket: \"telemetry\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"interface_counters\")\n |> filter(fn: (r) => r.source =~ /${router:regex}/)\n |> filter(fn: (r) => r.name =~ /${interface:regex}/)\n |> filter(fn: (r) => r._field == \"packets_received\" or r._field == \"packets_sent\")\n |> derivative(unit: 1s, nonNegative: true)\n |> map(fn: (r) => ({r with _value: if r._value < 0.0 then 0.0 else r._value}))", "query": "from(bucket: \"telemetry\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"interface_counters\")\n |> filter(fn: (r) => r.source =~ /${router:regex}/)\n |> filter(fn: (r) => r.name =~ /${interface:regex}/)\n |> filter(fn: (r) => r._field == \"in-pkts\" or r._field == \"out-pkts\")\n |> derivative(unit: 1s, nonNegative: true)\n |> map(fn: (r) => ({r with _value: if r._value < 0.0 then 0.0 else r._value}))",
"refId": "A" "refId": "A"
} }
], ],
@ -109,7 +109,7 @@
"targets": [ "targets": [
{ {
"datasource": {"type": "influxdb","uid": "obmp_influxdb"}, "datasource": {"type": "influxdb","uid": "obmp_influxdb"},
"query": "from(bucket: \"telemetry\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"interface_counters\")\n |> filter(fn: (r) => r.source =~ /${router:regex}/)\n |> filter(fn: (r) => r.name =~ /${interface:regex}/)\n |> filter(fn: (r) => r._field == \"bytes_received\" or r._field == \"bytes_sent\")\n |> derivative(unit: 1s, nonNegative: true)\n |> group(columns: [\"source\", \"name\", \"_field\"])\n |> sum()\n |> group(columns: [\"source\", \"name\"])\n |> sum()\n |> group()\n |> sort(columns: [\"_value\"], desc: true)\n |> limit(n: 20)", "query": "from(bucket: \"telemetry\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"interface_counters\")\n |> filter(fn: (r) => r.source =~ /${router:regex}/)\n |> filter(fn: (r) => r.name =~ /${interface:regex}/)\n |> filter(fn: (r) => r._field == \"in-octets\" or r._field == \"out-octets\")\n |> derivative(unit: 1s, nonNegative: true)\n |> group(columns: [\"source\", \"name\", \"_field\"])\n |> sum()\n |> group(columns: [\"source\", \"name\"])\n |> sum()\n |> group()\n |> sort(columns: [\"_value\"], desc: true)\n |> limit(n: 20)",
"refId": "A" "refId": "A"
} }
], ],
@ -122,7 +122,7 @@
"id": 4, "id": 4,
"options": { "options": {
"code": {"language": "plaintext","showLineNumbers": false,"showMiniMap": false}, "code": {"language": "plaintext","showLineNumbers": false,"showMiniMap": false},
"content": "## Interface Utilization Dashboard\n\nThis dashboard displays real-time interface utilization metrics collected via **gNMI streaming telemetry** from IOS-XR routers.\n\n- **Data source:** InfluxDB (Telegraf gNMI input plugin)\n- **YANG model:** `Cisco-IOS-XR-infra-statsd-oper`\n- **Subscription path:** `/infra-statistics/interfaces/interface/latest/generic-counters`\n- **Sample interval:** 10 seconds\n\nUse the **Router** and **Interface** template variables at the top to filter the view.", "content": "## Interface Utilization Dashboard\n\nThis dashboard displays real-time interface utilization metrics collected via **gNMI streaming telemetry** from IOS-XR routers.\n\n- **Data source:** InfluxDB (Telegraf gNMI input plugin)\n- **YANG model:** OpenConfig (`openconfig-interfaces`)\n- **Subscription path:** `/interfaces/interface/state/counters`\n- **Sample interval:** 10 seconds\n\nUse the **Router** and **Interface** template variables at the top to filter the view.",
"mode": "markdown" "mode": "markdown"
}, },
"title": "About", "title": "About",

View File

@ -17,43 +17,40 @@
# INPUT PLUGINS # # INPUT PLUGINS #
############################################################################### ###############################################################################
## CORE routers (directly reachable on port 57400 from host)
## R9K routers (10.100.0.1-7) are blocked by CML management network filtering
[[inputs.gnmi]] [[inputs.gnmi]]
addresses = [ addresses = [
"10.100.0.100:57400", "10.100.0.100:57400",
"10.100.0.200:57400", "10.100.0.200:57400"
"10.100.0.1:57400",
"10.100.0.2:57400",
"10.100.0.3:57400",
"10.100.0.4:57400",
"10.100.0.5:57400",
"10.100.0.6:57400",
"10.100.0.7:57400"
] ]
username = "webui" username = "webui"
password = "cisco" password = "cisco"
## Do not verify the server certificate ## No TLS (lab environment)
enable_tls = false enable_tls = false
## gNMI encoding requested (one of: "proto", "json", "json_ietf", "bytes") ## Use json_ietf encoding (supported by IOS-XR 24.3.1)
encoding = "proto" encoding = "json_ietf"
## Redial in case of failures after ## Redial in case of failures after
redial = "10s" redial = "10s"
## OpenConfig interface counters (bytes, packets, errors, discards)
[[inputs.gnmi.subscription]] [[inputs.gnmi.subscription]]
name = "interface_counters" name = "interface_counters"
origin = "Cisco-IOS-XR-infra-statsd-oper" origin = "openconfig-interfaces"
path = "/infra-statistics/interfaces/interface/latest/generic-counters" path = "/interfaces/interface/state/counters"
subscription_mode = "sample" subscription_mode = "sample"
sample_interval = "10s" sample_interval = "10s"
## OpenConfig interface state (admin/oper status, description, type)
[[inputs.gnmi.subscription]] [[inputs.gnmi.subscription]]
name = "interface_rates" name = "interface_state"
origin = "Cisco-IOS-XR-infra-statsd-oper" origin = "openconfig-interfaces"
path = "/infra-statistics/interfaces/interface/latest/data-rate" path = "/interfaces/interface/state"
subscription_mode = "sample" subscription_mode = "sample"
sample_interval = "10s" sample_interval = "30s"
############################################################################### ###############################################################################
# OUTPUT PLUGINS # # OUTPUT PLUGINS #