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 #