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
Tobias WaldekranzandGitHub 45ff5d232e Merge pull request #1322 from kernelkit/upgrade-sysrepo
Upgrade sysrepo
2025-12-17 08:37:21 +01:00
Mattias Walström 773aab6ec1 factory-config.cfg: Update all static factory-config.cfg to the new syntax in ietf-netconf 2025-12-16 18:30:03 +01:00
Mattias Walström 542fd4006a Bump sysrepo,lignetconf2, libyang and netopeer2
This update got pretty messy, since libyang had some
breaking changes. And unfortunatly rousette, libyang-cpp
and sysrepo-cpp has not made any new release yet.
2025-12-16 18:30:03 +01:00
Mattias Walström e69d53a7b5 confd: Add migrate script to migrate configuration to new YANG syntax 2025-12-16 18:30:02 +01:00
Mattias Walström b0643430ec confd: factory-config: Change syntax to follow new yang model 2025-12-16 18:30:02 +01:00
Mattias Walström e49b3c9206 confd: Adapt YANG models after libyang/netopeer2 upgrade
We previously run a draft version of a lot of YANG models,
netconf-server,ssh-server and mode. Now they are released
with minor changes.

infix-meta is an exception, here we had to remove the obsolete
marker of the node and value, since this is no longer allowed in
libyang.
2025-12-16 18:30:01 +01:00
Tobias WaldekranzandGitHub 86eaea6237 Merge pull request #1323 from kernelkit/cn913x-ecc-stat
CN913x ECC Statistics Collection
2025-12-16 16:11:02 +01:00
Mattias Walström 2dba50db76 confd: Step up version of confd
Incompatible changes in YANG models for netconf.
2025-12-16 14:28:25 +01:00
Mattias Walström 0fe9cfad46 confd: Use correct YANG modules after upgrading libnetconf2 and sysrepo 2025-12-16 14:28:25 +01:00
Mattias Walström 0c60649c62 confd: Let rousette install its own yang models in rootfs
Previously rousette did not install the yang models so we kept a copy
of them in confd. Now it is implemented in rousette, let them install
their models.
2025-12-16 14:28:24 +01:00
Mattias Walström 9224bcc819 copy: Adapt to libyang4 API 2025-12-16 14:28:24 +01:00
Mattias Walström 0c7651705b confd, statd, libsrx: Adapt to libyang4 api 2025-12-16 14:28:24 +01:00
Tobias Waldekranz 8ae018baae support: cn913x: Collect DRAM ECC status
On platforms based on CN913{0,1,2}, collect all ECC related counters
so that we know if any correctable errors have been encountered since
the most recent boot.
2025-12-16 14:26:47 +01:00
Tobias Waldekranz 15aa98b7e3 support: Exec hardware specific scripts from /etc/support.d
Allow particular boards or platforms install hardware specific scripts
in /etc/support.d, via the usual /usr/share/product logic, which are
then run by /sbin/support and included in the resulting bundle.

This lets us collect hardware specific information, e.g., SoC-specific
error counters, without /sbin/support needing to know about all the
nitty gritty details.
2025-12-16 14:26:46 +01:00
Tobias WaldekranzandGitHub 2aeeadb045 Merge pull request #1321 from kernelkit/cn9130-wdg
CN9130 Watchdog
2025-12-16 11:17:05 +01:00
Tobias Waldekranz 553d659eac test/case/hardware/watchdog: Add test
Verify that if the system encounters a hard lockup, i.e., interrupts
are no longer being serviced, then the system's watchdog will
correctly reboot it.
2025-12-16 09:34:00 +01:00
Tobias Waldekranz 3460169bc5 aarch64/styx-dcp-sc-28p-a: Fix warm reset on old rev. a boards
Rev. A has a FLASH that Linux reconfigures to 4B-addressing, but must
be in 3B-addressing when accessed by the BootROM. Since it has no
reset input, this would not work on warm resets. Linux has a
workaround for this issue for the common case of a controlled reboot,
which this change enables on those boards (which are admittedly few
and far between, but still useful as testing vehicles).

(This won't take effect until Finit is updated to at least 4.15, which
switches over from watchdog- to reboot(2)- based reboot, by default)
2025-12-16 09:33:59 +01:00
Tobias Waldekranz d720323b50 kernel: Enable test_lockup module
This will be us the ability to test a hardware watchdog's ability to
trigger on different kinds of lockups.
2025-12-16 09:33:58 +01:00
Tobias Waldekranz 8eb706b564 aarch64: kernel: Enable SBSA watchdog
Usable by all Server Base System Architecture (SBSA) compliant SoCs,
e.g., CN9130.
2025-12-16 09:33:57 +01:00
Tobias Waldekranz 0298dc3c1a test: ssh: Wait for target SSH server to launch, by default
By default, ensure that the target device accepts TCP connections on
the specified port before returning the device object. This aligns the
SSH implementation with {NET,REST}CONF.

This solves the issue where tests that use SSH commands early in their
execution would sometimes fail as the remote server had not started up
yet.
2025-12-16 09:33:51 +01:00
Mattias Walström 0ea3380349 Upgrade python-libyang to support libyang4 2025-12-16 08:06:38 +01:00
Richard AlpeandGitHub 7fcbc88105 Merge pull request #1316 from kernelkit/statd-journaling
Add operational data journal with retention policy
2025-12-15 16:55:20 +01:00
Richard Alpe cb9aeea351 statd: compress journal snapshots with gzip
Store historical snapshots as compressed .json.gz files to reduce disk
usage. The operational.json file remains uncompressed for easy access.

Signed-off-by: Richard Alpe <richard@bit42.se>
2025-12-15 11:07:45 +01:00
Joachim WibergandGitHub 8d5e9c7ea0 Merge pull request #1320 from kernelkit/kernel-upgrade-c0ebffc2-d335-4b53-be81-f87050bba030
Upgrade to kernel 6.12.62
2025-12-13 07:13:35 +01:00
ael-bot 9fb9ac41d6 Upgrade Linux kernel to 6.12.62 2025-12-13 03:59:40 +00:00
Joachim WibergandGitHub 231bf1ed96 Merge pull request #1318 from kernelkit/rusage
Kernel config changes and resource usage monitoring
2025-12-12 11:55:11 +01:00
Tobias WaldekranzandGitHub 17d99aec41 Merge pull request #1312 from kernelkit/run-vs-qemu
Update documentation: qemu.sh -> run.sh
2025-12-11 00:05:55 +01:00
Richard Alpe c767a6f9a0 statd: add operational data journal with retention policy
Dump operational datastore to timestamped JSON snapshots every 5 minutes
(in /var/lib/statd/). The operational.json symlink always points to the
latest snapshot.

Implement hierarchical retention policy that keeps the first snapshot of
each time period (hour/day/week/month/year), providing fine-grained recent
history while preventing unbounded disk usage.

This will allow us to plot / track how the system state evolves as
well as give us somewhat fine-grained info in the case of an event,
such as a crash.

Add unit test simulating months of snapshots to verify retention
behavior using a statd stub that only runs the retention code locally
(unit test)

Signed-off-by: Richard Alpe <richard@bit42.se>
2025-12-09 16:16:53 +01:00
Joachim Wiberg c2a9ee4fbc board/*/linux_defconfig: panic on lockups and hung tasks
Turn on OOPS-to-panic, soft/hard lockup panic, hung-task panic, and
extra workqueue watchdog reporting. This makes latent stalls visible
instead of silently freezing, improving diagnosis of issues like the
recent resource-pressure lockup.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-08 17:01:54 +01:00
Joachim Wiberg d155f33b7c board/common: log disk/mem/filenr resource usage every hour
Dec  8 15:22:44 ix-00-00-00 watchdogd[2599]: Memory usage: 195036 kB, cached: 69740 kB, total: 423628 kB
Dec  8 15:22:44 ix-00-00-00 watchdogd[2599]: File system /var usage: blocks 4710/52564 inodes 80/65456
Dec  8 15:22:44 ix-00-00-00 watchdogd[2599]: File descriptor usage: 640/34603

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-08 17:01:54 +01:00
Joachim Wiberg f374bc12c5 Follow-up to ec0ed82b, update documentation
[skip ci]

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-07 09:15:47 +01:00
193 changed files with 11158 additions and 2666 deletions
+8 -1
View File
@@ -387,7 +387,14 @@ CONFIG_MAGIC_SYSRQ=y
CONFIG_DEBUG_FS=y
CONFIG_PANIC_ON_OOPS=y
CONFIG_PANIC_TIMEOUT=20
CONFIG_DETECT_HUNG_TASK=y
CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=y
CONFIG_HARDLOCKUP_DETECTOR=y
CONFIG_HARDLOCKUP_DETECTOR_PREFER_BUDDY=y
CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y
CONFIG_BOOTPARAM_HUNG_TASK_PANIC=y
CONFIG_WQ_WATCHDOG=y
CONFIG_WQ_CPU_INTENSIVE_REPORT=y
CONFIG_TEST_LOCKUP=m
# CONFIG_SCHED_DEBUG is not set
# CONFIG_RCU_TRACE is not set
CONFIG_FUNCTION_TRACER=y
@@ -41,14 +41,31 @@
"ietf-ip:ipv4": {
"infix-dhcp-client:dhcp": {
"option": [
{"id": "netmask"},
{"id": "broadcast"},
{"id": "router"},
{"id": "domain"},
{"id": "hostname"},
{"id": "dns-server"},
{"id": "ntp-server"},
{"id": "vendor-class", "value": "Raspberry Pi 2 Model B"}
{
"id": "netmask"
},
{
"id": "broadcast"
},
{
"id": "router"
},
{
"id": "domain"
},
{
"id": "hostname"
},
{
"id": "dns-server"
},
{
"id": "ntp-server"
},
{
"id": "vendor-class",
"value": "Raspberry Pi 2 Model B"
}
]
}
}
@@ -122,7 +139,11 @@
"name": "default-ssh",
"ssh": {
"tcp-server-parameters": {
"local-address": "::"
"local-bind": [
{
"local-address": "::"
}
]
},
"ssh-server-parameters": {
"server-identity": {
@@ -168,7 +189,7 @@
"infix-system:motd-banner": "Li0tLS0tLS0uCnwgIC4gLiAgfCBJbmZpeCBPUyDigJQgSW1tdXRhYmxlLkZyaWVuZGx5LlNlY3VyZQp8LS4gdiAuLXwgaHR0cHM6Ly9rZXJuZWxraXQub3JnCictJy0tLSctJwo="
},
"infix-meta:meta": {
"version": "1.6"
"version": "1.7"
},
"infix-services:mdns": {
"enabled": true
@@ -102,24 +102,49 @@
"ietf-ip:ipv4": {
"infix-dhcp-client:dhcp": {
"option": [
{"id": "ntp-server"},
{"id": "broadcast"},
{"id": "domain"},
{"id": "hostname"},
{"id": "dns-server"},
{"id": "router"},
{"id": "netmask"},
{"id": "vendor-class", "value": "Banana Pi BPI-R3"}
{
"id": "ntp-server"
},
{
"id": "broadcast"
},
{
"id": "domain"
},
{
"id": "hostname"
},
{
"id": "dns-server"
},
{
"id": "router"
},
{
"id": "netmask"
},
{
"id": "vendor-class",
"value": "Banana Pi BPI-R3"
}
]
}
},
"ietf-ip:ipv6": {
"infix-dhcpv6-client:dhcp": {
"option": [
{"id": "ntp-server"},
{"id": "client-fqdn"},
{"id": "domain-search"},
{"id": "dns-server"}
{
"id": "ntp-server"
},
{
"id": "client-fqdn"
},
{
"id": "domain-search"
},
{
"id": "dns-server"
}
]
}
}
@@ -135,18 +160,18 @@
]
},
"ietf-keystore:keystore": {
"asymmetric-keys": {
"asymmetric-key": [
{
"name": "genkey",
"public-key-format": "infix-crypto-types:ssh-public-key-format",
"public-key": "",
"private-key-format": "infix-crypto-types:rsa-private-key-format",
"cleartext-private-key": "",
"certificates": {}
}
]
"asymmetric-keys": {
"asymmetric-key": [
{
"name": "genkey",
"public-key-format": "infix-crypto-types:ssh-public-key-format",
"public-key": "",
"private-key-format": "infix-crypto-types:rsa-private-key-format",
"cleartext-private-key": "",
"certificates": {}
}
]
}
},
"ietf-netconf-acm:nacm": {
"enable-nacm": true,
@@ -201,7 +226,11 @@
"name": "default-ssh",
"ssh": {
"tcp-server-parameters": {
"local-address": "::"
"local-bind": [
{
"local-address": "::"
}
]
},
"ssh-server-parameters": {
"server-identity": {
@@ -293,7 +322,7 @@
"policy": [
{
"name": "lan-to-wan",
"action": "accept",
"action": "accept",
"ingress": [
"lan"
],
@@ -305,7 +334,7 @@
]
},
"infix-meta:meta": {
"version": "1.6"
"version": "1.7"
},
"infix-services:mdns": {
"enabled": true
@@ -341,4 +370,3 @@
}
}
}
@@ -61,24 +61,49 @@
"ietf-ip:ipv4": {
"infix-dhcp-client:dhcp": {
"option": [
{"id": "ntp-server"},
{"id": "broadcast"},
{"id": "domain"},
{"id": "hostname"},
{"id": "dns-server"},
{"id": "router"},
{"id": "netmask"},
{"id": "vendor-class", "value": "NanoPi R2S"}
{
"id": "ntp-server"
},
{
"id": "broadcast"
},
{
"id": "domain"
},
{
"id": "hostname"
},
{
"id": "dns-server"
},
{
"id": "router"
},
{
"id": "netmask"
},
{
"id": "vendor-class",
"value": "NanoPi R2S"
}
]
}
},
"ietf-ip:ipv6": {
"infix-dhcpv6-client:dhcp": {
"option": [
{"id": "ntp-server"},
{"id": "client-fqdn"},
{"id": "domain-search"},
{"id": "dns-server"}
{
"id": "ntp-server"
},
{
"id": "client-fqdn"
},
{
"id": "domain-search"
},
{
"id": "dns-server"
}
]
}
}
@@ -86,18 +111,18 @@
]
},
"ietf-keystore:keystore": {
"asymmetric-keys": {
"asymmetric-key": [
{
"name": "genkey",
"public-key-format": "infix-crypto-types:ssh-public-key-format",
"public-key": "",
"private-key-format": "infix-crypto-types:rsa-private-key-format",
"cleartext-private-key": "",
"certificates": {}
}
]
"asymmetric-keys": {
"asymmetric-key": [
{
"name": "genkey",
"public-key-format": "infix-crypto-types:ssh-public-key-format",
"public-key": "",
"private-key-format": "infix-crypto-types:rsa-private-key-format",
"cleartext-private-key": "",
"certificates": {}
}
]
}
},
"ietf-netconf-acm:nacm": {
"enable-nacm": true,
@@ -152,7 +177,11 @@
"name": "default-ssh",
"ssh": {
"tcp-server-parameters": {
"local-address": "::"
"local-bind": [
{
"local-address": "::"
}
]
},
"ssh-server-parameters": {
"server-identity": {
@@ -244,7 +273,7 @@
"policy": [
{
"name": "lan-to-wan",
"action": "accept",
"action": "accept",
"ingress": [
"lan"
],
@@ -256,7 +285,7 @@
]
},
"infix-meta:meta": {
"version": "1.6"
"version": "1.7"
},
"infix-services:mdns": {
"enabled": true
+9 -1
View File
@@ -407,6 +407,7 @@ CONFIG_WATCHDOG=y
CONFIG_WATCHDOG_SYSFS=y
CONFIG_SOFT_WATCHDOG=y
CONFIG_GPIO_WATCHDOG=y
CONFIG_ARM_SBSA_WATCHDOG=y
CONFIG_ARMADA_37XX_WATCHDOG=y
CONFIG_I6300ESB_WDT=y
CONFIG_MFD_MAX77620=y
@@ -557,7 +558,14 @@ CONFIG_MAGIC_SYSRQ=y
CONFIG_DEBUG_FS=y
CONFIG_PANIC_ON_OOPS=y
CONFIG_PANIC_TIMEOUT=20
CONFIG_DETECT_HUNG_TASK=y
CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=y
CONFIG_HARDLOCKUP_DETECTOR=y
CONFIG_HARDLOCKUP_DETECTOR_PREFER_BUDDY=y
CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y
CONFIG_BOOTPARAM_HUNG_TASK_PANIC=y
CONFIG_WQ_WATCHDOG=y
CONFIG_WQ_CPU_INTENSIVE_REPORT=y
CONFIG_TEST_LOCKUP=m
# CONFIG_SCHED_DEBUG is not set
# CONFIG_RCU_TRACE is not set
CONFIG_FUNCTION_TRACER=y
+1 -1
View File
@@ -108,7 +108,7 @@ To configure WiFi as a client, first store your WiFi password in the keystore:
admin@infix:/> configure
admin@infix:/config/> edit keystore symmetric-key mywifi
admin@infix:/config/keystore/…/mywifi/> set key-format wifi-preshared-key-format
admin@infix:/config/keystore/…/mywifi/> set cleartext-key YourWiFiPassword
admin@infix:/config/keystore/…/mywifi/> set cleartext-symmetric-key YourWiFiPassword
admin@infix:/config/keystore/…/mywifi/> leave
```
@@ -41,14 +41,31 @@
"ietf-ip:ipv4": {
"infix-dhcp-client:dhcp": {
"option": [
{"id": "netmask"},
{"id": "broadcast"},
{"id": "router"},
{"id": "domain"},
{"id": "hostname"},
{"id": "dns-server"},
{"id": "ntp-server"},
{"id": "vendor-class", "value": "Raspberry Pi 4 Model B"}
{
"id": "netmask"
},
{
"id": "broadcast"
},
{
"id": "router"
},
{
"id": "domain"
},
{
"id": "hostname"
},
{
"id": "dns-server"
},
{
"id": "ntp-server"
},
{
"id": "vendor-class",
"value": "Raspberry Pi 4 Model B"
}
]
}
}
@@ -126,7 +143,11 @@
"name": "default-ssh",
"ssh": {
"tcp-server-parameters": {
"local-address": "::"
"local-bind": [
{
"local-address": "::"
}
]
},
"ssh-server-parameters": {
"server-identity": {
@@ -172,7 +193,7 @@
"infix-system:motd-banner": "Li0tLS0tLS0uCnwgIC4gLiAgfCBJbmZpeCBPUyDigJQgSW1tdXRhYmxlLkZyaWVuZGx5LlNlY3VyZQp8LS4gdiAuLXwgaHR0cHM6Ly9rZXJuZWxraXQub3JnCictJy0tLSctJwo="
},
"infix-meta:meta": {
"version": "1.6"
"version": "1.7"
},
"infix-services:mdns": {
"enabled": true
@@ -0,0 +1,26 @@
#!/bin/sh
set -e
ecc_stat()
{
local chan=
local base=
for chan in 0 1; do
base=$((0xf0020360 + 0x200 * chan))
echo "DRAM Channel $chan ECC Status"
echo -n " Log config: "; devmem $((base + 0x0)) 32
echo -n " 1b errors: "; devmem $((base + 0x4)) 32
echo -n " Info 0: "; devmem $((base + 0x8)) 32
echo -n " Info 1: "; devmem $((base + 0xc)) 32
echo
done
}
[ -n "$1" ] || { echo "usage: $0 OUT-DIR"; exit 1; }
work="$1"/marvell-cn913x
mkdir -p "${work}"
ecc_stat >"${work}"/ecc-stat
@@ -59,3 +59,9 @@
XSWP(a, "e13", 13, &sfp0);
};
};
&cp0_spi1 {
spi-flash@0 {
broken-flash-reset;
};
};
+3 -3
View File
@@ -34,7 +34,7 @@ Depending on how your Linux installation is set up, the following may
require being run with superuser privileges, i.e., you may need to
repend the command with 'sudo'.
$ ./qemu.sh
$ ./qemu/run.sh
You should now see the Infix init system booting up. When the final
"Please press Enter to activate this console." is shown, press Enter
@@ -75,13 +75,13 @@ interface, which requires the following extra package:
We can now enter the configuration:
$ ./qemu.sh -c
$ ./qemu/run.sh -c
Go down to *Networking*, select *TAP*, now you can change the *Number of
TAPs*, e.g. to 10. Exit and save the configuration, then you can start
Qemu again:
./qemu.sh
$ ./qemu/run.sh
> Make sure to do a factory reset from the CLI, otherwise you will be
> stuck with that single interface from before.
+3 -3
View File
@@ -10,12 +10,12 @@
#
# and then call this script with:
#
# ./qemu.sh -c
# ./run.sh -c
#
# To bring up a menuconfig dialog. Select `Exit` and save the changes.
# For more help, see:_
#
# ./qemu.sh -h
# ./run.sh -h
#
# shellcheck disable=SC3037
@@ -40,7 +40,7 @@ usage()
echo " Also, qemu.cfg has QEMU_APPEND which can affect this."
echo
echo "Example:"
echo " qemu.sh -- finit.debug"
echo " $prognm -- finit.debug"
echo "___________________________________________________________________"
echo "Note: 'kconfig-frontends' package (Debian/Ubuntu) must be installed"
echo " for -c to work: sudo apt install kconfig-frontents"
+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;
+33 -24
View File
@@ -90,9 +90,9 @@ reset-reason {
# Monitors file descriptor leaks based on /proc/sys/fs/file-nr
filenr {
# enabled = true
interval = 300
logmark = false
enabled = true
interval = 3600
logmark = true
warning = 0.9
critical = 1.0
# script = "/path/to/alt-reboot-action.sh"
@@ -102,33 +102,42 @@ filenr {
# The script is called with fsmon as the first argument and there
# are two environment variables FSMON_NAME, for the monitored path,
# and FSMON_TYPE indicating either 'blocks' or 'inodes'.
#fsmon /var {
# enabled = true
# interval = 300
# logmark = false
# warning = 0.95
# critical = 1.0
# script = "/path/to/alt-reboot-action.sh"
#}
# Monitors load average based on sysinfo() from /proc/loadavg
# The level is composed from the average of the 1 and 5 min marks.
loadavg {
# enabled = true
interval = 300
logmark = false
warning = 1.0
critical = 2.0
fsmon /var {
enabled = true
interval = 3600
logmark = true
warning = 0.95
critical = 1.0
# script = "/path/to/alt-reboot-action.sh"
}
fsmon /tmp {
enabled = true
interval = 3600
logmark = true
warning = 0.95
critical = 1.0
# script = "/path/to/alt-reboot-action.sh"
}
# Monitors load average based on sysinfo() from /proc/loadavg
# The level is composed from the average of the 1 and 5 min marks.
#loadavg {
# enabled = true
# interval = 300
# logmark = true
# warning = 1.0
# critical = 2.0
# script = "/path/to/alt-reboot-action.sh"
#}
# Monitors free RAM based on data from /proc/meminfo
meminfo {
# enabled = true
interval = 300
logmark = false
enabled = true
interval = 3600
logmark = true
warning = 0.9
critical = 0.95
critical = 0.97
# script = "/path/to/alt-reboot-action.sh"
}
+9 -1
View File
@@ -472,8 +472,16 @@ CONFIG_DEBUG_FS=y
# CONFIG_SLUB_DEBUG is not set
CONFIG_DEBUG_RODATA_TEST=y
CONFIG_DEBUG_WX=y
CONFIG_SOFTLOCKUP_DETECTOR=y
CONFIG_PANIC_ON_OOPS=y
CONFIG_PANIC_TIMEOUT=20
CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=y
CONFIG_HARDLOCKUP_DETECTOR=y
CONFIG_HARDLOCKUP_DETECTOR_PREFER_BUDDY=y
CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y
CONFIG_BOOTPARAM_HUNG_TASK_PANIC=y
CONFIG_WQ_WATCHDOG=y
CONFIG_WQ_CPU_INTENSIVE_REPORT=y
CONFIG_TEST_LOCKUP=m
# CONFIG_SCHED_DEBUG is not set
CONFIG_STACKTRACE=y
CONFIG_RCU_CPU_STALL_TIMEOUT=60
+7 -1
View File
@@ -263,7 +263,13 @@ CONFIG_MAGIC_SYSRQ=y
CONFIG_DEBUG_FS=y
CONFIG_PANIC_ON_OOPS=y
CONFIG_PANIC_TIMEOUT=20
CONFIG_DETECT_HUNG_TASK=y
CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=y
CONFIG_HARDLOCKUP_DETECTOR=y
CONFIG_HARDLOCKUP_DETECTOR_PREFER_BUDDY=y
CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y
CONFIG_BOOTPARAM_HUNG_TASK_PANIC=y
CONFIG_WQ_WATCHDOG=y
CONFIG_WQ_CPU_INTENSIVE_REPORT=y
CONFIG_TEST_LOCKUP=m
CONFIG_FUNCTION_TRACER=y
CONFIG_UNWINDER_FRAME_POINTER=y
+6
View File
@@ -95,6 +95,12 @@ BR2_PACKAGE_RAUC_JSON=y
BR2_PACKAGE_SYSKLOGD=y
BR2_PACKAGE_SYSKLOGD_LOGGER=y
BR2_PACKAGE_WATCHDOGD=y
BR2_PACKAGE_WATCHDOGD_GENERIC=y
BR2_PACKAGE_WATCHDOGD_LOADAVG=y
BR2_PACKAGE_WATCHDOGD_FILENR=y
BR2_PACKAGE_WATCHDOGD_MEMINFO=y
BR2_PACKAGE_WATCHDOGD_FSMON=y
BR2_PACKAGE_WATCHDOGD_TEMPMON
BR2_PACKAGE_LESS=y
BR2_PACKAGE_MG=y
BR2_PACKAGE_NANO=y
+6
View File
@@ -95,6 +95,12 @@ BR2_PACKAGE_RAUC_JSON=y
BR2_PACKAGE_SYSKLOGD=y
BR2_PACKAGE_SYSKLOGD_LOGGER=y
BR2_PACKAGE_WATCHDOGD=y
BR2_PACKAGE_WATCHDOGD_GENERIC=y
BR2_PACKAGE_WATCHDOGD_LOADAVG=y
BR2_PACKAGE_WATCHDOGD_FILENR=y
BR2_PACKAGE_WATCHDOGD_MEMINFO=y
BR2_PACKAGE_WATCHDOGD_FSMON=y
BR2_PACKAGE_WATCHDOGD_TEMPMON
BR2_PACKAGE_LESS=y
BR2_PACKAGE_MG=y
BR2_PACKAGE_NANO=y
+7 -1
View File
@@ -27,7 +27,7 @@ BR2_ROOTFS_OVERLAY="${BR2_EXTERNAL_INFIX_PATH}/board/common/rootfs ${BR2_EXTERNA
BR2_ROOTFS_POST_BUILD_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-build.sh"
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.61"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.62"
BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/aarch64/linux_defconfig"
BR2_LINUX_KERNEL_INSTALL_TARGET=y
@@ -119,6 +119,12 @@ BR2_PACKAGE_RAUC_JSON=y
BR2_PACKAGE_SYSKLOGD=y
BR2_PACKAGE_SYSKLOGD_LOGGER=y
BR2_PACKAGE_WATCHDOGD=y
BR2_PACKAGE_WATCHDOGD_GENERIC=y
BR2_PACKAGE_WATCHDOGD_LOADAVG=y
BR2_PACKAGE_WATCHDOGD_FILENR=y
BR2_PACKAGE_WATCHDOGD_MEMINFO=y
BR2_PACKAGE_WATCHDOGD_FSMON=y
BR2_PACKAGE_WATCHDOGD_TEMPMON
BR2_PACKAGE_LESS=y
BR2_PACKAGE_MG=y
BR2_PACKAGE_NANO=y
+7 -1
View File
@@ -26,7 +26,7 @@ BR2_ROOTFS_OVERLAY="${BR2_EXTERNAL_INFIX_PATH}/board/common/rootfs ${BR2_EXTERNA
BR2_ROOTFS_POST_BUILD_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-build.sh"
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.61"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.62"
BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/aarch64/linux_defconfig"
BR2_LINUX_KERNEL_INSTALL_TARGET=y
@@ -97,6 +97,12 @@ BR2_PACKAGE_RAUC_JSON=y
BR2_PACKAGE_SYSKLOGD=y
BR2_PACKAGE_SYSKLOGD_LOGGER=y
BR2_PACKAGE_WATCHDOGD=y
BR2_PACKAGE_WATCHDOGD_GENERIC=y
BR2_PACKAGE_WATCHDOGD_LOADAVG=y
BR2_PACKAGE_WATCHDOGD_FILENR=y
BR2_PACKAGE_WATCHDOGD_MEMINFO=y
BR2_PACKAGE_WATCHDOGD_FSMON=y
BR2_PACKAGE_WATCHDOGD_TEMPMON
BR2_PACKAGE_LESS=y
BR2_PACKAGE_MG=y
BR2_PACKAGE_NANO=y
+6
View File
@@ -131,6 +131,12 @@ BR2_PACKAGE_RAUC_JSON=y
BR2_PACKAGE_SYSKLOGD=y
BR2_PACKAGE_SYSKLOGD_LOGGER=y
BR2_PACKAGE_WATCHDOGD=y
BR2_PACKAGE_WATCHDOGD_GENERIC=y
BR2_PACKAGE_WATCHDOGD_LOADAVG=y
BR2_PACKAGE_WATCHDOGD_FILENR=y
BR2_PACKAGE_WATCHDOGD_MEMINFO=y
BR2_PACKAGE_WATCHDOGD_FSMON=y
BR2_PACKAGE_WATCHDOGD_TEMPMON
BR2_PACKAGE_LESS=y
BR2_PACKAGE_MG=y
BR2_PACKAGE_NANO=y
+7 -1
View File
@@ -26,7 +26,7 @@ BR2_ROOTFS_OVERLAY="${BR2_EXTERNAL_INFIX_PATH}/board/common/rootfs ${BR2_EXTERNA
BR2_ROOTFS_POST_BUILD_SCRIPT="board/qemu/x86_64/post-build.sh ${BR2_EXTERNAL_INFIX_PATH}/board/common/post-build.sh"
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.61"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.62"
BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/x86_64/linux_defconfig"
BR2_LINUX_KERNEL_INSTALL_TARGET=y
@@ -115,6 +115,12 @@ BR2_PACKAGE_RAUC_JSON=y
BR2_PACKAGE_SYSKLOGD=y
BR2_PACKAGE_SYSKLOGD_LOGGER=y
BR2_PACKAGE_WATCHDOGD=y
BR2_PACKAGE_WATCHDOGD_GENERIC=y
BR2_PACKAGE_WATCHDOGD_LOADAVG=y
BR2_PACKAGE_WATCHDOGD_FILENR=y
BR2_PACKAGE_WATCHDOGD_MEMINFO=y
BR2_PACKAGE_WATCHDOGD_FSMON=y
BR2_PACKAGE_WATCHDOGD_TEMPMON
BR2_PACKAGE_LESS=y
BR2_PACKAGE_MG=y
BR2_PACKAGE_NANO=y
+7 -1
View File
@@ -26,7 +26,7 @@ BR2_ROOTFS_OVERLAY="${BR2_EXTERNAL_INFIX_PATH}/board/common/rootfs ${BR2_EXTERNA
BR2_ROOTFS_POST_BUILD_SCRIPT="board/qemu/x86_64/post-build.sh ${BR2_EXTERNAL_INFIX_PATH}/board/common/post-build.sh"
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.61"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.62"
BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/x86_64/linux_defconfig"
BR2_LINUX_KERNEL_INSTALL_TARGET=y
@@ -93,6 +93,12 @@ BR2_PACKAGE_RAUC_JSON=y
BR2_PACKAGE_SYSKLOGD=y
BR2_PACKAGE_SYSKLOGD_LOGGER=y
BR2_PACKAGE_WATCHDOGD=y
BR2_PACKAGE_WATCHDOGD_GENERIC=y
BR2_PACKAGE_WATCHDOGD_LOADAVG=y
BR2_PACKAGE_WATCHDOGD_FILENR=y
BR2_PACKAGE_WATCHDOGD_MEMINFO=y
BR2_PACKAGE_WATCHDOGD_FSMON=y
BR2_PACKAGE_WATCHDOGD_TEMPMON=y
BR2_PACKAGE_LESS=y
BR2_PACKAGE_MG=y
BR2_PACKAGE_NANO=y
+6 -1
View File
@@ -8,7 +8,10 @@ All notable changes to the project are documented in this file.
### Changes
- Upgrade Linux kernel to 6.12.61 (LTS)
- Upgrade Linux kernel to 6.12.62 (LTS)
- Upgrade libyang to 4.2.2
- Upgrade sysrepo to 4.2.10
- Upgrade netopeer2 (NETCONF) to 2.7.0
- Add RIPv2 routing support, issue #582
- Add support for configurable OSPF debug logging, issue #1281. Debug options
can now be enabled per category (bfd, packet, ism, nsm, default-information,
@@ -16,6 +19,8 @@ All notable changes to the project are documented in this file.
production environments. See the documentation for usage examples
- Add support for "routing interfaces", issue #647. Lists interfaces with IP
forwarding. Inspect from CLI using `show interface`, look for `⇅` flag
- Add operational data journal to statd with hierarchical time-based retention
policy, keeping snapshots from every 5 minutes (recent) to yearly (historical)
### Fixes
+4 -4
View File
@@ -18,11 +18,11 @@ QEMU
> relevant Qemu packages are pulled in as well. This trick, installing
> [virt-manager][virt], helps set up Qemu networking on your system.
A virtualized Infix x86_64 instance can easily be launched from a Linux
system, with [Qemu][] installed, by issuing:
A virtualized Infix x86_64 instance can be launched from a Linux system,
with [Qemu][] installed, by issuing:
```
$ ./qemu.sh
$ ./qemu/run.sh
...
```
@@ -35,7 +35,7 @@ $ make run
```
To change settings, e.g. networking, <kbd>make run-menuconfig</kbd>, or
from a pre-built Infix release tarball, using <kbd>./qemu.sh -c</kbd>
from a pre-built Infix release tarball, using <kbd>./qemu/run.sh -c</kbd>
The Infix test suite is built around Qemu and [Qeneth][qeth], see:
+1 -1
View File
@@ -84,7 +84,7 @@ and 63 characters
admin@example:/> configure
admin@example:/config/> edit keystore symmetric-key example
admin@example:/config/keystore/…/example/> set key-format wifi-preshared-key-format
admin@example:/config/keystore/…/example/> set cleartext-key mysecret
admin@example:/config/keystore/…/example/> set cleartext-symmetric-key mysecret
admin@example:/config/keystore/…/example/> leave
admin@example:/>
```
+2 -2
View File
@@ -4,13 +4,13 @@
#
################################################################################
CONFD_VERSION = 1.6
CONFD_VERSION = 1.7
CONFD_SITE_METHOD = local
CONFD_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/confd
CONFD_LICENSE = BSD-3-Clause
CONFD_LICENSE_FILES = LICENSE
CONFD_REDISTRIBUTE = NO
CONFD_DEPENDENCIES = host-sysrepo sysrepo netopeer2 jansson libite sysrepo libsrx libglib2
CONFD_DEPENDENCIES = host-sysrepo sysrepo rousette netopeer2 jansson libite sysrepo libsrx libglib2
CONFD_AUTORECONF = YES
CONFD_CONF_OPTS += --disable-silent-rules --with-crypt=$(BR2_PACKAGE_CONFD_DEFAULT_CRYPT)
CONFD_SYSREPO_SHM_PREFIX = sr_buildroot$(subst /,_,$(CONFIG_DIR))_confd
@@ -1,3 +1,3 @@
# Locally calculated
sha256 9d9d33b873917ca5d0bdcc47a36d2fd385971ab0c045d1472fcadf95ee5bcf5b LICENCE
sha256 cfe1064849a158f3eb9e82f12a709723115fc9bc4d30056e0e96ea0ba03672a2 klish-plugin-sysrepo-4e86a836c9b09a14c9625ea3d3b56b6578be4833-git4.tar.gz
sha256 71802708c96521a8bf43e38d6e5ac36de5481e087eaabdbebbc282ec0e29b8d3 klish-plugin-sysrepo-0846fa6b8d5d665eebfc7b1664542646915a79f7-git4.tar.gz
@@ -4,7 +4,7 @@
#
################################################################################
KLISH_PLUGIN_SYSREPO_VERSION = 4e86a836c9b09a14c9625ea3d3b56b6578be4833
KLISH_PLUGIN_SYSREPO_VERSION = 0846fa6b8d5d665eebfc7b1664542646915a79f7
KLISH_PLUGIN_SYSREPO_SITE = https://github.com/kernelkit/klish-plugin-sysrepo.git
#KLISH_PLUGIN_SYSREPO_VERSION = cdd3eb51a7f7ee0ed5bd925fa636061d3b1b85fb
#KLISH_PLUGIN_SYSREPO_SITE = https://src.libcode.org/pkun/klish-plugin-sysrepo.git
@@ -1,7 +1,7 @@
From 22b41e7ed8268b94ccc139138e2037c390a3a616 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= <bedrich.schindler@gmail.com>
Date: Thu, 10 Jul 2025 15:00:45 +0200
Subject: [PATCH] Implement `SchemaNode::actionRpcs()`
Subject: [PATCH 1/7] Implement `SchemaNode::actionRpcs()`
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -0,0 +1,38 @@
From 1f8e502cf0308faa0336b1077308184de6a83ee0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Mon, 20 Oct 2025 11:45:06 +0200
Subject: [PATCH 2/7] CI: pin to libyang v3
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: Idba085ad9c9c137874801f1d4089f8ee6bb390c7
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
.zuul.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.zuul.yaml b/.zuul.yaml
index 7b92766..fd89549 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -4,13 +4,13 @@
- f38-gcc-cover:
required-projects:
- name: github/CESNET/libyang
- override-checkout: devel
+ override-checkout: cesnet/2025-08-07
- name: github/doctest/doctest
override-checkout: v2.3.6
- f38-clang-asan-ubsan:
required-projects: &projects
- name: github/CESNET/libyang
- override-checkout: devel
+ override-checkout: cesnet/2025-08-07
- name: github/doctest/doctest
override-checkout: v2.4.11
- f38-clang-tsan:
--
2.43.0
@@ -0,0 +1,216 @@
From b27a65d5d5f6ffffbd2c26c1b5bea57a6de584d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Mon, 20 Oct 2025 18:46:38 +0200
Subject: [PATCH 3/7] wrap lyd_validate_op
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
This patch wraps lyd_validate_op from libyang. Downstream users can now
validate operations (RPC input, replies, and notifications).
Change-Id: Ib03070bbc3e1d0dccd6bf38eda82e944db1093b1
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
include/libyang-cpp/DataNode.hpp | 2 +
src/DataNode.cpp | 24 +++++++++++
tests/context.cpp | 2 +-
tests/data_node.cpp | 72 ++++++++++++++++++++++++++++++++
tests/example_schema.hpp | 22 ++++++++++
5 files changed, 121 insertions(+), 1 deletion(-)
diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp
index 50d6c0e..e202a96 100644
--- a/include/libyang-cpp/DataNode.hpp
+++ b/include/libyang-cpp/DataNode.hpp
@@ -59,6 +59,7 @@ template <typename Operation, typename Siblings>
void handleLyTreeOperation(DataNode* affectedNode, Operation operation, Siblings siblings, std::shared_ptr<internal_refcount> newRefs);
LIBYANG_CPP_EXPORT void validateAll(std::optional<libyang::DataNode>& node, const std::optional<ValidationOptions>& opts = std::nullopt);
+LIBYANG_CPP_EXPORT void validateOp(libyang::DataNode& input, const std::optional<libyang::DataNode>& opsTree, OperationType opType);
LIBYANG_CPP_EXPORT Set<DataNode> findXPathAt(
const std::optional<libyang::DataNode>& contextNode,
@@ -147,6 +148,7 @@ public:
friend LIBYANG_CPP_EXPORT lyd_node* getRawNode(DataNode node);
friend LIBYANG_CPP_EXPORT void validateAll(std::optional<libyang::DataNode>& node, const std::optional<ValidationOptions>& opts);
+ friend LIBYANG_CPP_EXPORT void validateOp(libyang::DataNode& input, const std::optional<libyang::DataNode>& opsTree, OperationType opType);
friend LIBYANG_CPP_EXPORT Set<DataNode> findXPathAt(const std::optional<libyang::DataNode>& contextNode, const libyang::DataNode& forest, const std::string& xpath);
bool operator==(const DataNode& node) const;
diff --git a/src/DataNode.cpp b/src/DataNode.cpp
index 7e87917..e28ef30 100644
--- a/src/DataNode.cpp
+++ b/src/DataNode.cpp
@@ -1226,6 +1226,30 @@ void validateAll(std::optional<libyang::DataNode>& node, const std::optional<Val
}
}
+/** @brief Validate op after parsing with lyd_parse_op.
+ *
+ * Wraps `lyd_validate_op`.
+ *
+ * @param input The tree with the op to validate.
+ * @param opsTree The optional data tree to validate the input against.
+ * @param opType The operation type. Contrary to `lyd_validate_op`, we accept not only YANG but also NETCONF and RESTCONF operation types (and internally convert them to YANG).
+ */
+void validateOp(libyang::DataNode& input, const std::optional<libyang::DataNode>& opsTree, OperationType opType)
+{
+ if (opType == OperationType::RpcYang || opType == OperationType::RpcRestconf || opType == OperationType::RpcNetconf) {
+ opType = OperationType::RpcYang;
+ } else if (opType == OperationType::ReplyYang || opType == OperationType::ReplyRestconf || opType == OperationType::ReplyNetconf) {
+ opType = OperationType::ReplyYang;
+ } else if (opType == OperationType::NotificationYang || opType == OperationType::NotificationRestconf || opType == OperationType::NotificationNetconf) {
+ opType = OperationType::NotificationYang;
+ } else {
+ throw Error("validateOp: DataYang datatype is not supported");
+ }
+
+ auto ret = lyd_validate_op(input.m_node, opsTree ? opsTree->m_node : nullptr, utils::toOpType(opType), nullptr);
+ throwIfError(ret, "libyang:validateOp: lyd_validate_op failed");
+}
+
/** @short Find instances matching the provided XPath
*
* @param contextNode The node which serves as the "context node" for XPath evaluation. Use nullopt to start at root.
diff --git a/tests/context.cpp b/tests/context.cpp
index 6c5dde8..c0b7e09 100644
--- a/tests/context.cpp
+++ b/tests/context.cpp
@@ -154,7 +154,7 @@ TEST_CASE("context")
{
auto mod = ctx->parseModule(example_schema, libyang::SchemaFormat::YANG);
auto rpcs = mod.actionRpcs();
- REQUIRE(rpcs.size() == 1);
+ REQUIRE(rpcs.size() == 2);
REQUIRE(rpcs[0].module().name() == "example-schema");
REQUIRE(rpcs[0].name() == "myRpc");
diff --git a/tests/data_node.cpp b/tests/data_node.cpp
index db5a28e..9215b12 100644
--- a/tests/data_node.cpp
+++ b/tests/data_node.cpp
@@ -2430,6 +2430,78 @@ TEST_CASE("Data Node manipulation")
"Can't parse into operation data tree: LY_EVALID", libyang::Error);
}
}
+
+ DOCTEST_SUBCASE("Validation")
+ {
+ DOCTEST_SUBCASE("Valid input")
+ {
+ std::string rpcInput;
+ std::string rpcPath;
+ std::string expected;
+ std::optional<libyang::DataNode> depTree;
+
+ DOCTEST_SUBCASE("RPC")
+ {
+ rpcInput = R"({"example-schema:input": { "number": 42 } })";
+ rpcPath = "/example-schema:rpc-with-choice";
+ expected = R"({
+ "example-schema:rpc-with-choice": {
+ "number": 42
+ }
+}
+)";
+ }
+
+ DOCTEST_SUBCASE("Action")
+ {
+ rpcInput = R"({ "example-schema:input": { "friend": "Kuba" } })";
+ rpcPath = "/example-schema:person[name='Franta']/poke-a-friend";
+ expected = R"({
+ "example-schema:poke-a-friend": {
+ "friend": "Kuba"
+ }
+}
+)";
+ depTree = ctx.newPath("/example-schema:person[name='Kuba']");
+ }
+
+ auto [parent, rpcTree] = ctx.newPath2(rpcPath);
+ auto rpcOp = rpcTree->parseOp(rpcInput, dataTypeFor(rpcInput), libyang::OperationType::RpcRestconf);
+
+ REQUIRE(!rpcOp.op);
+ REQUIRE(rpcOp.tree);
+
+ libyang::validateOp(*rpcTree, depTree, libyang::OperationType::RpcRestconf);
+ REQUIRE(*rpcTree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == expected);
+ }
+
+ DOCTEST_SUBCASE("Nodes in disjunctive cases defined together")
+ {
+ auto rpcInput = R"({ "example-schema:input": { "number": 42, "text": "The ultimate answer" } })";
+
+ auto rpcTree = ctx.newPath("/example-schema:rpc-with-choice");
+ auto rpcOp = rpcTree.parseOp(rpcInput, dataTypeFor(rpcInput), libyang::OperationType::RpcRestconf);
+
+ REQUIRE(!rpcOp.op);
+ REQUIRE(rpcOp.tree);
+
+ REQUIRE_THROWS_WITH_AS(libyang::validateOp(rpcTree, std::nullopt, libyang::OperationType::RpcRestconf), "libyang:validateOp: lyd_validate_op failed: LY_EVALID", libyang::Error);
+ }
+
+ DOCTEST_SUBCASE("Action without the leafref node")
+ {
+ auto rpcInput = R"({ "example-schema:input": { "friend": "Kuba" } })";
+ auto rpcPath = "/example-schema:person[name='Franta']/poke-a-friend";
+
+ auto [parent, rpcTree] = ctx.newPath2(rpcPath);
+ auto rpcOp = rpcTree->parseOp(rpcInput, dataTypeFor(rpcInput), libyang::OperationType::RpcRestconf);
+
+ REQUIRE(!rpcOp.op);
+ REQUIRE(rpcOp.tree);
+
+ REQUIRE_THROWS_WITH_AS(libyang::validateOp(*rpcTree, std::nullopt, libyang::OperationType::RpcRestconf), "libyang:validateOp: lyd_validate_op failed: LY_EVALID", libyang::Error);
+ }
+ }
}
DOCTEST_SUBCASE("comparing") {
diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp
index 02d3d74..acc6ecc 100644
--- a/tests/example_schema.hpp
+++ b/tests/example_schema.hpp
@@ -108,6 +108,16 @@ module example-schema {
}
action poke { }
+
+ action poke-a-friend {
+ input {
+ leaf friend {
+ type leafref {
+ path '../../../person/name';
+ }
+ }
+ }
+ }
}
leaf bossPerson {
@@ -284,6 +294,18 @@ module example-schema {
}
}
}
+ rpc rpc-with-choice {
+ input {
+ choice the-impossible-choice {
+ leaf text {
+ type string;
+ }
+ leaf number {
+ type int32;
+ }
+ }
+ }
+ }
anydata myData {
}
--
2.43.0
@@ -0,0 +1,435 @@
From 4c00c24dee6bbc694d62e7174b7ac14f8aa95f14 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 30 Oct 2025 21:16:13 +0100
Subject: [PATCH 4/7] port to libyang v4
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Apart from the usual API/ABI changes, the default values of a `leaf` or
a `leaf-list` are no longer canonicalized.
This used to be missing from libyang v4, but it was added recently.
Sill, chances are that the form which was used in the YANG model's
source code is actually *the* intended presentation, so let's skip
canonicalization altogether.
Upstream has removed some flags and defines, so let's sync their list
with whatever is currently available in the `devel` branch.
Change-Id: I52dcec88eeee003b45dd6cf400f4d6875abbcc05
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
.zuul.yaml | 4 ++--
CMakeLists.txt | 4 ++--
README.md | 2 +-
include/libyang-cpp/Context.hpp | 6 +++++-
include/libyang-cpp/DataNode.hpp | 5 ++++-
include/libyang-cpp/Enum.hpp | 15 +++++++++++++--
src/Context.cpp | 12 ++++++++++--
src/DataNode.cpp | 11 +++++++++--
src/SchemaNode.cpp | 6 +++---
src/Type.cpp | 2 +-
src/utils/enum.hpp | 21 +++++++++++++++++++--
tests/context.cpp | 32 +++++++++++++++++---------------
tests/data_node.cpp | 2 +-
tests/schema_node.cpp | 4 ++--
14 files changed, 89 insertions(+), 37 deletions(-)
diff --git a/.zuul.yaml b/.zuul.yaml
index fd89549..7b92766 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -4,13 +4,13 @@
- f38-gcc-cover:
required-projects:
- name: github/CESNET/libyang
- override-checkout: cesnet/2025-08-07
+ override-checkout: devel
- name: github/doctest/doctest
override-checkout: v2.3.6
- f38-clang-asan-ubsan:
required-projects: &projects
- name: github/CESNET/libyang
- override-checkout: cesnet/2025-08-07
+ override-checkout: devel
- name: github/doctest/doctest
override-checkout: v2.4.11
- f38-clang-tsan:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c6db320..90f9aaa 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -20,7 +20,7 @@ add_custom_target(libyang-cpp-version-cmake
cmake/ProjectGitVersionRunner.cmake
)
include(cmake/ProjectGitVersion.cmake)
-set(LIBYANG_CPP_PKG_VERSION "4")
+set(LIBYANG_CPP_PKG_VERSION "5")
prepare_git_version(LIBYANG_CPP_VERSION ${LIBYANG_CPP_PKG_VERSION})
find_package(Doxygen)
@@ -28,7 +28,7 @@ option(WITH_DOCS "Create and install internal documentation (needs Doxygen)" ${D
option(BUILD_SHARED_LIBS "By default, shared libs are enabled. Turn off for a static build." ON)
find_package(PkgConfig REQUIRED)
-pkg_check_modules(LIBYANG REQUIRED libyang>=3.10.1 IMPORTED_TARGET)
+pkg_check_modules(LIBYANG REQUIRED libyang>=4.1.0 IMPORTED_TARGET)
# FIXME from gcc 14.1 on we should be able to use the calendar/time from libstdc++ and thus remove the date dependency
find_package(date)
diff --git a/README.md b/README.md
index 8291e00..0cef2ff 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
Object lifetimes are managed automatically via RAII.
## Dependencies
-- [libyang v3](https://github.com/CESNET/libyang) - the `devel` branch (even for the `master` branch of *libyang-cpp*)
+- [libyang v4](https://github.com/CESNET/libyang) - the `devel` branch (even for the `master` branch of *libyang-cpp*)
- C++20 compiler (e.g., GCC 10.x+, clang 10+)
- CMake 3.19+
- optionally for built-in tests, [Doctest](https://github.com/doctest/doctest/) as a C++ unit test framework
diff --git a/include/libyang-cpp/Context.hpp b/include/libyang-cpp/Context.hpp
index ca89063..e695914 100644
--- a/include/libyang-cpp/Context.hpp
+++ b/include/libyang-cpp/Context.hpp
@@ -108,7 +108,11 @@ public:
std::optional<SubmoduleParsed> getSubmodule(const std::string& name, const std::optional<std::string>& revision) const;
void registerModuleCallback(std::function<ModuleCallback> callback);
- ParsedOp parseOp(const std::string& input, const DataFormat format, const OperationType opType) const;
+ ParsedOp parseOp(
+ const std::string& input,
+ const DataFormat format,
+ const OperationType opType,
+ const std::optional<ParseOptions> parseOpts = std::nullopt) const;
DataNode newPath(const std::string& path, const std::optional<std::string>& value = std::nullopt, const std::optional<CreationOptions> options = std::nullopt) const;
CreatedNodes newPath2(const std::string& path, const std::optional<std::string>& value = std::nullopt, const std::optional<CreationOptions> options = std::nullopt) const;
diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp
index e202a96..35e5936 100644
--- a/include/libyang-cpp/DataNode.hpp
+++ b/include/libyang-cpp/DataNode.hpp
@@ -123,7 +123,10 @@ public:
Collection<DataNode, IterationType::Sibling> siblings() const;
Collection<DataNode, IterationType::Sibling> immediateChildren() const;
- ParsedOp parseOp(const std::string& input, const DataFormat format, const OperationType opType) const;
+ ParsedOp parseOp(const std::string& input,
+ const DataFormat format,
+ const OperationType opType,
+ const std::optional<ParseOptions> parseOpts = std::nullopt) const;
void parseSubtree(
const std::string& data,
diff --git a/include/libyang-cpp/Enum.hpp b/include/libyang-cpp/Enum.hpp
index 85dcc83..32142f9 100644
--- a/include/libyang-cpp/Enum.hpp
+++ b/include/libyang-cpp/Enum.hpp
@@ -136,6 +136,7 @@ enum class DuplicationOptions : uint32_t {
WithFlags = 0x08,
NoExt = 0x10,
WithPriv = 0x20,
+ NoLyds = 0x40,
};
enum class NodeType : uint16_t {
@@ -167,6 +168,13 @@ enum class ContextOptions : uint16_t {
PreferSearchDirs = 0x20,
SetPrivParsed = 0x40,
ExplicitCompile = 0x80,
+ EnableImpFeatures = 0x100,
+ CompileObsolete = 0x200,
+ LybHashes = 0x400,
+ LeafrefExtended = 0x800,
+ LeafrefLinking = 0x1000,
+ BuiltinPluginsOnly = 0x2000,
+ StaticPluginsOnly = 0x4000,
};
/**
@@ -203,7 +211,6 @@ enum class AnydataValueType : uint32_t {
String,
XML,
JSON,
- LYB
};
/**
@@ -252,6 +259,7 @@ enum class ValidationOptions {
MultiError = 0x0004,
Operational = 0x0008,
NoDefaults = 0x0010,
+ NotFinal = 0x0020,
};
/**
@@ -262,11 +270,14 @@ enum class ParseOptions {
Strict = 0x020000,
Opaque = 0x040000,
NoState = 0x080000,
- LybModUpdate = 0x100000,
+ LybSkipCtxCheck = 0x100000,
Ordered = 0x200000,
Subtree = 0x400000, /**< Do not use this one for parsing of data subtrees */
WhenTrue = 0x800000,
NoNew = 0x1000000,
+ StoreOnly = 0x2010000,
+ JsonNull = 0x4000000,
+ JsonStringDataTypes = 0x8000000,
};
/**
diff --git a/src/Context.cpp b/src/Context.cpp
index fec2f27..f5bdc71 100644
--- a/src/Context.cpp
+++ b/src/Context.cpp
@@ -239,7 +239,7 @@ std::optional<DataNode> Context::parseExtData(
* Note: to parse a NETCONF RPC reply, you MUST parse the original NETCONF RPC request (that is, you have to use
* this method with OperationType::RpcNetconf).
*/
-ParsedOp Context::parseOp(const std::string& input, const DataFormat format, const OperationType opType) const
+ParsedOp Context::parseOp(const std::string& input, const DataFormat format, const OperationType opType, const std::optional<ParseOptions> parseOpts) const
{
auto in = wrap_ly_in_new_memory(input);
@@ -251,7 +251,15 @@ ParsedOp Context::parseOp(const std::string& input, const DataFormat format, con
case OperationType::NotificationYang: {
lyd_node* op = nullptr;
lyd_node* tree = nullptr;
- auto err = lyd_parse_op(m_ctx.get(), nullptr, in.get(), utils::toLydFormat(format), utils::toOpType(opType), &tree, &op);
+ auto err = lyd_parse_op(
+ m_ctx.get(),
+ nullptr,
+ in.get(),
+ utils::toLydFormat(format),
+ utils::toOpType(opType),
+ parseOpts ? utils::toParseOptions(*parseOpts) : 0,
+ &tree,
+ &op);
ParsedOp res;
res.tree = tree ? std::optional{libyang::wrapRawNode(tree)} : std::nullopt;
diff --git a/src/DataNode.cpp b/src/DataNode.cpp
index e28ef30..edabf9b 100644
--- a/src/DataNode.cpp
+++ b/src/DataNode.cpp
@@ -391,7 +391,7 @@ DataNodeAny DataNode::asAny() const
*
* Wraps `lyd_parse_op`.
*/
-ParsedOp DataNode::parseOp(const std::string& input, const DataFormat format, const OperationType opType) const
+ParsedOp DataNode::parseOp(const std::string& input, const DataFormat format, const OperationType opType, const std::optional<ParseOptions> parseOpts) const
{
auto in = wrap_ly_in_new_memory(input);
@@ -401,7 +401,14 @@ ParsedOp DataNode::parseOp(const std::string& input, const DataFormat format, co
case OperationType::ReplyRestconf: {
lyd_node* op = nullptr;
lyd_node* tree = nullptr;
- auto err = lyd_parse_op(m_node->schema->module->ctx, m_node, in.get(), utils::toLydFormat(format), utils::toOpType(opType), &tree, nullptr);
+ auto err = lyd_parse_op(m_node->schema->module->ctx,
+ m_node,
+ in.get(),
+ utils::toLydFormat(format),
+ utils::toOpType(opType),
+ parseOpts ? utils::toParseOptions(*parseOpts) : 0,
+ &tree,
+ nullptr);
ParsedOp res{
.tree = tree ? std::optional{libyang::wrapRawNode(tree)} : std::nullopt,
.op = op ? std::optional{libyang::wrapRawNode(op)} : std::nullopt
diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp
index ce24203..2e0351a 100644
--- a/src/SchemaNode.cpp
+++ b/src/SchemaNode.cpp
@@ -564,7 +564,7 @@ std::vector<std::string> LeafList::defaultValuesStr() const
auto dflts = reinterpret_cast<const lysc_node_leaflist*>(m_node)->dflts;
std::vector<std::string> res;
for (const auto& it : std::span(dflts, LY_ARRAY_COUNT(dflts))) {
- res.emplace_back(lyd_value_get_canonical(m_ctx.get(), it));
+ res.emplace_back(it.str);
}
return res;
}
@@ -660,8 +660,8 @@ bool LeafList::isUserOrdered() const
std::optional<std::string> Leaf::defaultValueStr() const
{
auto dflt = reinterpret_cast<const lysc_node_leaf*>(m_node)->dflt;
- if (dflt) {
- return lyd_value_get_canonical(m_ctx.get(), dflt);
+ if (dflt.str) {
+ return std::string{dflt.str};
} else {
return std::nullopt;
}
diff --git a/src/Type.cpp b/src/Type.cpp
index 01b2c4f..4d2db46 100644
--- a/src/Type.cpp
+++ b/src/Type.cpp
@@ -240,7 +240,7 @@ std::optional<std::string> Type::description() const
*/
std::string Type::internalPluginId() const
{
- return m_type->plugin->id;
+ return lysc_get_type_plugin(m_type->plugin_ref)->id;
}
/**
diff --git a/src/utils/enum.hpp b/src/utils/enum.hpp
index 5e02d24..0b2e2d7 100644
--- a/src/utils/enum.hpp
+++ b/src/utils/enum.hpp
@@ -91,6 +91,7 @@ static_assert(LYD_DUP_WITH_FLAGS == toDuplicationOptions(DuplicationOptions::Wit
static_assert(LYD_DUP_WITH_PARENTS == toDuplicationOptions(DuplicationOptions::WithParents));
static_assert(LYD_DUP_NO_EXT == toDuplicationOptions(DuplicationOptions::NoExt));
static_assert(LYD_DUP_WITH_PRIV == toDuplicationOptions(DuplicationOptions::WithPriv));
+static_assert(LYD_DUP_NO_LYDS == toDuplicationOptions(DuplicationOptions::NoLyds));
static_assert((LYD_DUP_NO_META | LYD_DUP_NO_EXT) ==
toDuplicationOptions(DuplicationOptions::NoMeta | DuplicationOptions::NoExt));
@@ -131,6 +132,13 @@ static_assert(toContextOptions(ContextOptions::DisableSearchCwd) == LY_CTX_DISAB
static_assert(toContextOptions(ContextOptions::PreferSearchDirs) == LY_CTX_PREFER_SEARCHDIRS);
static_assert(toContextOptions(ContextOptions::SetPrivParsed) == LY_CTX_SET_PRIV_PARSED);
static_assert(toContextOptions(ContextOptions::ExplicitCompile) == LY_CTX_EXPLICIT_COMPILE);
+static_assert(toContextOptions(ContextOptions::EnableImpFeatures) == LY_CTX_ENABLE_IMP_FEATURES);
+static_assert(toContextOptions(ContextOptions::CompileObsolete) == LY_CTX_COMPILE_OBSOLETE);
+static_assert(toContextOptions(ContextOptions::LybHashes) == LY_CTX_LYB_HASHES);
+static_assert(toContextOptions(ContextOptions::LeafrefExtended) == LY_CTX_LEAFREF_EXTENDED);
+static_assert(toContextOptions(ContextOptions::LeafrefLinking) == LY_CTX_LEAFREF_LINKING);
+static_assert(toContextOptions(ContextOptions::BuiltinPluginsOnly) == LY_CTX_BUILTIN_PLUGINS_ONLY);
+static_assert(toContextOptions(ContextOptions::StaticPluginsOnly) == LY_CTX_STATIC_PLUGINS_ONLY);
constexpr uint16_t toLogOptions(const LogOptions options)
{
@@ -198,6 +206,10 @@ constexpr uint32_t toValidationOptions(const ValidationOptions opts)
static_assert(toValidationOptions(ValidationOptions::NoState) == LYD_VALIDATE_NO_STATE);
static_assert(toValidationOptions(ValidationOptions::Present) == LYD_VALIDATE_PRESENT);
+static_assert(toValidationOptions(ValidationOptions::MultiError) == LYD_VALIDATE_MULTI_ERROR);
+static_assert(toValidationOptions(ValidationOptions::Operational) == LYD_VALIDATE_OPERATIONAL);
+static_assert(toValidationOptions(ValidationOptions::NoDefaults) == LYD_VALIDATE_NO_DEFAULTS);
+static_assert(toValidationOptions(ValidationOptions::NotFinal) == LYD_VALIDATE_NOT_FINAL);
constexpr uint32_t toParseOptions(const ParseOptions opts)
{
@@ -208,8 +220,14 @@ static_assert(toParseOptions(ParseOptions::ParseOnly) == LYD_PARSE_ONLY);
static_assert(toParseOptions(ParseOptions::Strict) == LYD_PARSE_STRICT);
static_assert(toParseOptions(ParseOptions::Opaque) == LYD_PARSE_OPAQ);
static_assert(toParseOptions(ParseOptions::NoState) == LYD_PARSE_NO_STATE);
-static_assert(toParseOptions(ParseOptions::LybModUpdate) == LYD_PARSE_LYB_MOD_UPDATE);
+static_assert(toParseOptions(ParseOptions::LybSkipCtxCheck) == LYD_PARSE_LYB_SKIP_CTX_CHECK);
static_assert(toParseOptions(ParseOptions::Ordered) == LYD_PARSE_ORDERED);
+static_assert(toParseOptions(ParseOptions::Subtree) == LYD_PARSE_SUBTREE);
+static_assert(toParseOptions(ParseOptions::WhenTrue) == LYD_PARSE_WHEN_TRUE);
+static_assert(toParseOptions(ParseOptions::NoNew) == LYD_PARSE_NO_NEW);
+static_assert(toParseOptions(ParseOptions::StoreOnly) == LYD_PARSE_STORE_ONLY);
+static_assert(toParseOptions(ParseOptions::JsonNull) == LYD_PARSE_JSON_NULL);
+static_assert(toParseOptions(ParseOptions::JsonStringDataTypes) == LYD_PARSE_JSON_STRING_DATATYPES);
constexpr lyd_type toOpType(const OperationType type)
{
@@ -242,7 +260,6 @@ static_assert(toAnydataValueType(AnydataValueType::DataTree) == LYD_ANYDATA_DATA
static_assert(toAnydataValueType(AnydataValueType::String) == LYD_ANYDATA_STRING);
static_assert(toAnydataValueType(AnydataValueType::XML) == LYD_ANYDATA_XML);
static_assert(toAnydataValueType(AnydataValueType::JSON) == LYD_ANYDATA_JSON);
-static_assert(toAnydataValueType(AnydataValueType::LYB) == LYD_ANYDATA_LYB);
constexpr LYS_OUTFORMAT toLysOutFormat(const SchemaOutputFormat format)
{
diff --git a/tests/context.cpp b/tests/context.cpp
index c0b7e09..eaedebf 100644
--- a/tests/context.cpp
+++ b/tests/context.cpp
@@ -359,25 +359,27 @@ TEST_CASE("context")
ctx->loadModule("mod1", std::nullopt, {});
ctx->parseModule(valid_yang_model, libyang::SchemaFormat::YANG);
auto modules = ctx->modules();
- REQUIRE(modules.size() == 8);
+ REQUIRE(modules.size() == 9);
REQUIRE(modules.at(0).name() == "ietf-yang-metadata");
REQUIRE(modules.at(0).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-metadata");
REQUIRE(modules.at(1).name() == "yang");
REQUIRE(modules.at(1).ns() == "urn:ietf:params:xml:ns:yang:1");
- REQUIRE(modules.at(2).name() == "ietf-inet-types");
- REQUIRE(modules.at(2).ns() == "urn:ietf:params:xml:ns:yang:ietf-inet-types");
- REQUIRE(modules.at(3).name() == "ietf-yang-types");
- REQUIRE(modules.at(3).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-types");
- REQUIRE(modules.at(4).name() == "ietf-yang-schema-mount");
- REQUIRE(modules.at(4).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-schema-mount");
- REQUIRE(modules.at(5).name() == "ietf-yang-structure-ext");
- REQUIRE(modules.at(5).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-structure-ext");
- REQUIRE(modules.at(6).name() == "mod1");
- REQUIRE(modules.at(6).ns() == "http://example.com");
- REQUIRE(*modules.at(6).revision() == "2021-11-15");
- REQUIRE(modules.at(7).name() == "test");
+ REQUIRE(modules.at(2).name() == "default");
+ REQUIRE(modules.at(2).ns() == "urn:ietf:params:xml:ns:netconf:default:1.0");
+ REQUIRE(modules.at(3).name() == "ietf-inet-types");
+ REQUIRE(modules.at(3).ns() == "urn:ietf:params:xml:ns:yang:ietf-inet-types");
+ REQUIRE(modules.at(4).name() == "ietf-yang-types");
+ REQUIRE(modules.at(4).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-types");
+ REQUIRE(modules.at(5).name() == "ietf-yang-schema-mount");
+ REQUIRE(modules.at(5).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-schema-mount");
+ REQUIRE(modules.at(6).name() == "ietf-yang-structure-ext");
+ REQUIRE(modules.at(6).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-structure-ext");
+ REQUIRE(modules.at(7).name() == "mod1");
REQUIRE(modules.at(7).ns() == "http://example.com");
- REQUIRE(modules.at(7).revision() == std::nullopt);
+ REQUIRE(*modules.at(7).revision() == "2021-11-15");
+ REQUIRE(modules.at(8).name() == "test");
+ REQUIRE(modules.at(8).ns() == "http://example.com");
+ REQUIRE(modules.at(8).revision() == std::nullopt);
}
DOCTEST_SUBCASE("Module comparison")
@@ -704,7 +706,7 @@ TEST_CASE("context")
DOCTEST_SUBCASE("schema printing")
{
- std::optional<libyang::Context> ctx_pp{std::in_place, std::nullopt, libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd | libyang::ContextOptions::SetPrivParsed};
+ std::optional<libyang::Context> ctx_pp{std::in_place, std::nullopt, libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd | libyang::ContextOptions::SetPrivParsed | libyang::ContextOptions::CompileObsolete};
auto mod = ctx_pp->parseModule(type_module, libyang::SchemaFormat::YANG);
REQUIRE(mod.printStr(libyang::SchemaOutputFormat::Tree) == R"(module: type_module
diff --git a/tests/data_node.cpp b/tests/data_node.cpp
index 9215b12..88ca5b9 100644
--- a/tests/data_node.cpp
+++ b/tests/data_node.cpp
@@ -2426,7 +2426,7 @@ TEST_CASE("Data Node manipulation")
"WTF": "foo bar baz"
}
}
- )", libyang::DataFormat::JSON, libyang::OperationType::RpcRestconf),
+ )", libyang::DataFormat::JSON, libyang::OperationType::RpcRestconf, libyang::ParseOptions::Strict),
"Can't parse into operation data tree: LY_EVALID", libyang::Error);
}
}
diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp
index 65ef462..58152a4 100644
--- a/tests/schema_node.cpp
+++ b/tests/schema_node.cpp
@@ -19,7 +19,7 @@ using namespace std::string_literals;
TEST_CASE("SchemaNode")
{
std::optional<libyang::Context> ctx{std::in_place, std::nullopt,
- libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd};
+ libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd | libyang::ContextOptions::CompileObsolete};
std::optional<libyang::Context> ctxWithParsed{std::in_place, std::nullopt,
libyang::ContextOptions::SetPrivParsed | libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd};
ctx->parseModule(example_schema, libyang::SchemaFormat::YANG);
@@ -841,7 +841,7 @@ TEST_CASE("SchemaNode")
DOCTEST_SUBCASE("LeafList::defaultValuesStr")
{
- REQUIRE(ctx->findPath("/type_module:leafListWithDefault").asLeafList().defaultValuesStr() == std::vector<std::string>{"-1", "512", "1024", "2048"});
+ REQUIRE(ctx->findPath("/type_module:leafListWithDefault").asLeafList().defaultValuesStr() == std::vector<std::string>{"-1", "+512", "0x400", "04000"});
REQUIRE(ctx->findPath("/type_module:leafListBasic").asLeafList().defaultValuesStr().size() == 0);
}
--
2.43.0
@@ -0,0 +1,77 @@
From 51d7457bd281e34bac92ff8231978766c7784657 Mon Sep 17 00:00:00 2001
From: Martin Bohal <mb@mbohal.cz>
Date: Tue, 4 Nov 2025 14:26:05 +0100
Subject: [PATCH 5/7] Add SchemaNode::isOutput() method
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
This commit introduces the `isOutput()` method to the `SchemaNode`
class, which checks whether the schema node is part of an output
statement subtree.
It complements an already existing `isInput()` method.
Change-Id: I8fe75f2e607c0159d8295d685a59d301155ca92e
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
include/libyang-cpp/SchemaNode.hpp | 1 +
src/SchemaNode.cpp | 10 ++++++++++
tests/schema_node.cpp | 7 +++++++
3 files changed, 18 insertions(+)
diff --git a/include/libyang-cpp/SchemaNode.hpp b/include/libyang-cpp/SchemaNode.hpp
index 9f49fd7..db5a63e 100644
--- a/include/libyang-cpp/SchemaNode.hpp
+++ b/include/libyang-cpp/SchemaNode.hpp
@@ -58,6 +58,7 @@ public:
Status status() const;
Config config() const;
bool isInput() const;
+ bool isOutput() const;
NodeType nodeType() const;
// It is possible to cast SchemaNode to another type via the following methods. The types are children classes of
// SchemaNode. No problems with slicing can occur, because these types are value-based and aren't constructible
diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp
index 2e0351a..9241b23 100644
--- a/src/SchemaNode.cpp
+++ b/src/SchemaNode.cpp
@@ -193,6 +193,16 @@ bool SchemaNode::isInput() const
return m_node->flags & LYS_INPUT;
}
+/**
+ * @brief Checks whether this node is inside a subtree of an output statement.
+ *
+ * Wraps `LYS_OUTPUT`.
+ */
+bool SchemaNode::isOutput() const
+{
+ return m_node->flags & LYS_OUTPUT;
+}
+
/**
* Returns the node type of this node (e.g. leaf, container...).
*
diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp
index 58152a4..01c2f19 100644
--- a/tests/schema_node.cpp
+++ b/tests/schema_node.cpp
@@ -135,6 +135,13 @@ TEST_CASE("SchemaNode")
REQUIRE(!ctx->findPath("/type_module:leafString").isInput());
}
+ DOCTEST_SUBCASE("SchemaNode::isOutput")
+ {
+ REQUIRE(ctx->findPath("/example-schema:myRpc/outputLeaf", libyang::InputOutputNodes::Output).isOutput());
+ REQUIRE(!ctx->findPath("/example-schema:myRpc/inputLeaf").isOutput());
+ REQUIRE(!ctx->findPath("/type_module:leafString").isOutput());
+ }
+
DOCTEST_SUBCASE("SchemaNode::module")
{
REQUIRE(ctx->findPath("/type_module:leafString").module().name() == "type_module");
--
2.43.0
@@ -0,0 +1,364 @@
From 38d5c10009df602594e1b9a53bac41111a9ba430 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 20 Nov 2025 15:32:03 +0100
Subject: [PATCH 6/7] adapt to changes in libyang v4.2
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
I was *so* tempted to leave the C++ enums as-is, just to save us from
this needless sed exercise, but hey, let's not start confusing any
possible downstream consumers now that upstream has changed these
identifiers.
Change-Id: I9b34585822b41be0ca1578051f1c0eb8588a0a51
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 4 +--
README.md | 2 +-
include/libyang-cpp/Enum.hpp | 4 +--
src/utils/enum.hpp | 4 +--
tests/context.cpp | 10 +++----
tests/data_node.cpp | 58 ++++++++++++++++++------------------
6 files changed, 41 insertions(+), 41 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 90f9aaa..cbaf82a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -20,7 +20,7 @@ add_custom_target(libyang-cpp-version-cmake
cmake/ProjectGitVersionRunner.cmake
)
include(cmake/ProjectGitVersion.cmake)
-set(LIBYANG_CPP_PKG_VERSION "5")
+set(LIBYANG_CPP_PKG_VERSION "6")
prepare_git_version(LIBYANG_CPP_VERSION ${LIBYANG_CPP_PKG_VERSION})
find_package(Doxygen)
@@ -28,7 +28,7 @@ option(WITH_DOCS "Create and install internal documentation (needs Doxygen)" ${D
option(BUILD_SHARED_LIBS "By default, shared libs are enabled. Turn off for a static build." ON)
find_package(PkgConfig REQUIRED)
-pkg_check_modules(LIBYANG REQUIRED libyang>=4.1.0 IMPORTED_TARGET)
+pkg_check_modules(LIBYANG REQUIRED libyang>=4.2.1 IMPORTED_TARGET)
# FIXME from gcc 14.1 on we should be able to use the calendar/time from libstdc++ and thus remove the date dependency
find_package(date)
diff --git a/README.md b/README.md
index 0cef2ff..842573f 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
Object lifetimes are managed automatically via RAII.
## Dependencies
-- [libyang v4](https://github.com/CESNET/libyang) - the `devel` branch (even for the `master` branch of *libyang-cpp*)
+- [libyang v4.2+](https://github.com/CESNET/libyang) - the `devel` branch (even for the `master` branch of *libyang-cpp*)
- C++20 compiler (e.g., GCC 10.x+, clang 10+)
- CMake 3.19+
- optionally for built-in tests, [Doctest](https://github.com/doctest/doctest/) as a C++ unit test framework
diff --git a/include/libyang-cpp/Enum.hpp b/include/libyang-cpp/Enum.hpp
index 32142f9..096e030 100644
--- a/include/libyang-cpp/Enum.hpp
+++ b/include/libyang-cpp/Enum.hpp
@@ -67,9 +67,9 @@ enum class OperationType : uint32_t {
*/
enum class PrintFlags : uint32_t {
WithDefaultsExplicit = 0x00,
- WithSiblings = 0x01,
+ Siblings = 0x01,
Shrink = 0x02,
- KeepEmptyCont = 0x04,
+ EmptyContainers = 0x04,
WithDefaultsTrim = 0x10,
WithDefaultsAll = 0x20,
WithDefaultsAllTag = 0x40,
diff --git a/src/utils/enum.hpp b/src/utils/enum.hpp
index 0b2e2d7..a8902ff 100644
--- a/src/utils/enum.hpp
+++ b/src/utils/enum.hpp
@@ -33,7 +33,7 @@ constexpr uint32_t toPrintFlags(const PrintFlags flags)
}
// These tests ensure that I used the right numbers when defining my enum.
// TODO: add asserts for operator|(PrintFlags, PrintFlags)
-static_assert(LYD_PRINT_KEEPEMPTYCONT == toPrintFlags(PrintFlags::KeepEmptyCont));
+static_assert(LYD_PRINT_EMPTY_CONT == toPrintFlags(PrintFlags::EmptyContainers));
static_assert(LYD_PRINT_SHRINK == toPrintFlags(PrintFlags::Shrink));
static_assert(LYD_PRINT_WD_ALL == toPrintFlags(PrintFlags::WithDefaultsAll));
static_assert(LYD_PRINT_WD_ALL_TAG == toPrintFlags(PrintFlags::WithDefaultsAllTag));
@@ -41,7 +41,7 @@ static_assert(LYD_PRINT_WD_EXPLICIT == toPrintFlags(PrintFlags::WithDefaultsExpl
static_assert(LYD_PRINT_WD_IMPL_TAG == toPrintFlags(PrintFlags::WithDefaultsImplicitTag));
static_assert(LYD_PRINT_WD_MASK == toPrintFlags(PrintFlags::WithDefaultsMask));
static_assert(LYD_PRINT_WD_TRIM == toPrintFlags(PrintFlags::WithDefaultsTrim));
-static_assert(LYD_PRINT_WITHSIBLINGS == toPrintFlags(PrintFlags::WithSiblings));
+static_assert(LYD_PRINT_SIBLINGS == toPrintFlags(PrintFlags::Siblings));
#ifndef _MSC_VER
// MSVC doesn't respect the underlying enum size
diff --git a/tests/context.cpp b/tests/context.cpp
index eaedebf..355860b 100644
--- a/tests/context.cpp
+++ b/tests/context.cpp
@@ -502,7 +502,7 @@ TEST_CASE("context")
auto errorsNode = node->findXPath("/ietf-restconf:errors");
REQUIRE(errorsNode.size() == 1);
REQUIRE(errorsNode.begin()->path() == "/ietf-restconf:errors");
- REQUIRE(*errorsNode.begin()->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont) == R"({
+ REQUIRE(*errorsNode.begin()->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers) == R"({
"ietf-restconf:errors": {
"error": [
{
@@ -589,7 +589,7 @@ TEST_CASE("context")
auto firstValue = edits.begin()->findPath("value");
REQUIRE(firstValue);
- REQUIRE(*firstValue->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == R"({
+ REQUIRE(*firstValue->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::EmptyContainers) == R"({
"ietf-yang-patch:value": {
"example-schema:person": {
"name": "John"
@@ -597,7 +597,7 @@ TEST_CASE("context")
}
}
)");
- REQUIRE(*firstValue->printStr(libyang::DataFormat::XML, libyang::PrintFlags::KeepEmptyCont) == R"(<value xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
+ REQUIRE(*firstValue->printStr(libyang::DataFormat::XML, libyang::PrintFlags::EmptyContainers) == R"(<value xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
<person xmlns="http://example.com/coze">
<name>John</name>
</person>
@@ -607,8 +607,8 @@ TEST_CASE("context")
auto secondValueNode = (edits.begin() + 1)->findPath("value");
REQUIRE(secondValueNode);
auto secondValue = std::get<libyang::DataNode>(secondValueNode->asAny().releaseValue().value());
- REQUIRE(*secondValue.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == "{\n \"example-schema:dummy\": \"I am a dummy\"\n}\n");
- REQUIRE(*secondValue.printStr(libyang::DataFormat::XML, libyang::PrintFlags::KeepEmptyCont) == "<dummy xmlns=\"http://example.com/coze\">I am a dummy</dummy>\n");
+ REQUIRE(*secondValue.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::EmptyContainers) == "{\n \"example-schema:dummy\": \"I am a dummy\"\n}\n");
+ REQUIRE(*secondValue.printStr(libyang::DataFormat::XML, libyang::PrintFlags::EmptyContainers) == "<dummy xmlns=\"http://example.com/coze\">I am a dummy</dummy>\n");
}
}
diff --git a/tests/data_node.cpp b/tests/data_node.cpp
index 88ca5b9..3f874de 100644
--- a/tests/data_node.cpp
+++ b/tests/data_node.cpp
@@ -137,7 +137,7 @@ TEST_CASE("Data Node manipulation")
DOCTEST_SUBCASE("Printing")
{
auto node = ctx.parseData(data, libyang::DataFormat::JSON);
- auto str = node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont);
+ auto str = node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers);
const auto expected = R"({
"example-schema:leafInt32": 420,
"example-schema:first": {
@@ -158,7 +158,7 @@ TEST_CASE("Data Node manipulation")
REQUIRE(str == expected);
auto emptyCont = ctx.newPath("/example-schema:first");
- REQUIRE(emptyCont.printStr(libyang::DataFormat::XML, libyang::PrintFlags::WithSiblings) == std::nullopt);
+ REQUIRE(emptyCont.printStr(libyang::DataFormat::XML, libyang::PrintFlags::Siblings) == std::nullopt);
}
DOCTEST_SUBCASE("Overwriting a tree with a different tree")
@@ -504,7 +504,7 @@ TEST_CASE("Data Node manipulation")
{
auto node = std::optional{ctx.newPath("/example-schema:leafInt32", "420")};
libyang::validateAll(node, libyang::ValidationOptions::NoState);
- auto str = node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont);
+ auto str = node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers);
REQUIRE(str == data);
}
@@ -725,7 +725,7 @@ TEST_CASE("Data Node manipulation")
}
// The original tree should still be accesible.
- node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings);
+ node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings);
}
}
@@ -855,7 +855,7 @@ TEST_CASE("Data Node manipulation")
auto cont = ctx.newPath2("/example-schema2:contWithTwoNodes").createdNode;
data->unlinkWithSiblings();
cont->insertChild(*data);
- REQUIRE(*cont->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings) == R"({
+ REQUIRE(*cont->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings) == R"({
"example-schema2:contWithTwoNodes": {
"one": 333,
"two": 666
@@ -1501,9 +1501,9 @@ TEST_CASE("Data Node manipulation")
REQUIRE(std::holds_alternative<libyang::DataNode>(rawVal));
auto retrieved = std::get<libyang::DataNode>(rawVal);
REQUIRE(retrieved.path() == "/key");
- REQUIRE(*retrieved.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings)
+ REQUIRE(*retrieved.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings)
== R"|({"key":"value"})|");
- REQUIRE(*retrieved.printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings)
+ REQUIRE(*retrieved.printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings)
== R"|(<key>value</key>)|");
}
}
@@ -1525,9 +1525,9 @@ TEST_CASE("Data Node manipulation")
REQUIRE(std::holds_alternative<libyang::DataNode>(rawVal));
auto retrieved = std::get<libyang::DataNode>(rawVal);
REQUIRE(retrieved.path() == "/something");
- REQUIRE(*retrieved.printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings)
+ REQUIRE(*retrieved.printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings)
== R"|(<something>lol</something>)|");
- REQUIRE(*retrieved.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings)
+ REQUIRE(*retrieved.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings)
== R"|({"something":"lol"})|");
}
}
@@ -1572,9 +1572,9 @@ TEST_CASE("Data Node manipulation")
val = jsonAnyXmlNode.createdNode->asAny().releaseValue();
}
- REQUIRE(*jsonAnyXmlNode.createdNode->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings)
+ REQUIRE(*jsonAnyXmlNode.createdNode->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings)
== R"|({"example-schema:ax":[1,2,3]})|"s);
- REQUIRE(*jsonAnyXmlNode.createdNode->printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings)
+ REQUIRE(*jsonAnyXmlNode.createdNode->printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings)
== R"|(<ax xmlns="http://example.com/coze">)|"s + origJSON + "</ax>");
}
@@ -1603,9 +1603,9 @@ TEST_CASE("Data Node manipulation")
}
REQUIRE(root);
- REQUIRE(*root->printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings)
+ REQUIRE(*root->printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings)
== origXML);
- REQUIRE(*root->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings)
+ REQUIRE(*root->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings)
== origJSON);
auto node = root->findPath("/example-schema:ax");
@@ -1665,9 +1665,9 @@ TEST_CASE("Data Node manipulation")
auto retrieved = std::get<libyang::DataNode>(*val);
val.reset();
REQUIRE(retrieved.path() == "/a");
- REQUIRE(*retrieved.printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings)
+ REQUIRE(*retrieved.printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings)
== origXML);
- REQUIRE(*retrieved.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings)
+ REQUIRE(*retrieved.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings)
== origJSON);
}
}
@@ -1806,7 +1806,7 @@ TEST_CASE("Data Node manipulation")
nodeX.parseSubtree(data, libyang::DataFormat::JSON,
libyang::ParseOptions::Strict | libyang::ParseOptions::NoState | libyang::ParseOptions::ParseOnly);
- REQUIRE(*nodeX.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings) == R"({
+ REQUIRE(*nodeX.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings) == R"({
"example-schema5:x": {
"x_b": {
"x_b_leaf": 666
@@ -1948,7 +1948,7 @@ TEST_CASE("Data Node manipulation")
{
netconfDeletePresenceCont.newMeta(netconf, "operation", "delete");
netconfDeletePresenceCont.newMeta(ietfOrigin, "origin", "ietf-origin:default");
- REQUIRE(*netconfDeletePresenceCont.printStr(libyang::DataFormat::XML, libyang::PrintFlags::WithSiblings)
+ REQUIRE(*netconfDeletePresenceCont.printStr(libyang::DataFormat::XML, libyang::PrintFlags::Siblings)
== R"(<presenceContainer xmlns="http://example.com/coze" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" nc:operation="delete" xmlns:or="urn:ietf:params:xml:ns:yang:ietf-origin" or:origin="or:default"/>)" "\n");
}
@@ -1957,7 +1957,7 @@ TEST_CASE("Data Node manipulation")
auto opaqueLeaf = ctx.newPath("/example-schema:leafInt32", std::nullopt, libyang::CreationOptions::Opaque);
REQUIRE_THROWS(opaqueLeaf.newMeta(netconf, "operation", "delete"));
opaqueLeaf.newAttrOpaqueJSON("ietf-netconf", "operation", "delete");
- REQUIRE(*opaqueLeaf.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings)
+ REQUIRE(*opaqueLeaf.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings)
== R"({
"example-schema:leafInt32": "",
"@example-schema:leafInt32": {
@@ -1976,7 +1976,7 @@ TEST_CASE("Data Node manipulation")
auto discard2 = ctx.newOpaqueJSON(libyang::OpaqueName{"sysrepo", "sysrepo", "discard-items"}, libyang::JSON{"/example-schema:b"});
REQUIRE(!!discard2);
discard1->insertSibling(*discard2);
- REQUIRE(*discard1->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings)
+ REQUIRE(*discard1->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings)
== R"({
"sysrepo:discard-items": "/example-schema:a",
"sysrepo:discard-items": "/example-schema:b"
@@ -1991,7 +1991,7 @@ TEST_CASE("Data Node manipulation")
auto leafInt16 = ctx.newPath("/example-schema:leafInt16", "666");
leafInt16.insertSibling(*discard1);
- REQUIRE(*discard1->firstSibling().printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings)
+ REQUIRE(*discard1->firstSibling().printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings)
== R"({
"example-schema:leafInt16": 666,
"sysrepo:discard-items": "/example-schema:a",
@@ -2020,7 +2020,7 @@ TEST_CASE("Data Node manipulation")
dummy.insertSibling(*discard3);
leafInt16.insertSibling(dummy);
- REQUIRE(*discard1->firstSibling().printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings)
+ REQUIRE(*discard1->firstSibling().printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings)
== R"({
"example-schema:dummy": "blah",
"example-schema:leafInt16": 666,
@@ -2112,8 +2112,8 @@ TEST_CASE("Data Node manipulation")
data->unlinkWithSiblings();
out->insertChild(*data);
- REQUIRE(*out->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings) == expectedJson);
- REQUIRE(*out->printStr(libyang::DataFormat::XML, libyang::PrintFlags::WithSiblings) == expectedXml);
+ REQUIRE(*out->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings) == expectedJson);
+ REQUIRE(*out->printStr(libyang::DataFormat::XML, libyang::PrintFlags::Siblings) == expectedXml);
}
DOCTEST_SUBCASE("libyang internal metadata")
@@ -2148,7 +2148,7 @@ TEST_CASE("Data Node manipulation")
auto node = ctx.newExtPath(ext, "/ietf-restconf:errors", std::nullopt, std::nullopt);
REQUIRE(node);
REQUIRE(node->schema().name() == "errors");
- REQUIRE(*node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont) == R"({
+ REQUIRE(*node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers) == R"({
"ietf-restconf:errors": {}
}
)");
@@ -2157,7 +2157,7 @@ TEST_CASE("Data Node manipulation")
REQUIRE(node->newPath("ietf-restconf:error[1]/error-tag", "invalid-attribute"));
REQUIRE(node->newExtPath(ext, "/ietf-restconf:errors/error[1]/error-message", "ahoj"));
REQUIRE_THROWS_WITH(node->newPath("ietf-restconf:error[1]/error-message", "duplicate create"), "Couldn't create a node with path 'ietf-restconf:error[1]/error-message': LY_EEXIST");
- REQUIRE(*node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont) == R"({
+ REQUIRE(*node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers) == R"({
"ietf-restconf:errors": {
"error": [
{
@@ -2173,7 +2173,7 @@ TEST_CASE("Data Node manipulation")
REQUIRE(node->newExtPath(ext, "/ietf-restconf:errors/error[2]/error-type", "transport"));
REQUIRE(node->newExtPath(ext, "/ietf-restconf:errors/error[2]/error-tag", "invalid-attribute"));
REQUIRE(node->newPath("ietf-restconf:error[2]/error-message", "aaa"));
- REQUIRE(*node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont) == R"({
+ REQUIRE(*node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers) == R"({
"ietf-restconf:errors": {
"error": [
{
@@ -2397,8 +2397,8 @@ TEST_CASE("Data Node manipulation")
REQUIRE(response.tree->path() == "/example-schema:output");
REQUIRE(response.tree->isOpaque());
REQUIRE(!response.tree->child()); // nothing gets "parsed" here, the result is put into the tree that parseOp() operated on (!)
- CAPTURE(*response.tree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont));
- CAPTURE(*replyTree.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont));
+ CAPTURE(*response.tree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers));
+ CAPTURE(*replyTree.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers));
node = replyTree.findPath("/example-schema:myRpc/outputLeaf", libyang::InputOutputNodes::Output);
REQUIRE(!!node);
@@ -2472,7 +2472,7 @@ TEST_CASE("Data Node manipulation")
REQUIRE(rpcOp.tree);
libyang::validateOp(*rpcTree, depTree, libyang::OperationType::RpcRestconf);
- REQUIRE(*rpcTree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == expected);
+ REQUIRE(*rpcTree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::EmptyContainers) == expected);
}
DOCTEST_SUBCASE("Nodes in disjunctive cases defined together")
--
2.43.0
@@ -0,0 +1,112 @@
From af187d1bc27a7390b358d7ad3b9f8f44cabe41ad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 20 Nov 2025 12:46:49 +0100
Subject: [PATCH 7/7] don't segfault when listing module's features
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
This is especially relevant when working with the "printed context" from
sysrepo, because that context has all the parsed info removed. As a
result, it is not possible to query a module's list of features. Let's
stop dereferencing a null pointer in that case.
Bug: https://github.com/sysrepo/sysrepo/issues/3695
Change-Id: I6258f580b4e5aa02cae2d60260b84593aefcf587
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 1 +
include/libyang-cpp/Utils.hpp | 1 +
src/Module.cpp | 3 +++
src/utils/exception.cpp | 5 +++++
tests/context.cpp | 12 ++++++++++++
5 files changed, 22 insertions(+)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index cbaf82a..b9da66b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -118,6 +118,7 @@ if(BUILD_TESTING)
endfunction()
libyang_cpp_test(context)
+ target_link_libraries(test_context PkgConfig::LIBYANG)
libyang_cpp_test(data_node)
target_link_libraries(test_data_node PkgConfig::LIBYANG)
libyang_cpp_test(schema_node)
diff --git a/include/libyang-cpp/Utils.hpp b/include/libyang-cpp/Utils.hpp
index 934714d..428b265 100644
--- a/include/libyang-cpp/Utils.hpp
+++ b/include/libyang-cpp/Utils.hpp
@@ -35,6 +35,7 @@ public:
class LIBYANG_CPP_EXPORT ParsedInfoUnavailable : public Error {
public:
explicit ParsedInfoUnavailable();
+ explicit ParsedInfoUnavailable(const std::string& what);
};
/**
diff --git a/src/Module.cpp b/src/Module.cpp
index d6d4023..8eb8fb4 100644
--- a/src/Module.cpp
+++ b/src/Module.cpp
@@ -155,6 +155,9 @@ void Module::setImplemented(const AllFeatures)
*/
std::vector<Feature> Module::features() const
{
+ if (!m_module->parsed) {
+ throw ParsedInfoUnavailable{"Module::features: lys_module::parsed is not available"};
+ }
std::vector<Feature> res;
for (const auto& feature : std::span(m_module->parsed->features, LY_ARRAY_COUNT(m_module->parsed->features))) {
res.emplace_back(Feature{&feature, m_ctx});
diff --git a/src/utils/exception.cpp b/src/utils/exception.cpp
index ecd2b85..a8d908b 100644
--- a/src/utils/exception.cpp
+++ b/src/utils/exception.cpp
@@ -22,6 +22,11 @@ ParsedInfoUnavailable::ParsedInfoUnavailable()
{
}
+ParsedInfoUnavailable::ParsedInfoUnavailable(const std::string& what)
+ : Error(what)
+{
+}
+
ErrorCode ErrorWithCode::code() const
{
return m_errCode;
diff --git a/tests/context.cpp b/tests/context.cpp
index 355860b..5fce0f4 100644
--- a/tests/context.cpp
+++ b/tests/context.cpp
@@ -10,6 +10,7 @@
#include <fstream>
#include <libyang-cpp/Context.hpp>
#include <libyang-cpp/Utils.hpp>
+#include <libyang/context.h>
#include "example_schema.hpp"
#include "pretty_printers.hpp"
#include "test_vars.hpp"
@@ -333,6 +334,17 @@ TEST_CASE("context")
REQUIRE(enabledFeatures == expectedEnabledFeatures);
}
+ DOCTEST_SUBCASE("printed context")
+ {
+ ctx->setSearchDir(TESTS_DIR / "yang");
+ auto mod = ctx->loadModule("mod1", std::nullopt, {"*"});
+ REQUIRE(mod.features().size() == 3);
+ ly_ctx_free_parsed(retrieveContext(*ctx));
+ REQUIRE_THROWS_WITH_AS(mod.features(),
+ "Module::features: lys_module::parsed is not available",
+ libyang::ParsedInfoUnavailable);
+ }
+
DOCTEST_SUBCASE("Module::setImplemented")
{
ctx->setSearchDir(TESTS_DIR / "yang");
--
2.43.0
+1 -1
View File
@@ -1,5 +1,5 @@
# md5, sha256 from https://pypi.org/pypi/libyang/json
md5 46332891cac852441b523f7e71a507f7 libyang-3.0.1.tar.gz
sha256 57f8b234131fb073518313351288194e535d74cc8fdff7f0ff7e31c0ab33e663 libyang-3.0.1.tar.gz
sha256 97da2c908a8a3607ac834f2acec224ea53a4b9e9770c13e5598513641decf637 libyang-3.3.0.tar.gz
# Locally computed sha256 checksums
sha256 c8d554f918d32b1467f35829e1cfb753764dd683c6edcecc877a4d7e9f05b2a7 LICENSE
+2 -2
View File
@@ -4,9 +4,9 @@
#
################################################################################
PYTHON_LIBYANG_VERSION = 3.0.1
PYTHON_LIBYANG_VERSION = 3.3.0
PYTHON_LIBYANG_SOURCE = libyang-$(PYTHON_LIBYANG_VERSION).tar.gz
PYTHON_LIBYANG_SITE = https://files.pythonhosted.org/packages/91/2e/ff13ee16c874d232d5d3fdff83f629cbc9ac47f9aaf2b59256b6a1bdbc16
PYTHON_LIBYANG_SITE = https://files.pythonhosted.org/packages/34/d8/ebe9de6d29eaf62a684b34d30118294e48a05d7fbd1bb0af002b7627140d
PYTHON_LIBYANG_SETUP_TYPE = setuptools
PYTHON_LIBYANG_LICENSE = MIT
PYTHON_LIBYANG_LICENSE_FILES = LICENSE
@@ -1,7 +1,7 @@
From c0d13b8e426608a30b1eea382f908fb6447fd01d Mon Sep 17 00:00:00 2001
From a4136d889237dadb9253ea7eb668a525dd779e6d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 12 Jun 2025 10:33:42 +0100
Subject: [PATCH 01/13] Log HTTP headers and the input data payload
Subject: [PATCH 01/38] Log HTTP headers and the input data payload
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -11,7 +11,7 @@ Organization: Wires
Hažlinský just found.
Change-Id: I2a930a02c7d30c051390fe73e6af9849edd580b4
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/restconf/Server.cpp | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
@@ -1,8 +1,11 @@
From 027a47ac3ff6c369caab7c007a5a171a68270829 Mon Sep 17 00:00:00 2001
From 6c5b482ea5c9fbc1149a0864b05d1bb1fa7100bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Fri, 13 Jun 2025 10:47:55 +0200
Subject: [PATCH 02/13] restconf: prevent throwing exception in
Subject: [PATCH 02/38] restconf: prevent throwing exception in
withRestconfExceptions wrapper
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
In case the catch block of withRestconfExceptions is reached and an
@@ -19,7 +22,7 @@ handle such situations.
Bug: https://github.com/CESNET/rousette/issues/19
Change-Id: Ifbd74b9bdc0ca66c4e5449a7673ef2f12ae9215e
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/restconf/Server.cpp | 2 +-
tests/restconf-plain-patch.cpp | 24 ++++++++++++++++++++++++
@@ -0,0 +1,31 @@
From 41c9d9cab47a88ee6c70ab8009b789226c0982fe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Tue, 17 Jun 2025 12:46:27 +0200
Subject: [PATCH 03/38] CI: switch to the new cloud's Swift URL
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: I69f8351394262a2a9b691422592741bfb40a8e38
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
ci/build.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ci/build.sh b/ci/build.sh
index d49bd56..59b31ab 100755
--- a/ci/build.sh
+++ b/ci/build.sh
@@ -67,7 +67,7 @@ if [[ -z "${ARTIFACT_URL}" ]]; then
# nothing ahead in the pipeline -> fallback to the latest promoted artifact
DEPSRCDIR=$(jq < ~/zuul-env.json -e -r ".projects[] | select(.name == \"CzechLight/dependencies\").src_dir")
DEP_SUBMODULE_COMMIT=$(git --git-dir ${HOME}/${DEPSRCDIR}/.git rev-parse HEAD)
- ARTIFACT_URL="https://object-store.cloud.muni.cz/swift/v1/ci-artifacts-${ZUUL_TENANT}/${ZUUL_GERRIT_HOSTNAME}/CzechLight/dependencies/deps-${ZUUL_JOB_NAME_NO_PROJECT%%-cover?(-previous)}/${DEP_SUBMODULE_COMMIT}.tar.zst"
+ ARTIFACT_URL="https://object-store.brno.openstack.cloud.e-infra.cz/swift/v1/KEY_b637b9c937414b29b3e277b4a85cc658/ci-artifacts-${ZUUL_TENANT}/${ZUUL_GERRIT_HOSTNAME}/CzechLight/dependencies/deps-${ZUUL_JOB_NAME_NO_PROJECT%%-cover?(-previous)}/${DEP_SUBMODULE_COMMIT}.tar.zst"
fi
curl ${ARTIFACT_URL} | unzstd --stdout | tar -C ${PREFIX} -xf -
--
2.43.0
@@ -1,7 +1,7 @@
From d1c56f14b54b113df8fba759924bb47cfa27366c Mon Sep 17 00:00:00 2001
From ec8673126929b6459fcd99c84a79993a725b40e1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Fri, 13 Jun 2025 10:47:55 +0200
Subject: [PATCH 03/13] restconf: crash instead of a deadlock when the handler
Subject: [PATCH 04/38] restconf: crash instead of a deadlock when the handler
throws
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
@@ -37,7 +37,7 @@ called twice [3].
Signed-off-by: Jan Kundrát <jan.kundrat@cesnet.cz>
Bug: https://github.com/CESNET/rousette/issues/19
Change-Id: I2c090b9a76b062101ba422a7d50e8e699779e203
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/restconf/Server.cpp | 28 ++++++++++++++++++++++++++++
src/restconf/Server.h | 4 ++++
@@ -0,0 +1,29 @@
From 37ca95c387d76c3f296a4e44b211772a1ca155ab Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Wed, 18 Jun 2025 12:01:04 +0200
Subject: [PATCH 05/38] doc: let's stop calling this "an almost-RESTCONF
server"
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: If55ace481c78d838a811ded76a564f8fb59f9233
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0796dce..a42c64d 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# An almost-RESTCONF server
+# A RESTCONF server for sysrepo
![License](https://img.shields.io/github/license/cesnet/rousette)
[![Gerrit](https://img.shields.io/badge/patches-via%20Gerrit-blue)](https://gerrit.cesnet.cz/q/project:CzechLight/rousette)
--
2.43.0
@@ -1,7 +1,10 @@
From db64d7b3fc0a0373067e6c5ac9e41b93351778a1 Mon Sep 17 00:00:00 2001
From 4eae6200aa812950ebbac1660a1899f4edf41e11 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Wed, 7 Aug 2024 19:07:35 +0200
Subject: [PATCH 04/13] close long-lived connections on SIGTERM
Subject: [PATCH 06/38] close long-lived connections on SIGTERM
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Without this patch, all the ongoing SSE streams would be left alive for
@@ -20,7 +23,7 @@ hope it is).
Change-Id: If442134783ba1d699de47c51a9068378f53e8339
Co-authored-by: Tomas Pecka <tomas.pecka@cesnet.cz>
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 1 +
src/clock.cpp | 7 +--
@@ -1,7 +1,10 @@
From 5c8a139d04d9b3dfb28b8c0e1a4aa7b945a1e8fc Mon Sep 17 00:00:00 2001
From ca2894d4888c673d227fc196a25f83ded20e8f04 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Mon, 30 Jun 2025 15:38:02 +0200
Subject: [PATCH 05/13] restconf: refactor server stop
Subject: [PATCH 07/38] restconf: refactor server stop
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
In 94b24795 ("server: simplify listening/stopping") we needed to call
@@ -17,7 +20,7 @@ does not seem like it should be called from *every* io_service.)
[2] https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/reference/post.html
Change-Id: I2f33c38a78dce4081a03326c9a9bb25817fc9d2f
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/restconf/Server.cpp | 13 +++++--------
1 file changed, 5 insertions(+), 8 deletions(-)
@@ -0,0 +1,33 @@
From 9961430bacdd8dbac64a01b2a2ffb6a4b7e806b6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Tue, 13 May 2025 13:48:35 +0200
Subject: [PATCH 08/38] tests: use std::string::starts_with
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
We are C++20, so we can use line, which is more readable.
Change-Id: I40d4038b421f6bc1fcf320f609b50d5ce7018a45
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
tests/restconf_utils.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp
index cbfce2c..08b9623 100644
--- a/tests/restconf_utils.cpp
+++ b/tests/restconf_utils.cpp
@@ -228,7 +228,7 @@ std::vector<std::string> SSEClient::parseEvents(const std::string& msg)
std::string event;
while (std::getline(iss, line)) {
- if (line.compare(0, prefix.size(), prefix) == 0) {
+ if (line.starts_with(prefix)) {
event += line.substr(prefix.size());
} else if (line.empty()) {
res.emplace_back(std::move(event));
--
2.43.0
@@ -0,0 +1,172 @@
From 746c0cdfef6808f393be7946630b8acbb0636706 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Tue, 22 Jul 2025 17:47:50 +0200
Subject: [PATCH 09/38] tests: add SSE event watcher for comments
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
I want to test whether client receives even the Server Sent Events
which should be ignored, i.e., lines starting with colon.
Change-Id: If54f0af05b4884aab01325f12fd0a6859791b41b
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
tests/event_watchers.cpp | 7 ++++++-
tests/event_watchers.h | 4 +++-
tests/restconf_utils.cpp | 36 ++++++++++++++++++++----------------
tests/restconf_utils.h | 12 +++++++++---
4 files changed, 38 insertions(+), 21 deletions(-)
diff --git a/tests/event_watchers.cpp b/tests/event_watchers.cpp
index 690338e..26ba951 100644
--- a/tests/event_watchers.cpp
+++ b/tests/event_watchers.cpp
@@ -64,7 +64,7 @@ void RestconfNotificationWatcher::setDataFormat(const libyang::DataFormat dataFo
this->dataFormat = dataFormat;
}
-void RestconfNotificationWatcher::operator()(const std::string& msg) const
+void RestconfNotificationWatcher::dataEvent(const std::string& msg) const
{
spdlog::trace("Client received data: {}", msg);
auto notifDataNode = ctx.parseOp(msg,
@@ -79,3 +79,8 @@ void RestconfNotificationWatcher::operator()(const std::string& msg) const
data(*dataRoot->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink));
}
+
+void RestconfNotificationWatcher::commentEvent(const std::string& msg) const
+{
+ comment(msg);
+}
diff --git a/tests/event_watchers.h b/tests/event_watchers.h
index 3b533ba..7022b64 100644
--- a/tests/event_watchers.h
+++ b/tests/event_watchers.h
@@ -39,8 +39,10 @@ struct RestconfNotificationWatcher {
RestconfNotificationWatcher(const libyang::Context& ctx);
void setDataFormat(const libyang::DataFormat dataFormat);
- void operator()(const std::string& msg) const;
+ void dataEvent(const std::string& msg) const;
+ void commentEvent(const std::string& msg) const;
+ MAKE_CONST_MOCK1(comment, void(const std::string&));
MAKE_CONST_MOCK1(data, void(const std::string&));
};
diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp
index 08b9623..5b1ebe3 100644
--- a/tests/restconf_utils.cpp
+++ b/tests/restconf_utils.cpp
@@ -169,10 +169,11 @@ SSEClient::SSEClient(
const std::string& server_address,
const std::string& server_port,
std::binary_semaphore& requestSent,
- const RestconfNotificationWatcher& notification,
+ const RestconfNotificationWatcher& eventWatcher,
const std::string& uri,
const std::map<std::string, std::string>& headers,
- const boost::posix_time::seconds silenceTimeout)
+ const boost::posix_time::seconds silenceTimeout,
+ const ReportIgnoredLines reportIgnoredLines)
: client(std::make_shared<ng_client::session>(io, server_address, server_port))
, t(io, silenceTimeout)
{
@@ -191,17 +192,15 @@ SSEClient::SSEClient(
}
});
- client->on_connect([&, uri, reqHeaders, silenceTimeout, server_address, server_port](auto) {
+ client->on_connect([&, uri, reqHeaders, silenceTimeout, server_address, server_port, reportIgnoredLines](auto) {
boost::system::error_code ec;
auto req = client->submit(ec, "GET", serverAddressAndPort(server_address, server_port) + uri, "", reqHeaders);
- req->on_response([&, silenceTimeout](const ng_client::response& res) {
+ req->on_response([&, silenceTimeout, reportIgnoredLines](const ng_client::response& res) {
requestSent.release();
- res.on_data([&, silenceTimeout](const uint8_t* data, std::size_t len) {
+ res.on_data([&, silenceTimeout, reportIgnoredLines](const uint8_t* data, std::size_t len) {
// not a production-ready code. In real-life condition the data received in one callback might probably be incomplete
- for (const auto& event : parseEvents(std::string(reinterpret_cast<const char*>(data), len))) {
- notification(event);
- }
+ parseEvents(std::string(reinterpret_cast<const char*>(data), len), eventWatcher, reportIgnoredLines);
t.expires_from_now(silenceTimeout);
});
});
@@ -218,25 +217,30 @@ SSEClient::SSEClient(
});
}
-std::vector<std::string> SSEClient::parseEvents(const std::string& msg)
+void SSEClient::parseEvents(const std::string& msg, const RestconfNotificationWatcher& eventWatcher, const ReportIgnoredLines reportIgnoredLines)
{
- static const std::string prefix = "data:";
+ static const std::string dataPrefix = "data:";
+ static const std::string ignorePrefix = ":";
- std::vector<std::string> res;
std::istringstream iss(msg);
std::string line;
std::string event;
while (std::getline(iss, line)) {
- if (line.starts_with(prefix)) {
- event += line.substr(prefix.size());
- } else if (line.empty()) {
- res.emplace_back(std::move(event));
+ if (line.starts_with(ignorePrefix) && reportIgnoredLines == ReportIgnoredLines::Yes) {
+ eventWatcher.commentEvent(line);
+ } else if (line.starts_with(ignorePrefix)) {
+ continue;
+ } else if (line.starts_with(dataPrefix)) {
+ event += line.substr(dataPrefix.size());
+ } else if (line.empty() && !event.empty()) {
+ eventWatcher.dataEvent(event);
event.clear();
+ } else if (line.empty()) {
+ continue;
} else {
CAPTURE(msg);
FAIL("Unprefixed response");
}
}
- return res;
}
diff --git a/tests/restconf_utils.h b/tests/restconf_utils.h
index 9efe398..9dde10b 100644
--- a/tests/restconf_utils.h
+++ b/tests/restconf_utils.h
@@ -88,17 +88,23 @@ struct SSEClient {
std::shared_ptr<ng_client::session> client;
boost::asio::deadline_timer t;
+ enum class ReportIgnoredLines {
+ No,
+ Yes,
+ };
+
SSEClient(
boost::asio::io_service& io,
const std::string& server_address,
const std::string& server_port,
std::binary_semaphore& requestSent,
- const RestconfNotificationWatcher& notification,
+ const RestconfNotificationWatcher& eventWatcher,
const std::string& uri,
const std::map<std::string, std::string>& headers,
- const boost::posix_time::seconds silenceTimeout = boost::posix_time::seconds(1)); // test code; the server should respond "soon"
+ const boost::posix_time::seconds silenceTimeout = boost::posix_time::seconds(1), // test code; the server should respond "soon"
+ const ReportIgnoredLines reportIgnoredLines = ReportIgnoredLines::No);
- static std::vector<std::string> parseEvents(const std::string& msg);
+ static void parseEvents(const std::string& msg, const RestconfNotificationWatcher& eventWatcher, const ReportIgnoredLines reportIgnoredLines);
};
#define PREPARE_LOOP_WITH_EXCEPTIONS \
--
2.43.0
@@ -1,7 +1,10 @@
From 086cfe1847e406df90852dc3cb52ece3d0cbe22b Mon Sep 17 00:00:00 2001
From 5becffe8a1dd47c8836ce1800a1b72acdf86021f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Mon, 12 May 2025 14:56:54 +0200
Subject: [PATCH 06/13] http: send keep-alive pings from EventStream
Subject: [PATCH 10/38] http: send keep-alive pings from EventStream
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
The nghttp2-asio server disconnects client after 60 seconds of no
@@ -28,7 +31,7 @@ to the server when the server sends these "keep-alive comments".
[3] https://html.spec.whatwg.org/multipage/server-sent-events.html#authoring-notes
Change-Id: I57e510d0b61ac7ed032c582779780c64768b7d53
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/clock.cpp | 4 +-
src/http/EventStream.cpp | 44 +++++++++++++--
@@ -1,7 +1,10 @@
From 06e2751e4da052195db96aaa90549fba398208bd Mon Sep 17 00:00:00 2001
From ff4ff1c193083feca76d9f0f4485e4b175c373c2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Wed, 23 Jul 2025 14:27:26 +0200
Subject: [PATCH 07/13] refactor: event streams use named constructors
Subject: [PATCH 11/38] refactor: event streams use named constructors
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
This commit refactors the EventStream and NotificationStream to use
@@ -14,7 +17,7 @@ named constructors which perform the double initialization themselves.
This way, the code is more readable and the intention should be clearer.
Change-Id: Iac96c49c20670dfe924d7c8db33328ed9c2fc9dd
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/clock.cpp | 3 +--
src/http/EventStream.cpp | 20 ++++++++++++++++++++
@@ -1,13 +1,16 @@
From e98e079d0fdedf48facbfcf187950946c1ffed7a Mon Sep 17 00:00:00 2001
From f4602a03adc9134a9b7a9d338e900b40557da6c4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 7 Aug 2025 12:14:02 +0200
Subject: [PATCH 08/13] refactor: a better convention for weak_from_this->lock
Subject: [PATCH 12/38] refactor: a better convention for weak_from_this->lock
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
It is not a "client", so let's stop calling it a "client". My bad.
Change-Id: Id8dc4d92c3ade8d86697366d0102e84bd466f504
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/http/EventStream.cpp | 44 ++++++++++++++++++++--------------------
1 file changed, 22 insertions(+), 22 deletions(-)
@@ -1,78 +0,0 @@
From ba43271df7cc83806a08d6937719544786fd331d Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Sun, 28 Sep 2025 06:04:12 +0200
Subject: [PATCH 13/13] Add '--log-level LEVEL' command line option
Organization: Wires
The default 'trace' log level is quite verbose for production systems.
This commit changes the default to 'info' and adds a command line option
to control the log level.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
src/restconf/main.cpp | 27 +++++++++++++++++++++++++--
1 file changed, 25 insertions(+), 2 deletions(-)
diff --git a/src/restconf/main.cpp b/src/restconf/main.cpp
index 477e846..f253421 100644
--- a/src/restconf/main.cpp
+++ b/src/restconf/main.cpp
@@ -26,11 +26,12 @@
static const char usage[] =
R"(Rousette - RESTCONF server
Usage:
- rousette [--syslog] [--timeout <SECONDS>] [--help]
+ rousette [--syslog] [--timeout <SECONDS>] [--log-level <LEVEL>] [--help]
Options:
-h --help Show this screen.
-t --timeout <SECONDS> Change default timeout in sysrepo (if not set, use sysrepo internal).
--syslog Log to syslog.
+ --log-level <LEVEL> Set log level (trace, debug, info, warn, error, critical, off) [default: info].
)";
#ifdef HAVE_SYSTEMD
@@ -75,10 +76,32 @@ int main(int argc, char* argv [])
{
auto args = docopt::docopt(usage, {argv + 1, argv + argc}, true,""/* version */, true);
auto timeout = std::chrono::milliseconds{0};
+ auto logLevel = spdlog::level::info;
if (args["--timeout"]) {
timeout = std::chrono::milliseconds{args["--timeout"].asLong() * 1000};
}
+
+ if (args["--log-level"]) {
+ auto levelStr = args["--log-level"].asString();
+ if (levelStr == "trace") {
+ logLevel = spdlog::level::trace;
+ } else if (levelStr == "debug") {
+ logLevel = spdlog::level::debug;
+ } else if (levelStr == "info") {
+ logLevel = spdlog::level::info;
+ } else if (levelStr == "warn" || levelStr == "warning") {
+ logLevel = spdlog::level::warn;
+ } else if (levelStr == "error" || levelStr == "err") {
+ logLevel = spdlog::level::err;
+ } else if (levelStr == "critical") {
+ logLevel = spdlog::level::critical;
+ } else if (levelStr == "off") {
+ logLevel = spdlog::level::off;
+ } else {
+ throw std::runtime_error("Invalid log level: " + levelStr + ". Valid levels: trace, debug, info, warn, error, critical, off");
+ }
+ }
if (args["--syslog"].asBool()) {
auto syslog_sink = std::make_shared<spdlog::sinks::syslog_sink_mt>("rousette", LOG_PID, LOG_USER, true);
auto logger = std::make_shared<spdlog::logger>("rousette", syslog_sink);
@@ -94,7 +117,7 @@ int main(int argc, char* argv [])
auto logger = std::make_shared<spdlog::logger>("rousette", stdout_sink);
spdlog::set_default_logger(logger);
}
- spdlog::set_level(spdlog::level::trace);
+ spdlog::set_level(logLevel);
/* We will parse URIs using boost::spirit's alnum/alpha/... matchers which are locale-dependent.
* Let's use something stable no matter what the system is using
--
2.43.0
@@ -1,12 +1,15 @@
From de7c55a1e8d3dda20ee3dc408612f2ddb309e888 Mon Sep 17 00:00:00 2001
From 4e9b535a59861f25c0602eaa1fc39126d7cd9899 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 7 Aug 2025 12:21:54 +0200
Subject: [PATCH 09/13] fix a possible bad_weak_ptr exception
Subject: [PATCH 13/38] fix a possible bad_weak_ptr exception
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: I8c7f7a943a1d848f15527988cb76c2a0a10089e6
Fixes: 4eae6200 (close long-lived connections on SIGTERM)
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/http/EventStream.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
@@ -1,7 +1,10 @@
From 0f034d6da3e6d0a91bbe09717cb33aaf3a3ed747 Mon Sep 17 00:00:00 2001
From 6bca750f866b5b14c4d9c3da68e5c8f1e4eee36c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Mon, 19 May 2025 12:11:09 +0200
Subject: [PATCH 10/13] http: add optional callbacks to EventStream
Subject: [PATCH 14/38] http: add optional callbacks to EventStream
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
In some cases, it might be useful to have a callback that is called when
@@ -11,7 +14,7 @@ implementation where we would like to clean up stuff after client
disconnects.
Change-Id: Icfc2959e38b812b7c18f45976415209b29151c7b
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/http/EventStream.cpp | 18 +++++++++++++++---
src/http/EventStream.h | 10 ++++++++--
@@ -1,7 +1,10 @@
From d72adb3da31cb509e64c8f33174e375fa9468b0b Mon Sep 17 00:00:00 2001
From f6ee629de8abef42a24c42b185052b0c8e78bd6b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Mon, 19 May 2025 12:23:16 +0200
Subject: [PATCH 11/13] restconf: add internal RPC handler dispatcher
Subject: [PATCH 15/38] restconf: add internal RPC handler dispatcher
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
This code adds a new internal RPC handler dispatcher that checks if user
@@ -11,7 +14,7 @@ So far, there are no RPCs processed, but support for the
ietf-subscribed-notifications:establish-subscription RPC is coming soon.
Change-Id: I99121a511011229e4098f95e91601b39d333444a
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/restconf/Server.cpp | 24 ++++++++++++++++++++----
tests/restconf-rpc.cpp | 18 ++++++++++++++++++
@@ -0,0 +1,120 @@
From b7966613b43b01402c9f0af286a0b3237161779d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Tue, 2 Sep 2025 15:33:43 +0200
Subject: [PATCH 16/38] tests: processing incomplete events in SSE client
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
During my testing I noticed that I obtained incomplete message in
on_data callback. The message was split into two callbacks.
This commit fixes the FIXME by buffering the data and processing
a complete event, once available.
Change-Id: Ied07e69e8b518f20fcc82134a4c041e7ec3a06d6
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
tests/restconf_utils.cpp | 53 +++++++++++++++++++++++-----------------
tests/restconf_utils.h | 3 ++-
2 files changed, 33 insertions(+), 23 deletions(-)
diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp
index 5b1ebe3..740b1fa 100644
--- a/tests/restconf_utils.cpp
+++ b/tests/restconf_utils.cpp
@@ -199,8 +199,8 @@ SSEClient::SSEClient(
req->on_response([&, silenceTimeout, reportIgnoredLines](const ng_client::response& res) {
requestSent.release();
res.on_data([&, silenceTimeout, reportIgnoredLines](const uint8_t* data, std::size_t len) {
- // not a production-ready code. In real-life condition the data received in one callback might probably be incomplete
- parseEvents(std::string(reinterpret_cast<const char*>(data), len), eventWatcher, reportIgnoredLines);
+ dataBuffer.append(std::string(reinterpret_cast<const char*>(data), len));
+ parseEvents(eventWatcher, reportIgnoredLines);
t.expires_from_now(silenceTimeout);
});
});
@@ -217,30 +217,39 @@ SSEClient::SSEClient(
});
}
-void SSEClient::parseEvents(const std::string& msg, const RestconfNotificationWatcher& eventWatcher, const ReportIgnoredLines reportIgnoredLines)
+void SSEClient::parseEvents(const RestconfNotificationWatcher& eventWatcher, const ReportIgnoredLines reportIgnoredLines)
{
static const std::string dataPrefix = "data:";
static const std::string ignorePrefix = ":";
- std::istringstream iss(msg);
- std::string line;
- std::string event;
-
- while (std::getline(iss, line)) {
- if (line.starts_with(ignorePrefix) && reportIgnoredLines == ReportIgnoredLines::Yes) {
- eventWatcher.commentEvent(line);
- } else if (line.starts_with(ignorePrefix)) {
- continue;
- } else if (line.starts_with(dataPrefix)) {
- event += line.substr(dataPrefix.size());
- } else if (line.empty() && !event.empty()) {
- eventWatcher.dataEvent(event);
- event.clear();
- } else if (line.empty()) {
- continue;
- } else {
- CAPTURE(msg);
- FAIL("Unprefixed response");
+ std::size_t pos = 0;
+ constexpr auto EVENT_SEPARATOR = "\n\n"; // FIXME: Not a production-ready code; does not deal with all possible newline combinations of CR and LF
+
+ while ((pos = dataBuffer.find(EVENT_SEPARATOR)) != std::string::npos) {
+ // extract event
+ auto rawEvent = dataBuffer.substr(0, pos + std::char_traits<char>::length(EVENT_SEPARATOR));
+ std::istringstream stream(rawEvent);
+ dataBuffer.erase(0, pos + std::char_traits<char>::length(EVENT_SEPARATOR));
+
+ // split on newlines
+ std::string line;
+ std::string event;
+ while (std::getline(stream, line)) {
+ if (line.starts_with(ignorePrefix) && reportIgnoredLines == ReportIgnoredLines::Yes) {
+ eventWatcher.commentEvent(line);
+ } else if (line.starts_with(ignorePrefix)) {
+ continue;
+ } else if (line.starts_with(dataPrefix)) {
+ event += line.substr(dataPrefix.size());
+ } else if (line.empty() && !event.empty()) {
+ eventWatcher.dataEvent(event);
+ event.clear();
+ } else if (line.empty()) {
+ continue;
+ } else {
+ CAPTURE(rawEvent);
+ FAIL("Unprefixed response");
+ }
}
}
}
diff --git a/tests/restconf_utils.h b/tests/restconf_utils.h
index 9dde10b..c35df54 100644
--- a/tests/restconf_utils.h
+++ b/tests/restconf_utils.h
@@ -87,6 +87,7 @@ void setupRealNacm(sysrepo::Session session);
struct SSEClient {
std::shared_ptr<ng_client::session> client;
boost::asio::deadline_timer t;
+ std::string dataBuffer;
enum class ReportIgnoredLines {
No,
@@ -104,7 +105,7 @@ struct SSEClient {
const boost::posix_time::seconds silenceTimeout = boost::posix_time::seconds(1), // test code; the server should respond "soon"
const ReportIgnoredLines reportIgnoredLines = ReportIgnoredLines::No);
- static void parseEvents(const std::string& msg, const RestconfNotificationWatcher& eventWatcher, const ReportIgnoredLines reportIgnoredLines);
+ void parseEvents(const RestconfNotificationWatcher& eventWatcher, const ReportIgnoredLines reportIgnoredLines);
};
#define PREPARE_LOOP_WITH_EXCEPTIONS \
--
2.43.0
@@ -1,7 +1,10 @@
From 86ea8a1fdc2cb8baa1edbd32bd4f8b45acdef816 Mon Sep 17 00:00:00 2001
From 1067e05674633b97d64b428686aff44822230c5f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Mon, 8 Sep 2025 20:24:15 +0200
Subject: [PATCH 12/13] EventStream: fix possible heap-use-after-free
Subject: [PATCH 17/38] EventStream: fix possible heap-use-after-free
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
I have started getting heap-use-after-free errors from sanitizers (see
@@ -61,7 +64,7 @@ before actually resuming the response.
(...)
Change-Id: Ifdb1f8610cacffca3bb49da17aa9b1d267cdd472
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/http/EventStream.cpp | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
@@ -0,0 +1,52 @@
From fff342945eb3c8deb6f7aa67aca54f9c9f87f3ec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 11 Sep 2025 18:56:31 +0200
Subject: [PATCH 18/38] Use a working version of nghttp2-asio
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
The latest upstream version does not build on Boost 1.87, and there's a
nasty crash in the destructor which led to a nice little heap of hair on
our desks. It's time to fork that thing.
Link: https://github.com/nghttp2/nghttp2-asio/pull/9
Link: https://github.com/nghttp2/nghttp2-asio/pull/25
Change-Id: Iec9619ce45e7e76f0781d39966d0b6c7cc6fa778
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
.zuul.yaml | 2 +-
README.md | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/.zuul.yaml b/.zuul.yaml
index e5a87b6..a994f34 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -6,7 +6,7 @@
required-projects: &projects
- CzechLight/dependencies
- name: github/nghttp2/nghttp2-asio
- override-checkout: main
+ override-checkout: cesnet/main
vars:
zuul_work_dir: "{{ zuul.projects[zuul.project.canonical_hostname + '/CzechLight/rousette'].src_dir }}"
diff --git a/README.md b/README.md
index a42c64d..437afd3 100644
--- a/README.md
+++ b/README.md
@@ -84,7 +84,8 @@ In practical terms, this means that the NACM access rules for the following XPat
## Dependencies
-- [nghttp2-asio](https://github.com/nghttp2/nghttp2-asio) - asynchronous C++ library for HTTP/2
+- [nghttp2-asio](https://github.com/CESNET/nghttp2-asio) - asynchronous C++ library for HTTP/2
+ - we strongly recommend using [our fork](https://github.com/CESNET/nghttp2-asio) which appears to be better maintained, and which contains important bugfixes which have not been merged upstream yet
- [sysrepo-cpp](https://github.com/sysrepo/sysrepo-cpp) - object-oriented bindings of the [*sysrepo*](https://github.com/sysrepo/sysrepo) library
- [libyang-cpp](https://github.com/CESNET/libyang-cpp) - C++ bindings for *libyang*
- [PAM](http://www.linux-pam.org/) - for authentication
--
2.43.0
@@ -0,0 +1,61 @@
From ffc52b2e454ae4ac4fa1948cb76a9469026d453e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Mon, 6 Oct 2025 22:35:27 +0200
Subject: [PATCH 19/38] ping: use a monotonic timer
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
The deadline_timer uses system's UTC clock which is not monotonic (i.e.,
it jumps when the wall clock is adjusted). Let's switch to a monotonic
timer.
Given that boost::asio::steady_timer::expires_from_now() is marked as
deprecated, use the other function.
Change-Id: I40383721ecb0f12bfcb3f638124a32150a29bf48
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/http/EventStream.cpp | 2 +-
src/http/EventStream.h | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp
index f97d953..280addd 100644
--- a/src/http/EventStream.cpp
+++ b/src/http/EventStream.cpp
@@ -180,7 +180,7 @@ void EventStream::enqueue(const std::string& fieldName, const std::string& what)
void EventStream::start_ping()
{
- ping.expires_from_now(boost::posix_time::seconds(m_keepAlivePingInterval.count()));
+ ping.expires_after(m_keepAlivePingInterval);
ping.async_wait([weak = weak_from_this()](const boost::system::error_code& ec) {
auto myself = weak.lock();
if (!myself) {
diff --git a/src/http/EventStream.h b/src/http/EventStream.h
index 87d8c82..f9f0dd6 100644
--- a/src/http/EventStream.h
+++ b/src/http/EventStream.h
@@ -7,7 +7,7 @@
#pragma once
-#include <boost/asio/deadline_timer.hpp>
+#include <boost/asio/steady_timer.hpp>
#include <boost/signals2.hpp>
#include <list>
#include <memory>
@@ -50,7 +50,7 @@ private:
};
State state = WaitingForEvents;
- boost::asio::deadline_timer ping;
+ boost::asio::steady_timer ping;
std::list<std::string> queue;
mutable std::mutex mtx; // for `state` and `queue`
boost::signals2::scoped_connection eventSub, terminateSub;
--
2.43.0
@@ -0,0 +1,81 @@
From 4493316404f31354ea18bc92a1706a3834ad972b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Mon, 6 Oct 2025 22:50:11 +0200
Subject: [PATCH 20/38] tests: use a monotonic timer
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: I89cdd08082b025643dac81788ca174d3c2177089
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
tests/restconf-eventstream.cpp | 2 +-
tests/restconf_utils.cpp | 6 +++---
tests/restconf_utils.h | 4 ++--
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/tests/restconf-eventstream.cpp b/tests/restconf-eventstream.cpp
index ad1040d..ecb86e3 100644
--- a/tests/restconf-eventstream.cpp
+++ b/tests/restconf-eventstream.cpp
@@ -101,7 +101,7 @@ TEST_CASE("Event stream tests")
netconfWatcher,
"/streams/NETCONF/JSON",
std::map<std::string, std::string>{AUTH_ROOT},
- boost::posix_time::seconds{5},
+ std::chrono::seconds{5},
SSEClient::ReportIgnoredLines::Yes);
RUN_LOOP_WITH_EXCEPTIONS;
diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp
index 740b1fa..cb4e26a 100644
--- a/tests/restconf_utils.cpp
+++ b/tests/restconf_utils.cpp
@@ -172,10 +172,10 @@ SSEClient::SSEClient(
const RestconfNotificationWatcher& eventWatcher,
const std::string& uri,
const std::map<std::string, std::string>& headers,
- const boost::posix_time::seconds silenceTimeout,
+ const std::chrono::seconds silenceTimeout,
const ReportIgnoredLines reportIgnoredLines)
: client(std::make_shared<ng_client::session>(io, server_address, server_port))
- , t(io, silenceTimeout)
+ , t(io, std::chrono::seconds(silenceTimeout))
{
ng::header_map reqHeaders;
for (const auto& [name, value] : headers) {
@@ -201,7 +201,7 @@ SSEClient::SSEClient(
res.on_data([&, silenceTimeout, reportIgnoredLines](const uint8_t* data, std::size_t len) {
dataBuffer.append(std::string(reinterpret_cast<const char*>(data), len));
parseEvents(eventWatcher, reportIgnoredLines);
- t.expires_from_now(silenceTimeout);
+ t.expires_after(std::chrono::seconds(silenceTimeout));
});
});
diff --git a/tests/restconf_utils.h b/tests/restconf_utils.h
index c35df54..a5676f4 100644
--- a/tests/restconf_utils.h
+++ b/tests/restconf_utils.h
@@ -86,7 +86,7 @@ void setupRealNacm(sysrepo::Session session);
struct SSEClient {
std::shared_ptr<ng_client::session> client;
- boost::asio::deadline_timer t;
+ boost::asio::steady_timer t;
std::string dataBuffer;
enum class ReportIgnoredLines {
@@ -102,7 +102,7 @@ struct SSEClient {
const RestconfNotificationWatcher& eventWatcher,
const std::string& uri,
const std::map<std::string, std::string>& headers,
- const boost::posix_time::seconds silenceTimeout = boost::posix_time::seconds(1), // test code; the server should respond "soon"
+ const std::chrono::seconds silenceTimeout = std::chrono::seconds{1}, // test code; the server should respond "soon"
const ReportIgnoredLines reportIgnoredLines = ReportIgnoredLines::No);
void parseEvents(const RestconfNotificationWatcher& eventWatcher, const ReportIgnoredLines reportIgnoredLines);
--
2.43.0
@@ -0,0 +1,360 @@
From 8077a63ac97529d56782d2315f23c44f6036b85f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 23 Oct 2025 18:17:37 +0200
Subject: [PATCH 21/38] port to libyang v4
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
- the prefix of the "ietf-netconf-with-defaults" module has changed,
- RPC parsing needs a flag to reject unknown input data nodes,
- due to the "printed context" mess, we now need an extra flag to ensure
that we can still access the YANG schema source.
The last item is interesting. We could also read directly from the
on-disk location, but that looks a bit meh. It's also a bit annoying
that one has to change this thing over a lot of places, but I think that
this is much nicer than trying to force a change over an existing
connection (e.g., via a connection-specific call in the Server::Server
ctor which might hit some internal locks depending on how the sysrepo
state look like).
I was considering adding a wrapper for `ly_ctx_get_options()` and a
check in Server::Server, but then I decided that I won't bother.
Oh, and this "disable printed context" is also needed for the RESTCONF
*client* which is used in the unit test. The `lyd_parse_op` cannot parse
notifications *iff* the `notifications` module from RFC 5277 is missing
*and* when the libyang context does not have the "parsed" info from the
`ietf-yang-types` available. The printed context, as provided by
sysrepo, does not have that data available. This is all papered over as
long as the context flag setting works, which is the case with
post-4.2.4 version of sysrepo (still untagged). Yay.
We still set these flags to a single value in all of the unit tests and
in the main() as well to maintain the remaining bits of our sanity, of
course.
Change-Id: I142f650affb53411b5c1dd81311400a2aa6dec20
Depends-on: https://gerrit.cesnet.cz/c/CzechLight/dependencies/+/8990
Bug: https://github.com/CESNET/libyang/pull/2448
Depends-on: https://github.com/sysrepo/sysrepo/commit/6dc5641762962b93d54a54443e2fd43aa319a7a6
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 6 +++---
src/restconf/Server.cpp | 2 +-
src/restconf/main.cpp | 4 ++++
tests/restconf-defaults.cpp | 2 ++
tests/restconf-delete.cpp | 2 ++
tests/restconf-eventstream.cpp | 1 +
tests/restconf-nacm.cpp | 2 ++
tests/restconf-notifications.cpp | 1 +
tests/restconf-plain-patch.cpp | 2 ++
tests/restconf-reading.cpp | 12 +++++++-----
tests/restconf-rpc.cpp | 2 ++
tests/restconf-writing.cpp | 2 ++
tests/restconf-yang-patch.cpp | 2 ++
tests/restconf-yang-schema.cpp | 2 ++
14 files changed, 33 insertions(+), 9 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c0304af..1223ded 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -74,9 +74,9 @@ find_package(PkgConfig)
pkg_check_modules(nghttp2 REQUIRED IMPORTED_TARGET libnghttp2_asio>=0.0.90 libnghttp2) # To compile under boost 1.87 you have to patch nghttp2-asio using https://github.com/nghttp2/nghttp2-asio/issues/23
find_package(Boost 1.66 REQUIRED CONFIG COMPONENTS system thread)
-pkg_check_modules(SYSREPO REQUIRED sysrepo>=3.6.5 IMPORTED_TARGET)
-pkg_check_modules(SYSREPO-CPP REQUIRED IMPORTED_TARGET sysrepo-cpp>=6)
-pkg_check_modules(LIBYANG-CPP REQUIRED IMPORTED_TARGET libyang-cpp>=4)
+pkg_check_modules(SYSREPO REQUIRED sysrepo IMPORTED_TARGET)
+pkg_check_modules(SYSREPO-CPP REQUIRED IMPORTED_TARGET sysrepo-cpp>=7)
+pkg_check_modules(LIBYANG-CPP REQUIRED IMPORTED_TARGET libyang-cpp>=5)
pkg_check_modules(SYSTEMD IMPORTED_TARGET libsystemd)
pkg_check_modules(PAM REQUIRED IMPORTED_TARGET pam)
pkg_check_modules(DOCOPT REQUIRED IMPORTED_TARGET docopt)
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 74c06f7..633f0bb 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -487,7 +487,7 @@ void processActionOrRPC(std::shared_ptr<RequestContext> requestCtx, const std::c
auto [parent, rpcNode] = ctx.newPath2(requestCtx->restconfRequest.path);
if (!requestCtx->payload.empty()) {
- rpcNode->parseOp(requestCtx->payload, *requestCtx->dataFormat.request, libyang::OperationType::RpcRestconf);
+ rpcNode->parseOp(requestCtx->payload, *requestCtx->dataFormat.request, libyang::OperationType::RpcRestconf, libyang::ParseOptions::Strict);
}
std::optional<libyang::DataNode> rpcReply;
diff --git a/src/restconf/main.cpp b/src/restconf/main.cpp
index 477e846..b1e2bc6 100644
--- a/src/restconf/main.cpp
+++ b/src/restconf/main.cpp
@@ -22,6 +22,7 @@
#include <docopt.h>
#include <spdlog/spdlog.h>
#include <sysrepo-cpp/Session.hpp>
+#include <sysrepo-cpp/utils/utils.hpp>
#include "restconf/Server.h"
static const char usage[] =
R"(Rousette - RESTCONF server
@@ -103,6 +104,9 @@ int main(int argc, char* argv [])
throw std::runtime_error("Could not set locale C.UTF-8");
}
+ // schema access is required
+ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate);
+
auto conn = sysrepo::Connection{};
auto server = rousette::restconf::Server{conn, "::1", "10080", timeout};
diff --git a/tests/restconf-defaults.cpp b/tests/restconf-defaults.cpp
index 89083f1..e783e59 100644
--- a/tests/restconf-defaults.cpp
+++ b/tests/restconf-defaults.cpp
@@ -9,6 +9,7 @@
static const auto SERVER_PORT = "10087";
#include <nghttp2/asio_http2.h>
#include <spdlog/spdlog.h>
+#include <sysrepo-cpp/utils/utils.hpp>
#include "restconf/Server.h"
#include "tests/aux-utils.h"
#include "tests/event_watchers.h"
@@ -18,6 +19,7 @@ TEST_CASE("default handling")
trompeloeil::sequence seq1;
spdlog::set_level(spdlog::level::trace);
+ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate);
auto srConn = sysrepo::Connection{};
auto srSess = srConn.sessionStart(sysrepo::Datastore::Running);
auto nacmGuard = manageNacm(srSess);
diff --git a/tests/restconf-delete.cpp b/tests/restconf-delete.cpp
index 4818ff3..3b716bd 100644
--- a/tests/restconf-delete.cpp
+++ b/tests/restconf-delete.cpp
@@ -8,6 +8,7 @@
static const auto SERVER_PORT = "10086";
#include <nghttp2/asio_http2.h>
#include <spdlog/spdlog.h>
+#include <sysrepo-cpp/utils/utils.hpp>
#include "restconf/Server.h"
#include "tests/aux-utils.h"
#include "tests/event_watchers.h"
@@ -16,6 +17,7 @@ static const auto SERVER_PORT = "10086";
TEST_CASE("deleting data")
{
spdlog::set_level(spdlog::level::trace);
+ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate);
auto srConn = sysrepo::Connection{};
auto srSess = srConn.sessionStart(sysrepo::Datastore::Running);
auto nacmGuard = manageNacm(srSess);
diff --git a/tests/restconf-eventstream.cpp b/tests/restconf-eventstream.cpp
index ecb86e3..a099f54 100644
--- a/tests/restconf-eventstream.cpp
+++ b/tests/restconf-eventstream.cpp
@@ -29,6 +29,7 @@ TEST_CASE("Event stream tests")
std::vector<std::unique_ptr<trompeloeil::expectation>> expectations;
+ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate);
auto srConn = sysrepo::Connection{};
auto srSess = srConn.sessionStart(sysrepo::Datastore::Running);
srSess.sendRPC(srSess.getContext().newPath("/ietf-factory-default:factory-reset"));
diff --git a/tests/restconf-nacm.cpp b/tests/restconf-nacm.cpp
index 7799fe8..0e06d56 100644
--- a/tests/restconf-nacm.cpp
+++ b/tests/restconf-nacm.cpp
@@ -10,11 +10,13 @@ static const auto SERVER_PORT = "10082";
#include "tests/aux-utils.h"
#include <nghttp2/asio_http2.h>
#include <spdlog/spdlog.h>
+#include <sysrepo-cpp/utils/utils.hpp>
#include "restconf/Server.h"
TEST_CASE("NACM")
{
spdlog::set_level(spdlog::level::trace);
+ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate);
auto srConn = sysrepo::Connection{};
auto srSess = srConn.sessionStart(sysrepo::Datastore::Running);
auto nacmGuard = manageNacm(srSess);
diff --git a/tests/restconf-notifications.cpp b/tests/restconf-notifications.cpp
index 04131d7..ac355d8 100644
--- a/tests/restconf-notifications.cpp
+++ b/tests/restconf-notifications.cpp
@@ -29,6 +29,7 @@ TEST_CASE("NETCONF notification streams")
std::vector<std::unique_ptr<trompeloeil::expectation>> expectations;
+ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate);
auto srConn = sysrepo::Connection{};
auto srSess = srConn.sessionStart(sysrepo::Datastore::Running);
srSess.sendRPC(srSess.getContext().newPath("/ietf-factory-default:factory-reset"));
diff --git a/tests/restconf-plain-patch.cpp b/tests/restconf-plain-patch.cpp
index 34813d1..c7e035d 100644
--- a/tests/restconf-plain-patch.cpp
+++ b/tests/restconf-plain-patch.cpp
@@ -8,6 +8,7 @@
static const auto SERVER_PORT = "10089";
#include <nghttp2/asio_http2.h>
#include <spdlog/spdlog.h>
+#include <sysrepo-cpp/utils/utils.hpp>
#include "restconf/Server.h"
#include "tests/aux-utils.h"
#include "tests/event_watchers.h"
@@ -16,6 +17,7 @@ static const auto SERVER_PORT = "10089";
TEST_CASE("Plain patch")
{
spdlog::set_level(spdlog::level::trace);
+ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate);
auto srConn = sysrepo::Connection{};
auto srSess = srConn.sessionStart(sysrepo::Datastore::Running);
auto nacmGuard = manageNacm(srSess);
diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp
index 4f0d2ee..b32a598 100644
--- a/tests/restconf-reading.cpp
+++ b/tests/restconf-reading.cpp
@@ -10,12 +10,14 @@ static const auto SERVER_PORT = "10081";
#include "tests/aux-utils.h"
#include <nghttp2/asio_http2.h>
#include <spdlog/spdlog.h>
+#include <sysrepo-cpp/utils/utils.hpp>
#include "restconf/Server.h"
#include "tests/event_watchers.h"
TEST_CASE("reading data")
{
spdlog::set_level(spdlog::level::trace);
+ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate);
auto srConn = sysrepo::Connection{};
auto srSess = srConn.sessionStart(sysrepo::Datastore::Running);
srSess.sendRPC(srSess.getContext().newPath("/ietf-factory-default:factory-reset"));
@@ -794,7 +796,7 @@ TEST_CASE("reading data")
"c": {
"enabled": true,
"@enabled": {
- "ietf-netconf-with-defaults:default": true
+ "default:default": true
}
}
},
@@ -803,7 +805,7 @@ TEST_CASE("reading data")
"c": {
"enabled": true,
"@enabled": {
- "ietf-netconf-with-defaults:default": true
+ "default:default": true
}
}
}
@@ -868,7 +870,7 @@ TEST_CASE("reading data")
"c": {
"enabled": true,
"@enabled": {
- "ietf-netconf-with-defaults:default": true
+ "default:default": true
}
}
},
@@ -877,7 +879,7 @@ TEST_CASE("reading data")
"c": {
"enabled": true,
"@enabled": {
- "ietf-netconf-with-defaults:default": true
+ "default:default": true
}
}
}
@@ -932,7 +934,7 @@ TEST_CASE("reading data")
"c": {
"enabled": true,
"@enabled": {
- "ietf-netconf-with-defaults:default": true
+ "default:default": true
}
}
}
diff --git a/tests/restconf-rpc.cpp b/tests/restconf-rpc.cpp
index 43aaed4..bcbd397 100644
--- a/tests/restconf-rpc.cpp
+++ b/tests/restconf-rpc.cpp
@@ -8,6 +8,7 @@
static const auto SERVER_PORT = "10084";
#include <nghttp2/asio_http2.h>
#include <spdlog/spdlog.h>
+#include <sysrepo-cpp/utils/utils.hpp>
#include "restconf/Server.h"
#include "tests/aux-utils.h"
#include "tests/event_watchers.h"
@@ -33,6 +34,7 @@ std::map<std::string, std::string> nodesToMap(libyang::DataNode node)
TEST_CASE("invoking actions and rpcs")
{
spdlog::set_level(spdlog::level::trace);
+ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate);
auto srConn = sysrepo::Connection{};
auto srSess = srConn.sessionStart(sysrepo::Datastore::Running);
SUBSCRIBE_MODULE(sub1, srSess, "example");
diff --git a/tests/restconf-writing.cpp b/tests/restconf-writing.cpp
index d37ab03..3797b72 100644
--- a/tests/restconf-writing.cpp
+++ b/tests/restconf-writing.cpp
@@ -8,6 +8,7 @@
static const auto SERVER_PORT = "10083";
#include <nghttp2/asio_http2.h>
#include <spdlog/spdlog.h>
+#include <sysrepo-cpp/utils/utils.hpp>
#include "restconf/Server.h"
#include "tests/aux-utils.h"
#include "tests/event_watchers.h"
@@ -16,6 +17,7 @@ static const auto SERVER_PORT = "10083";
TEST_CASE("writing data")
{
spdlog::set_level(spdlog::level::trace);
+ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate);
auto srConn = sysrepo::Connection{};
auto srSess = srConn.sessionStart(sysrepo::Datastore::Running);
auto nacmGuard = manageNacm(srSess);
diff --git a/tests/restconf-yang-patch.cpp b/tests/restconf-yang-patch.cpp
index 2b35c59..19e0405 100644
--- a/tests/restconf-yang-patch.cpp
+++ b/tests/restconf-yang-patch.cpp
@@ -8,6 +8,7 @@
static const auto SERVER_PORT = "10090";
#include <nghttp2/asio_http2.h>
#include <spdlog/spdlog.h>
+#include <sysrepo-cpp/utils/utils.hpp>
#include "restconf/Server.h"
#include "tests/aux-utils.h"
#include "tests/event_watchers.h"
@@ -16,6 +17,7 @@ static const auto SERVER_PORT = "10090";
TEST_CASE("YANG patch")
{
spdlog::set_level(spdlog::level::trace);
+ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate);
auto srConn = sysrepo::Connection{};
auto srSess = srConn.sessionStart(sysrepo::Datastore::Running);
auto nacmGuard = manageNacm(srSess);
diff --git a/tests/restconf-yang-schema.cpp b/tests/restconf-yang-schema.cpp
index 6e374b1..b56367d 100644
--- a/tests/restconf-yang-schema.cpp
+++ b/tests/restconf-yang-schema.cpp
@@ -9,12 +9,14 @@
static const auto SERVER_PORT = "10085";
#include <nghttp2/asio_http2.h>
#include <spdlog/spdlog.h>
+#include <sysrepo-cpp/utils/utils.hpp>
#include "restconf/Server.h"
#include "tests/aux-utils.h"
TEST_CASE("obtaining YANG schemas")
{
spdlog::set_level(spdlog::level::trace);
+ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate);
auto srConn = sysrepo::Connection{};
auto srSess = srConn.sessionStart(sysrepo::Datastore::Running);
srSess.sendRPC(srSess.getContext().newPath("/ietf-factory-default:factory-reset"));
--
2.43.0
@@ -0,0 +1,63 @@
From b4bf655502d949a5f351e6acd313f47eecefb062 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Mon, 13 Oct 2025 12:26:46 +0200
Subject: [PATCH 22/38] restconf: prevent lock order inversion
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
The locked section in EventStream's implementation of nghttp's on_close
callback. Without doing that, we might get a lock order inversion in
case somebody tries to lock the mutex inside onClientDisconnectedCb. And
this would indeed happen with the next patch. If that is applied, then
there are indeed two different mutexes involved and the lock order
inversion might happen.
- Case 1: A thread acquires the mutex (let's call it M1) in
DynamicNotificationHTTPStream::awaitNextNotification and
then sends the notification signal, which acquires the mutex inside
EventStream (mutex M2).
- Case 2: the nghttp's on_close callback acquired M2 and
then proceeds to call onClientDisconnectedCb which, in our case,
acquires M1.
One can view this as a fixup of commit 6bca750 ("http: add optional
callbacks to EventStream"), I am not sure.
See-also: 6bca750f866b5b14c4d9c3da68e5c8f1e4eee36c ("http: add optional callbacks to EventStream")
Change-Id: Ib6626c127ff2eb5feb1d17dece28e70265749df4
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/http/EventStream.cpp | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp
index 280addd..9f19030 100644
--- a/src/http/EventStream.cpp
+++ b/src/http/EventStream.cpp
@@ -82,11 +82,15 @@ void EventStream::activate()
res.on_close([myself](const auto ec) {
spdlog::debug("{}: closed ({})", myself->peer, nghttp2_http2_strerror(ec));
- std::lock_guard lock{myself->mtx};
- myself->ping.cancel();
- myself->eventSub.disconnect();
- myself->terminateSub.disconnect();
- myself->state = Closed;
+
+ {
+ std::lock_guard lock{myself->mtx};
+ myself->ping.cancel();
+ myself->eventSub.disconnect();
+ myself->terminateSub.disconnect();
+ myself->state = Closed;
+ }
+
if (myself->onClientDisconnectedCb) {
myself->onClientDisconnectedCb();
}
--
2.43.0
@@ -0,0 +1,39 @@
From 27cbe6750bdfc9f346421e55ebe555b8c036d77c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Tue, 11 Nov 2025 15:01:21 +0100
Subject: [PATCH 23/38] refactor: do not pollute scope with unnecessary
variables
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
The parent variable was not used in the function, it contributed
nothing.
Change-Id: I5b9b1a077d44d088084c3b2b93359ab7464ec8aa
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/restconf/Server.cpp | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 633f0bb..77ae1be 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -446,9 +446,9 @@ std::optional<libyang::DataNode> processInternalRPC(sysrepo::Session& sess, cons
const auto rpcPath = rpcInput.path();
// Is the user authorized to call the operation?
- auto [parent, rpcNode] = sess.getContext().newPath2(rpcPath, std::nullopt);
- if (!sess.checkNacmOperation(*rpcNode)) {
- throw ErrorResponse(403, "application", "access-denied", "Access denied.", rpcNode->path());
+ auto rpcNode = sess.getContext().newPath2(rpcPath, std::nullopt);
+ if (!sess.checkNacmOperation(*rpcNode.createdNode)) {
+ throw ErrorResponse(403, "application", "access-denied", "Access denied.", rpcNode.createdNode->path());
}
if (auto it = handlers.find(rpcPath); it != handlers.end()) {
--
2.43.0
@@ -0,0 +1,89 @@
From 5592ae49cd7607fdcb2c9e554a521b53545e761b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Tue, 21 Oct 2025 17:23:44 +0200
Subject: [PATCH 24/38] restconf: validate input for internal rpcs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Jan found an issue where invalid input to internal rpcs was accepted. It
turns out that the issue was in parsing, but not validating input for
internally processed RPCs (those RPCs for subscribed notifications).
This patch adds the validation using a wrapper for lyd_validate_op from
libyang-cpp.
I shuffled a little bit of the code around together with this change to
make it cleaner.
Also, to make the code generic, we store the name of the internal RPC
and a XPath for fetching data required for validation (see
lyd_validate_op docs). The code validates the input data before processing
the RPC.
Change-Id: I55296895e6993b4ca27d21ce2a64ec2d159a35fc
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/restconf/Server.cpp | 36 +++++++++++++++++++++++++++++-------
1 file changed, 29 insertions(+), 7 deletions(-)
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 77ae1be..7e8db58 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -438,9 +438,12 @@ libyang::CreatedNodes createEditForPutAndPatch(libyang::Context& ctx, const std:
return {editNode, replacementNode};
}
-std::optional<libyang::DataNode> processInternalRPC(sysrepo::Session& sess, const libyang::DataNode& rpcInput, const libyang::DataFormat requestEncoding)
+std::optional<libyang::DataNode> processInternalRPC(sysrepo::Session& sess, libyang::DataNode& rpcInput, const libyang::DataFormat requestEncoding)
{
- using InternalRPCHandler = std::function<void(sysrepo::Session&, const libyang::DataFormat, const libyang::DataNode&, libyang::DataNode&)>;
+ struct InternalRPCHandler {
+ std::optional<std::string> validationDataXPath; ///< XPath to data used for RPC input validation
+ std::function<void(sysrepo::Session&, const libyang::DataFormat, const libyang::DataNode&, libyang::DataNode&)> rpcHandler; // The function that processes the RPC
+ };
const std::map<std::string, InternalRPCHandler> handlers;
const auto rpcPath = rpcInput.path();
@@ -451,13 +454,32 @@ std::optional<libyang::DataNode> processInternalRPC(sysrepo::Session& sess, cons
throw ErrorResponse(403, "application", "access-denied", "Access denied.", rpcNode.createdNode->path());
}
- if (auto it = handlers.find(rpcPath); it != handlers.end()) {
- auto [parent, rpcOutput] = sess.getContext().newPath2(rpcPath, std::nullopt);
- it->second(sess, requestEncoding, rpcInput, *rpcOutput);
- return *parent;
+ auto handlerIt = handlers.find(rpcPath);
+ if (handlerIt == handlers.end()) {
+ throw ErrorResponse(501, "application", "operation-not-supported", "Unsupported RPC call to " + rpcPath, rpcPath);
+ }
+
+ // RPC input validation.
+ sess.switchDatastore(sysrepo::Datastore::Operational);
+ auto validationData = handlerIt->second.validationDataXPath ? sess.getData(*handlerIt->second.validationDataXPath) : std::nullopt;
+
+ try {
+ libyang::validateOp(rpcInput, validationData, libyang::OperationType::RpcRestconf);
+ } catch (const libyang::ErrorWithCode& exc) {
+ const auto errors = sess.getContext().getErrors();
+
+ if (!errors.empty()) {
+ const auto& error = errors.back();
+ throw ErrorResponse(400, "protocol", "invalid-value", error.message, error.dataPath.value_or(rpcInput.path()));
+ } else {
+ // Fallback if no detailed error is available
+ throw ErrorResponse(400, "protocol", "invalid-value", "Input validation failed"s, rpcInput.path());
+ }
}
- throw ErrorResponse(501, "application", "operation-not-supported", "Unsupported RPC call to " + rpcPath, rpcPath);
+ auto [parent, rpcOutput] = sess.getContext().newPath2(rpcPath, std::nullopt);
+ handlerIt->second.rpcHandler(sess, requestEncoding, rpcInput, *rpcOutput);
+ return *parent;
}
void processActionOrRPC(std::shared_ptr<RequestContext> requestCtx, const std::chrono::milliseconds timeout)
--
2.43.0
@@ -0,0 +1,71 @@
From c2413237501eea3125fe31a9701919d14402737f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Tue, 11 Nov 2025 17:30:33 +0100
Subject: [PATCH 25/38] tests: allow read from parts
ietf-subscribed-notifications for anonymous user
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
The subsequent commits will add more tests for subscribed notifications.
Because the validation of
ietf-subscribed-notifications:establish-subscription will require read
access to ietf-subscribed-notifications:filters, we need to allow the
access for users.
Change-Id: I50e49c8c60b7e6be47c51f3d8cd546fbb6f49294
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
tests/restconf-reading.cpp | 5 ++++-
tests/restconf_utils.cpp | 4 ++++
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp
index b32a598..b11bc48 100644
--- a/tests/restconf-reading.cpp
+++ b/tests/restconf-reading.cpp
@@ -110,6 +110,7 @@ TEST_CASE("reading data")
]
}
},
+ "ietf-subscribed-notifications:filters": {},
"ietf-system:system": {
"contact": "contact",
"hostname": "hostname",
@@ -168,6 +169,7 @@ TEST_CASE("reading data")
]
}
},
+ "ietf-subscribed-notifications:filters": {},
"ietf-system:system": {
"contact": "contact",
"hostname": "hostname",
@@ -195,7 +197,8 @@ TEST_CASE("reading data")
"example:config-nonconfig": {
"config-node": "foo-config-true"
},
- "example:ordered-lists": {}
+ "example:ordered-lists": {},
+ "ietf-subscribed-notifications:filters": {}
}
)"});
}
diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp
index cb4e26a..80dca05 100644
--- a/tests/restconf_utils.cpp
+++ b/tests/restconf_utils.cpp
@@ -156,6 +156,10 @@ void setupRealNacm(sysrepo::Session session)
session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='16']/action", "permit");
session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='16']/access-operations", "exec");
session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='16']/rpc-name", "establish-subscription");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='17']/module-name", "ietf-subscribed-notifications");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='17']/action", "permit");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='17']/access-operations", "read");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='17']/path", "/ietf-subscribed-notifications:filters");
session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='99']/module-name", "*");
session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='99']/action", "deny");
session.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/group[.='optics']", "");
--
2.43.0
@@ -0,0 +1,679 @@
From aa04b0a99fb51aed998aa9f1ab0a527cc09e647f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Mon, 19 May 2025 12:11:09 +0200
Subject: [PATCH 26/38] restconf: establish-subscription RPC for subscribed
notifications
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
This is a very bare-bones implementation of the subscribed
notifications concept from RFC 8639 [1]. We only support the
establish-subscription RPC to the NETCONF event stream.
This patch *does not* ship the code which implements the endpoint for
client connection because this patch is already too large and the
endpoint is not strictly necessary for the RPC to work. It will come in
the follow-up patch.
[1] https://datatracker.ietf.org/doc/rfc8639/
Change-Id: I0217e5abc56cfd73859dbcc610fb1f342dc33a10
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 8 +-
src/restconf/DynamicSubscriptions.cpp | 177 ++++++++++++++
src/restconf/DynamicSubscriptions.h | 57 +++++
src/restconf/Server.cpp | 25 +-
src/restconf/Server.h | 2 +
tests/restconf-subscribed-notifications.cpp | 253 ++++++++++++++++++++
6 files changed, 513 insertions(+), 9 deletions(-)
create mode 100644 src/restconf/DynamicSubscriptions.cpp
create mode 100644 src/restconf/DynamicSubscriptions.h
create mode 100644 tests/restconf-subscribed-notifications.cpp
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1223ded..18af5ba 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -110,6 +110,7 @@ add_library(rousette-auth STATIC
target_link_libraries(rousette-auth PUBLIC spdlog::spdlog PkgConfig::SYSREPO-CPP PkgConfig::PAM rousette-auth-pam PkgConfig::nghttp2)
add_library(rousette-restconf STATIC
+ src/restconf/DynamicSubscriptions.cpp
src/restconf/NotificationStream.cpp
src/restconf/Server.cpp
src/restconf/YangSchemaLocations.cpp
@@ -211,7 +212,11 @@ if(BUILD_TESTING)
--install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-restconf@2017-01-26.yang
--install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-restconf-monitoring@2017-01-26.yang
--install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-yang-patch@2017-02-22.yang
- --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-subscribed-notifications@2019-09-09.yang
+ --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/iana-if-type@2014-05-08.yang
+ --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-interfaces@2018-02-20.yang
+ --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-ip@2018-02-22.yang
+ --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-network-instance@2019-01-21.yang
+ --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-subscribed-notifications@2019-09-09.yang --enable-feature replay --enable-feature encode-xml --enable-feature encode-json
--install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-restconf-subscribed-notifications@2019-11-17.yang
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example.yang --enable-feature f1
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-delete.yang
@@ -228,6 +233,7 @@ if(BUILD_TESTING)
rousette_test(NAME restconf-plain-patch LIBRARIES rousette-restconf FIXTURE common-models WRAP_PAM)
rousette_test(NAME restconf-yang-patch LIBRARIES rousette-restconf FIXTURE common-models WRAP_PAM)
rousette_test(NAME restconf-eventstream LIBRARIES rousette-restconf FIXTURE common-models WRAP_PAM)
+ rousette_test(NAME restconf-subscribed-notifications LIBRARIES rousette-restconf FIXTURE common-models WRAP_PAM)
set(nested-models
${common-models}
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/root-mod.yang)
diff --git a/src/restconf/DynamicSubscriptions.cpp b/src/restconf/DynamicSubscriptions.cpp
new file mode 100644
index 0000000..7b13990
--- /dev/null
+++ b/src/restconf/DynamicSubscriptions.cpp
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@cesnet.cz>
+ *
+*/
+#include <boost/uuid/uuid_io.hpp>
+#include <fmt/ostream.h>
+#include <libyang-cpp/Time.hpp>
+#include <nghttp2/asio_http2_server.h>
+#include <spdlog/spdlog.h>
+#include <sysrepo-cpp/utils/exception.hpp>
+#include "restconf/DynamicSubscriptions.h"
+#include "restconf/Exceptions.h"
+
+namespace {
+
+/** @brief Parses the YANG date-and-time attribute from the RPC input, if present
+ *
+ * @param rpcInput The RPC input node.
+ * @param path The path to the YANG leaf.
+ */
+std::optional<sysrepo::NotificationTimeStamp> optionalTime(const libyang::DataNode& rpcInput, const std::string& path)
+{
+ if (auto stopTimeNode = rpcInput.findPath(path)) {
+ return libyang::fromYangTimeFormat<sysrepo::NotificationTimeStamp::clock>(stopTimeNode->asTerm().valueStr());
+ }
+
+ return std::nullopt;
+}
+
+libyang::DataFormat getEncoding(const libyang::DataNode& rpcInput, const libyang::DataFormat requestEncoding)
+{
+ /* FIXME: So far we allow only encode-json or encode-xml encoding values and not their derived values.
+ * We do not know what those derived values might mean and how do they change the meaning of the encoding leaf.
+ */
+ if (auto encodingNode = rpcInput.findPath("encoding")) {
+ const auto encodingStr = encodingNode->asTerm().valueStr();
+ if (encodingStr == "ietf-subscribed-notifications:encode-json") {
+ return libyang::DataFormat::JSON;
+ } else if (encodingStr == "ietf-subscribed-notifications:encode-xml") {
+ return libyang::DataFormat::XML;
+ } else {
+ throw rousette::restconf::ErrorResponse(400, "application", "invalid-attribute", "Unsupported encoding in establish-subscription: '" + encodingStr + "'. Currently we support only 'encode-xml' and 'encode-json' identities.");
+ }
+ }
+
+ return requestEncoding;
+}
+
+sysrepo::DynamicSubscription makeStreamSubscription(sysrepo::Session& session, const libyang::DataNode& rpcInput)
+{
+ auto streamNode = rpcInput.findPath("stream");
+
+ if (!streamNode) {
+ throw rousette::restconf::ErrorResponse(400, "application", "invalid-attribute", "Stream is required");
+ }
+
+ if (rpcInput.findPath("stream-filter-name")) {
+ throw rousette::restconf::ErrorResponse(400, "application", "invalid-attribute", "Stream filtering is not supported");
+ }
+
+ auto stopTime = optionalTime(rpcInput, "stop-time");
+
+ return session.subscribeNotifications(
+ std::nullopt /* TODO xpath filter */,
+ streamNode->asTerm().valueStr(),
+ stopTime,
+ std::nullopt /* TODO replayStart */);
+}
+}
+
+namespace rousette::restconf {
+
+DynamicSubscriptions::DynamicSubscriptions(const std::string& streamRootUri)
+ : m_restconfStreamUri(streamRootUri)
+ , m_uuidGenerator(boost::uuids::random_generator())
+{
+}
+
+DynamicSubscriptions::~DynamicSubscriptions() = default;
+
+void DynamicSubscriptions::establishSubscription(sysrepo::Session& session, const libyang::DataFormat requestEncoding, const libyang::DataNode& rpcInput, libyang::DataNode& rpcOutput)
+{
+ // Generate a new UUID associated with the subscription. The UUID will be used as a part of the URI so that the URI is not predictable (RFC 8650, section 5)
+ auto uuid = makeUUID();
+
+ auto dataFormat = getEncoding(rpcInput, requestEncoding);
+
+ try {
+ auto sub = makeStreamSubscription(session, rpcInput);
+
+ rpcOutput.newPath("id", std::to_string(sub.subscriptionId()), libyang::CreationOptions::Output);
+ rpcOutput.newPath("ietf-restconf-subscribed-notifications:uri", m_restconfStreamUri + "subscribed/" + boost::uuids::to_string(uuid), libyang::CreationOptions::Output);
+
+ std::lock_guard lock(m_mutex);
+ m_subscriptions[uuid] = std::make_shared<SubscriptionData>(
+ std::move(sub),
+ dataFormat,
+ uuid,
+ *session.getNacmUser());
+ } catch (const sysrepo::ErrorWithCode& e) {
+ throw ErrorResponse(400, "application", "invalid-attribute", e.what());
+ }
+}
+
+void DynamicSubscriptions::terminateSubscription(const uint32_t subId)
+{
+ std::lock_guard lock(m_mutex);
+
+ auto it = std::find_if(m_subscriptions.begin(), m_subscriptions.end(), [subId](const auto& entry) {
+ return entry.second->subscription.subscriptionId() == subId;
+ });
+
+ if (it == m_subscriptions.end()) {
+ spdlog::warn("Requested termination of subscription with id {}, but subscription not found", subId);
+ return;
+ }
+
+ const auto& [uuid, subscriptionData] = *it;
+ spdlog::debug("{}: termination requested", fmt::streamed(*subscriptionData));
+ subscriptionData->subscription.terminate("ietf-subscribed-notifications:no-such-subscription");
+ m_subscriptions.erase(uuid);
+}
+
+/** @brief Returns the subscription data for the given UUID and user.
+ *
+ * @param uuid The UUID of the subscription.
+ * @return A shared pointer to the SubscriptionData object if found and user is the one who established the subscription (or NACM recovery user), otherwise nullptr.
+ */
+std::shared_ptr<DynamicSubscriptions::SubscriptionData> DynamicSubscriptions::getSubscriptionForUser(const boost::uuids::uuid& uuid, const std::optional<std::string>& user)
+{
+ std::lock_guard lock(m_mutex);
+ if (auto it = m_subscriptions.find(uuid); it != m_subscriptions.end() && (it->second->user == user || user == sysrepo::Session::getNacmRecoveryUser())) {
+ return it->second;
+ }
+
+ return nullptr;
+}
+
+boost::uuids::uuid DynamicSubscriptions::makeUUID()
+{
+ // uuid generator instance accesses must be synchronized (https://www.boost.org/doc/libs/1_88_0/libs/uuid/doc/html/uuid.html#design_notes)
+ std::lock_guard lock(m_mutex);
+ return m_uuidGenerator();
+}
+
+DynamicSubscriptions::SubscriptionData::SubscriptionData(
+ sysrepo::DynamicSubscription sub,
+ libyang::DataFormat format,
+ boost::uuids::uuid uuid,
+ const std::string& user)
+ : subscription(std::move(sub))
+ , dataFormat(format)
+ , uuid(uuid)
+ , user(user)
+{
+ spdlog::debug("{}: created", fmt::streamed(*this));
+}
+
+DynamicSubscriptions::SubscriptionData::~SubscriptionData()
+{
+ try {
+ subscription.terminate();
+ } catch (const sysrepo::ErrorWithCode& e) { // Maybe it was already terminated (stop-time).
+ spdlog::warn("Failed to terminate {}: {}", fmt::streamed(*this), e.what());
+ }
+}
+
+std::ostream& operator<<(std::ostream& os, const DynamicSubscriptions::SubscriptionData& sub)
+{
+ return os << "dynamic subscription (id " << sub.subscription.subscriptionId()
+ << ", user " << sub.user
+ << ", uuid " << boost::uuids::to_string(sub.uuid)
+ << ")";
+}
+}
diff --git a/src/restconf/DynamicSubscriptions.h b/src/restconf/DynamicSubscriptions.h
new file mode 100644
index 0000000..366d1a1
--- /dev/null
+++ b/src/restconf/DynamicSubscriptions.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@cesnet.cz>
+ *
+*/
+#pragma once
+
+#include <boost/asio.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/uuid/random_generator.hpp>
+#include <map>
+#include <memory>
+#include <sysrepo-cpp/Subscription.hpp>
+
+namespace libyang {
+enum class DataFormat;
+}
+
+namespace rousette::restconf {
+
+/** Dynamic subscriptions manager.
+ *
+ * Stores all dynamic subscriptions and provides a way to retrieve them by the UUID.
+ * */
+class DynamicSubscriptions {
+public:
+ struct SubscriptionData : public std::enable_shared_from_this<SubscriptionData> {
+ sysrepo::DynamicSubscription subscription;
+ libyang::DataFormat dataFormat; ///< Encoding of the notification stream
+ boost::uuids::uuid uuid; ///< UUID is part of the GET URI, it identifies subscriptions for clients
+ std::string user; ///< User who initiated the establish-subscription RPC
+
+ SubscriptionData(
+ sysrepo::DynamicSubscription sub,
+ libyang::DataFormat format,
+ boost::uuids::uuid uuid,
+ const std::string& user);
+ ~SubscriptionData();
+ };
+
+ DynamicSubscriptions(const std::string& streamRootUri);
+ ~DynamicSubscriptions();
+ std::shared_ptr<SubscriptionData> getSubscriptionForUser(const boost::uuids::uuid& uuid, const std::optional<std::string>& user);
+ void establishSubscription(sysrepo::Session& session, const libyang::DataFormat requestEncoding, const libyang::DataNode& rpcInput, libyang::DataNode& rpcOutput);
+
+private:
+ std::mutex m_mutex; ///< Lock for shared data (subscriptions storage and uuid generator)
+ std::string m_restconfStreamUri;
+ std::map<boost::uuids::uuid, std::shared_ptr<SubscriptionData>> m_subscriptions;
+ boost::uuids::random_generator m_uuidGenerator;
+
+ void terminateSubscription(const uint32_t subId);
+
+ boost::uuids::uuid makeUUID();
+};
+}
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 7e8db58..7287976 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -438,13 +438,16 @@ libyang::CreatedNodes createEditForPutAndPatch(libyang::Context& ctx, const std:
return {editNode, replacementNode};
}
-std::optional<libyang::DataNode> processInternalRPC(sysrepo::Session& sess, libyang::DataNode& rpcInput, const libyang::DataFormat requestEncoding)
+std::optional<libyang::DataNode> processInternalRPC(sysrepo::Session& sess, libyang::DataNode& rpcInput, const libyang::DataFormat requestEncoding, DynamicSubscriptions& dynamicSubscriptions)
{
struct InternalRPCHandler {
std::optional<std::string> validationDataXPath; ///< XPath to data used for RPC input validation
std::function<void(sysrepo::Session&, const libyang::DataFormat, const libyang::DataNode&, libyang::DataNode&)> rpcHandler; // The function that processes the RPC
};
- const std::map<std::string, InternalRPCHandler> handlers;
+ const std::map<std::string, InternalRPCHandler> handlers{
+ {"/ietf-subscribed-notifications:establish-subscription",
+ {"/ietf-subscribed-notifications:filters", [&dynamicSubscriptions](auto&&... args) { return dynamicSubscriptions.establishSubscription(std::forward<decltype(args)>(args)...); }}},
+ };
const auto rpcPath = rpcInput.path();
@@ -482,7 +485,7 @@ std::optional<libyang::DataNode> processInternalRPC(sysrepo::Session& sess, liby
return *parent;
}
-void processActionOrRPC(std::shared_ptr<RequestContext> requestCtx, const std::chrono::milliseconds timeout)
+void processActionOrRPC(std::shared_ptr<RequestContext> requestCtx, const std::chrono::milliseconds timeout, DynamicSubscriptions& dynamicSubscriptions)
{
requestCtx->sess.switchDatastore(sysrepo::Datastore::Operational);
auto ctx = requestCtx->sess.getContext();
@@ -516,7 +519,7 @@ void processActionOrRPC(std::shared_ptr<RequestContext> requestCtx, const std::c
if (requestCtx->restconfRequest.type == RestconfRequest::Type::Execute) {
rpcReply = requestCtx->sess.sendRPC(*rpcNode, timeout);
} else if (requestCtx->restconfRequest.type == RestconfRequest::Type::ExecuteInternal) {
- rpcReply = processInternalRPC(requestCtx->sess, *rpcNode, *requestCtx->dataFormat.request);
+ rpcReply = processInternalRPC(requestCtx->sess, *rpcNode, *requestCtx->dataFormat.request, dynamicSubscriptions);
}
if (!rpcReply || rpcReply->immediateChildren().empty()) {
@@ -902,10 +905,16 @@ std::vector<std::shared_ptr<boost::asio::io_context>> Server::io_services() cons
return server->io_services();
}
-Server::Server(sysrepo::Connection conn, const std::string& address, const std::string& port, const std::chrono::milliseconds timeout, const std::chrono::seconds keepAlivePingInterval)
+Server::Server(
+ sysrepo::Connection conn,
+ const std::string& address,
+ const std::string& port,
+ const std::chrono::milliseconds timeout,
+ const std::chrono::seconds keepAlivePingInterval)
: m_monitoringSession(conn.sessionStart(sysrepo::Datastore::Operational))
, nacm(conn)
, server{std::make_unique<nghttp2::asio_http2::server::http2>()}
+ , m_dynamicSubscriptions(netconfStreamRoot)
, dwdmEvents{std::make_unique<sr::OpticalEvents>(conn.sessionStart())}
{
server->num_threads(1); // we only use one thread for the server, so we can call join() right away
@@ -917,7 +926,7 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std::
{"ietf-netconf", "", {}},
{"ietf-yang-library", "2019-01-04", {}},
{"ietf-yang-patch", "2017-02-22", {}},
- {"ietf-subscribed-notifications", "2019-09-09", {}},
+ {"ietf-subscribed-notifications", "2019-09-09", {"encode-xml", "encode-json"}},
{"ietf-restconf-subscribed-notifications", "2019-11-17", {}},
}) {
if (auto mod = m_monitoringSession.getContext().getModuleImplemented(module)) {
@@ -1217,12 +1226,12 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std::
case RestconfRequest::Type::ExecuteInternal: {
auto requestCtx = std::make_shared<RequestContext>(req, res, dataFormat, sess, restconfRequest);
- req.on_data([requestCtx, timeout, peer=http::peer_from_request(req)](const uint8_t* data, std::size_t length) {
+ req.on_data([this, requestCtx, timeout, peer=http::peer_from_request(req)](const uint8_t* data, std::size_t length) {
if (length > 0) {
requestCtx->payload.append(reinterpret_cast<const char*>(data), length);
} else {
spdlog::trace("{}: HTTP payload: {}", peer, requestCtx->payload);
- WITH_RESTCONF_EXCEPTIONS(processActionOrRPC, rejectWithError)(requestCtx, timeout);
+ WITH_RESTCONF_EXCEPTIONS(processActionOrRPC, rejectWithError)(requestCtx, timeout, m_dynamicSubscriptions);
}
});
break;
diff --git a/src/restconf/Server.h b/src/restconf/Server.h
index 0720c8d..c98f736 100644
--- a/src/restconf/Server.h
+++ b/src/restconf/Server.h
@@ -10,6 +10,7 @@
#include <sysrepo-cpp/Subscription.hpp>
#include "auth/Nacm.h"
#include "http/EventStream.h"
+#include "restconf/DynamicSubscriptions.h"
namespace nghttp2::asio_http2::server {
class http2;
@@ -43,6 +44,7 @@ private:
std::optional<sysrepo::Subscription> m_monitoringOperSub;
auth::Nacm nacm;
std::unique_ptr<nghttp2::asio_http2::server::http2> server;
+ DynamicSubscriptions m_dynamicSubscriptions;
std::unique_ptr<sr::OpticalEvents> dwdmEvents;
using JsonDiffSignal = boost::signals2::signal<void(const std::string& json)>;
JsonDiffSignal opticsChange;
diff --git a/tests/restconf-subscribed-notifications.cpp b/tests/restconf-subscribed-notifications.cpp
new file mode 100644
index 0000000..3e8e011
--- /dev/null
+++ b/tests/restconf-subscribed-notifications.cpp
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@cesnet.cz>
+ *
+ */
+
+#include "trompeloeil_doctest.h"
+static const auto SERVER_PORT = "10092";
+#include <libyang-cpp/Time.hpp>
+#include <nghttp2/asio_http2.h>
+#include <regex>
+#include <spdlog/spdlog.h>
+#include <sysrepo-cpp/utils/utils.hpp>
+#include "restconf/Server.h"
+#include "tests/aux-utils.h"
+#include "tests/event_watchers.h"
+#include "tests/pretty_printers.h"
+
+#define SEND_NOTIFICATION(DATA) notifSession.sendNotification(*ctx.parseOp(DATA, libyang::DataFormat::JSON, libyang::OperationType::NotificationYang).op, sysrepo::Wait::No);
+
+constexpr auto uuidV4Regex = "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[89abAB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}";
+
+using namespace std::chrono_literals;
+using namespace std::string_literals;
+
+/** @brief Calls establish-subscription rpc, returns the url of the stream associated with the created subscription */
+std::string establishSubscription(
+ const libyang::Context& ctx,
+ const libyang::DataFormat rpcEncoding,
+ const std::optional<std::pair<std::string, std::string>>& rpcRequestAuthHeader,
+ const std::optional<std::string>& encodingLeafValue)
+{
+ constexpr auto jsonPrefix = "ietf-subscribed-notifications";
+ constexpr auto xmlNamespace = "urn:ietf:params:xml:ns:yang:ietf-subscribed-notifications";
+
+ auto stopTime = libyang::yangTimeFormat(std::chrono::system_clock::now() + 5s, libyang::TimezoneInterpretation::Local);
+ std::map<std::string, std::string> requestHeaders;
+ ng::header_map expectedHeaders;
+
+ if (rpcRequestAuthHeader) {
+ requestHeaders.insert(*rpcRequestAuthHeader);
+ }
+
+ std::optional<libyang::DataNode> envelope;
+ auto rpcTree = ctx.newPath("/ietf-subscribed-notifications:establish-subscription");
+ rpcTree.newPath("stream", "NETCONF");
+ rpcTree.newPath("stop-time", stopTime);
+
+ if (encodingLeafValue) {
+ rpcTree.newPath("encoding", *encodingLeafValue);
+ }
+
+ switch (rpcEncoding) {
+ case libyang::DataFormat::JSON:
+ requestHeaders.insert(CONTENT_TYPE_JSON);
+ expectedHeaders = jsonHeaders;
+ envelope = ctx.newOpaqueJSON({jsonPrefix, jsonPrefix, "input"}, std::nullopt);
+ break;
+ case libyang::DataFormat::XML:
+ requestHeaders.insert(CONTENT_TYPE_XML);
+ expectedHeaders = xmlHeaders;
+ envelope = ctx.newOpaqueXML({xmlNamespace, jsonPrefix, "input"}, std::nullopt);
+ break;
+ default:
+ FAIL("Unhandled libyang DataFormat");
+ break;
+ }
+
+ // reconnect everything
+ auto data = rpcTree.child();
+ data->unlinkWithSiblings();
+ envelope->insertChild(*data);
+
+ auto body = *envelope->printStr(rpcEncoding, libyang::PrintFlags::WithSiblings);
+ auto resp = post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:establish-subscription", requestHeaders, body);
+ REQUIRE(resp.equalStatusCodeAndHeaders(Response{200, expectedHeaders, ""}));
+
+ auto reply = ctx.newPath("/ietf-subscribed-notifications:establish-subscription");
+ REQUIRE(reply.parseOp(resp.data, rpcEncoding, libyang::OperationType::ReplyRestconf).tree);
+
+ auto urlNode = reply.findPath("ietf-restconf-subscribed-notifications:uri", libyang::InputOutputNodes::Output);
+ REQUIRE(urlNode);
+
+ return urlNode->asTerm().valueStr();
+}
+
+TEST_CASE("RESTCONF subscribed notifications")
+{
+ trompeloeil::sequence seq1, seq2;
+ sysrepo::setLogLevelStderr(sysrepo::LogLevel::Information);
+ spdlog::set_level(spdlog::level::trace);
+
+ auto srConn = sysrepo::Connection{};
+ auto srSess = srConn.sessionStart(sysrepo::Datastore::Running);
+ srSess.sendRPC(srSess.getContext().newPath("/ietf-factory-default:factory-reset"));
+
+ auto nacmGuard = manageNacm(srSess);
+ auto server = rousette::restconf::Server{srConn, SERVER_ADDRESS, SERVER_PORT};
+ setupRealNacm(srSess);
+
+ RestconfNotificationWatcher netconfWatcher(srConn.sessionStart().getContext());
+
+ libyang::DataFormat rpcRequestEncoding = libyang::DataFormat::JSON;
+ std::optional<std::string> rpcSubscriptionEncoding;
+ std::optional<std::pair<std::string, std::string>> rpcRequestAuthHeader;
+
+ SECTION("NACM authorization")
+ {
+ SECTION("Anonymous access for establish-subscription is disabled")
+ {
+ /* Remove anonymous user's permission to execute RPCs in ietf-subscribed-notifications.
+ * Intentionally a SECTION. We can remove NACM rule for a while in order to test access denied.
+ * The rule gets automatically restored for the rest of the tests.
+ */
+ srSess.switchDatastore(sysrepo::Datastore::Running);
+ srSess.deleteItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='16']");
+ srSess.applyChanges();
+
+ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:establish-subscription", {CONTENT_TYPE_JSON}, R"###({ "ietf-subscribed-notifications:input": { "stream": "NETCONF" } })###")
+ == Response{403, jsonHeaders, R"###({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "application",
+ "error-tag": "access-denied",
+ "error-path": "/ietf-subscribed-notifications:establish-subscription",
+ "error-message": "Access denied."
+ }
+ ]
+ }
+}
+)###"});
+ }
+ }
+
+ SECTION("Invalid establish-subscription requests")
+ {
+ // stop-time in the past
+ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:establish-subscription", {CONTENT_TYPE_JSON}, R"###({ "ietf-subscribed-notifications:input": { "stream": "NETCONF", "stop-time": "1999-09-09T09:09:09Z" } })###")
+ == Response{400, jsonHeaders, R"###({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "application",
+ "error-tag": "invalid-attribute",
+ "error-message": "Couldn't create notification subscription: SR_ERR_INVAL_ARG\u000A Specified \"stop-time\" is in the past. (SR_ERR_INVAL_ARG)"
+ }
+ ]
+ }
+}
+)###"});
+
+ // invalid stream
+ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:establish-subscription", {CONTENT_TYPE_JSON}, R"###({ "ietf-subscribed-notifications:input": { "stream": "ajsdhauisds" } })###")
+ == Response{400, jsonHeaders, R"###({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "application",
+ "error-tag": "invalid-attribute",
+ "error-message": "Couldn't create notification subscription: SR_ERR_NOT_FOUND\u000A Failed to collect modules to subscribe to, invalid stream and/or XPath filter (Item not found). (SR_ERR_NOT_FOUND)"
+ }
+ ]
+ }
+}
+)###"});
+
+ // stream-filter-name is unsupported, but leafref validation triggers first
+ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:establish-subscription", {CONTENT_TYPE_JSON}, R"###({ "ietf-subscribed-notifications:input": { "stream": "NETCONF", "stream-filter-name": "xyz" } })###")
+ == Response{400, jsonHeaders, R"###({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "protocol",
+ "error-tag": "invalid-value",
+ "error-path": "/ietf-subscribed-notifications:establish-subscription/stream-filter-name",
+ "error-message": "Invalid leafref value \"xyz\" - no target instance \"/sn:filters/sn:stream-filter/sn:name\" with the same value."
+ }
+ ]
+ }
+}
+)###"});
+ }
+
+ SECTION("Valid requests")
+ {
+ SECTION("XML stream")
+ {
+ netconfWatcher.setDataFormat(libyang::DataFormat::XML);
+ rpcRequestAuthHeader = AUTH_ROOT;
+
+ SECTION("Stream encoding inferred from request content-type")
+ {
+ rpcRequestEncoding = libyang::DataFormat::XML;
+ }
+
+ SECTION("Explicitly asked for XML stream encoding")
+ {
+ rpcSubscriptionEncoding = "encode-xml";
+
+ SECTION("Request content-type JSON")
+ {
+ rpcRequestEncoding = libyang::DataFormat::JSON;
+ }
+
+ SECTION("Request content-type XML")
+ {
+ rpcRequestEncoding = libyang::DataFormat::XML;
+ }
+ }
+ }
+
+ SECTION("JSON stream")
+ {
+ netconfWatcher.setDataFormat(libyang::DataFormat::JSON);
+
+ SECTION("NACM: anonymous user cannot read example-notif module")
+ {
+ rpcRequestAuthHeader = std::nullopt;
+ rpcRequestEncoding = libyang::DataFormat::JSON;
+ rpcSubscriptionEncoding = "encode-json";
+ }
+
+ SECTION("Content-type with set encode leaf")
+ {
+ rpcRequestAuthHeader = AUTH_ROOT;
+
+ SECTION("Stream encoding inferred from request content-type")
+ {
+ rpcRequestEncoding = libyang::DataFormat::JSON;
+ }
+
+ SECTION("Explicitly asked for JSON stream encoding")
+ {
+ rpcSubscriptionEncoding = "encode-json";
+ SECTION("Request content-type JSON")
+ {
+ rpcRequestEncoding = libyang::DataFormat::JSON;
+ }
+
+ SECTION("Request content-type XML")
+ {
+ rpcRequestEncoding = libyang::DataFormat::XML;
+ }
+ }
+ }
+ }
+
+ auto uri = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding);
+ REQUIRE(std::regex_match(uri, std::regex("/streams/subscribed/"s + uuidV4Regex)));
+ }
+}
--
2.43.0
@@ -0,0 +1,165 @@
From e2c8036130a2265476579ffa9f4354741b54bbd5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Mon, 13 Oct 2025 16:10:51 +0200
Subject: [PATCH 27/38] refactor: restconf stream request types
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
In commit 97ceef1 ("restconf: refactor uri parser for stream URIs") we
prepared the URI parser for the new subscribed stream notifications.
What I did not think through was that the subscribed notifications do
not have query parameters. However, I refactored the types in a way
that the query parameters member is now in the common base type.
This patch fixes that by editing the types in a way that only the
stream notifications will have the query parameters member. The
type for subscribed notifications (which will come in a followup patch)
will not have it.
See-also: 97ceef119c900c37bbaa27860c3b43cfa6d69f95 ("restconf: refactor uri parser for stream URIs")
Change-Id: Idc1e05f7ff05cea84b5806f60e217605f75e7a9f
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/restconf/Server.cpp | 2 +-
src/restconf/uri.cpp | 15 ++++++++-------
src/restconf/uri.h | 14 +++++++-------
src/restconf/uri_impl.h | 2 +-
tests/uri-parser.cpp | 12 ++++++------
5 files changed, 23 insertions(+), 22 deletions(-)
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 7287976..616d7d4 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -1020,7 +1020,7 @@ Server::Server(
shutdownRequested,
keepAlivePingInterval,
sess,
- streamRequest.type.encoding,
+ streamRequest.encoding,
xpathFilter,
startTime,
stopTime);
diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp
index 956c155..6ee011c 100644
--- a/src/restconf/uri.cpp
+++ b/src/restconf/uri.cpp
@@ -58,11 +58,11 @@ const auto resources = x3::rule<class resources, URIPath>{"resources"} =
const auto uriGrammar = x3::rule<class grammar, URIPath>{"grammar"} = x3::lit("/restconf") >> resources;
-const auto netconfStream = x3::rule<class netconfStream, RestconfStreamRequest::NetconfStream>{"netconfStream"} =
+const auto netconfStream = x3::rule<class netconfStream, NetconfStreamRequest>{"netconfStream"} =
x3::lit("/NETCONF") >>
((x3::lit("/XML") >> x3::attr(libyang::DataFormat::XML)) |
(x3::lit("/JSON") >> x3::attr(libyang::DataFormat::JSON)));
-const auto streamUriGrammar = x3::rule<class grammar, RestconfStreamRequest::NetconfStream>{"streamsGrammar"} = x3::lit("/streams") >> netconfStream;
+const auto streamUriGrammar = x3::rule<class grammar, NetconfStreamRequest>{"streamsGrammar"} = x3::lit("/streams") >> netconfStream;
// clang-format on
}
@@ -204,9 +204,9 @@ std::optional<queryParams::QueryParams> parseQueryParams(const std::string& quer
return ret;
}
-std::optional<RestconfStreamRequest::NetconfStream> parseStreamUri(const std::string& uriPath)
+std::optional<RestconfStreamRequest> parseStreamUri(const std::string& uriPath)
{
- std::optional<RestconfStreamRequest::NetconfStream> ret;
+ std::optional<RestconfStreamRequest> ret;
if (!x3::parse(std::begin(uriPath), std::end(uriPath), streamUriGrammar >> x3::eoi, ret)) {
return std::nullopt;
@@ -668,8 +668,8 @@ std::optional<std::variant<libyang::Module, libyang::SubmoduleParsed>> asYangMod
return std::nullopt;
}
-RestconfStreamRequest::NetconfStream::NetconfStream() = default;
-RestconfStreamRequest::NetconfStream::NetconfStream(const libyang::DataFormat& encoding)
+NetconfStreamRequest::NetconfStreamRequest() = default;
+NetconfStreamRequest::NetconfStreamRequest(const libyang::DataFormat& encoding)
: encoding(encoding)
{
}
@@ -692,7 +692,8 @@ RestconfStreamRequest asRestconfStreamRequest(const std::string& httpMethod, con
validateQueryParametersForStream(*queryParameters);
- return {*type, *queryParameters};
+ type->queryParams = *queryParameters;
+ return *type;
}
/** @brief Returns a set of allowed HTTP methods for given URI. Usable for the 'allow' header */
diff --git a/src/restconf/uri.h b/src/restconf/uri.h
index d9dd29f..f6c63a7 100644
--- a/src/restconf/uri.h
+++ b/src/restconf/uri.h
@@ -197,16 +197,16 @@ struct RestconfRequest {
RestconfRequest(Type type, const boost::optional<ApiIdentifier>& datastore, const std::string& path, const queryParams::QueryParams& queryParams);
};
-struct RestconfStreamRequest {
- struct NetconfStream {
- libyang::DataFormat encoding;
-
- NetconfStream();
- NetconfStream(const libyang::DataFormat& encoding);
- } type;
+struct NetconfStreamRequest {
+ libyang::DataFormat encoding;
queryParams::QueryParams queryParams;
+
+ NetconfStreamRequest();
+ NetconfStreamRequest(const libyang::DataFormat& encoding);
};
+using RestconfStreamRequest = NetconfStreamRequest;
+
RestconfRequest asRestconfRequest(const libyang::Context& ctx, const std::string& httpMethod, const std::string& uriPath, const std::string& uriQueryString = "");
std::optional<libyang::SchemaNode> asLibyangSchemaNode(const libyang::Context& ctx, const std::vector<PathSegment>& pathSegments);
std::pair<std::string, PathSegment> asLibyangPathSplit(const libyang::Context& ctx, const std::string& uriPath);
diff --git a/src/restconf/uri_impl.h b/src/restconf/uri_impl.h
index 00b22f6..bc36cca 100644
--- a/src/restconf/uri_impl.h
+++ b/src/restconf/uri_impl.h
@@ -66,7 +66,7 @@ BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::impl::YangModule, name, revision);
BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::PathSegment, apiIdent, keys);
BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::ApiIdentifier, prefix, identifier);
-BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::RestconfStreamRequest::NetconfStream, encoding);
+BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::NetconfStreamRequest, encoding);
BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::queryParams::fields::ParenExpr, lhs, rhs);
BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::queryParams::fields::SlashExpr, lhs, rhs);
diff --git a/tests/uri-parser.cpp b/tests/uri-parser.cpp
index 23c2b79..38ca80f 100644
--- a/tests/uri-parser.cpp
+++ b/tests/uri-parser.cpp
@@ -1082,15 +1082,15 @@ TEST_CASE("URI path parser")
using rousette::restconf::RestconfStreamRequest;
{
- auto [type, queryParams] = asRestconfStreamRequest("GET", "/streams/NETCONF/XML", "");
- REQUIRE(type.encoding == libyang::DataFormat::XML);
- REQUIRE(queryParams.empty());
+ auto req = asRestconfStreamRequest("GET", "/streams/NETCONF/XML", "");
+ REQUIRE(req.encoding == libyang::DataFormat::XML);
+ REQUIRE(req.queryParams.empty());
}
{
- auto [type, queryParams] = asRestconfStreamRequest("GET", "/streams/NETCONF/JSON", "");
- REQUIRE(type.encoding == libyang::DataFormat::JSON);
- REQUIRE(queryParams.empty());
+ auto req = asRestconfStreamRequest("GET", "/streams/NETCONF/JSON", "");
+ REQUIRE(req.encoding == libyang::DataFormat::JSON);
+ REQUIRE(req.queryParams.empty());
}
REQUIRE_THROWS_WITH_AS(asRestconfStreamRequest("GET", "/streams/NETCONF", ""),
--
2.43.0
@@ -0,0 +1,893 @@
From b3a55ae0a1ef9e092b0c8f0d7c3dbd7baa708cc8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Tue, 29 Jul 2025 11:39:21 +0200
Subject: [PATCH 28/38] restconf: endpoint for subscribed notifications
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
This patch creates a new endpoint for the subscribed notification
streams in the RESTCONF server. The previous commit added the
establish-subscription RPC [1], which returned a stream URI, that was
not accessible in that patch. The patch was extra large even without
that functionality so I have split it into two commits.
There is one more catch worth mentioning both here and in the comments:
In case sysrepo sends events fast enough it is possible that the
function responsible for reading them blocks the event loop and the
function responsible for sending the actual data to the client is not
called. To mitigate that, I added limit to the number of events read
in one go.
However, we are not sure if that is enough. Is it possible that the
reading function might get called several times in a row? My knowledge
of boost::asio is not enough.
[1] https://datatracker.ietf.org/doc/rfc8639/
Change-Id: I07dcdd3fb9ef4f05f93ae6dda0e0d71094a0bbfc
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 1 +
src/restconf/DynamicSubscriptions.cpp | 127 ++++++++++++-
src/restconf/DynamicSubscriptions.h | 46 +++++
src/restconf/Server.cpp | 51 +++--
src/restconf/uri.cpp | 46 ++++-
src/restconf/uri.h | 10 +-
src/restconf/uri_impl.h | 1 +
src/restconf/utils/io.cpp | 30 +++
src/restconf/utils/io.h | 12 ++
tests/restconf-subscribed-notifications.cpp | 199 ++++++++++++++++++++
tests/uri-parser.cpp | 41 +++-
11 files changed, 527 insertions(+), 37 deletions(-)
create mode 100644 src/restconf/utils/io.cpp
create mode 100644 src/restconf/utils/io.h
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 18af5ba..5d3b7ce 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -116,6 +116,7 @@ add_library(rousette-restconf STATIC
src/restconf/YangSchemaLocations.cpp
src/restconf/uri.cpp
src/restconf/utils/dataformat.cpp
+ src/restconf/utils/io.cpp
src/restconf/utils/sysrepo.cpp
src/restconf/utils/yang.cpp
)
diff --git a/src/restconf/DynamicSubscriptions.cpp b/src/restconf/DynamicSubscriptions.cpp
index 7b13990..7f4b171 100644
--- a/src/restconf/DynamicSubscriptions.cpp
+++ b/src/restconf/DynamicSubscriptions.cpp
@@ -12,6 +12,8 @@
#include <sysrepo-cpp/utils/exception.hpp>
#include "restconf/DynamicSubscriptions.h"
#include "restconf/Exceptions.h"
+#include "restconf/utils/io.h"
+#include "restconf/utils/yang.h"
namespace {
@@ -119,7 +121,7 @@ void DynamicSubscriptions::terminateSubscription(const uint32_t subId)
const auto& [uuid, subscriptionData] = *it;
spdlog::debug("{}: termination requested", fmt::streamed(*subscriptionData));
- subscriptionData->subscription.terminate("ietf-subscribed-notifications:no-such-subscription");
+ subscriptionData->terminate("ietf-subscribed-notifications:no-such-subscription");
m_subscriptions.erase(uuid);
}
@@ -154,14 +156,54 @@ DynamicSubscriptions::SubscriptionData::SubscriptionData(
, dataFormat(format)
, uuid(uuid)
, user(user)
+ , state(State::Start)
{
spdlog::debug("{}: created", fmt::streamed(*this));
}
DynamicSubscriptions::SubscriptionData::~SubscriptionData()
{
+ terminate();
+}
+
+void DynamicSubscriptions::SubscriptionData::clientDisconnected()
+{
+ spdlog::debug("{}: client disconnected", fmt::streamed(*this));
+
+ std::lock_guard lock(mutex);
+ if (state == State::Terminating) {
+ return;
+ }
+
+ state = State::Start;
+}
+
+void DynamicSubscriptions::SubscriptionData::clientConnected()
+{
+ spdlog::debug("{}: client connected", fmt::streamed(*this));
+ std::lock_guard lock(mutex);
+ state = State::ReceiverActive;
+}
+
+bool DynamicSubscriptions::SubscriptionData::isReadyToAcceptClient() const
+{
+ std::lock_guard lock(mutex);
+ return state == State::Start;
+}
+
+void DynamicSubscriptions::SubscriptionData::terminate(const std::optional<std::string>& reason)
+{
+ std::lock_guard lock(mutex);
+
+ // already terminating, do nothing
+ if (state == State::Terminating) {
+ return;
+ }
+
+ state = State::Terminating;
+ spdlog::debug("{}: terminating subscription ({})", fmt::streamed(*this), reason.value_or("<no reason>"));
try {
- subscription.terminate();
+ subscription.terminate(reason);
} catch (const sysrepo::ErrorWithCode& e) { // Maybe it was already terminated (stop-time).
spdlog::warn("Failed to terminate {}: {}", fmt::streamed(*this), e.what());
}
@@ -174,4 +216,85 @@ std::ostream& operator<<(std::ostream& os, const DynamicSubscriptions::Subscript
<< ", uuid " << boost::uuids::to_string(sub.uuid)
<< ")";
}
+
+DynamicSubscriptionHttpStream::DynamicSubscriptionHttpStream(
+ const nghttp2::asio_http2::server::request& req,
+ const nghttp2::asio_http2::server::response& res,
+ rousette::http::EventStream::Termination& termination,
+ std::shared_ptr<rousette::http::EventStream::EventSignal> signal,
+ const std::chrono::seconds keepAlivePingInterval,
+ const std::shared_ptr<DynamicSubscriptions::SubscriptionData>& subscriptionData)
+ : EventStream(
+ req,
+ res,
+ termination,
+ *signal,
+ keepAlivePingInterval,
+ std::nullopt /* no initial event */,
+ [this]() { m_subscriptionData->terminate("ietf-subscribed-notifications:no-such-subscription"); },
+ [this]() { m_subscriptionData->clientDisconnected(); })
+ , m_subscriptionData(subscriptionData)
+ , m_signal(signal)
+ , m_stream(res.io_service(), m_subscriptionData->subscription.fd())
+{
+}
+
+DynamicSubscriptionHttpStream::~DynamicSubscriptionHttpStream()
+{
+ // The stream does not own the file descriptor, sysrepo does. It will be closed when the subscription terminates.
+ m_stream.release();
+}
+
+/** @brief Waits for the next notifications and process them */
+void DynamicSubscriptionHttpStream::awaitNextNotification()
+{
+ constexpr auto MAX_EVENTS = 50;
+
+ m_stream.async_wait(boost::asio::posix::stream_descriptor::wait_read, [this](const boost::system::error_code& err) {
+ // Unfortunately wait_read does not return operation_aborted when the file descriptor is closed and poll results in POLLHUP
+ if (err == boost::asio::error::operation_aborted || utils::pipeIsClosedAndNoData(m_subscriptionData->subscription.fd())) {
+ return;
+ }
+
+ size_t eventsProcessed = 0;
+ /* Process all the available notifications, but at most N
+ * In case sysrepo is providing the events fast enough, this loop would still run inside the event loop
+ * and the event responsible for sending the data to the client would not get to be processed.
+ * TODO: Is this enough? What if this async_wait keeps getting called and nothing gets sent?
+ */
+ while (++eventsProcessed < MAX_EVENTS && utils::pipeHasData(m_subscriptionData->subscription.fd())) {
+ std::lock_guard lock(m_subscriptionData->mutex); // sysrepo-cpp's processEvent and terminate is not thread safe
+ m_subscriptionData->subscription.processEvent([&](const std::optional<libyang::DataNode>& notificationTree, const sysrepo::NotificationTimeStamp& time) {
+ (*m_signal)(rousette::restconf::as_restconf_notification(
+ m_subscriptionData->subscription.getSession().getContext(),
+ m_subscriptionData->dataFormat,
+ *notificationTree,
+ time));
+ });
+ }
+
+ // and wait for more
+ awaitNextNotification();
+ });
+}
+
+void DynamicSubscriptionHttpStream::activate()
+{
+ m_subscriptionData->clientConnected();
+ EventStream::activate();
+ awaitNextNotification();
+}
+
+std::shared_ptr<DynamicSubscriptionHttpStream> DynamicSubscriptionHttpStream::create(
+ const nghttp2::asio_http2::server::request& req,
+ const nghttp2::asio_http2::server::response& res,
+ rousette::http::EventStream::Termination& termination,
+ const std::chrono::seconds keepAlivePingInterval,
+ const std::shared_ptr<DynamicSubscriptions::SubscriptionData>& subscriptionData)
+{
+ auto signal = std::make_shared<rousette::http::EventStream::EventSignal>();
+ auto stream = std::shared_ptr<DynamicSubscriptionHttpStream>(new DynamicSubscriptionHttpStream(req, res, termination, signal, keepAlivePingInterval, subscriptionData));
+ stream->activate();
+ return stream;
+}
}
diff --git a/src/restconf/DynamicSubscriptions.h b/src/restconf/DynamicSubscriptions.h
index 366d1a1..1874390 100644
--- a/src/restconf/DynamicSubscriptions.h
+++ b/src/restconf/DynamicSubscriptions.h
@@ -12,6 +12,7 @@
#include <map>
#include <memory>
#include <sysrepo-cpp/Subscription.hpp>
+#include "http/EventStream.h"
namespace libyang {
enum class DataFormat;
@@ -26,17 +27,28 @@ namespace rousette::restconf {
class DynamicSubscriptions {
public:
struct SubscriptionData : public std::enable_shared_from_this<SubscriptionData> {
+ mutable std::mutex mutex;
sysrepo::DynamicSubscription subscription;
libyang::DataFormat dataFormat; ///< Encoding of the notification stream
boost::uuids::uuid uuid; ///< UUID is part of the GET URI, it identifies subscriptions for clients
std::string user; ///< User who initiated the establish-subscription RPC
+ enum class State {
+ Start, ///< Subscription is ready to be consumed by a client
+ ReceiverActive, ///< Subscription is being consumed by a client
+ Terminating, ///< Subscription is being terminated by the server shutdown
+ } state;
+
SubscriptionData(
sysrepo::DynamicSubscription sub,
libyang::DataFormat format,
boost::uuids::uuid uuid,
const std::string& user);
~SubscriptionData();
+ void clientDisconnected();
+ void clientConnected();
+ void terminate(const std::optional<std::string>& reason = std::nullopt);
+ bool isReadyToAcceptClient() const;
};
DynamicSubscriptions(const std::string& streamRootUri);
@@ -54,4 +66,38 @@ private:
boost::uuids::uuid makeUUID();
};
+
+/** @brief Subscribes to sysrepo's subscribed notification and sends the notifications via HTTP/2 Event stream.
+ *
+ * @see rousette::http::EventStream
+ * @see rousette::http::NotificationStream
+ * */
+class DynamicSubscriptionHttpStream : public http::EventStream {
+public:
+ ~DynamicSubscriptionHttpStream();
+
+ static std::shared_ptr<DynamicSubscriptionHttpStream> create(
+ const nghttp2::asio_http2::server::request& req,
+ const nghttp2::asio_http2::server::response& res,
+ rousette::http::EventStream::Termination& termination,
+ const std::chrono::seconds keepAlivePingInterval,
+ const std::shared_ptr<DynamicSubscriptions::SubscriptionData>& subscriptionData);
+
+private:
+ std::shared_ptr<DynamicSubscriptions::SubscriptionData> m_subscriptionData;
+ std::shared_ptr<rousette::http::EventStream::EventSignal> m_signal;
+ boost::asio::posix::stream_descriptor m_stream;
+
+ void awaitNextNotification();
+
+protected:
+ DynamicSubscriptionHttpStream(
+ const nghttp2::asio_http2::server::request& req,
+ const nghttp2::asio_http2::server::response& res,
+ rousette::http::EventStream::Termination& termination,
+ std::shared_ptr<rousette::http::EventStream::EventSignal> signal,
+ const std::chrono::seconds keepAlivePingInterval,
+ const std::shared_ptr<DynamicSubscriptions::SubscriptionData>& subscriptionData);
+ void activate();
+};
}
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 616d7d4..2a8ad33 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -1003,27 +1003,40 @@ Server::Server(
auto streamRequest = asRestconfStreamRequest(req.method(), req.uri().path, req.uri().raw_query);
- if (auto it = streamRequest.queryParams.find("filter"); it != streamRequest.queryParams.end()) {
- xpathFilter = std::get<std::string>(it->second);
- }
+ if (auto *request = std::get_if<SubscribedStreamRequest>(&streamRequest)) {
+ if (auto sub = m_dynamicSubscriptions.getSubscriptionForUser(request->uuid, sess.getNacmUser())) {
+ if (!sub->isReadyToAcceptClient()) {
+ throw ErrorResponse(409, "application", "resource-denied", "There is already another GET request on this subscription.");
+ }
- if (auto it = streamRequest.queryParams.find("start-time"); it != streamRequest.queryParams.end()) {
- startTime = libyang::fromYangTimeFormat<std::chrono::system_clock>(std::get<std::string>(it->second));
- }
- if (auto it = streamRequest.queryParams.find("stop-time"); it != streamRequest.queryParams.end()) {
- stopTime = libyang::fromYangTimeFormat<std::chrono::system_clock>(std::get<std::string>(it->second));
- }
+ DynamicSubscriptionHttpStream::create(req, res, shutdownRequested, keepAlivePingInterval, sub);
+ } else {
+ throw ErrorResponse(404, "application", "invalid-value", "Subscription not found.");
+ }
+ } else if (auto *request = std::get_if<NetconfStreamRequest>(&streamRequest)) {
+ if (auto it = request->queryParams.find("filter"); it != request->queryParams.end()) {
+ xpathFilter = std::get<std::string>(it->second);
+ }
+ if (auto it = request->queryParams.find("start-time"); it != request->queryParams.end()) {
+ startTime = libyang::fromYangTimeFormat<std::chrono::system_clock>(std::get<std::string>(it->second));
+ }
+ if (auto it = request->queryParams.find("stop-time"); it != request->queryParams.end()) {
+ stopTime = libyang::fromYangTimeFormat<std::chrono::system_clock>(std::get<std::string>(it->second));
+ }
- NotificationStream::create(
- req,
- res,
- shutdownRequested,
- keepAlivePingInterval,
- sess,
- streamRequest.encoding,
- xpathFilter,
- startTime,
- stopTime);
+ NotificationStream::create(
+ req,
+ res,
+ shutdownRequested,
+ keepAlivePingInterval,
+ sess,
+ request->encoding,
+ xpathFilter,
+ startTime,
+ stopTime);
+ } else {
+ throw ErrorResponse(500, "protocol", "invalid-value", "Invalid request."); // should not happen
+ }
} catch (const auth::Error& e) {
processAuthError(req, res, e, [&res]() {
res.write_head(401, {TEXT_PLAIN, CORS});
diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp
index 6ee011c..99c3d1f 100644
--- a/src/restconf/uri.cpp
+++ b/src/restconf/uri.cpp
@@ -8,6 +8,8 @@
#include <boost/fusion/adapted/struct/adapt_struct.hpp>
#include <boost/fusion/include/std_pair.hpp>
#include <boost/spirit/home/x3/support/ast/variant.hpp>
+#include <boost/uuid/nil_generator.hpp>
+#include <boost/uuid/string_generator.hpp>
#include <experimental/iterator>
#include <libyang-cpp/Enum.hpp>
#include <map>
@@ -57,12 +59,30 @@ const auto resources = x3::rule<class resources, URIPath>{"resources"} =
((x3::lit("/") | x3::eps) /* /restconf/ and /restconf */ >> x3::attr(URIPrefix{URIPrefix::Type::RestconfRoot}) >> x3::attr(std::vector<PathSegment>{}));
const auto uriGrammar = x3::rule<class grammar, URIPath>{"grammar"} = x3::lit("/restconf") >> resources;
+const auto string_to_uuid = [](auto& ctx) {
+ try {
+ _val(ctx) = boost::uuids::string_generator()(_attr(ctx));
+ _pass(ctx) = true;
+ } catch (const std::runtime_error&) {
+ _pass(ctx) = false;
+ }
+};
+
+const auto uuid_impl = x3::rule<class uuid_impl, std::string>{"uuid_impl"} =
+ x3::repeat(8)[x3::xdigit] >> x3::char_('-') >>
+ x3::repeat(4)[x3::xdigit] >> x3::char_('-') >>
+ x3::repeat(4)[x3::xdigit] >> x3::char_('-') >>
+ x3::repeat(4)[x3::xdigit] >> x3::char_('-') >>
+ x3::repeat(12)[x3::xdigit];
+const auto uuid = x3::rule<class uuid, boost::uuids::uuid>{"uuid"} = uuid_impl[string_to_uuid];
+const auto subscribedStream = x3::rule<class subscribedStream, SubscribedStreamRequest>{"subscribedStream"} = x3::lit("/subscribed") >> "/" >> uuid;
const auto netconfStream = x3::rule<class netconfStream, NetconfStreamRequest>{"netconfStream"} =
x3::lit("/NETCONF") >>
((x3::lit("/XML") >> x3::attr(libyang::DataFormat::XML)) |
(x3::lit("/JSON") >> x3::attr(libyang::DataFormat::JSON)));
-const auto streamUriGrammar = x3::rule<class grammar, NetconfStreamRequest>{"streamsGrammar"} = x3::lit("/streams") >> netconfStream;
+const auto streamUriGrammar = x3::rule<class grammar, std::variant<NetconfStreamRequest, SubscribedStreamRequest>>{"streamsGrammar"} =
+ x3::lit("/streams") >> (netconfStream | subscribedStream);
// clang-format on
}
@@ -674,6 +694,16 @@ NetconfStreamRequest::NetconfStreamRequest(const libyang::DataFormat& encoding)
{
}
+SubscribedStreamRequest::SubscribedStreamRequest()
+ : uuid(boost::uuids::nil_uuid())
+{
+}
+
+SubscribedStreamRequest::SubscribedStreamRequest(const boost::uuids::uuid& uuid)
+ : uuid(uuid)
+{
+}
+
RestconfStreamRequest asRestconfStreamRequest(const std::string& httpMethod, const std::string& uriPath, const std::string& uriQueryString)
{
if (httpMethod != "GET" && httpMethod != "HEAD") {
@@ -685,14 +715,16 @@ RestconfStreamRequest asRestconfStreamRequest(const std::string& httpMethod, con
throw ErrorResponse(404, "application", "invalid-value", "Invalid stream");
}
- auto queryParameters = impl::parseQueryParams(uriQueryString);
- if (!queryParameters) {
- throw ErrorResponse(400, "protocol", "invalid-value", "Query parameters syntax error");
- }
+ if (auto* request = std::get_if<NetconfStreamRequest>(&type.value())) {
+ auto queryParameters = impl::parseQueryParams(uriQueryString);
+ if (!queryParameters) {
+ throw ErrorResponse(400, "protocol", "invalid-value", "Query parameters syntax error");
+ }
- validateQueryParametersForStream(*queryParameters);
+ validateQueryParametersForStream(*queryParameters);
+ request->queryParams = *queryParameters;
+ }
- type->queryParams = *queryParameters;
return *type;
}
diff --git a/src/restconf/uri.h b/src/restconf/uri.h
index f6c63a7..7893c6f 100644
--- a/src/restconf/uri.h
+++ b/src/restconf/uri.h
@@ -6,6 +6,7 @@
#pragma once
#include <boost/optional.hpp>
+#include <boost/uuid/uuid.hpp>
#include <boost/variant.hpp>
#include <libyang-cpp/Module.hpp>
#include <libyang-cpp/SchemaNode.hpp>
@@ -205,7 +206,14 @@ struct NetconfStreamRequest {
NetconfStreamRequest(const libyang::DataFormat& encoding);
};
-using RestconfStreamRequest = NetconfStreamRequest;
+struct SubscribedStreamRequest {
+ boost::uuids::uuid uuid;
+
+ SubscribedStreamRequest();
+ SubscribedStreamRequest(const boost::uuids::uuid& uuid);
+};
+
+using RestconfStreamRequest = std::variant<NetconfStreamRequest, SubscribedStreamRequest>;
RestconfRequest asRestconfRequest(const libyang::Context& ctx, const std::string& httpMethod, const std::string& uriPath, const std::string& uriQueryString = "");
std::optional<libyang::SchemaNode> asLibyangSchemaNode(const libyang::Context& ctx, const std::vector<PathSegment>& pathSegments);
diff --git a/src/restconf/uri_impl.h b/src/restconf/uri_impl.h
index bc36cca..cf57859 100644
--- a/src/restconf/uri_impl.h
+++ b/src/restconf/uri_impl.h
@@ -67,6 +67,7 @@ BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::PathSegment, apiIdent, keys);
BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::ApiIdentifier, prefix, identifier);
BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::NetconfStreamRequest, encoding);
+BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::SubscribedStreamRequest, uuid);
BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::queryParams::fields::ParenExpr, lhs, rhs);
BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::queryParams::fields::SlashExpr, lhs, rhs);
diff --git a/src/restconf/utils/io.cpp b/src/restconf/utils/io.cpp
new file mode 100644
index 0000000..d657096
--- /dev/null
+++ b/src/restconf/utils/io.cpp
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@cesnet.cz>
+ */
+
+#include <poll.h>
+#include "restconf/utils/io.h"
+
+namespace rousette::restconf::utils {
+bool pipeHasData(const int fd)
+{
+ pollfd fds = {
+ .fd = fd,
+ .events = POLLIN | POLLHUP,
+ .revents = 0};
+
+ return poll(&fds, 1, 0) == 1 && fds.revents & POLLIN;
+}
+
+bool pipeIsClosedAndNoData(const int fd)
+{
+ pollfd fds = {
+ .fd = fd,
+ .events = POLLIN | POLLHUP,
+ .revents = 0};
+
+ return poll(&fds, 1, 0) == 1 && fds.revents & POLLHUP && !(fds.revents & POLLIN);
+}
+}
diff --git a/src/restconf/utils/io.h b/src/restconf/utils/io.h
new file mode 100644
index 0000000..ac9f2f8
--- /dev/null
+++ b/src/restconf/utils/io.h
@@ -0,0 +1,12 @@
+/*
+ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@cesnet.cz>
+ */
+
+
+namespace rousette::restconf::utils {
+
+bool pipeHasData(const int fd);
+bool pipeIsClosedAndNoData(const int fd);
+}
diff --git a/tests/restconf-subscribed-notifications.cpp b/tests/restconf-subscribed-notifications.cpp
index 3e8e011..11dd992 100644
--- a/tests/restconf-subscribed-notifications.cpp
+++ b/tests/restconf-subscribed-notifications.cpp
@@ -91,6 +91,7 @@ TEST_CASE("RESTCONF subscribed notifications")
sysrepo::setLogLevelStderr(sysrepo::LogLevel::Information);
spdlog::set_level(spdlog::level::trace);
+ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate);
auto srConn = sysrepo::Connection{};
auto srSess = srConn.sessionStart(sysrepo::Datastore::Running);
srSess.sendRPC(srSess.getContext().newPath("/ietf-factory-default:factory-reset"));
@@ -99,6 +100,19 @@ TEST_CASE("RESTCONF subscribed notifications")
auto server = rousette::restconf::Server{srConn, SERVER_ADDRESS, SERVER_PORT};
setupRealNacm(srSess);
+ // parent for nested notification
+ srSess.switchDatastore(sysrepo::Datastore::Operational);
+ srSess.setItem("/example:tlc/list[name='k1']/choice1", "something must me here");
+ srSess.applyChanges();
+ std::vector<std::string> notificationsJSON{
+ R"({"example:eventA":{"message":"blabla","progress":11}})",
+ R"({"example:eventB":{}})",
+ R"({"example-notif:something-happened":{}})",
+ R"({"example:eventA":{"message":"almost finished","progress":99}})",
+ R"({"example:tlc":{"list":[{"name":"k1","notif":{"message":"nested"}}]}})",
+ };
+ std::vector<std::unique_ptr<trompeloeil::expectation>> expectations;
+
RestconfNotificationWatcher netconfWatcher(srConn.sessionStart().getContext());
libyang::DataFormat rpcRequestEncoding = libyang::DataFormat::JSON;
@@ -132,6 +146,78 @@ TEST_CASE("RESTCONF subscribed notifications")
}
)###"});
}
+
+ SECTION("User DWDM establishes subscription")
+ {
+ std::pair<std::string, std::string> user = AUTH_DWDM;
+ auto uri = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, user, std::nullopt);
+
+ std::map<std::string, std::string> headers;
+
+ SECTION("Users who cannot GET")
+ {
+ SECTION("anonymous") { }
+ SECTION("norules") { headers.insert(AUTH_NORULES); }
+ REQUIRE(get(uri, headers) == Response{404, plaintextHeaders, "Subscription not found."});
+ }
+
+ SECTION("Users who can GET")
+ {
+ SECTION("root") { headers.insert(AUTH_ROOT); }
+ SECTION("dwdm") { headers.insert(AUTH_DWDM); }
+ REQUIRE(head(uri, headers) == Response{200, eventStreamHeaders, ""});
+ }
+
+ SECTION("GET on the same subscription concurently")
+ {
+ std::map<std::string, std::string> headers;
+ std::string response;
+ int status;
+
+ SECTION("Allowed users")
+ {
+ response = "There is already another GET request on this subscription.";
+ status = 409;
+
+ SECTION("Same user")
+ {
+ headers.insert(AUTH_DWDM);
+ }
+ SECTION("Different user")
+ {
+ headers.insert(AUTH_ROOT);
+ }
+ }
+ SECTION("Disallowed user")
+ {
+ headers.insert(AUTH_NORULES);
+ response = "Subscription not found.";
+ status = 404;
+ }
+
+ PREPARE_LOOP_WITH_EXCEPTIONS
+ auto thr = std::jthread(wrap_exceptions_and_asio(bg, io, [&]() {
+ WAIT_UNTIL_SSE_CLIENT_REQUESTS;
+ REQUIRE(get(uri, headers) == Response{status, plaintextHeaders, response});
+ }));
+
+ SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, {AUTH_DWDM});
+ RUN_LOOP_WITH_EXCEPTIONS;
+ }
+
+ SECTION("GET on the same subscription sequentially")
+ {
+ boost::asio::io_service io;
+ {
+ std::binary_semaphore requestSent(0); // SSEClient needs to notify when the request is sent, but we don't care about it here.
+ SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, {AUTH_DWDM});
+ }
+ {
+ std::binary_semaphore requestSent(0);
+ SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, {AUTH_DWDM});
+ }
+ }
+ }
}
SECTION("Invalid establish-subscription requests")
@@ -209,6 +295,12 @@ TEST_CASE("RESTCONF subscribed notifications")
rpcRequestEncoding = libyang::DataFormat::XML;
}
}
+
+ EXPECT_NOTIFICATION(notificationsJSON[0], seq1);
+ EXPECT_NOTIFICATION(notificationsJSON[1], seq1);
+ EXPECT_NOTIFICATION(notificationsJSON[2], seq2);
+ EXPECT_NOTIFICATION(notificationsJSON[3], seq1);
+ EXPECT_NOTIFICATION(notificationsJSON[4], seq1);
}
SECTION("JSON stream")
@@ -220,6 +312,11 @@ TEST_CASE("RESTCONF subscribed notifications")
rpcRequestAuthHeader = std::nullopt;
rpcRequestEncoding = libyang::DataFormat::JSON;
rpcSubscriptionEncoding = "encode-json";
+
+ EXPECT_NOTIFICATION(notificationsJSON[0], seq1);
+ EXPECT_NOTIFICATION(notificationsJSON[1], seq1);
+ EXPECT_NOTIFICATION(notificationsJSON[3], seq1);
+ EXPECT_NOTIFICATION(notificationsJSON[4], seq1);
}
SECTION("Content-type with set encode leaf")
@@ -244,10 +341,112 @@ TEST_CASE("RESTCONF subscribed notifications")
rpcRequestEncoding = libyang::DataFormat::XML;
}
}
+
+ EXPECT_NOTIFICATION(notificationsJSON[0], seq1);
+ EXPECT_NOTIFICATION(notificationsJSON[1], seq1);
+ EXPECT_NOTIFICATION(notificationsJSON[2], seq2);
+ EXPECT_NOTIFICATION(notificationsJSON[3], seq1);
+ EXPECT_NOTIFICATION(notificationsJSON[4], seq1);
}
}
auto uri = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding);
REQUIRE(std::regex_match(uri, std::regex("/streams/subscribed/"s + uuidV4Regex)));
+
+ PREPARE_LOOP_WITH_EXCEPTIONS
+
+ // Here's how these two threads work together.
+ //
+ // The main test thread (this one):
+ // - sets up all the expectations
+ // - has an HTTP client which calls/spends the expectations based on the incoming SSE data
+ // - blocks while it runs the ASIO event loop
+ //
+ // The auxiliary thread (the notificationThread):
+ // - waits for the HTTP client having issued its long-lived HTTP GET
+ // - sends a bunch of notifications to sysrepo
+ // - waits for all the expectations getting spent, and then terminates the ASIO event loop cleanly
+
+ std::jthread notificationThread = std::jthread(wrap_exceptions_and_asio(bg, io, [&]() {
+ auto notifSession = sysrepo::Connection{}.sessionStart();
+ auto ctx = notifSession.getContext();
+
+ WAIT_UNTIL_SSE_CLIENT_REQUESTS;
+
+ SEND_NOTIFICATION(notificationsJSON[0]);
+ SEND_NOTIFICATION(notificationsJSON[1]);
+ std::this_thread::sleep_for(500ms); // simulate some delays; server might be slow in creating notifications, client should still remain connected
+ SEND_NOTIFICATION(notificationsJSON[2]);
+ SEND_NOTIFICATION(notificationsJSON[3]);
+ std::this_thread::sleep_for(500ms);
+ SEND_NOTIFICATION(notificationsJSON[4]);
+
+ // once the main thread has processed all the notifications, stop the ASIO loop
+ waitForCompletionAndBitMore(seq1);
+ waitForCompletionAndBitMore(seq2);
+ }));
+
+ std::map<std::string, std::string> streamHeaders;
+ if (rpcRequestAuthHeader) {
+ streamHeaders.insert(*rpcRequestAuthHeader);
+ }
+ SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, streamHeaders);
+ RUN_LOOP_WITH_EXCEPTIONS;
}
}
+
+TEST_CASE("Terminating server under notification load")
+{
+ trompeloeil::sequence seq1;
+ sysrepo::setLogLevelStderr(sysrepo::LogLevel::Information);
+ spdlog::set_level(spdlog::level::trace);
+
+ auto srConn = sysrepo::Connection{};
+ auto srSess = srConn.sessionStart(sysrepo::Datastore::Running);
+ srSess.sendRPC(srSess.getContext().newPath("/ietf-factory-default:factory-reset"));
+
+ auto nacmGuard = manageNacm(srSess);
+ auto server = std::make_unique<rousette::restconf::Server>(srConn, SERVER_ADDRESS, SERVER_PORT);
+ setupRealNacm(srSess);
+
+ RestconfNotificationWatcher netconfWatcher(srConn.sessionStart().getContext());
+ constexpr auto notif = R"({"example:eventB":{}})";
+
+ std::optional<std::string> rpcSubscriptionEncoding;
+ std::optional<std::pair<std::string, std::string>> rpcRequestAuthHeader;
+
+ std::pair<std::string, std::string> auth = AUTH_ROOT;
+ auto uri = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, auth, std::nullopt);
+
+ PREPARE_LOOP_WITH_EXCEPTIONS;
+
+ std::atomic<bool> serverRunning = true;
+ std::atomic<size_t> notificationsReceived = 0;
+ constexpr size_t NOTIFICATIONS_BEFORE_TERMINATE = 50;
+
+ auto notificationThread = std::jthread(wrap_exceptions_and_asio(bg, io, [&]() {
+ auto notifSession = sysrepo::Connection{}.sessionStart();
+ auto ctx = notifSession.getContext();
+
+ WAIT_UNTIL_SSE_CLIENT_REQUESTS;
+
+ while (serverRunning) {
+ SEND_NOTIFICATION(notif);
+ }
+ }));
+
+ auto serverShutdownThread = std::jthread([&]() {
+ while (notificationsReceived <= NOTIFICATIONS_BEFORE_TERMINATE) {
+ // A condition variable would be more elegant, but this is just a test...
+ std::this_thread::sleep_for(20ms);
+ }
+ server.reset();
+ serverRunning = false;
+ });
+
+ netconfWatcher.setDataFormat(libyang::DataFormat::JSON);
+ REQUIRE_CALL(netconfWatcher, data(notif)).IN_SEQUENCE(seq1).TIMES(AT_LEAST(NOTIFICATIONS_BEFORE_TERMINATE)).LR_SIDE_EFFECT(notificationsReceived++);
+ REQUIRE_CALL(netconfWatcher, data(R"({ietf-subscribed-notifications:subscription-terminated":{"id":1,"reason":"no-such-subscription"}})")).IN_SEQUENCE(seq1).TIMES(AT_MOST(1));
+ SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, {auth});
+ RUN_LOOP_WITH_EXCEPTIONS;
+}
diff --git a/tests/uri-parser.cpp b/tests/uri-parser.cpp
index 38ca80f..2ad244f 100644
--- a/tests/uri-parser.cpp
+++ b/tests/uri-parser.cpp
@@ -7,6 +7,7 @@
*/
#include "trompeloeil_doctest.h"
+#include <boost/uuid/string_generator.hpp>
#include <experimental/iterator>
#include <string>
#include <sysrepo-cpp/Connection.hpp>
@@ -1042,8 +1043,11 @@ TEST_CASE("URI path parser")
SECTION("filter")
{
+ using rousette::restconf::NetconfStreamRequest;
+
auto resp = asRestconfStreamRequest("GET", "/streams/NETCONF/XML", "filter=/asd");
- REQUIRE(resp.queryParams == QueryParams({{"filter", "/asd"s}}));
+ REQUIRE(std::holds_alternative<NetconfStreamRequest>(resp));
+ REQUIRE(std::get<NetconfStreamRequest>(resp).queryParams == QueryParams({{"filter", "/asd"s}}));
REQUIRE_THROWS_WITH_AS(asRestconfRequest(ctx, "GET", "/restconf/data/example:ordered-lists", "filter=something"),
serializeErrorResponse(400, "protocol", "invalid-value", "Query parameter 'filter' can be used only with streams").c_str(),
@@ -1052,8 +1056,11 @@ TEST_CASE("URI path parser")
SECTION("start-time")
{
+ using rousette::restconf::NetconfStreamRequest;
+
auto resp = asRestconfStreamRequest("GET", "/streams/NETCONF/XML", "start-time=2024-01-01T01:01:01Z");
- REQUIRE(resp.queryParams == QueryParams({{"start-time", "2024-01-01T01:01:01Z"s}}));
+ REQUIRE(std::holds_alternative<NetconfStreamRequest>(resp));
+ REQUIRE(std::get<NetconfStreamRequest>(resp).queryParams == QueryParams({{"start-time", "2024-01-01T01:01:01Z"s}}));
REQUIRE_THROWS_WITH_AS(asRestconfRequest(ctx, "GET", "/restconf/data/example:ordered-lists", "start-time=2024-01-01T01:01:01Z"),
serializeErrorResponse(400, "protocol", "invalid-value", "Query parameter 'start-time' can be used only with streams").c_str(),
@@ -1062,8 +1069,11 @@ TEST_CASE("URI path parser")
SECTION("stop-time")
{
+ using rousette::restconf::NetconfStreamRequest;
+
auto resp = asRestconfStreamRequest("GET", "/streams/NETCONF/XML", "stop-time=2024-01-01T01:01:01Z");
- REQUIRE(resp.queryParams == QueryParams({{"stop-time", "2024-01-01T01:01:01Z"s}}));
+ REQUIRE(std::holds_alternative<NetconfStreamRequest>(resp));
+ REQUIRE(std::get<NetconfStreamRequest>(resp).queryParams == QueryParams({{"stop-time", "2024-01-01T01:01:01Z"s}}));
REQUIRE_THROWS_WITH_AS(asRestconfRequest(ctx, "GET", "/restconf/data/example:ordered-lists", "stop-time=2024-01-01T01:01:01Z"),
serializeErrorResponse(400, "protocol", "invalid-value", "Query parameter 'stop-time' can be used only with streams").c_str(),
@@ -1079,18 +1089,27 @@ TEST_CASE("URI path parser")
SECTION("Streams")
{
using rousette::restconf::asRestconfStreamRequest;
- using rousette::restconf::RestconfStreamRequest;
+ using rousette::restconf::NetconfStreamRequest;
+ using rousette::restconf::SubscribedStreamRequest;
{
auto req = asRestconfStreamRequest("GET", "/streams/NETCONF/XML", "");
- REQUIRE(req.encoding == libyang::DataFormat::XML);
- REQUIRE(req.queryParams.empty());
+ REQUIRE(std::holds_alternative<NetconfStreamRequest>(req));
+ REQUIRE(std::get<NetconfStreamRequest>(req).encoding == libyang::DataFormat::XML);
+ REQUIRE(std::get<NetconfStreamRequest>(req).queryParams.empty());
}
{
auto req = asRestconfStreamRequest("GET", "/streams/NETCONF/JSON", "");
- REQUIRE(req.encoding == libyang::DataFormat::JSON);
- REQUIRE(req.queryParams.empty());
+ REQUIRE(std::holds_alternative<NetconfStreamRequest>(req));
+ REQUIRE(std::get<NetconfStreamRequest>(req).encoding == libyang::DataFormat::JSON);
+ REQUIRE(std::get<NetconfStreamRequest>(req).queryParams.empty());
+ }
+
+ {
+ auto req = asRestconfStreamRequest("GET", "/streams/subscribed/a40f0a50-061a-4832-a6ac-c4db7df81a10", "");
+ REQUIRE(std::holds_alternative<SubscribedStreamRequest>(req));
+ REQUIRE(std::get<SubscribedStreamRequest>(req).uuid == boost::uuids::string_generator()("a40f0a50-061a-4832-a6ac-c4db7df81a10"));
}
REQUIRE_THROWS_WITH_AS(asRestconfStreamRequest("GET", "/streams/NETCONF", ""),
@@ -1105,6 +1124,12 @@ TEST_CASE("URI path parser")
REQUIRE_THROWS_WITH_AS(asRestconfStreamRequest("GET", "/streams/NETCONF/XM", ""),
serializeErrorResponse(404, "application", "invalid-value", "Invalid stream").c_str(),
rousette::restconf::ErrorResponse);
+ REQUIRE_THROWS_WITH_AS(asRestconfStreamRequest("GET", "/streams/subscribed", ""),
+ serializeErrorResponse(404, "application", "invalid-value", "Invalid stream").c_str(),
+ rousette::restconf::ErrorResponse);
+ REQUIRE_THROWS_WITH_AS(asRestconfStreamRequest("GET", "/streams/subscribed/123-456-789", ""),
+ serializeErrorResponse(404, "application", "invalid-value", "Invalid stream").c_str(),
+ rousette::restconf::ErrorResponse);
for (const auto& httpMethod : {"OPTIONS", "PATCH", "DELETE", "POST", "PUT"}) {
--
2.43.0
@@ -0,0 +1,69 @@
From 9c39cdd8a8a6bb8fb94d4f943313ec8c4a1823d7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Mon, 10 Nov 2025 19:58:36 +0100
Subject: [PATCH 29/38] restconf: dynamic subscriptions shutdown method
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Dynamic subscriptions now have a proper shutdown method that sets a
Terminating state on all the subscriptions so that each subscription
cannot call its terminate() method.
Change-Id: I4532bbafff7c7386a18ed0636f1102842d2749fa
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/restconf/DynamicSubscriptions.cpp | 10 ++++++++++
src/restconf/DynamicSubscriptions.h | 1 +
src/restconf/Server.cpp | 1 +
3 files changed, 12 insertions(+)
diff --git a/src/restconf/DynamicSubscriptions.cpp b/src/restconf/DynamicSubscriptions.cpp
index 7f4b171..9c195f4 100644
--- a/src/restconf/DynamicSubscriptions.cpp
+++ b/src/restconf/DynamicSubscriptions.cpp
@@ -82,6 +82,16 @@ DynamicSubscriptions::DynamicSubscriptions(const std::string& streamRootUri)
DynamicSubscriptions::~DynamicSubscriptions() = default;
+void DynamicSubscriptions::stop()
+{
+ std::lock_guard lock(m_mutex);
+ for (const auto& [uuid, subscriptionData] : m_subscriptions) {
+ // We are already terminating and will destroy via destructor, calls to terminate() will do nothing
+ std::lock_guard lock(subscriptionData->mutex);
+ subscriptionData->state = SubscriptionData::State::Terminating;
+ }
+}
+
void DynamicSubscriptions::establishSubscription(sysrepo::Session& session, const libyang::DataFormat requestEncoding, const libyang::DataNode& rpcInput, libyang::DataNode& rpcOutput)
{
// Generate a new UUID associated with the subscription. The UUID will be used as a part of the URI so that the URI is not predictable (RFC 8650, section 5)
diff --git a/src/restconf/DynamicSubscriptions.h b/src/restconf/DynamicSubscriptions.h
index 1874390..0309cad 100644
--- a/src/restconf/DynamicSubscriptions.h
+++ b/src/restconf/DynamicSubscriptions.h
@@ -55,6 +55,7 @@ public:
~DynamicSubscriptions();
std::shared_ptr<SubscriptionData> getSubscriptionForUser(const boost::uuids::uuid& uuid, const std::optional<std::string>& user);
void establishSubscription(sysrepo::Session& session, const libyang::DataFormat requestEncoding, const libyang::DataNode& rpcInput, libyang::DataNode& rpcOutput);
+ void stop();
private:
std::mutex m_mutex; ///< Lock for shared data (subscriptions storage and uuid generator)
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 2a8ad33..5486e99 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -884,6 +884,7 @@ void Server::stop()
});
shutdownRequested();
+ m_dynamicSubscriptions.stop();
}
void Server::join()
--
2.43.0
@@ -0,0 +1,319 @@
From 3154d9209dc876fee43d9e83907ab44e06008c65 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Tue, 14 Oct 2025 12:25:20 +0200
Subject: [PATCH 30/38] restconf: terminate subsc. notification streams after
60s with no client
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
We allow anonymous users to create notification streams. However, such
users will not have permissions to delete the subscriptions which opens
a potential for resource exhaustion. To prevent this, we now terminate
the notification streams after 60 seconds of inactivity.
The period is configurable via the Server constructor, but the default
is 60 seconds.
Change-Id: Id399d108069704fa8ab79b9ef6ab855dd62cfc8d
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/restconf/DynamicSubscriptions.cpp | 72 ++++++++++++++++++---
src/restconf/DynamicSubscriptions.h | 22 ++++++-
src/restconf/Server.cpp | 5 +-
src/restconf/Server.h | 3 +-
tests/restconf-subscribed-notifications.cpp | 34 ++++++++++
5 files changed, 122 insertions(+), 14 deletions(-)
diff --git a/src/restconf/DynamicSubscriptions.cpp b/src/restconf/DynamicSubscriptions.cpp
index 9c195f4..f6defb4 100644
--- a/src/restconf/DynamicSubscriptions.cpp
+++ b/src/restconf/DynamicSubscriptions.cpp
@@ -74,9 +74,11 @@ sysrepo::DynamicSubscription makeStreamSubscription(sysrepo::Session& session, c
namespace rousette::restconf {
-DynamicSubscriptions::DynamicSubscriptions(const std::string& streamRootUri)
+DynamicSubscriptions::DynamicSubscriptions(const std::string& streamRootUri, const nghttp2::asio_http2::server::http2& server, const std::chrono::seconds inactivityTimeout)
: m_restconfStreamUri(streamRootUri)
+ , m_server(server)
, m_uuidGenerator(boost::uuids::random_generator())
+ , m_inactivityTimeout(inactivityTimeout)
{
}
@@ -86,9 +88,7 @@ void DynamicSubscriptions::stop()
{
std::lock_guard lock(m_mutex);
for (const auto& [uuid, subscriptionData] : m_subscriptions) {
- // We are already terminating and will destroy via destructor, calls to terminate() will do nothing
- std::lock_guard lock(subscriptionData->mutex);
- subscriptionData->state = SubscriptionData::State::Terminating;
+ subscriptionData->stop();
}
}
@@ -110,7 +110,10 @@ void DynamicSubscriptions::establishSubscription(sysrepo::Session& session, cons
std::move(sub),
dataFormat,
uuid,
- *session.getNacmUser());
+ *session.getNacmUser(),
+ *m_server.io_services().at(0),
+ m_inactivityTimeout,
+ [this, subId = sub.subscriptionId()]() { terminateSubscription(subId); });
} catch (const sysrepo::ErrorWithCode& e) {
throw ErrorResponse(400, "application", "invalid-attribute", e.what());
}
@@ -161,18 +164,29 @@ DynamicSubscriptions::SubscriptionData::SubscriptionData(
sysrepo::DynamicSubscription sub,
libyang::DataFormat format,
boost::uuids::uuid uuid,
- const std::string& user)
+ const std::string& user,
+ boost::asio::io_context& io,
+ std::chrono::seconds inactivityTimeout,
+ std::function<void()> onClientInactiveCallback)
: subscription(std::move(sub))
, dataFormat(format)
, uuid(uuid)
, user(user)
, state(State::Start)
+ , inactivityTimeout(inactivityTimeout)
+ , clientInactiveTimer(io)
+ , onClientInactiveCallback(std::move(onClientInactiveCallback))
{
spdlog::debug("{}: created", fmt::streamed(*this));
+
+ std::lock_guard lock(mutex);
+ inactivityStart();
}
DynamicSubscriptions::SubscriptionData::~SubscriptionData()
{
+ std::lock_guard lock(mutex);
+ inactivityCancel();
terminate();
}
@@ -186,12 +200,14 @@ void DynamicSubscriptions::SubscriptionData::clientDisconnected()
}
state = State::Start;
+ inactivityStart();
}
void DynamicSubscriptions::SubscriptionData::clientConnected()
{
spdlog::debug("{}: client connected", fmt::streamed(*this));
std::lock_guard lock(mutex);
+ inactivityCancel();
state = State::ReceiverActive;
}
@@ -201,10 +217,37 @@ bool DynamicSubscriptions::SubscriptionData::isReadyToAcceptClient() const
return state == State::Start;
}
-void DynamicSubscriptions::SubscriptionData::terminate(const std::optional<std::string>& reason)
+/** @pre The mutex must be locked by the caller. */
+void DynamicSubscriptions::SubscriptionData::inactivityStart()
{
- std::lock_guard lock(mutex);
+ if (state == State::Terminating) {
+ return;
+ }
+
+ spdlog::trace("{}: starting inactivity timer", fmt::streamed(*this));
+
+ clientInactiveTimer.expires_after(inactivityTimeout);
+ clientInactiveTimer.async_wait([weakThis = weak_from_this()](const boost::system::error_code& err) {
+ auto self = weakThis.lock();
+ if (!self || err == boost::asio::error::operation_aborted) {
+ return;
+ }
+ spdlog::trace("{}: client inactive, perform inactivity callback", fmt::streamed(*self));
+ self->onClientInactiveCallback();
+ });
+}
+
+/** @pre The mutex must be locked by the caller. */
+void DynamicSubscriptions::SubscriptionData::inactivityCancel()
+{
+ spdlog::trace("{}: cancelling inactivity timer", fmt::streamed(*this));
+ clientInactiveTimer.cancel();
+}
+
+/** @pre The mutex must be locked by the caller. */
+void DynamicSubscriptions::SubscriptionData::terminate(const std::optional<std::string>& reason)
+{
// already terminating, do nothing
if (state == State::Terminating) {
return;
@@ -219,6 +262,14 @@ void DynamicSubscriptions::SubscriptionData::terminate(const std::optional<std::
}
}
+void DynamicSubscriptions::SubscriptionData::stop()
+{
+ std::lock_guard lock(mutex);
+ inactivityCancel();
+ // We are already terminating and will destroy via destructor, calls to terminate() will do nothing
+ state = SubscriptionData::State::Terminating;
+}
+
std::ostream& operator<<(std::ostream& os, const DynamicSubscriptions::SubscriptionData& sub)
{
return os << "dynamic subscription (id " << sub.subscription.subscriptionId()
@@ -241,7 +292,10 @@ DynamicSubscriptionHttpStream::DynamicSubscriptionHttpStream(
*signal,
keepAlivePingInterval,
std::nullopt /* no initial event */,
- [this]() { m_subscriptionData->terminate("ietf-subscribed-notifications:no-such-subscription"); },
+ [this]() {
+ std::lock_guard lock(m_subscriptionData->mutex);
+ m_subscriptionData->terminate("ietf-subscribed-notifications:no-such-subscription");
+ },
[this]() { m_subscriptionData->clientDisconnected(); })
, m_subscriptionData(subscriptionData)
, m_signal(signal)
diff --git a/src/restconf/DynamicSubscriptions.h b/src/restconf/DynamicSubscriptions.h
index 0309cad..0422947 100644
--- a/src/restconf/DynamicSubscriptions.h
+++ b/src/restconf/DynamicSubscriptions.h
@@ -18,6 +18,10 @@ namespace libyang {
enum class DataFormat;
}
+namespace nghttp2::asio_http2::server {
+class http2;
+}
+
namespace rousette::restconf {
/** Dynamic subscriptions manager.
@@ -39,19 +43,31 @@ public:
Terminating, ///< Subscription is being terminated by the server shutdown
} state;
+ std::chrono::seconds inactivityTimeout; ///< Time after which the subscription is considered inactive and can be removed if no client is connected
+ boost::asio::steady_timer clientInactiveTimer; ///< Timer used for auto-destruction of subscriptions that are unused
+ std::function<void()> onClientInactiveCallback;
+
SubscriptionData(
sysrepo::DynamicSubscription sub,
libyang::DataFormat format,
boost::uuids::uuid uuid,
- const std::string& user);
+ const std::string& user,
+ boost::asio::io_context& io,
+ std::chrono::seconds inactivityTimeout,
+ std::function<void()> onClientInactiveCallback);
~SubscriptionData();
void clientDisconnected();
void clientConnected();
void terminate(const std::optional<std::string>& reason = std::nullopt);
bool isReadyToAcceptClient() const;
+ void stop();
+
+ private:
+ void inactivityStart();
+ void inactivityCancel();
};
- DynamicSubscriptions(const std::string& streamRootUri);
+ DynamicSubscriptions(const std::string& streamRootUri, const nghttp2::asio_http2::server::http2& server, const std::chrono::seconds inactivityTimeout);
~DynamicSubscriptions();
std::shared_ptr<SubscriptionData> getSubscriptionForUser(const boost::uuids::uuid& uuid, const std::optional<std::string>& user);
void establishSubscription(sysrepo::Session& session, const libyang::DataFormat requestEncoding, const libyang::DataNode& rpcInput, libyang::DataNode& rpcOutput);
@@ -60,8 +76,10 @@ public:
private:
std::mutex m_mutex; ///< Lock for shared data (subscriptions storage and uuid generator)
std::string m_restconfStreamUri;
+ const nghttp2::asio_http2::server::http2& m_server;
std::map<boost::uuids::uuid, std::shared_ptr<SubscriptionData>> m_subscriptions;
boost::uuids::random_generator m_uuidGenerator;
+ std::chrono::seconds m_inactivityTimeout;
void terminateSubscription(const uint32_t subId);
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 5486e99..0c7e855 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -911,11 +911,12 @@ Server::Server(
const std::string& address,
const std::string& port,
const std::chrono::milliseconds timeout,
- const std::chrono::seconds keepAlivePingInterval)
+ const std::chrono::seconds keepAlivePingInterval,
+ const std::chrono::seconds subNotifInactivityTimeout)
: m_monitoringSession(conn.sessionStart(sysrepo::Datastore::Operational))
, nacm(conn)
, server{std::make_unique<nghttp2::asio_http2::server::http2>()}
- , m_dynamicSubscriptions(netconfStreamRoot)
+ , m_dynamicSubscriptions(netconfStreamRoot, *server, subNotifInactivityTimeout)
, dwdmEvents{std::make_unique<sr::OpticalEvents>(conn.sessionStart())}
{
server->num_threads(1); // we only use one thread for the server, so we can call join() right away
diff --git a/src/restconf/Server.h b/src/restconf/Server.h
index c98f736..b4db948 100644
--- a/src/restconf/Server.h
+++ b/src/restconf/Server.h
@@ -33,7 +33,8 @@ public:
const std::string& address,
const std::string& port,
const std::chrono::milliseconds timeout = std::chrono::milliseconds{0},
- const std::chrono::seconds keepAlivePingInterval = std::chrono::seconds{55});
+ const std::chrono::seconds keepAlivePingInterval = std::chrono::seconds{55},
+ const std::chrono::seconds subNotifInactivityTimeout = std::chrono::seconds{60});
~Server();
void join();
void stop();
diff --git a/tests/restconf-subscribed-notifications.cpp b/tests/restconf-subscribed-notifications.cpp
index 11dd992..68e9286 100644
--- a/tests/restconf-subscribed-notifications.cpp
+++ b/tests/restconf-subscribed-notifications.cpp
@@ -450,3 +450,37 @@ TEST_CASE("Terminating server under notification load")
SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, {auth});
RUN_LOOP_WITH_EXCEPTIONS;
}
+
+TEST_CASE("Cleaning up inactive subscriptions")
+{
+ trompeloeil::sequence seq1, seq2;
+ sysrepo::setLogLevelStderr(sysrepo::LogLevel::Information);
+ spdlog::set_level(spdlog::level::trace);
+
+ auto srConn = sysrepo::Connection{};
+ auto srSess = srConn.sessionStart(sysrepo::Datastore::Running);
+ srSess.sendRPC(srSess.getContext().newPath("/ietf-factory-default:factory-reset"));
+
+ RestconfNotificationWatcher netconfWatcher(srConn.sessionStart().getContext());
+
+ auto nacmGuard = manageNacm(srSess);
+ constexpr auto inactivityTimeout = 2s;
+ auto server = rousette::restconf::Server{srConn, SERVER_ADDRESS, SERVER_PORT, 0ms, 55s, inactivityTimeout};
+
+ auto uri = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, {AUTH_ROOT}, std::nullopt);
+
+ SECTION("Client connects and disconnects")
+ {
+ PREPARE_LOOP_WITH_EXCEPTIONS
+ auto thr = std::jthread(wrap_exceptions_and_asio(bg, io, [&]() {
+ WAIT_UNTIL_SSE_CLIENT_REQUESTS;
+ std::this_thread::sleep_for(1s);
+ }));
+
+ SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, {AUTH_ROOT});
+ RUN_LOOP_WITH_EXCEPTIONS;
+ }
+
+ std::this_thread::sleep_for(inactivityTimeout + 1500ms);
+ REQUIRE(get(uri, {AUTH_ROOT}) == Response{404, plaintextHeaders, "Subscription not found."});
+}
--
2.43.0
@@ -0,0 +1,360 @@
From 22cfcbf52be604afe0caafc892f4b7befe03b9db Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Tue, 8 Apr 2025 17:00:59 +0200
Subject: [PATCH 31/38] restconf: add {kill,delete}-subscription RPCs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Add implementations for deleting and killing subscriptions [1,2].
Rousette allows establishing subscriptions by anonymous users. Anonymous
users can not call delete-subscription RPC though because we do not know
if it is the same anonymous user who initiated the subscription.
The subscriptions are killed after a minute of inactivity, so the
subscription will get deleted by rousette automatically after the client
disconnects.
[1] https://datatracker.ietf.org/doc/html/rfc8639.html#section-2.4.4
[2] https://datatracker.ietf.org/doc/html/rfc8639.html#section-2.4.5
Change-Id: I443d6fd88f2797045b5ae0a7d7e1a761fe74e7cb
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/restconf/DynamicSubscriptions.cpp | 44 +++++
src/restconf/DynamicSubscriptions.h | 2 +
src/restconf/Server.cpp | 4 +
tests/restconf-subscribed-notifications.cpp | 178 +++++++++++++++++++-
4 files changed, 222 insertions(+), 6 deletions(-)
diff --git a/src/restconf/DynamicSubscriptions.cpp b/src/restconf/DynamicSubscriptions.cpp
index f6defb4..fabc226 100644
--- a/src/restconf/DynamicSubscriptions.cpp
+++ b/src/restconf/DynamicSubscriptions.cpp
@@ -119,6 +119,32 @@ void DynamicSubscriptions::establishSubscription(sysrepo::Session& session, cons
}
}
+void DynamicSubscriptions::deleteSubscription(sysrepo::Session& session, const libyang::DataFormat, const libyang::DataNode& rpcInput, libyang::DataNode&)
+{
+ const auto isKill = rpcInput.findPath("/ietf-subscribed-notifications:kill-subscription") != std::nullopt;
+ const auto subId = std::get<uint32_t>(rpcInput.findPath("id")->asTerm().value());
+
+ // The RPC is already NACM-checked. Now, retrieve the subscription, if the current user has permission for it
+ auto subscriptionData = getSubscriptionForUser(subId, session.getNacmUser());
+ if (!subscriptionData) {
+ throw ErrorResponse(404, "application", "invalid-value", "Subscription not found.", rpcInput.path());
+ }
+
+ /* I *think* the RFC 8639 says that root can use delete-subscription only for subscriptions created by root.
+ * This checks if the current user is root and the subscription was created by a different user. If so, reject the request.
+ */
+ if (!isKill && session.getNacmUser() == session.getNacmRecoveryUser() && subscriptionData->user != session.getNacmRecoveryUser()) {
+ //FIXME: pass additional error info (rc:yang-data delete-subscription-error-info from RFC 8639)
+ throw ErrorResponse(400, "application", "invalid-attribute", "Trying to delete subscription not created by root. Use kill-subscription instead.", rpcInput.path());
+ }
+
+ spdlog::debug("Terminating subscription id {}", subId);
+ subscriptionData->subscription.terminate("ietf-subscribed-notifications:no-such-subscription");
+
+ std::unique_lock lock(m_mutex);
+ m_subscriptions.erase(subscriptionData->uuid);
+}
+
void DynamicSubscriptions::terminateSubscription(const uint32_t subId)
{
std::lock_guard lock(m_mutex);
@@ -153,6 +179,24 @@ std::shared_ptr<DynamicSubscriptions::SubscriptionData> DynamicSubscriptions::ge
return nullptr;
}
+/** @brief Returns the subscription data for the given subscription id and user.
+ *
+ * @param id The ID of the subscription.
+ * @return A shared pointer to the SubscriptionData object if found and user is the one who established the subscription (or NACM recovery user), otherwise nullptr.
+ */
+std::shared_ptr<DynamicSubscriptions::SubscriptionData> DynamicSubscriptions::getSubscriptionForUser(const uint32_t subId, const std::optional<std::string>& user)
+{
+ std::unique_lock lock(m_mutex);
+
+ // FIXME: This is linear search. Maybe use something like boost::multi_index?
+ if (auto it = std::find_if(m_subscriptions.begin(), m_subscriptions.end(), [subId](const auto& entry) { return entry.second->subscription.subscriptionId() == subId; });
+ it != m_subscriptions.end() && (it->second->user == user || user == sysrepo::Session::getNacmRecoveryUser())) {
+ return it->second;
+ }
+
+ return nullptr;
+}
+
boost::uuids::uuid DynamicSubscriptions::makeUUID()
{
// uuid generator instance accesses must be synchronized (https://www.boost.org/doc/libs/1_88_0/libs/uuid/doc/html/uuid.html#design_notes)
diff --git a/src/restconf/DynamicSubscriptions.h b/src/restconf/DynamicSubscriptions.h
index 0422947..8057b17 100644
--- a/src/restconf/DynamicSubscriptions.h
+++ b/src/restconf/DynamicSubscriptions.h
@@ -70,8 +70,10 @@ public:
DynamicSubscriptions(const std::string& streamRootUri, const nghttp2::asio_http2::server::http2& server, const std::chrono::seconds inactivityTimeout);
~DynamicSubscriptions();
std::shared_ptr<SubscriptionData> getSubscriptionForUser(const boost::uuids::uuid& uuid, const std::optional<std::string>& user);
+ std::shared_ptr<SubscriptionData> getSubscriptionForUser(const uint32_t subId, const std::optional<std::string>& user);
void establishSubscription(sysrepo::Session& session, const libyang::DataFormat requestEncoding, const libyang::DataNode& rpcInput, libyang::DataNode& rpcOutput);
void stop();
+ void deleteSubscription(sysrepo::Session& session, const libyang::DataFormat, const libyang::DataNode& rpcInput, libyang::DataNode&);
private:
std::mutex m_mutex; ///< Lock for shared data (subscriptions storage and uuid generator)
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 0c7e855..04cdf56 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -447,6 +447,10 @@ std::optional<libyang::DataNode> processInternalRPC(sysrepo::Session& sess, liby
const std::map<std::string, InternalRPCHandler> handlers{
{"/ietf-subscribed-notifications:establish-subscription",
{"/ietf-subscribed-notifications:filters", [&dynamicSubscriptions](auto&&... args) { return dynamicSubscriptions.establishSubscription(std::forward<decltype(args)>(args)...); }}},
+ {"/ietf-subscribed-notifications:kill-subscription",
+ {std::nullopt, [&dynamicSubscriptions](auto&&... args) { return dynamicSubscriptions.deleteSubscription(std::forward<decltype(args)>(args)...); }}},
+ {"/ietf-subscribed-notifications:delete-subscription",
+ {std::nullopt, [&dynamicSubscriptions](auto&&... args) { return dynamicSubscriptions.deleteSubscription(std::forward<decltype(args)>(args)...); }}},
};
const auto rpcPath = rpcInput.path();
diff --git a/tests/restconf-subscribed-notifications.cpp b/tests/restconf-subscribed-notifications.cpp
index 68e9286..7c7de4b 100644
--- a/tests/restconf-subscribed-notifications.cpp
+++ b/tests/restconf-subscribed-notifications.cpp
@@ -24,8 +24,13 @@ constexpr auto uuidV4Regex = "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[89a
using namespace std::chrono_literals;
using namespace std::string_literals;
+struct EstablishSubscriptionResult {
+ uint32_t id;
+ std::string url;
+};
+
/** @brief Calls establish-subscription rpc, returns the url of the stream associated with the created subscription */
-std::string establishSubscription(
+EstablishSubscriptionResult establishSubscription(
const libyang::Context& ctx,
const libyang::DataFormat rpcEncoding,
const std::optional<std::pair<std::string, std::string>>& rpcRequestAuthHeader,
@@ -79,10 +84,16 @@ std::string establishSubscription(
auto reply = ctx.newPath("/ietf-subscribed-notifications:establish-subscription");
REQUIRE(reply.parseOp(resp.data, rpcEncoding, libyang::OperationType::ReplyRestconf).tree);
+ auto idNode = reply.findPath("id", libyang::InputOutputNodes::Output);
+ REQUIRE(idNode);
+
auto urlNode = reply.findPath("ietf-restconf-subscribed-notifications:uri", libyang::InputOutputNodes::Output);
REQUIRE(urlNode);
- return urlNode->asTerm().valueStr();
+ return {
+ std::get<uint32_t>(idNode->asTerm().value()),
+ urlNode->asTerm().valueStr(),
+ };
}
TEST_CASE("RESTCONF subscribed notifications")
@@ -150,7 +161,7 @@ TEST_CASE("RESTCONF subscribed notifications")
SECTION("User DWDM establishes subscription")
{
std::pair<std::string, std::string> user = AUTH_DWDM;
- auto uri = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, user, std::nullopt);
+ auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, user, std::nullopt);
std::map<std::string, std::string> headers;
@@ -350,7 +361,7 @@ TEST_CASE("RESTCONF subscribed notifications")
}
}
- auto uri = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding);
+ auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding);
REQUIRE(std::regex_match(uri, std::regex("/streams/subscribed/"s + uuidV4Regex)));
PREPARE_LOOP_WITH_EXCEPTIONS
@@ -393,6 +404,161 @@ TEST_CASE("RESTCONF subscribed notifications")
SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, streamHeaders);
RUN_LOOP_WITH_EXCEPTIONS;
}
+
+ SECTION("delete-subscription")
+ {
+ std::optional<Response> expectedResponse;
+ std::map<std::string, std::string> headers;
+ headers.insert(CONTENT_TYPE_JSON);
+
+ SECTION("Subscription created by dwdm")
+ {
+ rpcRequestAuthHeader = AUTH_DWDM;
+
+ SECTION("dwdm (author) can delete")
+ {
+ headers.insert(AUTH_DWDM);
+ expectedResponse = Response{204, noContentTypeHeaders, ""};
+ }
+
+ SECTION("Anonymous cannot delete anything because of NACM")
+ {
+ expectedResponse = Response{403, jsonHeaders, R"({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "application",
+ "error-tag": "access-denied",
+ "error-path": "/ietf-subscribed-notifications:delete-subscription",
+ "error-message": "Access denied."
+ }
+ ]
+ }
+}
+)"};
+ }
+
+ SECTION("norules user")
+ {
+ headers.insert(AUTH_NORULES);
+ expectedResponse = Response{404, jsonHeaders, R"({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "application",
+ "error-tag": "invalid-value",
+ "error-path": "/ietf-subscribed-notifications:delete-subscription",
+ "error-message": "Subscription not found."
+ }
+ ]
+ }
+}
+)"};
+ }
+
+ SECTION("root user")
+ {
+ headers.insert(AUTH_ROOT);
+ expectedResponse = Response{400, jsonHeaders, R"({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "application",
+ "error-tag": "invalid-attribute",
+ "error-path": "/ietf-subscribed-notifications:delete-subscription",
+ "error-message": "Trying to delete subscription not created by root. Use kill-subscription instead."
+ }
+ ]
+ }
+}
+)"};
+ }
+
+ auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding);
+ auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}";
+ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:delete-subscription", headers, body) == expectedResponse);
+ }
+
+ SECTION("Anonymous user cannot delete subscription craeted by anonymous user")
+ {
+ rpcRequestAuthHeader = std::nullopt;
+ auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding);
+ auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}";
+ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:delete-subscription", headers, body) == Response{403, jsonHeaders, R"({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "application",
+ "error-tag": "access-denied",
+ "error-path": "/ietf-subscribed-notifications:delete-subscription",
+ "error-message": "Access denied."
+ }
+ ]
+ }
+}
+)"});
+ }
+ }
+
+ SECTION("kill-subscription")
+ {
+ std::optional<Response> expectedResponse;
+ std::map<std::string, std::string> headers;
+ headers.insert(CONTENT_TYPE_JSON);
+
+ SECTION("User cannot kill")
+ {
+ SECTION("dwdm (author)")
+ {
+ headers.insert(AUTH_DWDM);
+ }
+
+ SECTION("anonymous")
+ {
+ }
+
+ expectedResponse = Response{403, jsonHeaders, R"({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "application",
+ "error-tag": "access-denied",
+ "error-path": "/ietf-subscribed-notifications:kill-subscription",
+ "error-message": "Access denied."
+ }
+ ]
+ }
+}
+)"};
+ }
+
+ SECTION("root")
+ {
+ headers.insert(AUTH_ROOT);
+ expectedResponse = Response{204, noContentTypeHeaders, ""};
+ }
+
+ auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding);
+ auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}";
+ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:kill-subscription", headers, body) == expectedResponse);
+ }
+
+ SECTION("Invalid kill/delete-subscription requests")
+ {
+ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:kill-subscription", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"ietf-subscribed-notifications:input": {}})") == Response{400, jsonHeaders, R"({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "protocol",
+ "error-tag": "invalid-value",
+ "error-path": "/ietf-subscribed-notifications:kill-subscription",
+ "error-message": "Mandatory node \"id\" instance does not exist."
+ }
+ ]
+ }
+}
+)"});
+ }
}
TEST_CASE("Terminating server under notification load")
@@ -416,7 +582,7 @@ TEST_CASE("Terminating server under notification load")
std::optional<std::pair<std::string, std::string>> rpcRequestAuthHeader;
std::pair<std::string, std::string> auth = AUTH_ROOT;
- auto uri = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, auth, std::nullopt);
+ auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, auth, std::nullopt);
PREPARE_LOOP_WITH_EXCEPTIONS;
@@ -467,7 +633,7 @@ TEST_CASE("Cleaning up inactive subscriptions")
constexpr auto inactivityTimeout = 2s;
auto server = rousette::restconf::Server{srConn, SERVER_ADDRESS, SERVER_PORT, 0ms, 55s, inactivityTimeout};
- auto uri = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, {AUTH_ROOT}, std::nullopt);
+ auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, {AUTH_ROOT}, std::nullopt);
SECTION("Client connects and disconnects")
{
--
2.43.0
@@ -0,0 +1,232 @@
From c5558b12ee67280b6281b639c3d8652cec3c7fa6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Wed, 19 Mar 2025 14:39:05 +0100
Subject: [PATCH 32/38] restconf: add subscribed notifications filtering
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Implement filtering of subscribed notifications using xpath and subtree
filters.
Change-Id: Iee973a6a2d03b4a0d90f952afe5436ce701787ae
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 2 +-
src/restconf/DynamicSubscriptions.cpp | 13 ++++-
src/restconf/Server.cpp | 2 +-
tests/restconf-subscribed-notifications.cpp | 65 ++++++++++++++++++---
4 files changed, 70 insertions(+), 12 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5d3b7ce..28696b3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -217,7 +217,7 @@ if(BUILD_TESTING)
--install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-interfaces@2018-02-20.yang
--install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-ip@2018-02-22.yang
--install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-network-instance@2019-01-21.yang
- --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-subscribed-notifications@2019-09-09.yang --enable-feature replay --enable-feature encode-xml --enable-feature encode-json
+ --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-subscribed-notifications@2019-09-09.yang --enable-feature replay --enable-feature encode-xml --enable-feature encode-json --enable-feature xpath --enable-feature subtree
--install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-restconf-subscribed-notifications@2019-11-17.yang
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example.yang --enable-feature f1
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-delete.yang
diff --git a/src/restconf/DynamicSubscriptions.cpp b/src/restconf/DynamicSubscriptions.cpp
index fabc226..a13d344 100644
--- a/src/restconf/DynamicSubscriptions.cpp
+++ b/src/restconf/DynamicSubscriptions.cpp
@@ -59,13 +59,22 @@ sysrepo::DynamicSubscription makeStreamSubscription(sysrepo::Session& session, c
}
if (rpcInput.findPath("stream-filter-name")) {
- throw rousette::restconf::ErrorResponse(400, "application", "invalid-attribute", "Stream filtering is not supported");
+ /* TODO: This requires support for modifying subscriptions first; a change of entry in filters container must change all
+ * subscriptions using this stream-filter, see for instance https://datatracker.ietf.org/doc/html/rfc8639.html#section-2.7.2 */
+ throw rousette::restconf::ErrorResponse(400, "application", "invalid-attribute", "Stream filtering with predefined filters is not supported");
}
auto stopTime = optionalTime(rpcInput, "stop-time");
+ std::optional<std::variant<std::string, libyang::DataNodeAny>> filter;
+ if (auto node = rpcInput.findPath("stream-xpath-filter")) {
+ filter = node->asTerm().valueStr();
+ } else if (auto node = rpcInput.findPath("stream-subtree-filter")) {
+ filter = node->asAny();
+ }
+
return session.subscribeNotifications(
- std::nullopt /* TODO xpath filter */,
+ filter,
streamNode->asTerm().valueStr(),
stopTime,
std::nullopt /* TODO replayStart */);
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 04cdf56..76603b3 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -932,7 +932,7 @@ Server::Server(
{"ietf-netconf", "", {}},
{"ietf-yang-library", "2019-01-04", {}},
{"ietf-yang-patch", "2017-02-22", {}},
- {"ietf-subscribed-notifications", "2019-09-09", {"encode-xml", "encode-json"}},
+ {"ietf-subscribed-notifications", "2019-09-09", {"encode-xml", "encode-json", "xpath", "subtree"}},
{"ietf-restconf-subscribed-notifications", "2019-11-17", {}},
}) {
if (auto mod = m_monitoringSession.getContext().getModuleImplemented(module)) {
diff --git a/tests/restconf-subscribed-notifications.cpp b/tests/restconf-subscribed-notifications.cpp
index 7c7de4b..7fcf549 100644
--- a/tests/restconf-subscribed-notifications.cpp
+++ b/tests/restconf-subscribed-notifications.cpp
@@ -34,7 +34,8 @@ EstablishSubscriptionResult establishSubscription(
const libyang::Context& ctx,
const libyang::DataFormat rpcEncoding,
const std::optional<std::pair<std::string, std::string>>& rpcRequestAuthHeader,
- const std::optional<std::string>& encodingLeafValue)
+ const std::optional<std::string>& encodingLeafValue,
+ const std::variant<std::monostate, std::string, libyang::XML>& filter)
{
constexpr auto jsonPrefix = "ietf-subscribed-notifications";
constexpr auto xmlNamespace = "urn:ietf:params:xml:ns:yang:ietf-subscribed-notifications";
@@ -56,6 +57,14 @@ EstablishSubscriptionResult establishSubscription(
rpcTree.newPath("encoding", *encodingLeafValue);
}
+ if (std::holds_alternative<std::string>(filter)) {
+ rpcTree.newPath("stream-xpath-filter", std::get<std::string>(filter));
+ }
+
+ if (std::holds_alternative<libyang::XML>(filter)) {
+ rpcTree.newPath2("stream-subtree-filter", std::get<libyang::XML>(filter));
+ }
+
switch (rpcEncoding) {
case libyang::DataFormat::JSON:
requestHeaders.insert(CONTENT_TYPE_JSON);
@@ -128,6 +137,7 @@ TEST_CASE("RESTCONF subscribed notifications")
libyang::DataFormat rpcRequestEncoding = libyang::DataFormat::JSON;
std::optional<std::string> rpcSubscriptionEncoding;
+ std::variant<std::monostate, std::string, libyang::XML> rpcFilter;
std::optional<std::pair<std::string, std::string>> rpcRequestAuthHeader;
SECTION("NACM authorization")
@@ -161,7 +171,7 @@ TEST_CASE("RESTCONF subscribed notifications")
SECTION("User DWDM establishes subscription")
{
std::pair<std::string, std::string> user = AUTH_DWDM;
- auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, user, std::nullopt);
+ auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, user, std::nullopt, rpcFilter);
std::map<std::string, std::string> headers;
@@ -277,6 +287,23 @@ TEST_CASE("RESTCONF subscribed notifications")
]
}
}
+)###"});
+
+ srSess.switchDatastore(sysrepo::Datastore::Operational);
+ srSess.setItem("/ietf-subscribed-notifications:filters/stream-filter[name='xyz']/stream-xpath-filter", "/example:eventA");
+ srSess.applyChanges();
+ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:establish-subscription", {CONTENT_TYPE_JSON}, R"###({ "ietf-subscribed-notifications:input": { "stream": "NETCONF", "stream-filter-name": "xyz" } })###")
+ == Response{400, jsonHeaders, R"###({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "application",
+ "error-tag": "invalid-attribute",
+ "error-message": "Stream filtering with predefined filters is not supported"
+ }
+ ]
+ }
+}
)###"});
}
@@ -330,6 +357,28 @@ TEST_CASE("RESTCONF subscribed notifications")
EXPECT_NOTIFICATION(notificationsJSON[4], seq1);
}
+ SECTION("XPath filter set")
+ {
+ rpcRequestAuthHeader = AUTH_ROOT;
+ rpcRequestEncoding = libyang::DataFormat::JSON;
+ rpcFilter = "/example:eventA | /example:eventB";
+ rpcSubscriptionEncoding = "encode-json";
+ EXPECT_NOTIFICATION(notificationsJSON[0], seq1);
+ EXPECT_NOTIFICATION(notificationsJSON[1], seq1);
+ EXPECT_NOTIFICATION(notificationsJSON[3], seq1);
+ }
+
+ SECTION("Subtree filter set")
+ {
+ rpcRequestAuthHeader = AUTH_ROOT;
+ rpcRequestEncoding = libyang::DataFormat::JSON;
+ // Constructing the filter as XML is only an implementation detail. The tree is then constructed as JSON in establishSubscription
+ rpcFilter = libyang::XML{"<eventA xmlns='http://example.tld/example' />"};
+ rpcSubscriptionEncoding = "encode-json";
+ EXPECT_NOTIFICATION(notificationsJSON[0], seq1);
+ EXPECT_NOTIFICATION(notificationsJSON[3], seq1);
+ }
+
SECTION("Content-type with set encode leaf")
{
rpcRequestAuthHeader = AUTH_ROOT;
@@ -361,7 +410,7 @@ TEST_CASE("RESTCONF subscribed notifications")
}
}
- auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding);
+ auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter);
REQUIRE(std::regex_match(uri, std::regex("/streams/subscribed/"s + uuidV4Regex)));
PREPARE_LOOP_WITH_EXCEPTIONS
@@ -474,7 +523,7 @@ TEST_CASE("RESTCONF subscribed notifications")
)"};
}
- auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding);
+ auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter);
auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}";
REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:delete-subscription", headers, body) == expectedResponse);
}
@@ -482,7 +531,7 @@ TEST_CASE("RESTCONF subscribed notifications")
SECTION("Anonymous user cannot delete subscription craeted by anonymous user")
{
rpcRequestAuthHeader = std::nullopt;
- auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding);
+ auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter);
auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}";
REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:delete-subscription", headers, body) == Response{403, jsonHeaders, R"({
"ietf-restconf:errors": {
@@ -538,7 +587,7 @@ TEST_CASE("RESTCONF subscribed notifications")
expectedResponse = Response{204, noContentTypeHeaders, ""};
}
- auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding);
+ auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter);
auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}";
REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:kill-subscription", headers, body) == expectedResponse);
}
@@ -582,7 +631,7 @@ TEST_CASE("Terminating server under notification load")
std::optional<std::pair<std::string, std::string>> rpcRequestAuthHeader;
std::pair<std::string, std::string> auth = AUTH_ROOT;
- auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, auth, std::nullopt);
+ auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, auth, std::nullopt, {});
PREPARE_LOOP_WITH_EXCEPTIONS;
@@ -633,7 +682,7 @@ TEST_CASE("Cleaning up inactive subscriptions")
constexpr auto inactivityTimeout = 2s;
auto server = rousette::restconf::Server{srConn, SERVER_ADDRESS, SERVER_PORT, 0ms, 55s, inactivityTimeout};
- auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, {AUTH_ROOT}, std::nullopt);
+ auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, {AUTH_ROOT}, std::nullopt, {});
SECTION("Client connects and disconnects")
{
--
2.43.0
@@ -0,0 +1,36 @@
From fe91af1eb38888d7eacb36cb5a299bd3163f7c60 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Tue, 14 Oct 2025 17:22:00 +0200
Subject: [PATCH 33/38] cmake: wrap long line
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: Id94e18d581dbfbe2e6d386be6a232544f9344eec
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 28696b3..3d79dad 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -217,7 +217,12 @@ if(BUILD_TESTING)
--install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-interfaces@2018-02-20.yang
--install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-ip@2018-02-22.yang
--install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-network-instance@2019-01-21.yang
- --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-subscribed-notifications@2019-09-09.yang --enable-feature replay --enable-feature encode-xml --enable-feature encode-json --enable-feature xpath --enable-feature subtree
+ --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-subscribed-notifications@2019-09-09.yang
+ --enable-feature replay
+ --enable-feature encode-xml
+ --enable-feature encode-json
+ --enable-feature xpath
+ --enable-feature subtree
--install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-restconf-subscribed-notifications@2019-11-17.yang
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example.yang --enable-feature f1
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-delete.yang
--
2.43.0
@@ -0,0 +1,322 @@
From 817debab72d10f2f30e83f1630bebdcdf3ec9455 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Mon, 10 Mar 2025 15:33:08 +0100
Subject: [PATCH 34/38] restconf: support replays in subscribed notifications
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: Idfc3ceb99c8111c5575c1c8b94d539f234fa43be
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 1 +
src/restconf/DynamicSubscriptions.cpp | 24 ++++-
src/restconf/Server.cpp | 2 +-
tests/restconf-subscribed-notifications.cpp | 102 ++++++++++++++++++--
4 files changed, 115 insertions(+), 14 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3d79dad..f98a64b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -223,6 +223,7 @@ if(BUILD_TESTING)
--enable-feature encode-json
--enable-feature xpath
--enable-feature subtree
+ --enable-feature replay
--install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-restconf-subscribed-notifications@2019-11-17.yang
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example.yang --enable-feature f1
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-delete.yang
diff --git a/src/restconf/DynamicSubscriptions.cpp b/src/restconf/DynamicSubscriptions.cpp
index a13d344..9be3e92 100644
--- a/src/restconf/DynamicSubscriptions.cpp
+++ b/src/restconf/DynamicSubscriptions.cpp
@@ -50,7 +50,7 @@ libyang::DataFormat getEncoding(const libyang::DataNode& rpcInput, const libyang
return requestEncoding;
}
-sysrepo::DynamicSubscription makeStreamSubscription(sysrepo::Session& session, const libyang::DataNode& rpcInput)
+sysrepo::DynamicSubscription makeStreamSubscription(sysrepo::Session& session, const libyang::DataNode& rpcInput, libyang::DataNode& rpcOutput)
{
auto streamNode = rpcInput.findPath("stream");
@@ -73,11 +73,27 @@ sysrepo::DynamicSubscription makeStreamSubscription(sysrepo::Session& session, c
filter = node->asAny();
}
- return session.subscribeNotifications(
+ std::optional<sysrepo::NotificationTimeStamp> replayStartTime;
+ if (auto node = rpcInput.findPath("replay-start-time")) {
+ replayStartTime = libyang::fromYangTimeFormat<sysrepo::NotificationTimeStamp::clock>(node->asTerm().valueStr());
+ }
+
+ auto sub = session.subscribeNotifications(
filter,
streamNode->asTerm().valueStr(),
stopTime,
- std::nullopt /* TODO replayStart */);
+ replayStartTime);
+
+ /* Node replay-start-time-revision should be set only if time was revised to be different than the requested start time,
+ * i.e. when the "replay-start-time" contains a value that is earlier than what a publisher's retained history.
+ * Then the actual publisher's revised start time MUST be set in the returned "replay-start-time-revision" object.
+ * (RFC 8639, 2.4.2.1)
+ * */
+ if (auto replayStartTimeRevision = sub.replayStartTime(); replayStartTimeRevision && replayStartTime) {
+ rpcOutput.newPath("replay-start-time-revision", libyang::yangTimeFormat(*replayStartTimeRevision, libyang::TimezoneInterpretation::Local), libyang::CreationOptions::Output);
+ }
+
+ return sub;
}
}
@@ -109,7 +125,7 @@ void DynamicSubscriptions::establishSubscription(sysrepo::Session& session, cons
auto dataFormat = getEncoding(rpcInput, requestEncoding);
try {
- auto sub = makeStreamSubscription(session, rpcInput);
+ auto sub = makeStreamSubscription(session, rpcInput, rpcOutput);
rpcOutput.newPath("id", std::to_string(sub.subscriptionId()), libyang::CreationOptions::Output);
rpcOutput.newPath("ietf-restconf-subscribed-notifications:uri", m_restconfStreamUri + "subscribed/" + boost::uuids::to_string(uuid), libyang::CreationOptions::Output);
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 76603b3..d166236 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -932,7 +932,7 @@ Server::Server(
{"ietf-netconf", "", {}},
{"ietf-yang-library", "2019-01-04", {}},
{"ietf-yang-patch", "2017-02-22", {}},
- {"ietf-subscribed-notifications", "2019-09-09", {"encode-xml", "encode-json", "xpath", "subtree"}},
+ {"ietf-subscribed-notifications", "2019-09-09", {"encode-xml", "encode-json", "xpath", "subtree", "replay"}},
{"ietf-restconf-subscribed-notifications", "2019-11-17", {}},
}) {
if (auto mod = m_monitoringSession.getContext().getModuleImplemented(module)) {
diff --git a/tests/restconf-subscribed-notifications.cpp b/tests/restconf-subscribed-notifications.cpp
index 7fcf549..d42fa96 100644
--- a/tests/restconf-subscribed-notifications.cpp
+++ b/tests/restconf-subscribed-notifications.cpp
@@ -18,7 +18,7 @@ static const auto SERVER_PORT = "10092";
#include "tests/pretty_printers.h"
#define SEND_NOTIFICATION(DATA) notifSession.sendNotification(*ctx.parseOp(DATA, libyang::DataFormat::JSON, libyang::OperationType::NotificationYang).op, sysrepo::Wait::No);
-
+#define REPLAY_COMPLETED trompeloeil::re(R"(^\{"ietf-subscribed-notifications:replay-completed":\{"id":[0-9]+\}\}$)")
constexpr auto uuidV4Regex = "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[89abAB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}";
using namespace std::chrono_literals;
@@ -27,6 +27,7 @@ using namespace std::string_literals;
struct EstablishSubscriptionResult {
uint32_t id;
std::string url;
+ std::optional<sysrepo::NotificationTimeStamp> replayStartTimeRevision;
};
/** @brief Calls establish-subscription rpc, returns the url of the stream associated with the created subscription */
@@ -35,7 +36,8 @@ EstablishSubscriptionResult establishSubscription(
const libyang::DataFormat rpcEncoding,
const std::optional<std::pair<std::string, std::string>>& rpcRequestAuthHeader,
const std::optional<std::string>& encodingLeafValue,
- const std::variant<std::monostate, std::string, libyang::XML>& filter)
+ const std::variant<std::monostate, std::string, libyang::XML>& filter,
+ const std::optional<sysrepo::NotificationTimeStamp>& replayStartTime)
{
constexpr auto jsonPrefix = "ietf-subscribed-notifications";
constexpr auto xmlNamespace = "urn:ietf:params:xml:ns:yang:ietf-subscribed-notifications";
@@ -65,6 +67,10 @@ EstablishSubscriptionResult establishSubscription(
rpcTree.newPath2("stream-subtree-filter", std::get<libyang::XML>(filter));
}
+ if (replayStartTime) {
+ rpcTree.newPath("replay-start-time", libyang::yangTimeFormat(*replayStartTime, libyang::TimezoneInterpretation::Local));
+ }
+
switch (rpcEncoding) {
case libyang::DataFormat::JSON:
requestHeaders.insert(CONTENT_TYPE_JSON);
@@ -99,9 +105,15 @@ EstablishSubscriptionResult establishSubscription(
auto urlNode = reply.findPath("ietf-restconf-subscribed-notifications:uri", libyang::InputOutputNodes::Output);
REQUIRE(urlNode);
+ std::optional<sysrepo::NotificationTimeStamp> replayStartTimeRevision;
+ if (auto node = reply.findPath("ietf-subscribed-notifications:replay-start-time-revision", libyang::InputOutputNodes::Output)) {
+ replayStartTimeRevision = libyang::fromYangTimeFormat<sysrepo::NotificationTimeStamp::clock>(node->asTerm().valueStr());
+ }
+
return {
std::get<uint32_t>(idNode->asTerm().value()),
urlNode->asTerm().valueStr(),
+ replayStartTimeRevision
};
}
@@ -131,6 +143,8 @@ TEST_CASE("RESTCONF subscribed notifications")
R"({"example:eventA":{"message":"almost finished","progress":99}})",
R"({"example:tlc":{"list":[{"name":"k1","notif":{"message":"nested"}}]}})",
};
+ const std::string notificationForReplayJSON = R"({"example:eventA":{"message":"this-should-be-sent-very-early","progress":0}})";
+
std::vector<std::unique_ptr<trompeloeil::expectation>> expectations;
RestconfNotificationWatcher netconfWatcher(srConn.sessionStart().getContext());
@@ -138,7 +152,10 @@ TEST_CASE("RESTCONF subscribed notifications")
libyang::DataFormat rpcRequestEncoding = libyang::DataFormat::JSON;
std::optional<std::string> rpcSubscriptionEncoding;
std::variant<std::monostate, std::string, libyang::XML> rpcFilter;
+ std::optional<sysrepo::NotificationTimeStamp> rpcReplayStart;
std::optional<std::pair<std::string, std::string>> rpcRequestAuthHeader;
+ std::pair<sysrepo::NotificationTimeStamp, sysrepo::NotificationTimeStamp> replayedNotificationSendInterval; // bounds for replayed notification event time
+ bool shouldReviseStartTime = false;
SECTION("NACM authorization")
{
@@ -171,7 +188,7 @@ TEST_CASE("RESTCONF subscribed notifications")
SECTION("User DWDM establishes subscription")
{
std::pair<std::string, std::string> user = AUTH_DWDM;
- auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, user, std::nullopt, rpcFilter);
+ auto [id, uri, replayStartTimeRevision] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, user, std::nullopt, rpcFilter, rpcReplayStart);
std::map<std::string, std::string> headers;
@@ -304,6 +321,23 @@ TEST_CASE("RESTCONF subscribed notifications")
]
}
}
+)###"});
+
+ // replay-start-time > stop-time
+ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:establish-subscription",
+ {CONTENT_TYPE_JSON},
+ R"###({ "ietf-subscribed-notifications:input": { "stream": "NETCONF", "replay-start-time": "2000-11-11T11:22:33Z", "stop-time": "2000-01-01T00:00:00Z" } })###")
+ == Response{400, jsonHeaders, R"###({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "application",
+ "error-tag": "invalid-attribute",
+ "error-message": "Couldn't create notification subscription: SR_ERR_INVAL_ARG\u000A Specified \"stop-time\" is earlier than \"start-time\". (SR_ERR_INVAL_ARG)"
+ }
+ ]
+ }
+}
)###"});
}
@@ -379,6 +413,48 @@ TEST_CASE("RESTCONF subscribed notifications")
EXPECT_NOTIFICATION(notificationsJSON[3], seq1);
}
+ SECTION("Replays")
+ {
+ // Announce replay support and send one notification before the client connects
+ srConn.setModuleReplaySupport("example", true);
+
+ {
+ auto notifSession = sysrepo::Connection{}.sessionStart();
+ auto ctx = notifSession.getContext();
+ replayedNotificationSendInterval.first = std::chrono::system_clock::now();
+ SEND_NOTIFICATION(notificationForReplayJSON);
+ replayedNotificationSendInterval.second = std::chrono::system_clock::now();
+ }
+
+ rpcRequestAuthHeader = AUTH_ROOT;
+ rpcRequestEncoding = libyang::DataFormat::JSON;
+ rpcSubscriptionEncoding = "encode-json";
+
+ SECTION("replay-start-time-revision is announced to the client")
+ {
+ rpcReplayStart = std::chrono::system_clock::now() - 666s /* Reasonable time in the past, earlier than the replayed notification was sent */;
+
+ EXPECT_NOTIFICATION(notificationForReplayJSON, seq1);
+ EXPECT_NOTIFICATION(REPLAY_COMPLETED, seq1);
+ shouldReviseStartTime = true;
+ }
+
+ SECTION("replay-start-time-revision not announced")
+ {
+ /* Ask for replay since the time when the first notification was sent. This should not revise the start time
+ * because we are not asking for the start *before* our history. (RFC 8639, 2.4.2.1) */
+ rpcReplayStart = replayedNotificationSendInterval.second;
+
+ EXPECT_NOTIFICATION(REPLAY_COMPLETED, seq1);
+ }
+
+ EXPECT_NOTIFICATION(notificationsJSON[0], seq1);
+ EXPECT_NOTIFICATION(notificationsJSON[1], seq1);
+ EXPECT_NOTIFICATION(notificationsJSON[2], seq1);
+ EXPECT_NOTIFICATION(notificationsJSON[3], seq1);
+ EXPECT_NOTIFICATION(notificationsJSON[4], seq1);
+ }
+
SECTION("Content-type with set encode leaf")
{
rpcRequestAuthHeader = AUTH_ROOT;
@@ -410,9 +486,17 @@ TEST_CASE("RESTCONF subscribed notifications")
}
}
- auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter);
+ auto [id, uri, replayStartTimeRevision] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter, rpcReplayStart);
REQUIRE(std::regex_match(uri, std::regex("/streams/subscribed/"s + uuidV4Regex)));
+ if (shouldReviseStartTime) {
+ REQUIRE(replayStartTimeRevision);
+ REQUIRE(replayedNotificationSendInterval.first <= *replayStartTimeRevision);
+ REQUIRE(*replayStartTimeRevision <= replayedNotificationSendInterval.second);
+ } else {
+ REQUIRE(!replayStartTimeRevision);
+ }
+
PREPARE_LOOP_WITH_EXCEPTIONS
// Here's how these two threads work together.
@@ -523,7 +607,7 @@ TEST_CASE("RESTCONF subscribed notifications")
)"};
}
- auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter);
+ auto [id, uri, replayStartTimeRevision] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter, rpcReplayStart);
auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}";
REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:delete-subscription", headers, body) == expectedResponse);
}
@@ -531,7 +615,7 @@ TEST_CASE("RESTCONF subscribed notifications")
SECTION("Anonymous user cannot delete subscription craeted by anonymous user")
{
rpcRequestAuthHeader = std::nullopt;
- auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter);
+ auto [id, uri, replayStartTimeRevision] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter, rpcReplayStart);
auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}";
REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:delete-subscription", headers, body) == Response{403, jsonHeaders, R"({
"ietf-restconf:errors": {
@@ -587,7 +671,7 @@ TEST_CASE("RESTCONF subscribed notifications")
expectedResponse = Response{204, noContentTypeHeaders, ""};
}
- auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter);
+ auto [id, uri, replayStartTimeRevision] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter, rpcReplayStart);
auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}";
REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:kill-subscription", headers, body) == expectedResponse);
}
@@ -631,7 +715,7 @@ TEST_CASE("Terminating server under notification load")
std::optional<std::pair<std::string, std::string>> rpcRequestAuthHeader;
std::pair<std::string, std::string> auth = AUTH_ROOT;
- auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, auth, std::nullopt, {});
+ auto [id, uri, replayStartTimeRevision] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, auth, std::nullopt, {}, std::nullopt);
PREPARE_LOOP_WITH_EXCEPTIONS;
@@ -682,7 +766,7 @@ TEST_CASE("Cleaning up inactive subscriptions")
constexpr auto inactivityTimeout = 2s;
auto server = rousette::restconf::Server{srConn, SERVER_ADDRESS, SERVER_PORT, 0ms, 55s, inactivityTimeout};
- auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, {AUTH_ROOT}, std::nullopt, {});
+ auto [id, uri, replayStartTimeRevision] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, {AUTH_ROOT}, std::nullopt, {}, std::nullopt);
SECTION("Client connects and disconnects")
{
--
2.43.0
@@ -0,0 +1,95 @@
From 7d6b1a463fdacefaf7a7057564f6f4d4a8cb4632 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Tue, 25 Mar 2025 11:06:26 +0100
Subject: [PATCH 35/38] restconf: refactor getting datastore from string
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
This will be useful when processing YANG push requests.
Change-Id: I1e76335c8d674e81890d6cd1e973adaf521eb205
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/restconf/uri.cpp | 19 +++++--------------
src/restconf/utils/sysrepo.cpp | 16 ++++++++++++++++
src/restconf/utils/sysrepo.h | 1 +
3 files changed, 22 insertions(+), 14 deletions(-)
diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp
index 99c3d1f..6143435 100644
--- a/src/restconf/uri.cpp
+++ b/src/restconf/uri.cpp
@@ -17,6 +17,7 @@
#include "restconf/Exceptions.h"
#include "restconf/uri.h"
#include "restconf/uri_impl.h"
+#include "restconf/utils/sysrepo.h"
#include "restconf/utils/yang.h"
using namespace std::string_literals;
@@ -298,21 +299,11 @@ std::optional<sysrepo::Datastore> datastoreFromApiIdentifier(const boost::option
return std::nullopt;
}
- if (*datastore->prefix == "ietf-datastores") {
- if (datastore->identifier == "running") {
- return sysrepo::Datastore::Running;
- } else if (datastore->identifier == "operational") {
- return sysrepo::Datastore::Operational;
- } else if (datastore->identifier == "candidate") {
- return sysrepo::Datastore::Candidate;
- } else if (datastore->identifier == "startup") {
- return sysrepo::Datastore::Startup;
- } else if (datastore->identifier == "factory-default") {
- return sysrepo::Datastore::FactoryDefault;
- }
+ try {
+ return datastoreFromString(*datastore->prefix + ":"s + datastore->identifier);
+ } catch (const std::runtime_error&) {
+ throw ErrorResponse(400, "application", "operation-failed", "Unsupported datastore " + *datastore->prefix + ":" + datastore->identifier);
}
-
- throw ErrorResponse(400, "application", "operation-failed", "Unsupported datastore " + *datastore->prefix + ":" + datastore->identifier);
}
std::optional<std::variant<libyang::Module, libyang::SubmoduleParsed>> getModuleOrSubmodule(const libyang::Context& ctx, const std::string& name, const std::optional<std::string>& revision)
diff --git a/src/restconf/utils/sysrepo.cpp b/src/restconf/utils/sysrepo.cpp
index 622ab27..9f14cc7 100644
--- a/src/restconf/utils/sysrepo.cpp
+++ b/src/restconf/utils/sysrepo.cpp
@@ -22,4 +22,20 @@ ScopedDatastoreSwitch::~ScopedDatastoreSwitch()
m_session.switchDatastore(m_oldDatastore);
}
+sysrepo::Datastore datastoreFromString(const std::string& datastore)
+{
+ if (datastore == "ietf-datastores:running") {
+ return sysrepo::Datastore::Running;
+ } else if (datastore == "ietf-datastores:operational") {
+ return sysrepo::Datastore::Operational;
+ } else if (datastore == "ietf-datastores:candidate") {
+ return sysrepo::Datastore::Candidate;
+ } else if (datastore == "ietf-datastores:startup") {
+ return sysrepo::Datastore::Startup;
+ } else if (datastore == "ietf-datastores:factory-default") {
+ return sysrepo::Datastore::FactoryDefault;
+ }
+
+ throw std::runtime_error("Unknown datastore '" + datastore + "'");
+}
}
diff --git a/src/restconf/utils/sysrepo.h b/src/restconf/utils/sysrepo.h
index da6c359..0eced9e 100644
--- a/src/restconf/utils/sysrepo.h
+++ b/src/restconf/utils/sysrepo.h
@@ -26,4 +26,5 @@ public:
ScopedDatastoreSwitch& operator=(ScopedDatastoreSwitch&&) = delete;
};
+sysrepo::Datastore datastoreFromString(const std::string& datastore);
}
--
2.43.0
@@ -0,0 +1,31 @@
From c1d5d143020ddc51c3886b82e87d463cf3211fca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Fri, 21 Nov 2025 10:44:47 +0100
Subject: [PATCH 36/38] build: remove duplicate line
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Fixes: 817debab restconf: support replays in subscribed notifications
Change-Id: I4d2ea8429a4cb7b77f8739aff7a26a03cf535756
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 1 -
1 file changed, 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f98a64b..3d79dad 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -223,7 +223,6 @@ if(BUILD_TESTING)
--enable-feature encode-json
--enable-feature xpath
--enable-feature subtree
- --enable-feature replay
--install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-restconf-subscribed-notifications@2019-11-17.yang
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example.yang --enable-feature f1
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-delete.yang
--
2.43.0
@@ -0,0 +1,156 @@
From ed62a496744de0e0dc31171167d5d6f69f54af93 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Fri, 21 Nov 2025 12:54:44 +0100
Subject: [PATCH 37/38] remove obsolete targets
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: Idde217a4ae1bc6f03657d9609e0ec1b0a47a9747
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 9 --------
src/clock.cpp | 62 --------------------------------------------------
src/events.cpp | 33 ---------------------------
3 files changed, 104 deletions(-)
delete mode 100644 src/clock.cpp
delete mode 100644 src/events.cpp
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3d79dad..4a91128 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -125,14 +125,6 @@ target_link_libraries(rousette-restconf PUBLIC rousette-http rousette-sysrepo ro
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/src/auth/NacmIdentities.h.in ${CMAKE_CURRENT_BINARY_DIR}/NacmIdentities.h @ONLY)
include_directories(${CMAKE_CURRENT_BINARY_DIR})
-add_executable(clock-demo src/clock.cpp)
-target_link_libraries(clock-demo rousette-http Boost::system Threads::Threads)
-
-add_executable(watch-operational-ds
- src/events.cpp
-)
-target_link_libraries(watch-operational-ds PUBLIC rousette-sysrepo)
-
add_executable(rousette src/restconf/main.cpp)
target_link_libraries(rousette PUBLIC rousette-restconf PkgConfig::DOCOPT)
if(SYSTEMD_FOUND)
@@ -140,7 +132,6 @@ if(SYSTEMD_FOUND)
endif()
install(TARGETS
- # clock-demo
# watch-operational-ds
rousette
RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/)
diff --git a/src/clock.cpp b/src/clock.cpp
deleted file mode 100644
index aa72849..0000000
--- a/src/clock.cpp
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2016-2021 CESNET, https://photonics.cesnet.cz/
- *
- * Written by Jan Kundrát <jan.kundrat@cesnet.cz>
- *
-*/
-
-#include <boost/lexical_cast.hpp>
-#include <chrono>
-#include <nghttp2/asio_http2_server.h>
-#include <spdlog/spdlog.h>
-#include <thread>
-#include "http/EventStream.h"
-
-using namespace std::literals;
-
-constexpr auto keepAlivePingInterval = std::chrono::seconds{55};
-
-int main(int argc [[maybe_unused]], char** argv [[maybe_unused]])
-{
- spdlog::set_level(spdlog::level::trace);
-
- rousette::http::EventStream::Termination shutdown;
- rousette::http::EventStream::EventSignal sig;
- std::jthread timer{[&sig]() {
- for (int i = 0; /* forever */; ++i) {
- std::this_thread::sleep_for(666ms);
- spdlog::info("tick: {}", i);
- sig("ping #" + std::to_string(i));
- }
- }};
-
- nghttp2::asio_http2::server::http2 server;
- server.num_threads(4);
-
- server.handle("/events", [&shutdown, &sig](const auto& req, const auto& res) {
- rousette::http::EventStream::create(req, res, shutdown, sig, keepAlivePingInterval);
- });
-
- server.handle("/", [](const auto& req, const auto& resp) {
- spdlog::info("{}: {} {}", boost::lexical_cast<std::string>(req.remote_endpoint()), req.method(), req.uri().raw_path);
- resp.write_head(200, {{"content-type", {"text/html", false}}});
- resp.end(R"(<html><head><title>nghttp2 event stream</title></head>
-<body><h1>events</h1><ul id="x"></ul>
-<script type="text/javascript">
-const ev = new EventSource("/events");
-ev.onmessage = function(event) {
- const li = document.createElement("li");
- li.textContent = event.data;
- document.getElementById("x").appendChild(li);
-};
-</script>
-</body>
-</html>)");
- });
-
- boost::system::error_code ec;
- if (server.listen_and_serve(ec, "::", "10080")) {
- return 1;
- }
- return 0;
-}
diff --git a/src/events.cpp b/src/events.cpp
deleted file mode 100644
index dc0477a..0000000
--- a/src/events.cpp
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2016-2021 CESNET, https://photonics.cesnet.cz/
- *
- * Written by Jan Kundrát <jan.kundrat@cesnet.cz>
- *
-*/
-
-#include <csignal>
-#include <spdlog/spdlog.h>
-#include <sysrepo-cpp/Session.hpp>
-#include <unistd.h>
-#include "sr/AllEvents.h"
-
-using namespace rousette::sr;
-
-int main(int argc [[maybe_unused]], char* argv [[maybe_unused]] [])
-{
- spdlog::set_level(spdlog::level::trace);
-
- auto sess = sysrepo::Connection{}.sessionStart();
- auto e = AllEvents{
- sess,
- /* AllEvents::WithAttributes::All, */
- AllEvents::WithAttributes::RemoveEmptyOperationAndOrigin,
- /* AllEvents::WithAttributes::None, */
- };
-
- signal(SIGTERM, [](int) {});
- signal(SIGINT, [](int) {});
- pause();
-
- return 0;
-}
--
2.43.0
@@ -0,0 +1,156 @@
From 5a1d0dac28c1a78e90d3906693228584adcf3ab1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 20 Nov 2025 16:13:43 +0100
Subject: [PATCH 38/38] port to libyang v4.2
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Depends-on: https://gerrit.cesnet.cz/c/CzechLight/dependencies/+/9012
Change-Id: I1ac6b25a3c1034f06917594732f066fd77d4b26b
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 2 +-
src/restconf/Server.cpp | 10 +++++-----
src/restconf/utils/yang.cpp | 2 +-
src/sr/AllEvents.cpp | 4 ++--
src/sr/OpticalEvents.cpp | 2 +-
tests/event_watchers.cpp | 2 +-
tests/restconf-subscribed-notifications.cpp | 2 +-
7 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4a91128..4bb029b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -76,7 +76,7 @@ find_package(Boost 1.66 REQUIRED CONFIG COMPONENTS system thread)
pkg_check_modules(SYSREPO REQUIRED sysrepo IMPORTED_TARGET)
pkg_check_modules(SYSREPO-CPP REQUIRED IMPORTED_TARGET sysrepo-cpp>=7)
-pkg_check_modules(LIBYANG-CPP REQUIRED IMPORTED_TARGET libyang-cpp>=5)
+pkg_check_modules(LIBYANG-CPP REQUIRED IMPORTED_TARGET libyang-cpp>=6)
pkg_check_modules(SYSTEMD IMPORTED_TARGET libsystemd)
pkg_check_modules(PAM REQUIRED IMPORTED_TARGET pam)
pkg_check_modules(DOCOPT REQUIRED IMPORTED_TARGET docopt)
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index d166236..3c7c3c7 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -111,7 +111,7 @@ void rejectWithErrorImpl(libyang::Context ctx, const libyang::DataFormat& dataFo
}
res.write_head(code, headers);
- res.end(*parent.printStr(dataFormat, libyang::PrintFlags::WithSiblings));
+ res.end(*parent.printStr(dataFormat, libyang::PrintFlags::Siblings));
}
void rejectWithError(libyang::Context ctx, const libyang::DataFormat& dataFormat, const request& req, const response& res, const int code, const std::string errorType, const std::string& errorTag, const std::string& errorMessage, const std::optional<std::string>& errorPath)
@@ -543,7 +543,7 @@ void processActionOrRPC(std::shared_ptr<RequestContext> requestCtx, const std::c
contentType(requestCtx->dataFormat.response),
CORS,
});
- requestCtx->res.end(*envelope->printStr(requestCtx->dataFormat.response, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont));
+ requestCtx->res.end(*envelope->printStr(requestCtx->dataFormat.response, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers));
}
void processPost(std::shared_ptr<RequestContext> requestCtx, const std::chrono::milliseconds timeout)
@@ -709,7 +709,7 @@ void processYangPatch(std::shared_ptr<RequestContext> requestCtx, const std::chr
yangPatchStatus->newExtPath(yangPatchStatusExt, "/ietf-yang-patch:yang-patch-status/ok", std::nullopt);
requestCtx->res.write_head(200, {contentType(requestCtx->dataFormat.response), CORS});
- requestCtx->res.end(*yangPatchStatus->printStr(requestCtx->dataFormat.response, libyang::PrintFlags::WithSiblings));
+ requestCtx->res.end(*yangPatchStatus->printStr(requestCtx->dataFormat.response, libyang::PrintFlags::Siblings));
}
void processPutOrPlainPatch(std::shared_ptr<RequestContext> requestCtx, const std::chrono::milliseconds timeout)
@@ -842,7 +842,7 @@ libyang::PrintFlags libyangPrintFlags(const libyang::DataNode& dataNode, const s
} catch(const libyang::Error& e) {
}
- libyang::PrintFlags ret = libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont;
+ libyang::PrintFlags ret = libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers;
if (!withDefaults && node && (node->schema().nodeType() == libyang::NodeType::Leaf || node->schema().nodeType() == libyang::NodeType::Leaflist) && node->asTerm().isImplicitDefault()) {
return ret | libyang::PrintFlags::WithDefaultsAll;
@@ -1116,7 +1116,7 @@ Server::Server(
case RestconfRequest::Type::ListRPC:
res.write_head(200, {contentType(dataFormat.response), CORS});
res.end(*apiResource(sess.getContext(), restconfRequest.type, dataFormat.response)
- .printStr(dataFormat.response, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont));
+ .printStr(dataFormat.response, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers));
break;
case RestconfRequest::Type::GetData: {
diff --git a/src/restconf/utils/yang.cpp b/src/restconf/utils/yang.cpp
index 9358840..5c311c1 100644
--- a/src/restconf/utils/yang.cpp
+++ b/src/restconf/utils/yang.cpp
@@ -112,7 +112,7 @@ std::string as_restconf_notification(const libyang::Context& ctx, libyang::DataF
envelope->insertChild(*eventTime);
envelope->insertChild(notification);
- auto res = *envelope->printStr(dataFormat, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont);
+ auto res = *envelope->printStr(dataFormat, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers);
// notification node comes from sysrepo and sysrepo will free this; if not unlinked then envelope destructor would try to free this as well
notification.unlink();
diff --git a/src/sr/AllEvents.cpp b/src/sr/AllEvents.cpp
index 7b6226b..2202b36 100644
--- a/src/sr/AllEvents.cpp
+++ b/src/sr/AllEvents.cpp
@@ -110,10 +110,10 @@ sysrepo::ErrorCode AllEvents::onChange(sysrepo::Session session, const std::stri
break;
}
};
- auto json = *copy.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont);
+ auto json = *copy.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers);
spdlog::info("JSON: {}", json);
spdlog::warn("FULL JSON: {}",
- *session.getData('/' + module + ":*")->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings));
+ *session.getData('/' + module + ":*")->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings));
change(module, json);
}
diff --git a/src/sr/OpticalEvents.cpp b/src/sr/OpticalEvents.cpp
index 25906a8..3f78ddc 100644
--- a/src/sr/OpticalEvents.cpp
+++ b/src/sr/OpticalEvents.cpp
@@ -15,7 +15,7 @@
namespace {
std::string dumpDataFrom(sysrepo::Session session, const std::string& module)
{
- return *session.getData('/' + module + ":*")->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont);
+ return *session.getData('/' + module + ":*")->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers);
}
}
diff --git a/tests/event_watchers.cpp b/tests/event_watchers.cpp
index 26ba951..38ee7be 100644
--- a/tests/event_watchers.cpp
+++ b/tests/event_watchers.cpp
@@ -23,7 +23,7 @@ void datastoreChanges(auto session, auto& dsChangesMock, auto path)
void datastoreNewState(auto session, auto& dsChangesMock, auto path)
{
- dsChangesMock.contentAfterChange(session.getData(path)->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings));
+ dsChangesMock.contentAfterChange(session.getData(path)->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings));
}
}
diff --git a/tests/restconf-subscribed-notifications.cpp b/tests/restconf-subscribed-notifications.cpp
index d42fa96..73b9fab 100644
--- a/tests/restconf-subscribed-notifications.cpp
+++ b/tests/restconf-subscribed-notifications.cpp
@@ -92,7 +92,7 @@ EstablishSubscriptionResult establishSubscription(
data->unlinkWithSiblings();
envelope->insertChild(*data);
- auto body = *envelope->printStr(rpcEncoding, libyang::PrintFlags::WithSiblings);
+ auto body = *envelope->printStr(rpcEncoding, libyang::PrintFlags::Siblings);
auto resp = post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:establish-subscription", requestHeaders, body);
REQUIRE(resp.equalStatusCodeAndHeaders(Response{200, expectedHeaders, ""}));
--
2.43.0
@@ -0,0 +1,47 @@
From ee0ce2ea31f3ecba4a4753a761df08f0698c2726 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Mon, 20 Oct 2025 11:47:26 +0200
Subject: [PATCH 1/9] CI: pin to libyang v3
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Depends-on: https://gerrit.cesnet.cz/c/CzechLight/libyang-cpp/+/8978
Change-Id: I2521ad3a62d6197e757e636b88478deeb3bb5592
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
.zuul.yaml | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/.zuul.yaml b/.zuul.yaml
index 7ada6f7..cef174f 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -4,9 +4,9 @@
- f38-gcc-cover:
required-projects:
- name: github/CESNET/libyang
- override-checkout: devel
+ override-checkout: cesnet/2025-07-08
- name: github/sysrepo/sysrepo
- override-checkout: devel
+ override-checkout: cesnet/2025-06-02
- name: github/doctest/doctest
override-checkout: v2.4.8
- name: github/rollbear/trompeloeil
@@ -15,9 +15,9 @@
- f38-clang-asan-ubsan:
required-projects: &projects
- name: github/CESNET/libyang
- override-checkout: devel
+ override-checkout: cesnet/2025-07-08
- name: github/sysrepo/sysrepo
- override-checkout: devel
+ override-checkout: cesnet/2025-06-02
- name: github/doctest/doctest
override-checkout: v2.4.11
- name: github/rollbear/trompeloeil
--
2.43.0
@@ -0,0 +1,33 @@
From e49463aba8b44b932fdd342d1c1d117e70414aba Mon Sep 17 00:00:00 2001
From: Edoardo Bortolozzo <edoardo.bortolozzo@thinkquantum.com>
Date: Fri, 17 Oct 2025 18:08:47 +0200
Subject: [PATCH 2/9] fix: implicit dependency on optional
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
chrono imports optional in gcc >=13.
added the explicit include to compile in gcc <=12.
Change-Id: I18cc1a45ab522ee6738d4bc7080696cba8284903
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
include/sysrepo-cpp/Connection.hpp | 1 +
1 file changed, 1 insertion(+)
diff --git a/include/sysrepo-cpp/Connection.hpp b/include/sysrepo-cpp/Connection.hpp
index bf72bcd..a469a2c 100644
--- a/include/sysrepo-cpp/Connection.hpp
+++ b/include/sysrepo-cpp/Connection.hpp
@@ -7,6 +7,7 @@
*/
#pragma once
+#include <optional>
#include <chrono>
#include <memory>
#include <string>
--
2.43.0
@@ -0,0 +1,31 @@
From b8e349aacae4c7063517a1bc652e9e93f90e28ba Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Fri, 24 Oct 2025 17:05:52 +0200
Subject: [PATCH 3/9] fix #include for for our public headers
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: Idcbfe38348e3ca2e4331fc1fad542ae759b8d0f1
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/utils/utils.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp
index ff4a32c..c13b394 100644
--- a/src/utils/utils.cpp
+++ b/src/utils/utils.cpp
@@ -7,7 +7,7 @@
*/
#include <sysrepo-cpp/Connection.hpp>
-#include "sysrepo-cpp/utils/exception.hpp"
+#include <sysrepo-cpp/utils/exception.hpp>
extern "C" {
#include <sysrepo.h>
}
--
2.43.0
@@ -0,0 +1,82 @@
From 753a3e6eb346f2a9f6263b2d9fcd1c6dbb508247 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Fri, 24 Oct 2025 17:06:56 +0200
Subject: [PATCH 4/9] reduce number of files which are named "utils.hpp"
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
We used to have three (one in publicheaders, one in src/utils/, one
below tests/). Let's cut that by one to increase our collective sanity.
Change-Id: I9e3d93c65dffb29ba18b3ae325ddd7712a8be7a4
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/Connection.cpp | 2 +-
src/Session.cpp | 2 +-
src/Subscription.cpp | 2 +-
src/utils/{utils.hpp => misc.hpp} | 0
src/utils/utils.cpp | 2 +-
5 files changed, 4 insertions(+), 4 deletions(-)
rename src/utils/{utils.hpp => misc.hpp} (100%)
diff --git a/src/Connection.cpp b/src/Connection.cpp
index 0297ebe..1717b9a 100644
--- a/src/Connection.cpp
+++ b/src/Connection.cpp
@@ -13,7 +13,7 @@ extern "C" {
#include <sysrepo-cpp/utils/exception.hpp>
#include "utils/enum.hpp"
#include "utils/exception.hpp"
-#include "utils/utils.hpp"
+#include "utils/misc.hpp"
namespace sysrepo {
diff --git a/src/Session.cpp b/src/Session.cpp
index dfe1044..994ce87 100644
--- a/src/Session.cpp
+++ b/src/Session.cpp
@@ -21,7 +21,7 @@ extern "C" {
#include <utility>
#include "utils/enum.hpp"
#include "utils/exception.hpp"
-#include "utils/utils.hpp"
+#include "utils/misc.hpp"
using namespace std::string_literals;
namespace sysrepo {
diff --git a/src/Subscription.cpp b/src/Subscription.cpp
index 303b54c..5bb724e 100644
--- a/src/Subscription.cpp
+++ b/src/Subscription.cpp
@@ -16,7 +16,7 @@ extern "C" {
}
#include "utils/enum.hpp"
#include "utils/exception.hpp"
-#include "utils/utils.hpp"
+#include "utils/misc.hpp"
namespace sysrepo {
/**
diff --git a/src/utils/utils.hpp b/src/utils/misc.hpp
similarity index 100%
rename from src/utils/utils.hpp
rename to src/utils/misc.hpp
diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp
index c13b394..2e8d8b4 100644
--- a/src/utils/utils.cpp
+++ b/src/utils/utils.cpp
@@ -12,7 +12,7 @@ extern "C" {
#include <sysrepo.h>
}
#include "enum.hpp"
-#include "utils.hpp"
+#include "misc.hpp"
namespace sysrepo {
/**
--
2.43.0
@@ -0,0 +1,60 @@
From dc118efa44b13508e1af47d924ad02ce58c58cea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Fri, 24 Oct 2025 17:18:49 +0200
Subject: [PATCH 5/9] Fix a different function return type
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
The prototype that was specified in the header file did not match what
was used in the corresponding .cpp implementation. This has not been a
problem because that implementation .cpp file was not including the
actual header, so this was not detected. Then, at runtime, this was not
a problem due to the way how derived types are laid out in memory.
Fixes: 6ed5442 Utilities for working with existing sysrepo:discard-items nodes
Change-Id: Ie4cf8c0e39e9c8486a6289c0328510ad6f7360fb
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/utils/utils.cpp | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp
index 2e8d8b4..61d0c7f 100644
--- a/src/utils/utils.cpp
+++ b/src/utils/utils.cpp
@@ -8,6 +8,7 @@
#include <sysrepo-cpp/Connection.hpp>
#include <sysrepo-cpp/utils/exception.hpp>
+#include <sysrepo-cpp/utils/utils.hpp>
extern "C" {
#include <sysrepo.h>
}
@@ -78,7 +79,7 @@ void checkNoThreadFlag(const SubscribeOptions opts, const std::optional<FDHandli
* @see Session::operationalChanges()
* @see Session::dropForeignOperationalContent()
*/
-std::optional<libyang::DataNode> findMatchingDiscard(libyang::DataNode root, const std::string& xpath)
+std::optional<libyang::DataNodeOpaque> findMatchingDiscard(libyang::DataNode root, const std::string& xpath)
{
auto discard = root.firstOpaqueSibling();
while (discard) {
@@ -97,11 +98,11 @@ std::optional<libyang::DataNode> findMatchingDiscard(libyang::DataNode root, con
/**
* @short Find all sysrepo:discard-items nodes which match the given XPath or the descendants of this XPath
*/
-std::vector<libyang::DataNode> findMatchingDiscardPrefixes(libyang::DataNode root, const std::string& xpathPrefix)
+std::vector<libyang::DataNodeOpaque> findMatchingDiscardPrefixes(libyang::DataNode root, const std::string& xpathPrefix)
{
auto withSlash = (xpathPrefix.empty() || xpathPrefix[xpathPrefix.size() - 1] == '/') ? xpathPrefix : xpathPrefix + '/';
auto withBracket = (xpathPrefix.empty() || xpathPrefix[xpathPrefix.size() - 1] == '[') ? xpathPrefix : xpathPrefix + '[';
- std::vector<libyang::DataNode> res;
+ std::vector<libyang::DataNodeOpaque> res;
auto discard = root.firstOpaqueSibling();
while (discard) {
if (discard->name().matches("sysrepo", "discard-items")) {
--
2.43.0
@@ -0,0 +1,61 @@
From b943faea3cb00c5e86f21710647a2ca370e41f33 Mon Sep 17 00:00:00 2001
From: Edoardo Bortolozzo <edoardo.bortolozzo@thinkquantum.com>
Date: Fri, 24 Oct 2025 13:45:00 +0200
Subject: [PATCH 6/9] fix: raw strings and macros break gcc < 13
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
see bug report:
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=55971
Change-Id: I04afc2b3bee86a7e4b038b277aad0e3a150a1940
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
tests/subscriptions-dynamic.cpp | 21 ++++++++++++++-------
1 file changed, 14 insertions(+), 7 deletions(-)
diff --git a/tests/subscriptions-dynamic.cpp b/tests/subscriptions-dynamic.cpp
index 5ed173b..561df0c 100644
--- a/tests/subscriptions-dynamic.cpp
+++ b/tests/subscriptions-dynamic.cpp
@@ -39,21 +39,28 @@
REQUIRE(pipeStatus((SUBSCRIPTION).fd(), -1) == PipeStatus::DataReady); \
(SUBSCRIPTION).processEvent(cbNotif);
-#define SUBSCRIPTION_TERMINATED(SUBSCRIPTION) R"({
+auto SUBSCRIPTION_TERMINATED(const auto& SUBSCRIPTION)
+{
+ return R"({
"ietf-subscribed-notifications:subscription-terminated": {
- "id": )" + std::to_string((SUBSCRIPTION).subscriptionId()) + R"(,
+ "id": )"
+ + std::to_string((SUBSCRIPTION).subscriptionId()) + R"(,
"reason": "no-such-subscription"
}
}
-)"
+)";
+};
-#define REPLAY_COMPLETED(SUBSCRIPTION) R"({
+auto REPLAY_COMPLETED(const auto& SUBSCRIPTION)
+{
+ return R"({
"ietf-subscribed-notifications:replay-completed": {
- "id": )" \
- + std::to_string((SUBSCRIPTION).subscriptionId()) + R"(
+ "id": )"
+ + std::to_string((SUBSCRIPTION).subscriptionId()) + R"(
}
}
-)"
+)";
+};
#define REQUIRE_YANG_PUSH_UPDATE(SUBSCRIPTION, NOTIFICATION) \
TROMPELOEIL_REQUIRE_CALL(rec, recordYangPushUpdate((SUBSCRIPTION).subscriptionId(), NOTIFICATION)).IN_SEQUENCE(seq);
--
2.43.0
@@ -0,0 +1,305 @@
From 5fd8cf4fb8db1d65b6a20d2777bad22647e7f5e0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 23 Oct 2025 16:04:10 +0200
Subject: [PATCH 7/9] Port to libyang v4
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
I'm rather disappointed with the upstream changes that implement that
shared libyang "printed context" mapped to a fixed address, I think it's
effectively a step backwards. Upstream is happy with their solution, so
here we are.
Change-Id: I6b89422ec66bc694dc2c6ecd0d40aa1e7f76a741
Depends-on: https://gerrit.cesnet.cz/c/CzechLight/libyang-cpp/+/8984
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
.zuul.yaml | 8 +++---
CMakeLists.txt | 6 ++---
ci/build.sh | 4 +++
include/sysrepo-cpp/Enum.hpp | 18 ++++++++++---
include/sysrepo-cpp/utils/utils.hpp | 5 ++++
src/Connection.cpp | 3 +--
src/Session.cpp | 2 +-
src/utils/enum.hpp | 18 +++++++------
src/utils/utils.cpp | 18 +++++++++++++
tests/session.cpp | 39 +++++++++++++++++++++--------
10 files changed, 90 insertions(+), 31 deletions(-)
diff --git a/.zuul.yaml b/.zuul.yaml
index cef174f..7ada6f7 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -4,9 +4,9 @@
- f38-gcc-cover:
required-projects:
- name: github/CESNET/libyang
- override-checkout: cesnet/2025-07-08
+ override-checkout: devel
- name: github/sysrepo/sysrepo
- override-checkout: cesnet/2025-06-02
+ override-checkout: devel
- name: github/doctest/doctest
override-checkout: v2.4.8
- name: github/rollbear/trompeloeil
@@ -15,9 +15,9 @@
- f38-clang-asan-ubsan:
required-projects: &projects
- name: github/CESNET/libyang
- override-checkout: cesnet/2025-07-08
+ override-checkout: devel
- name: github/sysrepo/sysrepo
- override-checkout: cesnet/2025-06-02
+ override-checkout: devel
- name: github/doctest/doctest
override-checkout: v2.4.11
- name: github/rollbear/trompeloeil
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f2a9b09..49338dd 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -19,7 +19,7 @@ add_custom_target(sysrepo-cpp-version-cmake
cmake/ProjectGitVersionRunner.cmake
)
include(cmake/ProjectGitVersion.cmake)
-set(SYSREPO_CPP_PKG_VERSION "6")
+set(SYSREPO_CPP_PKG_VERSION "7")
prepare_git_version(SYSREPO_CPP_VERSION ${SYSREPO_CPP_PKG_VERSION})
find_package(Doxygen)
@@ -27,8 +27,8 @@ option(WITH_DOCS "Create and install internal documentation (needs Doxygen)" ${D
option(WITH_EXAMPLES "Build examples" ON)
find_package(PkgConfig)
-pkg_check_modules(LIBYANG_CPP REQUIRED libyang-cpp>=3 IMPORTED_TARGET)
-pkg_check_modules(SYSREPO REQUIRED sysrepo>=3.7.4 IMPORTED_TARGET)
+pkg_check_modules(LIBYANG_CPP REQUIRED libyang-cpp>=5 IMPORTED_TARGET)
+pkg_check_modules(SYSREPO REQUIRED sysrepo>=4.2.4 IMPORTED_TARGET)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
diff --git a/ci/build.sh b/ci/build.sh
index 785a811..67bc7d3 100755
--- a/ci/build.sh
+++ b/ci/build.sh
@@ -47,6 +47,10 @@ if [[ $ZUUL_JOB_NAME =~ .*-tsan ]]; then
# Our TSAN does not have interceptors for a variety of "less common" functions such as pthread_mutex_clocklock.
# Disable all functions which are optional in sysrepo/libnetconf2/Netopeer2.
CMAKE_OPTIONS="${CMAKE_OPTIONS} -DHAVE_PTHREAD_MUTEX_TIMEDLOCK=OFF -DHAVE_PTHREAD_MUTEX_CLOCKLOCK=OFF -DHAVE_PTHREAD_RWLOCK_CLOCKRDLOCK=OFF -DHAVE_PTHREAD_RWLOCK_CLOCKWRLOCK=OFF -DHAVE_PTHREAD_COND_CLOCKWAIT=OFF"
+
+ # This is currently broken on TSAN ("Failed to map the printed context (Operation not permitted).").
+ # Even if it wasn't broken, I want at least one build in the matrix with this (mis)feature disabled.
+ CMAKE_OPTIONS="${CMAKE_OPTIONS} -DPRINTED_CONTEXT_ADDRESS=0"
fi
if [[ $ZUUL_JOB_NAME =~ .*-cover.* ]]; then
diff --git a/include/sysrepo-cpp/Enum.hpp b/include/sysrepo-cpp/Enum.hpp
index 48467c6..505797a 100644
--- a/include/sysrepo-cpp/Enum.hpp
+++ b/include/sysrepo-cpp/Enum.hpp
@@ -176,12 +176,24 @@ enum class NotificationType : uint32_t {
};
/**
- * Wraps `sr_conn_flag_e`.
+ * Wraps `sr_context_flag_t`.
+ */
+enum class ContextFlags : uint32_t {
+ Default = 0x00, /**< SR_CTX_DEFAULT */
+ NoPrinted = 0x01, /**< SR_CTX_NO_PRINTED */
+ LibYangPrivParsed = 0x02, /**< SR_CTX_SET_PRIV_PARSED */
+};
+
+constexpr ContextFlags operator|(const ContextFlags a, const ContextFlags b)
+{
+ return implEnumBitOr(a, b);
+}
+
+/**
+ * Wraps `sr_conn_flag_t`.
*/
enum class ConnectionFlags : uint32_t {
Default = 0x00, /**< SR_CONN_DEFAULT */
- CacheRunning = 0x01, /**< SR_CONN_CACHE_RUNNING */
- LibYangPrivParsed = 0x02, /**< SR_CONN_CTX_SET_PRIV_PARSED */
};
constexpr ConnectionFlags operator|(const ConnectionFlags a, const ConnectionFlags b)
diff --git a/include/sysrepo-cpp/utils/utils.hpp b/include/sysrepo-cpp/utils/utils.hpp
index 64b1c2e..6f2446f 100644
--- a/include/sysrepo-cpp/utils/utils.hpp
+++ b/include/sysrepo-cpp/utils/utils.hpp
@@ -12,6 +12,11 @@
namespace sysrepo {
Session wrapUnmanagedSession(sr_session_ctx_s* session);
void setLogLevelStderr(const LogLevel);
+enum class GlobalContextEffect {
+ Lazy, /**< Set these flags the next time the shared context is applied */
+ Immediate, /**< Apply immediately */
+};
+ContextFlags setGlobalContextOptions(const ContextFlags flags, const GlobalContextEffect when);
std::optional<libyang::DataNodeOpaque> findMatchingDiscard(libyang::DataNode root, const std::string& xpath);
std::vector<libyang::DataNodeOpaque> findMatchingDiscardPrefixes(libyang::DataNode root, const std::string& xpathPrefix);
void unlinkFromForest(std::optional<libyang::DataNode>& root, libyang::DataNode node);
diff --git a/src/Connection.cpp b/src/Connection.cpp
index 1717b9a..96e340c 100644
--- a/src/Connection.cpp
+++ b/src/Connection.cpp
@@ -26,7 +26,7 @@ Connection::Connection(const ConnectionFlags options)
: ctx(nullptr)
{
sr_conn_ctx_t* ctx;
- auto res = sr_connect(static_cast<sr_conn_options_t>(options), &ctx);
+ auto res = sr_connect(static_cast<sr_conn_flag_t>(options), &ctx);
throwIfError(res, "Couldn't connect to sysrepo");
this->ctx = std::shared_ptr<sr_conn_ctx_t>(ctx, sr_disconnect);
@@ -96,5 +96,4 @@ uint32_t Connection::getId() const
{
return sr_get_cid(ctx.get());
}
-
}
diff --git a/src/Session.cpp b/src/Session.cpp
index 994ce87..95b8f24 100644
--- a/src/Session.cpp
+++ b/src/Session.cpp
@@ -241,7 +241,7 @@ std::optional<libyang::DataNode> Session::operationalChanges(const std::optional
void Session::discardOperationalChanges(const std::optional<std::string>& moduleName, std::chrono::milliseconds timeout)
{
SYSREPO_CPP_SESSION_MTX;
- auto res = sr_discard_oper_changes(nullptr, m_sess.get(), moduleName ? nullptr : moduleName->c_str(), timeout.count());
+ auto res = sr_discard_oper_changes(m_sess.get(), moduleName ? nullptr : moduleName->c_str(), timeout.count());
throwIfError(res, "Session::discardOoperationalChanges: Couldn't discard "s + (moduleName ? "for module \"" + *moduleName + "\"" : "globally"s), m_sess.get());
}
diff --git a/src/utils/enum.hpp b/src/utils/enum.hpp
index 2d3a298..e917b21 100644
--- a/src/utils/enum.hpp
+++ b/src/utils/enum.hpp
@@ -151,13 +151,16 @@ static_assert(toNotificationType(SR_EV_NOTIF_MODIFIED) == NotificationType::Modi
static_assert(toNotificationType(SR_EV_NOTIF_SUSPENDED) == NotificationType::Suspended);
static_assert(toNotificationType(SR_EV_NOTIF_RESUMED) == NotificationType::Resumed);
-static_assert(std::is_same_v<sr_conn_options_t, std::underlying_type_t<ConnectionFlags>>);
+static_assert(std::is_same_v<std::underlying_type_t<sr_conn_flag_t>, std::underlying_type_t<ConnectionFlags>>);
static_assert(static_cast<ConnectionFlags>(SR_CONN_DEFAULT) == ConnectionFlags::Default);
-static_assert(static_cast<ConnectionFlags>(SR_CONN_CACHE_RUNNING) == ConnectionFlags::CacheRunning);
-static_assert(static_cast<ConnectionFlags>(SR_CONN_CTX_SET_PRIV_PARSED) == ConnectionFlags::LibYangPrivParsed);
-static_assert(static_cast<ConnectionFlags>(SR_CONN_CACHE_RUNNING | SR_CONN_CTX_SET_PRIV_PARSED) == (ConnectionFlags::CacheRunning | ConnectionFlags::LibYangPrivParsed));
-static_assert(std::is_same_v<sr_get_options_t, std::underlying_type_t<GetOptions>>);
+static_assert(std::is_same_v<std::underlying_type_t<sr_context_flag_t>, std::underlying_type_t<ContextFlags>>);
+static_assert(static_cast<ContextFlags>(SR_CTX_DEFAULT) == ContextFlags::Default);
+static_assert(static_cast<ContextFlags>(SR_CTX_SET_PRIV_PARSED) == ContextFlags::LibYangPrivParsed);
+static_assert(static_cast<ContextFlags>(SR_CTX_NO_PRINTED) == ContextFlags::NoPrinted);
+static_assert(static_cast<ContextFlags>(SR_CTX_SET_PRIV_PARSED | SR_CTX_NO_PRINTED) == (ContextFlags::LibYangPrivParsed | ContextFlags::NoPrinted));
+
+// upstream project provides no dedicated type for these
static_assert(static_cast<GetOptions>(SR_OPER_DEFAULT) == GetOptions::Default);
static_assert(static_cast<GetOptions>(SR_OPER_NO_STATE) == GetOptions::OperNoState);
static_assert(static_cast<GetOptions>(SR_OPER_NO_CONFIG) == GetOptions::OperNoConfig);
@@ -168,9 +171,10 @@ static_assert(static_cast<GetOptions>(SR_OPER_NO_POLL_CACHED) == GetOptions::Ope
static_assert(static_cast<GetOptions>(SR_OPER_NO_RUN_CACHED) == GetOptions::OperNoRunningCached);
static_assert(static_cast<GetOptions>(SR_GET_NO_FILTER) == GetOptions::NoFilter);
-constexpr sr_get_options_t toGetOptions(const GetOptions opts)
+// upstream project provides no dedicated type for these
+constexpr uint32_t toGetOptions(const GetOptions opts)
{
- return static_cast<sr_get_options_t>(opts);
+ return static_cast<uint32_t>(opts);
}
static_assert(static_cast<YangPushChange>(SRSN_YP_CHANGE_CREATE) == YangPushChange::Create);
diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp
index 61d0c7f..dbbc5ce 100644
--- a/src/utils/utils.cpp
+++ b/src/utils/utils.cpp
@@ -13,6 +13,7 @@ extern "C" {
#include <sysrepo.h>
}
#include "enum.hpp"
+#include "exception.hpp"
#include "misc.hpp"
namespace sysrepo {
@@ -37,6 +38,23 @@ void setLogLevelStderr(const LogLevel level)
sr_log_stderr(toLogLevel(level));
}
+/**
+ * @brief Set global sysrepo-level context options
+ *
+ * Be advised of consequences of manipulating a shared global state, especially when using multiple connections.
+ *
+ * Wraps `sr_context_options`.
+ */
+ContextFlags setGlobalContextOptions(const ContextFlags flags, const GlobalContextEffect when)
+{
+ uint32_t old;
+ throwIfError(
+ sr_context_options(static_cast<uint32_t>(flags), when == GlobalContextEffect::Immediate, &old),
+ "sr_context_options failed",
+ nullptr);
+ return static_cast<ContextFlags>(old);
+}
+
std::timespec toTimespec(std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> tp)
{
// https://embeddedartistry.com/blog/2019/01/31/converting-between-timespec-stdchrono#-std-chrono-time_point-to-timespec-
diff --git a/tests/session.cpp b/tests/session.cpp
index 47b5631..a709a09 100644
--- a/tests/session.cpp
+++ b/tests/session.cpp
@@ -627,17 +627,34 @@ TEST_CASE("session")
DOCTEST_SUBCASE("libyang context flags")
{
- sess.setItem("/test_module:popelnice/s", "666");
- REQUIRE(sess.getOneNode("/test_module:popelnice/s").asTerm().valueStr() == "666");
- // Parsed type info is not preserved by libyang unless its context is constructed with a flag,
- // and that flag is not used by sysrepo by default...
- REQUIRE_THROWS_AS(sess.getOneNode("/test_module:popelnice/s").schema().asLeaf().valueType().asString().length(), libyang::ParsedInfoUnavailable);
-
- // ...unless we pass that flag explicitly as a parameter to the connection.
- auto sess2 = sysrepo::Connection{sysrepo::ConnectionFlags::LibYangPrivParsed}.sessionStart();
- sess2.setItem("/test_module:popelnice/s", "333");
- REQUIRE(sess2.getOneNode("/test_module:popelnice/s").asTerm().valueStr() == "333");
- REQUIRE(sess2.getOneNode("/test_module:popelnice/s").schema().asLeaf().valueType().asString().length().parts[0].max == 10);
+ DOCTEST_SUBCASE("default")
+ {
+ sess.setItem("/test_module:popelnice/s", "666");
+ REQUIRE(sess.getOneNode("/test_module:popelnice/s").asTerm().valueStr() == "666");
+ // Parsed type info is not preserved by libyang unless its context is constructed with a flag,
+ // and that flag is not used by sysrepo by default.
+ REQUIRE_THROWS_AS(sess.getOneNode("/test_module:popelnice/s").schema().asLeaf().valueType().asString().length(), libyang::ParsedInfoUnavailable);
+
+ // pending changes hold the context lock, so we cannot change the global context options
+ REQUIRE_THROWS_WITH_AS(
+ sysrepo::setGlobalContextOptions(
+ sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted,
+ sysrepo::GlobalContextEffect::Immediate),
+ "sr_context_options failed: SR_ERR_TIME_OUT",
+ sysrepo::ErrorWithCode);
+ }
+
+ // Since this is actually a shared global state, changing it is "tricky". The best bet is to actually
+ // ensure that there are no other connections (which we cannot do here, we already have a connection opened
+ // from the common code at the very top of this test case), and especially no internal sysrepo-level locks
+ // on the shared libyang context. For example, no uncommited changes -- hence a new subcase.
+ DOCTEST_SUBCASE("Connection::setGlobalContextOptions LibYangPrivParsed")
+ {
+ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate);
+ sess.setItem("/test_module:popelnice/s", "333");
+ REQUIRE(sess.getOneNode("/test_module:popelnice/s").asTerm().valueStr() == "333");
+ REQUIRE(sess.getOneNode("/test_module:popelnice/s").schema().asLeaf().valueType().asString().length().parts[0].max == 10);
+ }
}
DOCTEST_SUBCASE("replay support")
--
2.43.0
@@ -0,0 +1,90 @@
From 6e9dbd8513ce75e51f2f714df69c08af5b556d1d Mon Sep 17 00:00:00 2001
From: Edoardo Bortolozzo <edoardo.bortolozzo@thinkquantum.com>
Date: Tue, 28 Oct 2025 13:24:13 +0100
Subject: [PATCH 8/9] fix: onOperGet unconsistent with C library.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Fixes: https://github.com/sysrepo/sysrepo-cpp/issues/29
Change-Id: I5a21069889c59466df8e9457c3a316b58e2571bb
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
include/sysrepo-cpp/Session.hpp | 2 +-
include/sysrepo-cpp/Subscription.hpp | 2 +-
src/Session.cpp | 4 ++--
src/Subscription.cpp | 4 ++--
4 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/include/sysrepo-cpp/Session.hpp b/include/sysrepo-cpp/Session.hpp
index aae7068..3af4b10 100644
--- a/include/sysrepo-cpp/Session.hpp
+++ b/include/sysrepo-cpp/Session.hpp
@@ -127,7 +127,7 @@ public:
[[nodiscard]] Subscription onOperGet(
const std::string& moduleName,
OperGetCb cb,
- const std::optional<std::string>& xpath = std::nullopt,
+ const std::string& path,
const SubscribeOptions opts = SubscribeOptions::Default,
ExceptionHandler handler = nullptr,
const std::optional<FDHandling>& callbacks = std::nullopt);
diff --git a/include/sysrepo-cpp/Subscription.hpp b/include/sysrepo-cpp/Subscription.hpp
index c8f3ff8..14ecce4 100644
--- a/include/sysrepo-cpp/Subscription.hpp
+++ b/include/sysrepo-cpp/Subscription.hpp
@@ -32,7 +32,7 @@ public:
Subscription& operator=(Subscription&&) noexcept;
void onModuleChange(const std::string& moduleName, ModuleChangeCb cb, const std::optional<std::string>& xpath = std::nullopt, uint32_t priority = 0, const SubscribeOptions opts = SubscribeOptions::Default);
- void onOperGet(const std::string& moduleName, OperGetCb cb, const std::optional<std::string>& xpath, const SubscribeOptions opts = SubscribeOptions::Default);
+ void onOperGet(const std::string& moduleName, OperGetCb cb, const std::string& path, const SubscribeOptions opts = SubscribeOptions::Default);
void onRPCAction(const std::string& xpath, RpcActionCb cb, uint32_t priority = 0, const SubscribeOptions opts = SubscribeOptions::Default);
void onNotification(
const std::string& moduleName,
diff --git a/src/Session.cpp b/src/Session.cpp
index 95b8f24..6f0327d 100644
--- a/src/Session.cpp
+++ b/src/Session.cpp
@@ -505,14 +505,14 @@ Subscription Session::onModuleChange(
Subscription Session::onOperGet(
const std::string& moduleName,
OperGetCb cb,
- const std::optional<std::string>& xpath,
+ const std::string& path,
const SubscribeOptions opts,
ExceptionHandler handler,
const std::optional<FDHandling>& callbacks)
{
checkNoThreadFlag(opts, callbacks);
auto sub = Subscription{*this, handler, callbacks};
- sub.onOperGet(moduleName, cb, xpath, opts);
+ sub.onOperGet(moduleName, cb, path, opts);
return sub;
}
diff --git a/src/Subscription.cpp b/src/Subscription.cpp
index 5bb724e..dca1176 100644
--- a/src/Subscription.cpp
+++ b/src/Subscription.cpp
@@ -215,14 +215,14 @@ void Subscription::onModuleChange(const std::string& moduleName, ModuleChangeCb
* @param xpath XPath that identifies which data this subscription is able to provide.
* @param opts Options further changing the behavior of this method.
*/
-void Subscription::onOperGet(const std::string& moduleName, OperGetCb cb, const std::optional<std::string>& xpath, const SubscribeOptions opts)
+void Subscription::onOperGet(const std::string& moduleName, OperGetCb cb, const std::string& path, const SubscribeOptions opts)
{
SYSREPO_CPP_SESSION_MTX_OF(m_sess);
checkNoThreadFlag(opts, m_customEventLoopCbs);
auto& privRef = m_operGetCbs.emplace_back(PrivData{cb, m_exceptionHandler.get()});
sr_subscription_ctx_s* ctx = m_sub.get();
- auto res = sr_oper_get_subscribe(m_sess.m_sess.get(), moduleName.c_str(), xpath ? xpath->c_str() : nullptr, operGetItemsCb, reinterpret_cast<void*>(&privRef), toSubscribeOptions(opts), &ctx);
+ auto res = sr_oper_get_subscribe(m_sess.m_sess.get(), moduleName.c_str(), path.c_str(), operGetItemsCb, reinterpret_cast<void*>(&privRef), toSubscribeOptions(opts), &ctx);
throwIfError(res, "Couldn't create operational get items subscription", m_sess.m_sess.get());
saveContext(ctx);
--
2.43.0
@@ -0,0 +1,121 @@
From 60d729e4791e5447cf0d969be6da9f8f1878af64 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 20 Nov 2025 15:38:04 +0100
Subject: [PATCH 9/9] Update to libyang v4.2
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Depends-on: https://gerrit.cesnet.cz/c/CzechLight/libyang-cpp/+/9009
Change-Id: Ia4c53dba0befa8dade38e728489edc6473aa5f4f
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 4 ++--
tests/session.cpp | 14 +++++++-------
tests/subscriptions-dynamic.cpp | 4 ++--
3 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 49338dd..8234464 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -27,8 +27,8 @@ option(WITH_DOCS "Create and install internal documentation (needs Doxygen)" ${D
option(WITH_EXAMPLES "Build examples" ON)
find_package(PkgConfig)
-pkg_check_modules(LIBYANG_CPP REQUIRED libyang-cpp>=5 IMPORTED_TARGET)
-pkg_check_modules(SYSREPO REQUIRED sysrepo>=4.2.4 IMPORTED_TARGET)
+pkg_check_modules(LIBYANG_CPP REQUIRED libyang-cpp>=6 IMPORTED_TARGET)
+pkg_check_modules(SYSREPO REQUIRED sysrepo>=4.2.8 IMPORTED_TARGET)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
diff --git a/tests/session.cpp b/tests/session.cpp
index a709a09..0e92e65 100644
--- a/tests/session.cpp
+++ b/tests/session.cpp
@@ -105,7 +105,7 @@ TEST_CASE("session")
auto data = sess.getData("/test_module:popelnice", 0);
REQUIRE(data);
- REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == R"({
+ REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::EmptyContainers) == R"({
"test_module:popelnice": {
"content": {
"trash": [
@@ -126,14 +126,14 @@ TEST_CASE("session")
data = sess.getData("/test_module:popelnice", 1);
REQUIRE(data);
- REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == R"({
+ REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::EmptyContainers) == R"({
"test_module:popelnice": {}
}
)");
data = sess.getData("/test_module:popelnice", 2);
REQUIRE(data);
- REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == R"({
+ REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::EmptyContainers) == R"({
"test_module:popelnice": {
"content": {}
}
@@ -143,7 +143,7 @@ TEST_CASE("session")
// If a list should be returned, its keys are always returned as well.
data = sess.getData("/test_module:popelnice", 3);
REQUIRE(data);
- REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == R"({
+ REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::EmptyContainers) == R"({
"test_module:popelnice": {
"content": {
"trash": [
@@ -161,7 +161,7 @@ TEST_CASE("session")
data = sess.getData("/test_module:popelnice", 4);
REQUIRE(data);
- REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == R"({
+ REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::EmptyContainers) == R"({
"test_module:popelnice": {
"content": {
"trash": [
@@ -497,7 +497,7 @@ TEST_CASE("session")
{
sess.switchDatastore(sysrepo::Datastore::FactoryDefault);
auto data = sess.getData("/*");
- REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings) == "{\n\n}\n");
+ REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings) == "{\n\n}\n");
REQUIRE_THROWS_AS(sess.setItem(leaf, "123"), sysrepo::ErrorWithCode);
}
@@ -622,7 +622,7 @@ TEST_CASE("session")
}
// the original tree is not corrupted
- REQUIRE(*conf->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings) != "");
+ REQUIRE(*conf->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings) != "");
}
DOCTEST_SUBCASE("libyang context flags")
diff --git a/tests/subscriptions-dynamic.cpp b/tests/subscriptions-dynamic.cpp
index 561df0c..0d4ce7c 100644
--- a/tests/subscriptions-dynamic.cpp
+++ b/tests/subscriptions-dynamic.cpp
@@ -140,12 +140,12 @@ TEST_CASE("Dynamic subscriptions")
const auto subId = std::get<uint32_t>(idNode->asTerm().value());
idNode->unlink();
- rec.recordYangPushUpdate(subId, tree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings));
+ rec.recordYangPushUpdate(subId, tree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings));
};
auto cbNotif = [&](const std::optional<libyang::DataNode>& tree, auto) {
REQUIRE(tree);
- rec.recordNotification(tree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings));
+ rec.recordNotification(tree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings));
};
// write some initial data
--
2.43.0
+88 -6
View File
@@ -1,6 +1,77 @@
diff -urN frr-9.1.3.orig/lib/yang.c frr-9.1.3/lib/yang.c
--- frr-9.1.3.orig/lib/yang.c 2024-12-27 22:06:42.000000000 +0100
+++ frr-9.1.3/lib/yang.c 2025-03-04 12:23:06.723640114 +0100
diff --git a/lib/northbound.c b/lib/northbound.c
index 6ff5c24bd1..c6cca523b2 100644
--- a/lib/northbound.c
+++ b/lib/northbound.c
@@ -581,7 +581,7 @@ void nb_config_diff(const struct nb_config *config1,
char *s;
if (!lyd_print_mem(&s, diff, LYD_JSON,
- LYD_PRINT_WITHSIBLINGS | LYD_PRINT_WD_ALL)) {
+ LYD_PRINT_SIBLINGS | LYD_PRINT_WD_ALL)) {
zlog_debug("%s: %s", __func__, s);
free(s);
}
@@ -2283,7 +2283,7 @@ bool nb_operation_is_valid(enum nb_operation operation,
if (sleaf->when)
return true;
if (CHECK_FLAG(sleaf->flags, LYS_MAND_TRUE)
- || sleaf->dflt)
+ || sleaf->dflt.str)
return false;
break;
case LYS_CONTAINER:
diff --git a/lib/northbound_cli.c b/lib/northbound_cli.c
index 8003679ed5..0997b5f8c5 100644
--- a/lib/northbound_cli.c
+++ b/lib/northbound_cli.c
@@ -634,7 +634,7 @@ static int nb_cli_show_config_libyang(struct vty *vty, LYD_FORMAT format,
return CMD_WARNING;
}
- SET_FLAG(options, LYD_PRINT_WITHSIBLINGS);
+ SET_FLAG(options, LYD_PRINT_SIBLINGS);
if (with_defaults)
SET_FLAG(options, LYD_PRINT_WD_ALL);
else
@@ -1443,7 +1443,7 @@ DEFPY (show_yang_operational_data,
struct ly_ctx *ly_ctx;
struct lyd_node *dnode;
char *strp;
- uint32_t print_options = LYD_PRINT_WITHSIBLINGS;
+ uint32_t print_options = LYD_PRINT_SIBLINGS;
int ret;
if (xml)
diff --git a/lib/northbound_db.c b/lib/northbound_db.c
index 74abcde955..7d51f39291 100644
--- a/lib/northbound_db.c
+++ b/lib/northbound_db.c
@@ -79,7 +79,7 @@ int nb_db_transaction_save(const struct nb_transaction *transaction,
* values too, as this covers the case where defaults may change.
*/
if (lyd_print_mem(&config_str, transaction->config->dnode, LYD_XML,
- LYD_PRINT_WITHSIBLINGS | LYD_PRINT_WD_ALL)
+ LYD_PRINT_SIBLINGS | LYD_PRINT_WD_ALL)
!= 0)
goto exit;
diff --git a/lib/northbound_grpc.cpp b/lib/northbound_grpc.cpp
index 6c33351cef..b3e14828f3 100644
--- a/lib/northbound_grpc.cpp
+++ b/lib/northbound_grpc.cpp
@@ -370,7 +370,7 @@ static LY_ERR data_tree_from_dnode(frr::DataTree *dt,
char *strp;
int options = 0;
- SET_FLAG(options, LYD_PRINT_WITHSIBLINGS);
+ SET_FLAG(options, LYD_PRINT_SIBLINGS);
if (with_defaults)
SET_FLAG(options, LYD_PRINT_WD_ALL);
else
diff --git a/lib/yang.c b/lib/yang.c
index 4dd8654217..44563d4922 100644
--- a/lib/yang.c
+++ b/lib/yang.c
@@ -10,11 +10,23 @@
#include "lib_errors.h"
#include "yang.h"
@@ -25,7 +96,18 @@ diff -urN frr-9.1.3.orig/lib/yang.c frr-9.1.3/lib/yang.c
/* libyang container. */
struct ly_ctx *ly_native_ctx;
@@ -657,7 +669,12 @@
@@ -329,9 +341,7 @@ const char *yang_snode_get_default(const struct lysc_node *snode)
switch (snode->nodetype) {
case LYS_LEAF:
sleaf = (const struct lysc_node_leaf *)snode;
- return sleaf->dflt ? lyd_value_get_canonical(sleaf->module->ctx,
- sleaf->dflt)
- : NULL;
+ return sleaf->dflt.str;
case LYS_LEAFLIST:
/* TODO: check leaf-list default values */
return NULL;
@@ -657,7 +667,12 @@ struct yang_data *yang_data_list_find(const struct list *list,
}
/* Make libyang log its errors using FRR logging infrastructure. */
@@ -39,7 +121,7 @@ diff -urN frr-9.1.3.orig/lib/yang.c frr-9.1.3/lib/yang.c
{
int priority = LOG_ERR;
@@ -674,8 +691,14 @@
@@ -674,8 +689,14 @@ static void ly_log_cb(LY_LOG_LEVEL level, const char *msg, const char *path)
break;
}
@@ -56,7 +138,7 @@ diff -urN frr-9.1.3.orig/lib/yang.c frr-9.1.3/lib/yang.c
else
zlog(priority, "libyang: %s", msg);
}
@@ -752,7 +775,7 @@
@@ -752,7 +773,7 @@ struct ly_ctx *yang_ctx_new_setup(bool embedded_modules, bool explicit_compile)
void yang_init(bool embedded_modules, bool defer_compile)
{
/* Initialize libyang global parameters that affect all containers. */
@@ -0,0 +1,22 @@
diff --git a/src/server_config.c b/src/server_config.c
index 48d362e..dc9f59d 100644
--- a/src/server_config.c
+++ b/src/server_config.c
@@ -4151,6 +4151,8 @@ config_asymmetric_key_certs(const struct lyd_node *node, enum nc_operation paren
struct lyd_node *n;
enum nc_operation op;
+ if (!node)
+ return 0;
NC_NODE_GET_OP(node, parent_op, &op);
/* configure all certificates */
@@ -4225,7 +4227,7 @@ config_asymmetric_key(const struct lyd_node *node, enum nc_operation parent_op,
}
/* config asymmetric key certificates */
- NC_CHECK_RET(nc_lyd_find_child(node, "certificates", 1, &n));
+ NC_CHECK_RET(nc_lyd_find_child(node, "certificates", 0, &n));
NC_CHECK_RET(config_asymmetric_key_certs(n, op, entry));
/* config generate csr */
@@ -0,0 +1,741 @@
From 545473073552cd4cf5e0fc839e7170d4678ef646 Mon Sep 17 00:00:00 2001
From: Michal Vasko <mvasko@cesnet.cz>
Date: Wed, 10 Dec 2025 09:52:13 +0100
Subject: [PATCH 1/6] plugins ext BUGFIX printed context align all sizes and
mem
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Refs #2455
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/plugins_exts.h | 17 +++-
src/plugins_exts/metadata.c | 4 +-
src/plugins_exts/schema_mount.c | 24 +++---
src/plugins_exts/structure.c | 8 +-
src/printer_context.c | 148 +++++++++++++++-----------------
5 files changed, 100 insertions(+), 101 deletions(-)
diff --git a/src/plugins_exts.h b/src/plugins_exts.h
index f260ff787..e912fe908 100644
--- a/src/plugins_exts.h
+++ b/src/plugins_exts.h
@@ -953,13 +953,26 @@ LIBYANG_API_DECL void lyplg_ext_cfree_instance_substatements(const struct ly_ctx
* compiled size
*/
+/**
+ * @brief Alignment of all the printed data, ensures all memory access is aligned to this number (B)
+ */
+#define LY_CTXP_MEM_ALIGN 8
+
+/**
+ * @brief Adjust data size to an aligned size to make sure the following data is aligned.
+ *
+ * @param[in,out] SIZE Data size that is adjusted.
+ */
+#define LY_CTXP_MEM_SIZE(SIZE) ((SIZE) + ((~(SIZE) + 1) & (LY_CTXP_MEM_ALIGN - 1)))
+
/**
* @brief Callback to return the size of the custom compiled structure and substmts array. If there are none, do not
* define this callback.
*
* @param[in] ext Compiled extension structure.
* @param[in,out] addr_ht Hash table with addresses of shared structures that were already accounted for, can be added to.
- * @return Total size of the custom compiled structures and the substmts array;
+ * @return Total size of the custom compiled structures and the substmts array. Always use ::LY_CTXP_MEM_SIZE when
+ * adding to the size for correct alignment.
* @return -1 on error.
*/
typedef int (*lyplg_ext_compiled_size_clb)(const struct lysc_ext_instance *ext, struct ly_ht *addr_ht);
@@ -984,7 +997,7 @@ LIBYANG_API_DECL int lyplg_ext_compiled_stmts_storage_size(const struct lysc_ext
* @param[in] orig_ext Compiled extension structure to print.
* @param[in,out] ext Printed extension structure to modify.
* @param[in,out] mem Memory chunk of the size returned by ::lyplg_ext_compiled_size_clb() to print into, is moved
- * after all the printed data.
+ * after all the printed data. Always use ::LY_CTXP_MEM_SIZE when moving @p mem for correct alignment.
* @param[in,out] ptr_set Set with pointers to set to printed addresses.
* @param[in,out] addr_ht Hash table with pairs of addresses of shared structures to be printed and their printed
* addresses, can be added to.
diff --git a/src/plugins_exts/metadata.c b/src/plugins_exts/metadata.c
index d5492839e..c73c8f9a8 100644
--- a/src/plugins_exts/metadata.c
+++ b/src/plugins_exts/metadata.c
@@ -220,7 +220,7 @@ annotation_compiled_size(const struct lysc_ext_instance *ext, struct ly_ht *addr
{
int size = 0;
- size += sizeof(struct lysc_ext_metadata);
+ size += LY_CTXP_MEM_SIZE(sizeof(struct lysc_ext_metadata));
size += lyplg_ext_compiled_stmts_storage_size(ext->substmts, addr_ht);
return size;
@@ -233,7 +233,7 @@ annotation_compiled_print(const struct lysc_ext_instance *orig_ext, struct lysc_
struct lysc_ext_metadata *ann_cdata;
ann_cdata = ext->compiled = *mem;
- *mem = (char *)*mem + sizeof *ann_cdata;
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *ann_cdata);
memset(ann_cdata, 0, sizeof *ann_cdata);
ext->substmts[1].storage_p = (void **)&ann_cdata->units;
diff --git a/src/plugins_exts/schema_mount.c b/src/plugins_exts/schema_mount.c
index df736e5ad..3dd23f992 100644
--- a/src/plugins_exts/schema_mount.c
+++ b/src/plugins_exts/schema_mount.c
@@ -79,8 +79,6 @@ struct sprinter_tree_priv {
lyplg_ext_compile_log(cctx, ext, LY_LLERR, LY_EINT, "Internal error (%s:%d).", __FILE__, __LINE__); \
return LY_EINT
-#define CTXP_MEM_SIZE(SIZE) ((SIZE) + ((~(SIZE) + 1) & (8 - 1)))
-
/**
* @brief Check if given mount point is unique among its siblings
*
@@ -1468,7 +1466,7 @@ schema_mount_compiled_size(const struct lysc_ext_instance *ext, struct ly_ht *ad
return 0;
}
- size += CTXP_MEM_SIZE(sizeof *sm_data);
+ size += LY_CTXP_MEM_SIZE(sizeof *sm_data);
/* ht addr check, make sure the shared context is stored only once */
hash = lyht_hash((const char *)&sm_data->shared, sizeof sm_data->shared);
@@ -1476,12 +1474,12 @@ schema_mount_compiled_size(const struct lysc_ext_instance *ext, struct ly_ht *ad
return size;
}
- size += CTXP_MEM_SIZE(sizeof *sm_data->shared);
- size += CTXP_MEM_SIZE(sm_data->shared->schema_count * sizeof *sm_data->shared->schemas);
+ size += LY_CTXP_MEM_SIZE(sizeof *sm_data->shared);
+ size += LY_CTXP_MEM_SIZE(sm_data->shared->schema_count * sizeof *sm_data->shared->schemas);
for (i = 0; i < sm_data->shared->schema_count; ++i) {
- size += CTXP_MEM_SIZE(ly_ctx_compiled_size(sm_data->shared->schemas[i].ctx));
- size += CTXP_MEM_SIZE(strlen(sm_data->shared->schemas[i].mount_point) + 1);
- size += CTXP_MEM_SIZE(strlen(sm_data->shared->schemas[i].content_id) + 1);
+ size += LY_CTXP_MEM_SIZE(ly_ctx_compiled_size(sm_data->shared->schemas[i].ctx));
+ size += LY_CTXP_MEM_SIZE(strlen(sm_data->shared->schemas[i].mount_point) + 1);
+ size += LY_CTXP_MEM_SIZE(strlen(sm_data->shared->schemas[i].content_id) + 1);
}
/* inlined contexts cannot be reused and will not be printed */
@@ -1501,7 +1499,7 @@ schema_mount_compiled_print(const struct lysc_ext_instance *orig_ext, struct lys
/* sm_data */
ext->compiled = sm_data = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *sm_data);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *sm_data);
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
@@ -1517,7 +1515,7 @@ schema_mount_compiled_print(const struct lysc_ext_instance *orig_ext, struct lys
/* sm_data->shared */
sm_data->shared = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *sm_data->shared);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *sm_data->shared);
sm_data->shared->schema_count = orig_sm_data->shared->schema_count;
sm_data->shared->ref_count = orig_sm_data->shared->ref_count;
@@ -1525,7 +1523,7 @@ schema_mount_compiled_print(const struct lysc_ext_instance *orig_ext, struct lys
/* sm_data->shared->schemas */
if (orig_sm_data->shared->schemas) {
sm_data->shared->schemas = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sm_data->shared->schema_count * sizeof *sm_data->shared->schemas);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sm_data->shared->schema_count * sizeof *sm_data->shared->schemas);
for (i = 0; i < sm_data->shared->schema_count; ++i) {
/* ctx */
@@ -1538,12 +1536,12 @@ schema_mount_compiled_print(const struct lysc_ext_instance *orig_ext, struct lys
/* mount_point */
strcpy(*mem, orig_sm_data->shared->schemas[i].mount_point);
sm_data->shared->schemas[i].mount_point = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(strlen(sm_data->shared->schemas[i].mount_point) + 1);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(strlen(sm_data->shared->schemas[i].mount_point) + 1);
/* content_id */
strcpy(*mem, orig_sm_data->shared->schemas[i].content_id);
sm_data->shared->schemas[i].content_id = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(strlen(sm_data->shared->schemas[i].content_id) + 1);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(strlen(sm_data->shared->schemas[i].content_id) + 1);
}
}
diff --git a/src/plugins_exts/structure.c b/src/plugins_exts/structure.c
index b88c9a9a5..6309a2c30 100644
--- a/src/plugins_exts/structure.c
+++ b/src/plugins_exts/structure.c
@@ -329,9 +329,9 @@ structure_compiled_size(const struct lysc_ext_instance *ext, struct ly_ht *addr_
struct_cdata = ext->compiled;
- size += sizeof *struct_cdata;
+ size += LY_CTXP_MEM_SIZE(sizeof *struct_cdata);
size += lyplg_ext_compiled_stmts_storage_size(ext->substmts, addr_ht);
- size += sizeof *struct_cdata->top_cont;
+ size += LY_CTXP_MEM_SIZE(sizeof *struct_cdata->top_cont);
return size;
}
@@ -347,12 +347,12 @@ structure_compiled_print(const struct lysc_ext_instance *orig_ext, struct lysc_e
/* ext structure */
struct_cdata = ext->compiled = *mem;
- *mem = (char *)*mem + sizeof *struct_cdata;
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *struct_cdata);
memset(struct_cdata, 0, sizeof *struct_cdata);
/* top_cont */
struct_cdata->top_cont = *mem;
- *mem = (char *)*mem + sizeof *struct_cdata->top_cont;
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *struct_cdata->top_cont);
lyplg_ext_compiled_print_add_addr(addr_ht, orig_cdata->top_cont, struct_cdata->top_cont);
memset(struct_cdata->top_cont, 0, sizeof *struct_cdata->top_cont);
diff --git a/src/printer_context.c b/src/printer_context.c
index 57d5f90e1..3fb50ac0a 100644
--- a/src/printer_context.c
+++ b/src/printer_context.c
@@ -46,10 +46,10 @@
if (ORIG_ARRAY) { \
LY_ARRAY_COUNT_TYPE count = LY_ARRAY_COUNT(ORIG_ARRAY); \
memcpy(*MEM, &count, sizeof count); \
- *MEM = (char *)*MEM + CTXP_MEM_SIZE(sizeof count); \
+ *MEM = (char *)*MEM + LY_CTXP_MEM_SIZE(sizeof count); \
\
ARRAY = *MEM; \
- *MEM = (char *)*MEM + CTXP_MEM_SIZE(count * sizeof *ORIG_ARRAY); \
+ *MEM = (char *)*MEM + LY_CTXP_MEM_SIZE(count * sizeof *ORIG_ARRAY); \
} else { \
ARRAY = NULL; \
}
@@ -67,24 +67,12 @@
#define CTXP_OPTIONAL_STRUCT(FUNC, ORIG_PTR, PTR, ADDR_HT, PTR_SET, MEM) \
if (ORIG_PTR) { \
PTR = *MEM; \
- *MEM = (char *)*MEM + CTXP_MEM_SIZE(sizeof *PTR); \
+ *MEM = (char *)*MEM + LY_CTXP_MEM_SIZE(sizeof *PTR); \
FUNC(ORIG_PTR, PTR, ADDR_HT, PTR_SET, MEM); \
} else { \
PTR = NULL; \
}
-/**
- * @brief Alignment of all the printed data, ensures all memory access is aligned to this number (B)
- */
-#define CTXP_MEM_ALIGN 8
-
-/**
- * @brief Adjust data size to an aligned size to make sure the following data is aligned.
- *
- * @param[in,out] SIZE Data size that is adjusted.
- */
-#define CTXP_MEM_SIZE(SIZE) ((SIZE) + ((~(SIZE) + 1) & (CTXP_MEM_ALIGN - 1)))
-
static void
ctxs_dict_strings(const struct ly_ht *ht, int *size)
{
@@ -96,7 +84,7 @@ ctxs_dict_strings(const struct ly_ht *ht, int *size)
dict_rec = (struct ly_dict_rec *)&rec->val;
/* strings */
- *size += CTXP_MEM_SIZE(strlen(dict_rec->value) + 1);
+ *size += LY_CTXP_MEM_SIZE(strlen(dict_rec->value) + 1);
}
}
@@ -117,7 +105,7 @@ ctxs_exts(const struct lysc_ext_instance *exts, struct ly_ht *ht, int *size)
/* compiled, substmts storage */
if (exts[u].def->plugin_ref && (ext_plg = LYSC_GET_EXT_PLG(exts[u].def->plugin_ref)) && ext_plg->compiled_size) {
- *size += CTXP_MEM_SIZE(ext_plg->compiled_size(&exts[u], ht));
+ *size += LY_CTXP_MEM_SIZE(ext_plg->compiled_size(&exts[u], ht));
}
}
}
@@ -131,7 +119,7 @@ ctxs_prefixes(const struct lysc_prefix *prefixes, int *size)
LY_ARRAY_FOR(prefixes, u) {
/* string not in the dictionary */
if (prefixes[u].prefix) {
- *size += CTXP_MEM_SIZE(strlen(prefixes[u].prefix) + 1);
+ *size += LY_CTXP_MEM_SIZE(strlen(prefixes[u].prefix) + 1);
}
}
}
@@ -141,20 +129,20 @@ ctxs_expr(const struct lyxp_expr *exp, int *size)
{
uint32_t i, j;
- *size += CTXP_MEM_SIZE(sizeof *exp);
+ *size += LY_CTXP_MEM_SIZE(sizeof *exp);
- *size += CTXP_MEM_SIZE(strlen(exp->expr) + 1);
- *size += CTXP_MEM_SIZE(exp->used * sizeof *exp->tokens);
- *size += CTXP_MEM_SIZE(exp->used * sizeof *exp->tok_pos);
- *size += CTXP_MEM_SIZE(exp->used * sizeof *exp->tok_len);
- *size += CTXP_MEM_SIZE(exp->used * sizeof *exp->repeat);
+ *size += LY_CTXP_MEM_SIZE(strlen(exp->expr) + 1);
+ *size += LY_CTXP_MEM_SIZE(exp->used * sizeof *exp->tokens);
+ *size += LY_CTXP_MEM_SIZE(exp->used * sizeof *exp->tok_pos);
+ *size += LY_CTXP_MEM_SIZE(exp->used * sizeof *exp->tok_len);
+ *size += LY_CTXP_MEM_SIZE(exp->used * sizeof *exp->repeat);
for (i = 0; i < exp->used; ++i) {
if (!exp->repeat[i]) {
continue;
}
for (j = 0; exp->repeat[i][j]; ++j) {}
- *size += CTXP_MEM_SIZE((j + 1) * sizeof **exp->repeat);
+ *size += LY_CTXP_MEM_SIZE((j + 1) * sizeof **exp->repeat);
}
}
@@ -182,7 +170,7 @@ ctxs_when(const struct lysc_when *when, struct ly_ht *ht, int *size)
return;
}
- *size += CTXP_MEM_SIZE(sizeof *when);
+ *size += LY_CTXP_MEM_SIZE(sizeof *when);
ctxs_expr(when->cond, size);
ctxs_prefixes(when->prefixes, size);
@@ -207,7 +195,7 @@ ctxs_range(const struct lysc_range *range, struct ly_ht *ht, int *size)
return;
}
- *size += CTXP_MEM_SIZE(sizeof *range);
+ *size += LY_CTXP_MEM_SIZE(sizeof *range);
*size += CTXS_SIZED_ARRAY(range->parts);
ctxs_exts(range->exts, ht, size);
@@ -227,7 +215,7 @@ ctxs_patterns(const struct lysc_pattern **patterns, struct ly_ht *ht, int *size)
continue;
}
- *size += CTXP_MEM_SIZE(sizeof *patterns[u]);
+ *size += LY_CTXP_MEM_SIZE(sizeof *patterns[u]);
ctxs_exts(patterns[u]->exts, ht, size);
}
}
@@ -270,7 +258,7 @@ ctxs_type(const struct lysc_type *type, struct ly_ht *ht, int *size)
switch (type->basetype) {
case LY_TYPE_BINARY:
type_bin = (const struct lysc_type_bin *)type;
- *size += CTXP_MEM_SIZE(sizeof *type_bin);
+ *size += LY_CTXP_MEM_SIZE(sizeof *type_bin);
ctxs_range(type_bin->length, ht, size);
break;
@@ -283,13 +271,13 @@ ctxs_type(const struct lysc_type *type, struct ly_ht *ht, int *size)
case LY_TYPE_INT32:
case LY_TYPE_INT64:
type_num = (const struct lysc_type_num *)type;
- *size += CTXP_MEM_SIZE(sizeof *type_num);
+ *size += LY_CTXP_MEM_SIZE(sizeof *type_num);
ctxs_range(type_num->range, ht, size);
break;
case LY_TYPE_STRING:
type_str = (const struct lysc_type_str *)type;
- *size += CTXP_MEM_SIZE(sizeof *type_str);
+ *size += LY_CTXP_MEM_SIZE(sizeof *type_str);
ctxs_range(type_str->length, ht, size);
ctxs_patterns((const struct lysc_pattern **)type_str->patterns, ht, size);
@@ -297,33 +285,33 @@ ctxs_type(const struct lysc_type *type, struct ly_ht *ht, int *size)
case LY_TYPE_BITS:
case LY_TYPE_ENUM:
type_enum_bits = (const struct lysc_type_enum *)type;
- *size += CTXP_MEM_SIZE(sizeof *type_enum_bits);
+ *size += LY_CTXP_MEM_SIZE(sizeof *type_enum_bits);
ctxs_enums(type_enum_bits->enums, ht, size);
break;
case LY_TYPE_BOOL:
case LY_TYPE_EMPTY:
- *size += CTXP_MEM_SIZE(sizeof *type);
+ *size += LY_CTXP_MEM_SIZE(sizeof *type);
break;
case LY_TYPE_DEC64:
type_dec = (const struct lysc_type_dec *)type;
- *size += CTXP_MEM_SIZE(sizeof *type_dec);
+ *size += LY_CTXP_MEM_SIZE(sizeof *type_dec);
ctxs_range(type_dec->range, ht, size);
break;
case LY_TYPE_IDENT:
type_identref = (const struct lysc_type_identityref *)type;
- *size += CTXP_MEM_SIZE(sizeof *type_identref);
+ *size += LY_CTXP_MEM_SIZE(sizeof *type_identref);
*size += CTXS_SIZED_ARRAY(type_identref->bases);
break;
case LY_TYPE_INST:
type_instid = (const struct lysc_type_instanceid *)type;
- *size += CTXP_MEM_SIZE(sizeof *type_instid);
+ *size += LY_CTXP_MEM_SIZE(sizeof *type_instid);
break;
case LY_TYPE_LEAFREF:
type_lref = (const struct lysc_type_leafref *)type;
- *size += CTXP_MEM_SIZE(sizeof *type_lref);
+ *size += LY_CTXP_MEM_SIZE(sizeof *type_lref);
ctxs_expr(type_lref->path, size);
ctxs_prefixes(type_lref->prefixes, size);
@@ -331,7 +319,7 @@ ctxs_type(const struct lysc_type *type, struct ly_ht *ht, int *size)
break;
case LY_TYPE_UNION:
type_union = (const struct lysc_type_union *)type;
- *size += CTXP_MEM_SIZE(sizeof *type_union);
+ *size += LY_CTXP_MEM_SIZE(sizeof *type_union);
*size += CTXS_SIZED_ARRAY(type_union->types);
LY_ARRAY_FOR(type_union->types, u) {
@@ -366,7 +354,7 @@ ctxs_node(const struct lysc_node *node, struct ly_ht *ht, int *size)
switch (node->nodetype) {
case LYS_CONTAINER:
cont = (const struct lysc_node_container *)node;
- *size += CTXP_MEM_SIZE(sizeof *cont);
+ *size += LY_CTXP_MEM_SIZE(sizeof *cont);
LY_LIST_FOR(cont->child, child) {
ctxs_node(child, ht, size);
@@ -382,7 +370,7 @@ ctxs_node(const struct lysc_node *node, struct ly_ht *ht, int *size)
break;
case LYS_CHOICE:
choic = (const struct lysc_node_choice *)node;
- *size += CTXP_MEM_SIZE(sizeof *choic);
+ *size += LY_CTXP_MEM_SIZE(sizeof *choic);
LY_LIST_FOR((const struct lysc_node *)choic->cases, child) {
ctxs_node(child, ht, size);
@@ -391,7 +379,7 @@ ctxs_node(const struct lysc_node *node, struct ly_ht *ht, int *size)
break;
case LYS_LEAF:
leaf = (const struct lysc_node_leaf *)node;
- *size += CTXP_MEM_SIZE(sizeof *leaf);
+ *size += LY_CTXP_MEM_SIZE(sizeof *leaf);
ctxs_musts(leaf->musts, ht, size);
ctxs_whens((const struct lysc_when **)leaf->when, ht, size);
@@ -400,7 +388,7 @@ ctxs_node(const struct lysc_node *node, struct ly_ht *ht, int *size)
break;
case LYS_LEAFLIST:
llist = (const struct lysc_node_leaflist *)node;
- *size += CTXP_MEM_SIZE(sizeof *llist);
+ *size += LY_CTXP_MEM_SIZE(sizeof *llist);
ctxs_musts(llist->musts, ht, size);
ctxs_whens((const struct lysc_when **)llist->when, ht, size);
@@ -412,7 +400,7 @@ ctxs_node(const struct lysc_node *node, struct ly_ht *ht, int *size)
break;
case LYS_LIST:
list = (const struct lysc_node_list *)node;
- *size += CTXP_MEM_SIZE(sizeof *list);
+ *size += LY_CTXP_MEM_SIZE(sizeof *list);
LY_LIST_FOR(list->child, child) {
ctxs_node(child, ht, size);
@@ -427,21 +415,21 @@ ctxs_node(const struct lysc_node *node, struct ly_ht *ht, int *size)
}
*size += CTXS_SIZED_ARRAY(list->uniques);
LY_ARRAY_FOR(list->uniques, u) {
- *size += CTXP_MEM_SIZE(sizeof(LY_ARRAY_COUNT_TYPE)) +
- CTXP_MEM_SIZE(LY_ARRAY_COUNT(list->uniques[u]) * sizeof **list->uniques);
+ *size += LY_CTXP_MEM_SIZE(sizeof(LY_ARRAY_COUNT_TYPE)) +
+ LY_CTXP_MEM_SIZE(LY_ARRAY_COUNT(list->uniques[u]) * sizeof **list->uniques);
}
break;
case LYS_ANYXML:
case LYS_ANYDATA:
any = (const struct lysc_node_anydata *)node;
- *size += CTXP_MEM_SIZE(sizeof *any);
+ *size += LY_CTXP_MEM_SIZE(sizeof *any);
ctxs_musts(any->musts, ht, size);
ctxs_whens((const struct lysc_when **)any->when, ht, size);
break;
case LYS_CASE:
cas = (const struct lysc_node_case *)node;
- *size += CTXP_MEM_SIZE(sizeof *cas);
+ *size += LY_CTXP_MEM_SIZE(sizeof *cas);
LY_LIST_FOR(cas->child, child) {
ctxs_node(child, ht, size);
@@ -451,7 +439,7 @@ ctxs_node(const struct lysc_node *node, struct ly_ht *ht, int *size)
case LYS_RPC:
case LYS_ACTION:
act = (const struct lysc_node_action *)node;
- *size += CTXP_MEM_SIZE(sizeof *act);
+ *size += LY_CTXP_MEM_SIZE(sizeof *act);
ctxs_whens((const struct lysc_when **)act->when, ht, size);
ctxs_node((struct lysc_node *)&act->input, ht, size);
@@ -469,7 +457,7 @@ ctxs_node(const struct lysc_node *node, struct ly_ht *ht, int *size)
break;
case LYS_NOTIF:
notif = (const struct lysc_node_notif *)node;
- *size += CTXP_MEM_SIZE(sizeof *notif);
+ *size += LY_CTXP_MEM_SIZE(sizeof *notif);
LY_LIST_FOR(notif->child, child) {
ctxs_node(child, ht, size);
@@ -493,7 +481,7 @@ ctxs_compiled(const struct lysc_module *compiled, struct ly_ht *ht, int *size)
}
/* compiled module */
- *size += CTXP_MEM_SIZE(sizeof *compiled);
+ *size += LY_CTXP_MEM_SIZE(sizeof *compiled);
*size += CTXS_SIZED_ARRAY(compiled->features);
LY_LIST_FOR(compiled->data, node) {
@@ -539,7 +527,7 @@ static void
ctxs_module(const struct lys_module *mod, struct ly_ht *ht, int *size)
{
/* module */
- *size += CTXP_MEM_SIZE(sizeof *mod);
+ *size += LY_CTXP_MEM_SIZE(sizeof *mod);
/* compiled module */
ctxs_compiled(mod->compiled, ht, size);
@@ -561,13 +549,13 @@ ly_ctx_compiled_size_context(const struct ly_ctx *ctx, struct ly_ht *addr_ht, in
const struct lys_module *mod;
/* context */
- *size += CTXP_MEM_SIZE(sizeof *ctx);
+ *size += LY_CTXP_MEM_SIZE(sizeof *ctx);
/* all the strings in the dictionary */
ctxs_dict_strings(ctx->dict.hash_tab, size);
/* module set */
- *size += CTXP_MEM_SIZE(ctx->modules.count * sizeof ctx->modules.objs);
+ *size += LY_CTXP_MEM_SIZE(ctx->modules.count * sizeof ctx->modules.objs);
for (i = 0; i < ctx->modules.count; ++i) {
mod = ctx->modules.objs[i];
@@ -720,7 +708,7 @@ ctxp_set(const struct ly_set *orig_set, struct ly_set *set, void **mem)
set->size = orig_set->count;
set->count = orig_set->count;
set->objs = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(set->count * sizeof set->objs);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(set->count * sizeof set->objs);
}
static void
@@ -741,7 +729,7 @@ ctxp_dict_strings(const struct ly_ht *orig_dict, struct ly_ht *addr_ht, void **m
orig_str = dict_rec->value;
str = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(len);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(len);
memcpy(str, orig_str, len);
@@ -801,30 +789,30 @@ ctxp_expr(const struct lyxp_expr *orig_exp, struct lyxp_expr *exp, void **mem)
uint32_t i, len;
exp->expr = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(strlen(orig_exp->expr) + 1);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(strlen(orig_exp->expr) + 1);
memcpy(exp->expr, orig_exp->expr, strlen(orig_exp->expr) + 1);
exp->tokens = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(orig_exp->used * sizeof *exp->tokens);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(orig_exp->used * sizeof *exp->tokens);
memcpy(exp->tokens, orig_exp->tokens, orig_exp->used * sizeof *exp->tokens);
exp->tok_pos = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(orig_exp->used * sizeof *exp->tok_pos);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(orig_exp->used * sizeof *exp->tok_pos);
memcpy(exp->tok_pos, orig_exp->tok_pos, orig_exp->used * sizeof *exp->tok_pos);
exp->tok_len = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(orig_exp->used * sizeof *exp->tok_len);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(orig_exp->used * sizeof *exp->tok_len);
memcpy(exp->tok_len, orig_exp->tok_len, orig_exp->used * sizeof *exp->tok_len);
exp->repeat = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(orig_exp->used * sizeof *exp->repeat);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(orig_exp->used * sizeof *exp->repeat);
for (i = 0; i < orig_exp->used; ++i) {
if (orig_exp->repeat[i]) {
for (len = 0; orig_exp->repeat[i][len]; ++len) {}
++len;
exp->repeat[i] = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(len * sizeof **exp->repeat);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(len * sizeof **exp->repeat);
memcpy(exp->repeat[i], orig_exp->repeat[i], len * sizeof **exp->repeat);
} else {
exp->repeat[i] = NULL;
@@ -845,7 +833,7 @@ ctxp_prefix(const struct lysc_prefix *orig_prefix, struct lysc_prefix *prefix, s
size = strlen(orig_prefix->prefix) + 1;
prefix->prefix = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(size);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(size);
memcpy(prefix->prefix, orig_prefix->prefix, size);
} else {
prefix->prefix = NULL;
@@ -867,7 +855,7 @@ ctxp_must(const struct lysc_must *orig_must, struct lysc_must *must, struct ly_h
}
must->cond = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *must->cond);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *must->cond);
ctxp_expr(orig_must->cond, must->cond, mem);
CTXP_SIZED_ARRAY(orig_must->prefixes, must->prefixes, mem);
@@ -901,7 +889,7 @@ ctxp_when(const struct lysc_when *orig_when, struct lysc_when **when, struct ly_
}
w = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *w);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *w);
if (orig_when->exts) {
/* may be referenced in the parent */
@@ -909,7 +897,7 @@ ctxp_when(const struct lysc_when *orig_when, struct lysc_when **when, struct ly_
}
w->cond = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *w->cond);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *w->cond);
ctxp_expr(orig_when->cond, w->cond, mem);
w->context = ly_ctx_compiled_addr_ht_get(addr_ht, orig_when->context, 0);
@@ -980,7 +968,7 @@ ctxp_children(const struct lysc_node *orig_child, struct lysc_node **child, stru
}
ch = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(node_size);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(node_size);
/* referenced by pointers */
ly_ctx_compiled_addr_ht_add(addr_ht, orig_child, ch);
@@ -1035,7 +1023,7 @@ ctxp_pattern(const struct lysc_pattern *orig_pattern, struct lysc_pattern **patt
}
p = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *p);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *p);
if (orig_pattern->exts) {
/* may be referenced in the parent */
@@ -1122,7 +1110,7 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_
case LY_TYPE_BINARY:
orig_type_bin = (const struct lysc_type_bin *)orig_type;
t = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_bin);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_bin);
t_bin = (struct lysc_type_bin *)t;
CTXP_OPTIONAL_STRUCT(ctxp_range, orig_type_bin->length, t_bin->length, addr_ht, ptr_set, mem);
@@ -1137,7 +1125,7 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_
case LY_TYPE_INT64:
orig_type_num = (const struct lysc_type_num *)orig_type;
t = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_num);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_num);
t_num = (struct lysc_type_num *)t;
CTXP_OPTIONAL_STRUCT(ctxp_range, orig_type_num->range, t_num->range, addr_ht, ptr_set, mem);
@@ -1145,7 +1133,7 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_
case LY_TYPE_STRING:
orig_type_str = (const struct lysc_type_str *)orig_type;
t = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_str);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_str);
t_str = (struct lysc_type_str *)t;
CTXP_OPTIONAL_STRUCT(ctxp_range, orig_type_str->length, t_str->length, addr_ht, ptr_set, mem);
@@ -1158,7 +1146,7 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_
case LY_TYPE_ENUM:
orig_type_enum_bits = (const struct lysc_type_enum *)orig_type;
t = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_enum_bits);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_enum_bits);
t_enum_bits = (struct lysc_type_enum *)t;
CTXP_SIZED_ARRAY(orig_type_enum_bits->enums, t_enum_bits->enums, mem);
@@ -1170,12 +1158,12 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_
case LY_TYPE_EMPTY:
/* no additional members */
t = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type);
break;
case LY_TYPE_DEC64:
orig_type_dec = (const struct lysc_type_dec *)orig_type;
t = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_dec);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_dec);
t_dec = (struct lysc_type_dec *)t;
t_dec->fraction_digits = orig_type_dec->fraction_digits;
@@ -1184,7 +1172,7 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_
case LY_TYPE_IDENT:
orig_type_identref = (const struct lysc_type_identityref *)orig_type;
t = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_identref);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_identref);
t_identref = (struct lysc_type_identityref *)t;
CTXP_SIZED_ARRAY(orig_type_identref->bases, t_identref->bases, mem);
@@ -1197,7 +1185,7 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_
case LY_TYPE_INST:
orig_type_instid = (const struct lysc_type_instanceid *)orig_type;
t = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_instid);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_instid);
t_instid = (struct lysc_type_instanceid *)t;
t_instid->require_instance = orig_type_instid->require_instance;
@@ -1205,11 +1193,11 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_
case LY_TYPE_LEAFREF:
orig_type_lref = (const struct lysc_type_leafref *)orig_type;
t = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_lref);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_lref);
t_lref = (struct lysc_type_leafref *)t;
t_lref->path = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *t_lref->path);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *t_lref->path);
ctxp_expr(orig_type_lref->path, t_lref->path, mem);
CTXP_SIZED_ARRAY(orig_type_lref->prefixes, t_lref->prefixes, mem);
@@ -1223,7 +1211,7 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_
case LY_TYPE_UNION:
orig_type_union = (const struct lysc_type_union *)orig_type;
t = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_union);
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_union);
t_union = (struct lysc_type_union *)t;
CTXP_SIZED_ARRAY(orig_type_union->types, t_union->types, mem);
@@ -1736,7 +1724,7 @@ ly_ctx_compiled_print_context(const struct ly_ctx *orig_ctx, struct ly_ctx *ctx,
for (i = 0; i < ctx->modules.count; ++i) {
/* allocate the shared module and store its new address in HT so it can be referenced */
ctx->modules.objs[i] = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof(struct lys_module));
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof(struct lys_module));
ly_ctx_compiled_addr_ht_add(addr_ht, orig_ctx->modules.objs[i], ctx->modules.objs[i]);
}
for (i = 0; i < ctx->modules.count; ++i) {
@@ -1864,7 +1852,7 @@ ly_ctx_compiled_ext_stmts_storage_print(const struct lysc_ext_substmt *orig_subs
case LY_STMT_LENGTH:
case LY_STMT_RANGE:
*substmts[u].storage_p = *mem;
- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof(struct lysc_range));
+ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof(struct lysc_range));
ctxp_range(*orig_substmts[u].storage_p, *substmts[u].storage_p, addr_ht, ptr_set, mem);
break;
case LY_STMT_MAX_ELEMENTS:
--
2.43.0
@@ -0,0 +1,46 @@
From 4d32b003a7aaad5ecca9a013db2dbaa36ef79692 Mon Sep 17 00:00:00 2001
From: Michal Vasko <mvasko@cesnet.cz>
Date: Wed, 10 Dec 2025 10:13:54 +0100
Subject: [PATCH 2/6] printer context BUGFIX missing mem alignment
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Refs #2455
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/context.c | 2 +-
src/printer_context.c | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/context.c b/src/context.c
index 88f4678b3..52a4ae46d 100644
--- a/src/context.c
+++ b/src/context.c
@@ -1459,7 +1459,7 @@ ly_ctx_compiled_print(const struct ly_ctx *ctx, void *mem, void **mem_end)
/* context, referenced */
pctx = mem;
- mem = (char *)mem + sizeof *pctx;
+ mem = (char *)mem + LY_CTXP_MEM_SIZE(sizeof *pctx);
ly_ctx_compiled_addr_ht_add(addr_ht, ctx, pctx);
/* members */
diff --git a/src/printer_context.c b/src/printer_context.c
index 3fb50ac0a..ade0a210d 100644
--- a/src/printer_context.c
+++ b/src/printer_context.c
@@ -33,7 +33,7 @@
* @return Size of @p ARRAY.
*/
#define CTXS_SIZED_ARRAY(ARRAY) \
- (ARRAY ? sizeof(LY_ARRAY_COUNT_TYPE) : 0) + LY_ARRAY_COUNT(ARRAY) * sizeof *ARRAY
+ (ARRAY ? LY_CTXP_MEM_SIZE(sizeof(LY_ARRAY_COUNT_TYPE)) : 0) + LY_CTXP_MEM_SIZE(LY_ARRAY_COUNT(ARRAY) * sizeof *ARRAY)
/**
* @brief Print (serialize) a sized array.
--
2.43.0
@@ -0,0 +1,102 @@
From 51f257fe100adde66efc4d93a5e40b2e5a9a9fab Mon Sep 17 00:00:00 2001
From: Michal Vasko <mvasko@cesnet.cz>
Date: Thu, 11 Dec 2025 12:05:02 +0100
Subject: [PATCH 3/6] lyb BUGFIX big-endian fixes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Refs #2455
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/parser_lyb.c | 8 +++++++-
src/printer_lyb.c | 18 ++++++++++++------
2 files changed, 19 insertions(+), 7 deletions(-)
diff --git a/src/parser_lyb.c b/src/parser_lyb.c
index 43ab02f95..82c759c3d 100644
--- a/src/parser_lyb.c
+++ b/src/parser_lyb.c
@@ -1425,7 +1425,10 @@ lyb_parse_node(struct lyd_lyb_ctx *lybctx, struct lyd_node *parent, struct lyd_n
char *mod_name = NULL, mod_rev[LY_REV_SIZE];
/* read node type */
- lyb_read((uint64_t *)&lyb_type, LYB_NODE_TYPE_BITS, lybctx->parse_ctx);
+ lyb_read(&lyb_type, LYB_NODE_TYPE_BITS, lybctx->parse_ctx);
+
+ /* correct byte order */
+ lyb_type = le32toh(lyb_type);
switch (lyb_type) {
case LYB_NODE_END:
@@ -1543,6 +1546,9 @@ lyb_parse_header(struct lyd_lyb_ctx *lybctx)
data_hash = 0;
lyb_read((uint8_t *)&data_hash, LYB_HEADER_CTX_HASH_BITS, pctx);
+ /* correct byte order */
+ data_hash = le32toh(data_hash);
+
if (!data_hash) {
/* fine for no data */
pctx->empty_hash = 1;
diff --git a/src/printer_lyb.c b/src/printer_lyb.c
index b35d3c0fe..7094467a7 100644
--- a/src/printer_lyb.c
+++ b/src/printer_lyb.c
@@ -447,12 +447,12 @@ lyb_write_count(uint32_t count, struct lylyb_print_ctx *lybctx)
return LY_EINT;
}
- /* correct byte order */
- count = htole32(count);
-
/* copy count to buf */
buf |= count << prefix_b;
+ /* correct byte order */
+ buf = htole32(buf);
+
return lyb_write(&buf, prefix_b + num_b, lybctx);
}
@@ -492,12 +492,12 @@ lyb_write_size(uint32_t size, struct lylyb_print_ctx *lybctx)
num_b = 32;
}
- /* correct byte order */
- size = htole32(size);
-
/* copy size to buf */
buf |= size << prefix_b;
+ /* correct byte order */
+ buf = htole32(buf);
+
return lyb_write(&buf, prefix_b + num_b, lybctx);
}
@@ -635,6 +635,9 @@ lyb_print_header(struct lylyb_print_ctx *lybctx)
/* context hash (is truncated), if not printing empty data */
if (lybctx->ctx) {
hash = lyb_truncate_hash_nonzero(ly_ctx_get_modules_hash(lybctx->ctx), LYB_HEADER_CTX_HASH_BITS);
+
+ /* correct byte order */
+ hash = htole32(hash);
} else {
hash = 0;
}
@@ -1019,6 +1022,9 @@ lyb_print_lyb_type(const struct lyd_node *node, struct lylyb_print_ctx *lybctx)
lyb_type = LYB_NODE_CHILD;
}
+ /* correct byte order */
+ lyb_type = htole32(lyb_type);
+
LY_CHECK_RET(lyb_write(&lyb_type, LYB_NODE_TYPE_BITS, lybctx));
return LY_SUCCESS;
--
2.43.0
@@ -0,0 +1,41 @@
From a44103219029ba70b67a87d5a33cfb8aec1e03f3 Mon Sep 17 00:00:00 2001
From: Michal Vasko <mvasko@cesnet.cz>
Date: Thu, 11 Dec 2025 12:05:51 +0100
Subject: [PATCH 4/6] bits BUGFIX operator priority
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Refs #2455
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/plugins_types/bits.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/plugins_types/bits.c b/src/plugins_types/bits.c
index ab7acb527..f5fa6dccb 100644
--- a/src/plugins_types/bits.c
+++ b/src/plugins_types/bits.c
@@ -61,7 +61,7 @@ lyplg_type_bits_is_bit_set(const char *bitmap, uint32_t size_bits, uint32_t bit_
uint32_t size;
/* get size in bytes */
- size = size_bits / 8 + (size_bits % 8) ? 1 : 0;
+ size = size_bits / 8 + ((size_bits % 8) ? 1 : 0);
/* find the byte with our bit */
(void)size;
@@ -93,7 +93,7 @@ bits_bit_set(char *bitmap, uint32_t size_bits, uint32_t bit_position)
uint32_t size;
/* get size in bytes */
- size = size_bits / 8 + (size_bits % 8) ? 1 : 0;
+ size = size_bits / 8 + ((size_bits % 8) ? 1 : 0);
/* find the byte with our bit */
(void)size;
--
2.43.0
@@ -0,0 +1,223 @@
From e2c06476b886642411181e0a236b95de14145940 Mon Sep 17 00:00:00 2001
From: Michal Vasko <mvasko@cesnet.cz>
Date: Thu, 11 Dec 2025 12:06:15 +0100
Subject: [PATCH 5/6] integer BUGFIX big-endian print fixes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Refs #2455
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/plugins_types/integer.c | 96 ++++++++++---------------------------
1 file changed, 24 insertions(+), 72 deletions(-)
diff --git a/src/plugins_types/integer.c b/src/plugins_types/integer.c
index 945d93f86..f05096970 100644
--- a/src/plugins_types/integer.c
+++ b/src/plugins_types/integer.c
@@ -291,64 +291,6 @@ lyplg_type_sort_int(const struct ly_ctx *UNUSED(ctx), const struct lyd_value *va
return 0;
}
-static const void *
-lyplg_type_print_int(const struct ly_ctx *UNUSED(ctx), const struct lyd_value *value, LY_VALUE_FORMAT format,
- void *UNUSED(prefix_data), ly_bool *dynamic, uint32_t *value_size_bits)
-{
- int64_t prev_num = 0, num = 0;
- void *buf;
-
- if (format == LY_VALUE_LYB) {
- switch (value->realtype->basetype) {
- case LY_TYPE_INT8:
- prev_num = num = value->int8;
- break;
- case LY_TYPE_INT16:
- prev_num = num = value->int16;
- break;
- case LY_TYPE_INT32:
- prev_num = num = value->int32;
- break;
- case LY_TYPE_INT64:
- prev_num = num = value->int64;
- break;
- default:
- break;
- }
-
- num = htole64(num);
- if (num == prev_num) {
- /* values are equal, little-endian or int8 */
- *dynamic = 0;
- if (value_size_bits) {
- /* the least amount of bits that can hold the number */
- *value_size_bits = lyplg_type_get_highest_set_bit_pos(num);
- }
- return &value->int64;
- } else {
- /* values differ, big-endian */
- buf = calloc(1, LYPLG_BITS2BYTES(lyplg_type_get_highest_set_bit_pos(num)));
- LY_CHECK_RET(!buf, NULL);
-
- *dynamic = 1;
- if (value_size_bits) {
- *value_size_bits = lyplg_type_get_highest_set_bit_pos(num);
- }
- memcpy(buf, &num, LYPLG_BITS2BYTES(lyplg_type_get_highest_set_bit_pos(num)));
- return buf;
- }
- }
-
- /* use the cached canonical value */
- if (dynamic) {
- *dynamic = 0;
- }
- if (value_size_bits) {
- *value_size_bits = strlen(value->_canonical) * 8;
- }
- return value->_canonical;
-}
-
static LY_ERR
lyplg_type_store_uint(const struct ly_ctx *ctx, const struct lysc_type *type, const void *value, uint32_t value_size_bits,
uint32_t options, LY_VALUE_FORMAT format, void *UNUSED(prefix_data), uint32_t hints,
@@ -573,31 +515,37 @@ lyplg_type_sort_uint(const struct ly_ctx *UNUSED(ctx), const struct lyd_value *v
}
static const void *
-lyplg_type_print_uint(const struct ly_ctx *UNUSED(ctx), const struct lyd_value *value, LY_VALUE_FORMAT format,
+lyplg_type_print_u_int(const struct ly_ctx *UNUSED(ctx), const struct lyd_value *value, LY_VALUE_FORMAT format,
void *UNUSED(prefix_data), ly_bool *dynamic, uint32_t *value_size_bits)
{
uint64_t num = 0;
+ uint8_t bytes_used;
+ uint16_t bits_used;
void *buf;
if (format == LY_VALUE_LYB) {
switch (value->realtype->basetype) {
case LY_TYPE_UINT8:
+ case LY_TYPE_INT8:
num = value->uint8;
break;
case LY_TYPE_UINT16:
+ case LY_TYPE_INT16:
num = value->uint16;
break;
case LY_TYPE_UINT32:
+ case LY_TYPE_INT32:
num = value->uint32;
break;
case LY_TYPE_UINT64:
+ case LY_TYPE_INT64:
num = value->uint64;
break;
default:
break;
}
- num = htole64(num);
- if (num == value->uint64) {
+
+ if (htole64(num) == value->uint64) {
/* values are equal, little-endian or uint8 */
*dynamic = 0;
if (value_size_bits) {
@@ -607,14 +555,18 @@ lyplg_type_print_uint(const struct ly_ctx *UNUSED(ctx), const struct lyd_value *
return &value->uint64;
} else {
/* values differ, big-endian */
- buf = calloc(1, LYPLG_BITS2BYTES(lyplg_type_get_highest_set_bit_pos(num)));
+ bits_used = lyplg_type_get_highest_set_bit_pos(num);
+ bytes_used = LYPLG_BITS2BYTES(bits_used);
+
+ buf = calloc(1, bytes_used);
LY_CHECK_RET(!buf, NULL);
+ num = htole64(num);
+ memcpy(buf, &num, bytes_used);
*dynamic = 1;
if (value_size_bits) {
- *value_size_bits = lyplg_type_get_highest_set_bit_pos(num);
+ *value_size_bits = bits_used;
}
- memcpy(buf, &num, LYPLG_BITS2BYTES(lyplg_type_get_highest_set_bit_pos(num)));
return buf;
}
}
@@ -649,7 +601,7 @@ const struct lyplg_type_record plugins_integer[] = {
.plugin.validate_tree = NULL,
.plugin.compare = lyplg_type_compare_uint,
.plugin.sort = lyplg_type_sort_uint,
- .plugin.print = lyplg_type_print_uint,
+ .plugin.print = lyplg_type_print_u_int,
.plugin.duplicate = lyplg_type_dup_simple,
.plugin.free = lyplg_type_free_simple,
}, {
@@ -664,7 +616,7 @@ const struct lyplg_type_record plugins_integer[] = {
.plugin.validate_tree = NULL,
.plugin.compare = lyplg_type_compare_uint,
.plugin.sort = lyplg_type_sort_uint,
- .plugin.print = lyplg_type_print_uint,
+ .plugin.print = lyplg_type_print_u_int,
.plugin.duplicate = lyplg_type_dup_simple,
.plugin.free = lyplg_type_free_simple,
}, {
@@ -679,7 +631,7 @@ const struct lyplg_type_record plugins_integer[] = {
.plugin.validate_tree = NULL,
.plugin.compare = lyplg_type_compare_uint,
.plugin.sort = lyplg_type_sort_uint,
- .plugin.print = lyplg_type_print_uint,
+ .plugin.print = lyplg_type_print_u_int,
.plugin.duplicate = lyplg_type_dup_simple,
.plugin.free = lyplg_type_free_simple,
}, {
@@ -694,7 +646,7 @@ const struct lyplg_type_record plugins_integer[] = {
.plugin.validate_tree = NULL,
.plugin.compare = lyplg_type_compare_uint,
.plugin.sort = lyplg_type_sort_uint,
- .plugin.print = lyplg_type_print_uint,
+ .plugin.print = lyplg_type_print_u_int,
.plugin.duplicate = lyplg_type_dup_simple,
.plugin.free = lyplg_type_free_simple,
}, {
@@ -709,7 +661,7 @@ const struct lyplg_type_record plugins_integer[] = {
.plugin.validate_tree = NULL,
.plugin.compare = lyplg_type_compare_int,
.plugin.sort = lyplg_type_sort_int,
- .plugin.print = lyplg_type_print_int,
+ .plugin.print = lyplg_type_print_u_int,
.plugin.duplicate = lyplg_type_dup_simple,
.plugin.free = lyplg_type_free_simple,
}, {
@@ -724,7 +676,7 @@ const struct lyplg_type_record plugins_integer[] = {
.plugin.validate_tree = NULL,
.plugin.compare = lyplg_type_compare_int,
.plugin.sort = lyplg_type_sort_int,
- .plugin.print = lyplg_type_print_int,
+ .plugin.print = lyplg_type_print_u_int,
.plugin.duplicate = lyplg_type_dup_simple,
.plugin.free = lyplg_type_free_simple,
}, {
@@ -739,7 +691,7 @@ const struct lyplg_type_record plugins_integer[] = {
.plugin.validate_tree = NULL,
.plugin.compare = lyplg_type_compare_int,
.plugin.sort = lyplg_type_sort_int,
- .plugin.print = lyplg_type_print_int,
+ .plugin.print = lyplg_type_print_u_int,
.plugin.duplicate = lyplg_type_dup_simple,
.plugin.free = lyplg_type_free_simple,
}, {
@@ -754,7 +706,7 @@ const struct lyplg_type_record plugins_integer[] = {
.plugin.validate_tree = NULL,
.plugin.compare = lyplg_type_compare_int,
.plugin.sort = lyplg_type_sort_int,
- .plugin.print = lyplg_type_print_int,
+ .plugin.print = lyplg_type_print_u_int,
.plugin.duplicate = lyplg_type_dup_simple,
.plugin.free = lyplg_type_free_simple,
},
--
2.43.0
@@ -0,0 +1,32 @@
From 0e911b67177252160f7f1361e2481a88200debc3 Mon Sep 17 00:00:00 2001
From: Michal Vasko <mvasko@cesnet.cz>
Date: Thu, 11 Dec 2025 15:20:28 +0100
Subject: [PATCH 6/6] context BUGFIX unexpected context node
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Fixes #2457
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/context.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/context.c b/src/context.c
index 52a4ae46d..1c287d9f2 100644
--- a/src/context.c
+++ b/src/context.c
@@ -370,7 +370,7 @@ ly_ctx_new_yl_legacy(struct ly_ctx *ctx, const struct lyd_node *yltree)
LY_ERR ret = LY_SUCCESS;
uint32_t i, j;
- LY_CHECK_RET(ret = lyd_find_xpath(yltree, "modules-state/module", &set));
+ LY_CHECK_RET(ret = lyd_find_xpath(yltree, "/ietf-yang-library:modules-state/module", &set));
if (!set->count) {
ret = LY_ENOTFOUND;
--
2.43.0
@@ -1,4 +1,4 @@
From 8c982a391f0b2e21e25552d7daa592b12368c76b Mon Sep 17 00:00:00 2001
From ee24238650f76b07ce3e108c9573f476df81e41d Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Tue, 12 Mar 2024 10:27:24 +0100
Subject: [PATCH 01/30] [FIX] net: dsa: mv88e6xxx: Fix timeout on waiting for
@@ -1,4 +1,4 @@
From dad4553127023de69d13565a34a8f32c14013da6 Mon Sep 17 00:00:00 2001
From b01d0f997304f02ff976922400c79e27f6be4943 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Wed, 27 Mar 2024 15:52:43 +0100
Subject: [PATCH 02/30] net: dsa: mv88e6xxx: Improve indirect register access
@@ -1,4 +1,4 @@
From a0829c84c0eb2e4bc4758ef5e2be5d267fa7626e Mon Sep 17 00:00:00 2001
From 37fc83e00b09a3dd65d01be3c0f1a73e45c4bcc4 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Mon, 22 Apr 2024 23:18:01 +0200
Subject: [PATCH 03/30] net: dsa: mv88e6xxx: Honor ports being managed via
@@ -1,4 +1,4 @@
From e32883eed619bc3eab8ad848c085fc29bafaf83a Mon Sep 17 00:00:00 2001
From 24cd80a365f6e8a88f6329542eba0f2fb68087cb Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Wed, 24 Apr 2024 22:41:04 +0200
Subject: [PATCH 04/30] net: dsa: mv88e6xxx: Limit rsvd2cpu policy to user
@@ -1,4 +1,4 @@
From 764be1a05c3411b8e369d089d4302f7b670602e8 Mon Sep 17 00:00:00 2001
From 40b1ddaa15d88067839c33cb9a6ae82ddb6679ac Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Thu, 16 Nov 2023 19:44:32 +0100
Subject: [PATCH 05/30] net: dsa: mv88e6xxx: Add LED infrastructure
@@ -1,4 +1,4 @@
From 4f0d8d9b78a77b54c105924baa2cafd39975b441 Mon Sep 17 00:00:00 2001
From 9f0aabfc16c4fb0d4aff691eba1cbf77ad7cb3dd Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Thu, 16 Nov 2023 21:59:35 +0100
Subject: [PATCH 06/30] net: dsa: mv88e6xxx: Add LED support for 6393X

Some files were not shown because too many files have changed in this diff Show More