Compare commits

...
Author SHA1 Message Date
Mattias Walström 68d98f6e78 WiP: Example on howto use restconf notifications
Start script with ./subscribe_ietf_system.py --url "https://10.10.10.100" --user admin --password admin --no-verify-ssl --no-filter
2025-12-18 12:31:54 +01:00
5 changed files with 461 additions and 1 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# /telemetry/optics is for streaming (not used atm)
location ~ ^/(restconf|yang|.well-known)/ {
location ~ ^/(restconf|yang|.well-known|streams)/ {
grpc_pass grpc://[::1]:10080;
grpc_set_header Host $host;
grpc_set_header X-Real-IP $remote_addr;
Executable
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
DEVICE="172.31.31.142"
AUTH="admin:admin"
echo "=== RESTCONF Notification Capabilities ==="
echo
echo "1. Available streams:"
curl -sk https://$DEVICE/restconf/data/ietf-restconf-monitoring:restconf-state/streams \
-u $AUTH | jq -r '.["ietf-restconf-monitoring:restconf-state"].streams.stream[]?.name' 2>/dev/null || echo "None found"
echo
echo "2. Testing NETCONF stream (5 sec timeout):"
timeout 5 curl -sk -N https://$DEVICE/restconf/streams/NETCONF \
-H "Accept: text/event-stream" -u $AUTH 2>&1 | head -10
echo
echo "3. Testing yang-push stream (5 sec timeout):"
timeout 5 curl -sk -N https://$DEVICE/restconf/streams/yang-push \
-H "Accept: text/event-stream" -u $AUTH 2>&1 | head -10
echo
echo "4. Current ietf-system state:"
curl -sk https://$DEVICE/restconf/data/ietf-system:system \
-u $AUTH | jq 2>/dev/null || echo "Failed"
+54
View File
@@ -1267,6 +1267,56 @@ static sr_error_t change_auth_check(struct confd *confd, sr_session_ctx_t *sessi
return SR_ERR_OK;
}
static sr_error_t send_auth_change_notification(sr_session_ctx_t *session)
{
struct lyd_node *notif;
const struct ly_ctx *ctx;
char timestamp[64];
time_t now;
struct tm tm;
int rc;
ctx = sr_acquire_context(sr_session_get_connection(session));
if (!ctx) {
ERROR("Failed to acquire libyang context for notification");
return SR_ERR_INTERNAL;
}
/* Create notification: system-authentication-changed */
rc = lyd_new_path(NULL, ctx, "/infix-system:system-authentication-changed", NULL, 0, &notif);
if (rc != LY_SUCCESS) {
ERROR("Failed to create authentication change notification: %d", rc);
sr_release_context(sr_session_get_connection(session));
return SR_ERR_INTERNAL;
}
/* Add timestamp */
now = time(NULL);
gmtime_r(&now, &tm);
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", &tm);
rc = lyd_new_path(notif, NULL, "event-time", timestamp, 0, NULL);
if (rc != LY_SUCCESS) {
ERROR("Failed to add timestamp to notification: %d", rc);
lyd_free_tree(notif);
sr_release_context(sr_session_get_connection(session));
return SR_ERR_INTERNAL;
}
/* Send the notification */
rc = sr_notif_send_tree(session, notif, 0, 0);
if (rc != SR_ERR_OK) {
ERROR("Failed to send authentication change notification: %s", sr_strerror(rc));
} else {
DEBUG("Authentication change notification sent successfully");
}
lyd_free_tree(notif);
sr_release_context(sr_session_get_connection(session));
return rc;
}
static sr_error_t change_auth_done(struct confd *confd, sr_session_ctx_t *session)
{
sr_error_t err;
@@ -1290,6 +1340,10 @@ static sr_error_t change_auth_done(struct confd *confd, sr_session_ctx_t *sessio
}
DEBUG("Changes to authentication saved.");
/* Send notification about authentication changes */
send_auth_change_notification(session);
cleanup:
return err;
}
+290
View File
@@ -0,0 +1,290 @@
#!/usr/bin/env python3
"""
Subscribe to RESTCONF notifications for ietf-system changes from rousette.
This script establishes a subscription to the NETCONF notification stream,
filters for ietf-system notifications, and displays them in real-time.
"""
import argparse
import json
import re
import sys
from typing import Optional
import requests
from requests.auth import HTTPBasicAuth
class SSEClient:
"""Simple Server-Sent Events (SSE) client."""
def __init__(self, response):
self.response = response
self.events = self._parse_events()
def _parse_events(self):
"""Parse SSE stream."""
data_buffer = []
for line in self.response.iter_lines(decode_unicode=True):
if line is None:
continue
# Empty line indicates end of event
if not line.strip():
if data_buffer:
yield '\n'.join(data_buffer)
data_buffer = []
continue
# SSE data line
if line.startswith('data:'):
data_buffer.append(line[5:].strip())
# SSE comment (ignore)
elif line.startswith(':'):
continue
def establish_subscription(
base_url: str,
username: Optional[str] = None,
password: Optional[str] = None,
xpath_filter: Optional[str] = None,
encoding: str = "encode-json",
verify_ssl: bool = True
) -> dict:
"""
Establish a RESTCONF notification subscription.
Args:
base_url: Base URL of the RESTCONF server (e.g., "http://localhost:10080")
username: Username for authentication (None for anonymous)
password: Password for authentication
xpath_filter: XPath filter for notifications (e.g., "/ietf-system:*")
encoding: Notification encoding ("encode-json" or "encode-xml")
Returns:
dict with 'id' and 'uri' keys
"""
url = f"{base_url}/restconf/operations/ietf-subscribed-notifications:establish-subscription"
# Build subscription request
payload = {
"ietf-subscribed-notifications:input": {
"stream": "NETCONF",
"encoding": encoding
}
}
# Add XPath filter if provided
if xpath_filter:
payload["ietf-subscribed-notifications:input"]["stream-xpath-filter"] = xpath_filter
headers = {
"Content-Type": "application/yang-data+json",
"Accept": "application/yang-data+json"
}
# Setup authentication
auth = None
if username:
auth = HTTPBasicAuth(username, password or "")
# Make subscription request
print(f"Establishing subscription to {url}...")
if xpath_filter:
print(f" XPath filter: {xpath_filter}")
if not verify_ssl:
print(" Warning: SSL certificate verification disabled")
# Disable SSL warnings if verify_ssl is False
if not verify_ssl:
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
response = requests.post(url, json=payload, headers=headers, auth=auth, verify=verify_ssl)
if response.status_code != 200:
print(f"Error: Failed to establish subscription (HTTP {response.status_code})")
print(f"Response: {response.text}")
sys.exit(1)
# Parse response
data = response.json()
output = data.get("ietf-subscribed-notifications:output", {})
subscription_id = output.get("id")
subscription_uri = output.get("ietf-restconf-subscribed-notifications:uri")
if not subscription_id or not subscription_uri:
print("Error: Invalid subscription response")
print(f"Response: {json.dumps(data, indent=2)}")
sys.exit(1)
print(f"✓ Subscription established (ID: {subscription_id})")
print(f" Stream URI: {subscription_uri}")
return {
"id": subscription_id,
"uri": subscription_uri
}
def receive_notifications(
base_url: str,
subscription_uri: str,
username: Optional[str] = None,
password: Optional[str] = None,
pretty: bool = True,
verify_ssl: bool = True
):
"""
Connect to the subscription stream and receive notifications.
Args:
base_url: Base URL of the RESTCONF server
subscription_uri: Subscription URI from establish_subscription
username: Username for authentication
password: Password for authentication
pretty: Pretty-print JSON notifications
"""
# Build full URL
url = f"{base_url}{subscription_uri}"
headers = {
"Accept": "text/event-stream"
}
# Setup authentication
auth = None
if username:
auth = HTTPBasicAuth(username, password or "")
print(f"\nConnecting to notification stream...")
print(f"Listening for ietf-system notifications (Ctrl+C to stop)...\n")
try:
# Disable SSL warnings if verify_ssl is False
if not verify_ssl:
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Open SSE connection
response = requests.get(url, headers=headers, auth=auth, stream=True, verify=verify_ssl)
if response.status_code != 200:
print(f"Error: Failed to connect to stream (HTTP {response.status_code})")
print(f"Response: {response.text}")
sys.exit(1)
# Process events
client = SSEClient(response)
for event_data in client.events:
try:
# Parse and display notification
if pretty:
data = json.loads(event_data)
print(json.dumps(data, indent=2))
else:
print(event_data)
print("-" * 80)
except json.JSONDecodeError:
print(f"Received non-JSON data: {event_data}")
print("-" * 80)
except KeyboardInterrupt:
print("\n\nStopping notification listener...")
except requests.exceptions.RequestException as e:
print(f"\nConnection error: {e}")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(
description="Subscribe to RESTCONF notifications for ietf-system changes",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Subscribe with authentication
%(prog)s --url http://localhost:10080 --user admin --password secret
# Subscribe anonymously (if allowed by server)
%(prog)s --url http://localhost:10080
# Subscribe with custom XPath filter
%(prog)s --url http://localhost:10080 --filter "/ietf-system:system-restart"
# Subscribe to all notifications (no filter)
%(prog)s --url http://localhost:10080 --no-filter
"""
)
parser.add_argument(
"--url",
default="http://localhost:10080",
help="Base URL of RESTCONF server (default: http://localhost:10080)"
)
parser.add_argument(
"--no-verify-ssl", "-k",
action="store_true",
help="Disable SSL certificate verification (for self-signed certificates)"
)
parser.add_argument(
"--user", "-u",
help="Username for authentication (omit for anonymous access)"
)
parser.add_argument(
"--password", "-p",
help="Password for authentication"
)
parser.add_argument(
"--filter",
default="/ietf-system:*",
help="XPath filter for notifications (default: /ietf-system:*)"
)
parser.add_argument(
"--no-filter",
action="store_true",
help="Don't apply any filter (receive all notifications)"
)
parser.add_argument(
"--encoding",
choices=["encode-json", "encode-xml"],
default="encode-json",
help="Notification encoding format (default: encode-json)"
)
parser.add_argument(
"--plain",
action="store_true",
help="Don't pretty-print JSON output"
)
args = parser.parse_args()
# Determine filter
xpath_filter = None if args.no_filter else args.filter
# Establish subscription
subscription = establish_subscription(
base_url=args.url,
username=args.user,
password=args.password,
xpath_filter=xpath_filter,
encoding=args.encoding,
verify_ssl=not args.no_verify_ssl
)
# Receive notifications
receive_notifications(
base_url=args.url,
subscription_uri=subscription["uri"],
username=args.user,
password=args.password,
pretty=not args.plain,
verify_ssl=not args.no_verify_ssl
)
if __name__ == "__main__":
main()
+91
View File
@@ -0,0 +1,91 @@
#!/bin/bash
# Simple bash script to subscribe to ietf-system notifications from rousette
# Uses curl to interact with RESTCONF API
set -euo pipefail
# Configuration
RESTCONF_URL="${RESTCONF_URL:-http://localhost:10080}"
USERNAME="${USERNAME:-}"
PASSWORD="${PASSWORD:-}"
XPATH_FILTER="${XPATH_FILTER:-/ietf-system:*}"
NO_VERIFY_SSL="${NO_VERIFY_SSL:-false}"
# Build authentication flags
AUTH_FLAGS=()
if [ -n "$USERNAME" ]; then
AUTH_FLAGS=(-u "$USERNAME:$PASSWORD")
fi
# Add SSL flags for self-signed certificates
if [ "$NO_VERIFY_SSL" = "true" ]; then
AUTH_FLAGS+=(-k)
echo "Warning: SSL certificate verification disabled" >&2
fi
echo "=== Establishing RESTCONF subscription ===" >&2
echo "Server: $RESTCONF_URL" >&2
echo "Filter: $XPATH_FILTER" >&2
echo >&2
# Step 1: Establish subscription
SUBSCRIPTION_REQUEST=$(cat <<EOF
{
"ietf-subscribed-notifications:input": {
"stream": "NETCONF",
"encoding": "encode-json",
"stream-xpath-filter": "$XPATH_FILTER"
}
}
EOF
)
SUBSCRIPTION_RESPONSE=$(curl -s "${AUTH_FLAGS[@]}" \
-H "Content-Type: application/yang-data+json" \
-H "Accept: application/yang-data+json" \
-X POST \
"$RESTCONF_URL/restconf/operations/ietf-subscribed-notifications:establish-subscription" \
-d "$SUBSCRIPTION_REQUEST")
# Parse response (using jq if available, otherwise grep/sed)
if command -v jq &> /dev/null; then
SUBSCRIPTION_ID=$(echo "$SUBSCRIPTION_RESPONSE" | jq -r '."ietf-subscribed-notifications:output".id')
SUBSCRIPTION_URI=$(echo "$SUBSCRIPTION_RESPONSE" | jq -r '."ietf-subscribed-notifications:output"."ietf-restconf-subscribed-notifications:uri"')
else
# Fallback parsing without jq
SUBSCRIPTION_ID=$(echo "$SUBSCRIPTION_RESPONSE" | grep -o '"id":[0-9]*' | cut -d: -f2)
SUBSCRIPTION_URI=$(echo "$SUBSCRIPTION_RESPONSE" | grep -o '"/streams/subscribed/[^"]*"' | tr -d '"')
fi
if [ -z "$SUBSCRIPTION_ID" ] || [ -z "$SUBSCRIPTION_URI" ]; then
echo "Error: Failed to establish subscription" >&2
echo "Response: $SUBSCRIPTION_RESPONSE" >&2
exit 1
fi
echo "✓ Subscription established (ID: $SUBSCRIPTION_ID)" >&2
echo " Stream URI: $SUBSCRIPTION_URI" >&2
echo >&2
# Step 2: Connect to notification stream
echo "=== Listening for notifications (Ctrl+C to stop) ===" >&2
echo >&2
curl "${AUTH_FLAGS[@]}" \
-H "Accept: text/event-stream" \
-N \
"$RESTCONF_URL$SUBSCRIPTION_URI" | while IFS= read -r line; do
# SSE format: lines starting with "data:" contain the actual notification
if [[ $line == data:* ]]; then
# Extract JSON data after "data:" prefix
notification="${line#data:}"
# Pretty-print with jq if available
if command -v jq &> /dev/null; then
echo "$notification" | jq '.'
else
echo "$notification"
fi
echo "----------------------------------------"
fi
done