diff --git a/board/common/rootfs/etc/nginx/restconf.app b/board/common/rootfs/etc/nginx/restconf.app index 7c8cadda..fc076cf8 100644 --- a/board/common/rootfs/etc/nginx/restconf.app +++ b/board/common/rootfs/etc/nginx/restconf.app @@ -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; diff --git a/restconf.sh b/restconf.sh new file mode 100755 index 00000000..92a15c68 --- /dev/null +++ b/restconf.sh @@ -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" diff --git a/src/confd/src/system.c b/src/confd/src/system.c index 05ddc3ea..76790fbc 100644 --- a/src/confd/src/system.c +++ b/src/confd/src/system.c @@ -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, ¬if); + 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; } diff --git a/subscribe_ietf_system.py b/subscribe_ietf_system.py new file mode 100755 index 00000000..f1894f0b --- /dev/null +++ b/subscribe_ietf_system.py @@ -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() diff --git a/subscribe_ietf_system.sh b/subscribe_ietf_system.sh new file mode 100755 index 00000000..d244bb62 --- /dev/null +++ b/subscribe_ietf_system.sh @@ -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 < /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