mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 09:13:01 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68d98f6e78 |
@@ -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
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Executable
+290
@@ -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()
|
||||
Executable
+91
@@ -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
|
||||
Reference in New Issue
Block a user