diff --git a/gnmi/gnmi_grpc_config.py b/gnmi/gnmi_grpc_config.py
index 77942a7..36f526a 100644
--- a/gnmi/gnmi_grpc_config.py
+++ b/gnmi/gnmi_grpc_config.py
@@ -8,7 +8,9 @@ telemetry data.
What this script applies per router:
- 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:
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)
"""
-from ncclient import manager
+import paramiko
+import time
import sys
-GRPC_NS = 'http://cisco.com/ns/yang/Cisco-IOS-XR-man-ems-cfg'
-
ROUTERS = [
('10.100.0.100', 'CORE-01'),
('10.100.0.200', 'CORE-02'),
@@ -33,40 +34,49 @@ ROUTERS = [
('10.100.0.7', 'R9K-07'),
]
-GRPC_CONFIG_XML = """
-
-
- 57400
-
-
-
-"""
+USERNAME = 'webui'
+PASSWORD = 'cisco'
+GRPC_PORT = 57400
+
+CONFIG_COMMANDS = [
+ 'configure terminal',
+ 'grpc',
+ f'port {GRPC_PORT}',
+ 'no-tls',
+ 'commit',
+ 'end',
+]
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" Configuring {label} ({mgmt_ip})")
print(f"{'─'*60}")
- print(f" Applying: gRPC port=57400 no-tls")
+ print(f" Applying: gRPC port={GRPC_PORT} no-tls")
try:
- with manager.connect(
- host=mgmt_ip,
- port=830,
- username='webui',
- password='cisco',
- hostkey_verify=False,
- device_params={'name': 'iosxr'},
- timeout=20,
- ) as m:
- print(" → Applying gRPC configuration...")
- m.edit_config(target='candidate', config=GRPC_CONFIG_XML)
+ client = paramiko.SSHClient()
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ client.connect(mgmt_ip, username=USERNAME, password=PASSWORD, timeout=10)
- print(" → Committing...")
- m.commit()
- print(f" ✓ {label} done.")
- return True
+ shell = client.invoke_shell()
+ time.sleep(1)
+ shell.recv(65535) # clear banner
+
+ for cmd in CONFIG_COMMANDS:
+ shell.send(cmd + '\n')
+ 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(f" ✓ {label} done.")
+ return True
except Exception as e:
print(f" ✗ ERROR on {label}: {e}")
@@ -74,30 +84,33 @@ def configure_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:
- with manager.connect(
- host=mgmt_ip, port=830, username='webui', password='cisco',
- hostkey_verify=False, device_params={'name': 'iosxr'}, timeout=10
- ) as m:
- filt_grpc = """
-
-
-
-
- """
- r_grpc = m.get_config(source='running', filter=filt_grpc)
+ client = paramiko.SSHClient()
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ client.connect(mgmt_ip, username=USERNAME, password=PASSWORD, timeout=10)
- has_grpc = '57400' in str(r_grpc)
- has_notls = ' 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"
}
],
diff --git a/obmp-grafana/dashboards/Telemetry-3001/interface_errors.json b/obmp-grafana/dashboards/Telemetry-3001/interface_errors.json
index 9daf0f3..4233642 100644
--- a/obmp-grafana/dashboards/Telemetry-3001/interface_errors.json
+++ b/obmp-grafana/dashboards/Telemetry-3001/interface_errors.json
@@ -59,7 +59,7 @@
"targets": [
{
"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"
}
],
@@ -84,7 +84,7 @@
"targets": [
{
"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"
}
],
@@ -102,19 +102,19 @@
"thresholds": {"mode": "absolute","steps": [{"color": "green","value": null},{"color": "yellow","value": 1},{"color": "red","value": 100}]}
},
"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": "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": "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": "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": "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": "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": "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": "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},
"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": [
{
"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"
}
],
diff --git a/obmp-grafana/dashboards/Telemetry-3001/interface_utilization.json b/obmp-grafana/dashboards/Telemetry-3001/interface_utilization.json
index 07fec81..2b2e3d6 100644
--- a/obmp-grafana/dashboards/Telemetry-3001/interface_utilization.json
+++ b/obmp-grafana/dashboards/Telemetry-3001/interface_utilization.json
@@ -59,7 +59,7 @@
"targets": [
{
"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"
}
],
@@ -84,7 +84,7 @@
"targets": [
{
"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"
}
],
@@ -109,7 +109,7 @@
"targets": [
{
"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"
}
],
@@ -122,7 +122,7 @@
"id": 4,
"options": {
"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"
},
"title": "About",
diff --git a/telegraf/telegraf.conf b/telegraf/telegraf.conf
index 2959e86..5aafc16 100644
--- a/telegraf/telegraf.conf
+++ b/telegraf/telegraf.conf
@@ -17,43 +17,40 @@
# 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]]
addresses = [
"10.100.0.100: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"
+ "10.100.0.200:57400"
]
username = "webui"
password = "cisco"
- ## Do not verify the server certificate
+ ## No TLS (lab environment)
enable_tls = false
- ## gNMI encoding requested (one of: "proto", "json", "json_ietf", "bytes")
- encoding = "proto"
+ ## Use json_ietf encoding (supported by IOS-XR 24.3.1)
+ encoding = "json_ietf"
## Redial in case of failures after
redial = "10s"
+ ## OpenConfig interface counters (bytes, packets, errors, discards)
[[inputs.gnmi.subscription]]
name = "interface_counters"
- origin = "Cisco-IOS-XR-infra-statsd-oper"
- path = "/infra-statistics/interfaces/interface/latest/generic-counters"
+ origin = "openconfig-interfaces"
+ path = "/interfaces/interface/state/counters"
subscription_mode = "sample"
sample_interval = "10s"
+ ## OpenConfig interface state (admin/oper status, description, type)
[[inputs.gnmi.subscription]]
- name = "interface_rates"
- origin = "Cisco-IOS-XR-infra-statsd-oper"
- path = "/infra-statistics/interfaces/interface/latest/data-rate"
+ name = "interface_state"
+ origin = "openconfig-interfaces"
+ path = "/interfaces/interface/state"
subscription_mode = "sample"
- sample_interval = "10s"
+ sample_interval = "30s"
###############################################################################
# OUTPUT PLUGINS #