Compare commits

..
32 Commits
Author SHA1 Message Date
Tobias WaldekranzandGitHub eb7c031270 Merge pull request #1332 from kernelkit/kernel-v25.02.x
Bump kernel and update ChangeLog for v25.08.2
2025-12-19 13:27:25 +01:00
Joachim Wiberg 1d23f73bfd doc: update ChangeLog for v25.08.2 release
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-19 11:36:54 +01:00
ael-botandJoachim Wiberg 119b92f94e Upgrade Linux kernel to 6.12.63
Backported from origin/main a999a93e63

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-19 11:36:54 +01:00
Tobias WaldekranzandGitHub 96fad208cb Merge pull request #1328 from kernelkit/cand/v25.08.x
Candidate backports for v25.08.2
2025-12-19 10:34:57 +01:00
Tobias WaldekranzandJoachim Wiberg 0a3879f521 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-18 14:12:03 +01:00
Tobias WaldekranzandJoachim Wiberg 6f6bb412f9 aarch64: kernel: Enable SBSA watchdog
Usable by all Server Base System Architecture (SBSA) compliant SoCs,
e.g., CN9130.
2025-12-18 14:12:02 +01:00
Joachim Wiberg b04bae1ad6 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-18 14:12:02 +01:00
Joachim Wiberg 73edf1bbd6 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-18 14:12:01 +01:00
Joachim Wiberg 2557eb4900 confd: add support for toggling OSPF debug logs
Backported from origin/main 7edc3c19f7

Fixes #1281

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-18 14:12:01 +01:00
Joachim Wiberg f71722c111 bin: add system support data collection
Backported from origin/main 5420fb01f2

Includes the latest DRAM ECC status, with/without sudo, and temperature.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-18 14:12:00 +01:00
Joachim Wiberg 5ea99680c8 bin: backport fixes to shell/cli copy command
This is a backport of the following commits from origin/main: 3b24fab
e6a04fb 92e80b4 ad96965 3e03ece 21256a8 b7d91e4 d26e311 0977ab4 f83fbc6

---

cli: fix 'copy FILE running-config' use-case

When copying to the running datastore we cannot use sr_copy_config(),
instead we must use sr_replace_config().  This fix covers both the case
of 'copy startup-config running-config' and 'copy FILE running-config'.

Fixes #1203

---

cli: add 'validate', or '-n', dry run to copy command

This commit adds config file validation to the copy command, discussed
in #373.  Allowing users to test their config files before restoring a
backup.  The feature could also be used for the automatic rollback when
downgrading to an earlier version of the OS.

Fixes #373

---

cli: fix copy to missing startup-config file

Fixes #981

---

cli: restrict copy and erase commands

This is a follow-up to PR #717 where path traversal protection was
discussed.  A year later and it's clear that having a user-friendly
copy tool in the shell is a good thing, but that we proably want to
restrict what it can do when called from the CLI.

A sanitize flag (-s) is added to control the behavior, when used in the
shell without -s, both commands act like traditional UNIX tools and do
assume . for relative paths, and allow ../, whereas when running from
the CLI only /media/ is allowed and otherwise files are assumed to be
in $HOME or /cfg

---

cli: sanitize regular file to file copy

The regular file-to-file copy, was missing calls to cfg_adjust(), this
commit fixes that and adds some helpful comments for each use-case.

Also, drop insecure mktemp() in favor of our own version which uses the
basename of the remote source file.

---

bin: add bash completion for copy command

Add bash completion for the common datastores, like we already do in the
CLI, and update the usage text accordingly.

Also, make sure to install to /usr/bin, not /bin since we've now merged
the hierarchies since a while back.

---

bin: copy: Refactor

copy() made some...creative...use of control flow that made it quite
difficult to follow.

Take a first priciples approach to simplify the logic.

---

bin: copy: Always get startup from sysrepo

This will make sure to apply NACM rules for all the data. It also
makes it possible for a luser access a subset of the data, even if
they to do not have read access to /cfg/startup-config.cfg.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-18 14:12:00 +01:00
Joachim Wiberg 55b038d999 .github: only set latest tag on .0 releases
Prevent patch releases from stealing the "latest" tag from newer minor
versions. Only vXX.YY and vXX.YY.0 releases should be marked as latest.

Fixes #1187

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-18 14:11:59 +01:00
Joachim Wiberg 83b55ee4d8 Follw-up to d7abe46, use correct compatible string for override
Actually disable iitod on Styx DCP-SC-28p to work around #670, this
prohibits software control of LEDs, leaving the default HW control,
which has proven more stable on this platform.

Backported from origin/main b0dce8a05e

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-18 14:11:31 +01:00
Joachim WibergandGitHub 68e7167902 Merge pull request #1184 from kernelkit/releng
Release prep for v25.08.1
2025-10-03 16:22:07 +02:00
Joachim Wiberg 25a26fe7c6 doc: update release notes for v25.08.1
[skip ci]

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-03 16:21:28 +02:00
Joachim Wiberg 6c8bd25cee .github; only publish to latest release from main branch
[skip ci]

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-03 16:21:27 +02:00
Joachim Wiberg ea300137ef test: relocate logic for creating test-report.pdf from workflow
It should be possible to create test reports manually, so logically
the GitHub workflow should call a make rule in test.mk

Untested: branding, or any case where Infix is used as a BR2_EXTERNAL

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-03 15:03:58 +02:00
Joachim Wiberg 44e9c87376 test/infamy: silence schema download in restconf backend
NETCONF backedn already silenced.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-03 15:03:58 +02:00
Joachim Wiberg e708f21444 test/infamy: refactor Furl class to support list of needles
Also, add some documentation to lower barrier of entry/use.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-03 15:03:58 +02:00
Joachim Wiberg 3508b8d0bb test: update container test specs.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-03 15:03:57 +02:00
Joachim Wiberg eec867bd5f test: verify ospf neighbors in setup with non-ospf interface
Extend OSPF basic with a regression test for issue #1169

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-03 15:03:57 +02:00
Joachim Wiberg daa0bd81e8 board/common: ensure efi-part.vfat is saved in build tree
Fixes #1132

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-03 15:03:56 +02:00
Joachim WibergandGitHub 4ccc70aeee Merge pull request #1172 from kernelkit/v25.08.x-backports
Backport fixes for v25.08.1

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-30 15:28:57 +02:00
Richard Alpe 923443b620 doc: update changelog for v25.08.1
Signed-off-by: Richard Alpe <richard@bit42.se>
2025-09-30 10:28:57 +02:00
Richard Alpe 9ad51c31bf OSPF: skip interfaces without OSPF enabled
Fix a bug where systems with OSPF enabled on some, but not all,
interfaces would cause the OSPF iterator to fail when accessing
iface['area'], which was missing. This caused the ospf_status.py
tool to return {}, resulting in empty OSPF data in sysrepo.

Fixes #1169 Expected neighbors not shown in sysrepocfg

Signed-off-by: Richard Alpe <richard@bit42.se>
2025-09-30 10:21:10 +02:00
Joachim WibergandRichard Alpe 6272c9c46c .github: prevent duplicate builds when ci:main label is added to PRs
Add concurrency control to trigger workflow to cancel in-progress builds
when new events (like adding ci:main label) occur on the same PR.  This
prevents resource waste from duplicate builds and handles the common
workflow where developers add the ci:main label after PR creation.

Fixes #1154
2025-09-30 10:21:09 +02:00
Joachim WibergandRichard Alpe 4750f96774 cli: fix 'show ospf' commands regression, introduced in 827dc098e
Fixes #1155

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-30 10:21:08 +02:00
Joachim WibergandRichard Alpe 81bc143883 board/common: set show-legacy wrapper as executable
Fix #1150

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-30 10:21:07 +02:00
Joachim WibergandRichard Alpe 58b3fb30be test/case/infix_services: refactor mdns-allow-deny
Major refactor to redesign how listeners and traffic is started in parallel.

Fixes #1130

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-30 10:21:06 +02:00
Joachim WibergandRichard Alpe 6dfc305f0b test/case/infix_container: refactor container-environment
Refactor test to use httpd container instead and return ENV with a CGI.

Fixes #1131

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-30 10:21:06 +02:00
Mattias WalströmandRichard Alpe 8ca3d3a3ff test: Add new sanity check tests
One to verify it is the correct version on the duts and one
that check that it is the correct bootorder.
2025-09-30 10:21:05 +02:00
Joachim Wiberg 7ae4de3a17 doc: prepare for v25.08.1
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-30 09:30:51 +02:00
552 changed files with 29655 additions and 7319 deletions
-1
View File
@@ -20,7 +20,6 @@ jobs:
- cn9130_crb_boot
- aarch64_qemu_boot
- rpi4_boot
- mt7986_sd_boot
env:
MAKEFLAGS: -j5
steps:
-225
View File
@@ -1,225 +0,0 @@
name: Create SD Card Images
on:
workflow_dispatch:
inputs:
board:
description: 'Board to create image for'
type: choice
required: true
options:
- raspberry-pi-4
- banana-pi-r3
default: 'raspberry-pi-4'
use_latest_release:
description: 'Use latest release artifacts instead of workflow artifacts'
type: boolean
default: false
jobs:
create-image:
name: Create SD Card Image for ${{ inputs.board }}
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
clean: true
fetch-depth: 0
submodules: recursive
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
genimage \
u-boot-tools \
parted \
gdisk \
qemu-utils \
dosfstools \
e2fsprogs \
genext2fs \
mtools \
jq
- name: Prepare build environment
run: |
# Set up directory structure similar to buildroot build
mkdir -p output/images
mkdir -p build
- name: Set bootloader and target based on board
run: |
case "${{ inputs.board }}" in
raspberry-pi-4)
echo "BOOTLOADER=rpi4_boot" >> $GITHUB_ENV
echo "TARGET=aarch64" >> $GITHUB_ENV
;;
banana-pi-r3)
echo "BOOTLOADER=mt7986_sd_boot" >> $GITHUB_ENV
echo "TARGET=aarch64" >> $GITHUB_ENV
;;
*)
echo "Error: Unknown board ${{ inputs.board }}"
exit 1
;;
esac
echo "Using bootloader: $BOOTLOADER and target: $TARGET for board: ${{ inputs.board }}"
- name: Download bootloader artifacts
if: ${{ !inputs.use_latest_release }}
run: |
# Download from latest bootloader build workflow on main branch
gh run list --workflow=build-boot.yml --branch=main --limit=1 --status=success --json databaseId --jq '.[0].databaseId' > latest_boot_run_id
BOOT_RUN_ID=$(cat latest_boot_run_id)
gh run download ${BOOT_RUN_ID} --name artifact-${BOOTLOADER} --dir temp_bootloader/
# Extract bootloader directly to output/images
cd temp_bootloader/
tar -xzf *.tar.gz --strip-components=1 -C ../output/images/
cd ../
rm -rf temp_bootloader/
echo "Bootloader files extracted to output/images:"
ls -la output/images/
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download Infix artifacts
if: ${{ !inputs.use_latest_release }}
run: |
# Download from latest Kernelkit Trigger workflow for main branch
gh run list --workflow=164295764 --branch=main --limit=1 --status=success --json databaseId --jq '.[0].databaseId' > latest_infix_run_id
INFIX_RUN_ID=$(cat latest_infix_run_id)
gh run download ${INFIX_RUN_ID} --name artifact-${TARGET} --dir temp_infix/
# Extract Infix directly to output/images
cd temp_infix/
tar -xzf *.tar.gz --strip-components=1 -C ../output/images/
cd ../
rm -rf temp_infix/
echo "Infix files extracted to output/images:"
ls -la output/images/
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download from latest releases
if: ${{ inputs.use_latest_release }}
run: |
# Download latest bootloader release
gh release download latest-boot --pattern "*${BOOTLOADER}*" --dir temp_bootloader/
cd temp_bootloader/
tar -xzf *.tar.gz --strip-components=1 -C ../output/images/
cd ../
rm -rf temp_bootloader/
# Download latest Infix release
gh release download latest --pattern "*${TARGET}*" --dir temp_infix/
cd temp_infix/
tar -xzf *.tar.gz --strip-components=1 -C ../output/images/
cd ../
rm -rf temp_infix/
echo "All files extracted to output/images:"
ls -la output/images/
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Verify extracted files
run: |
echo "Files available for mkimage.sh:"
ls -la output/images/
echo ""
echo "File types:"
file output/images/* || true
- name: Create SD card image
run: |
export BINARIES_DIR=$PWD/output/images
export BUILD_DIR=$PWD/build
export BR2_EXTERNAL_INFIX_PATH=$PWD
export RELEASE=""
export INFIX_ID="infix"
# Use the standardized mkimage.sh path for the selected board
# BOARD_SCRIPT="src/board/${{ inputs.board }}/mkimage.sh"
BOARD_SCRIPT="src/board/${{ inputs.board }}/mkimage.sh"
if [ -f "$BOARD_SCRIPT" ]; then
echo "Using board-specific image creation script: $BOARD_SCRIPT"
chmod +x "$BOARD_SCRIPT"
"$BOARD_SCRIPT"
else
echo "Error: Board script $BOARD_SCRIPT not found!"
exit 1
fi
- name: Verify created image
run: |
echo "Contents of output/images after mkimage.sh:"
ls -lh output/images/
# Look for SD card image with pattern: *-sdcard.img
if ls output/images/*-sdcard.img 1> /dev/null 2>&1; then
echo "Found SD card image(s):"
for img in output/images/*-sdcard.img; do
echo "- $(basename $img)"
file "$img"
fdisk -l "$img" 2>/dev/null || true
done
else
echo "No SD card image found matching pattern: *-sdcard.img"
echo "Available files:"
ls -la output/images/
exit 1
fi
- name: Upload SD card image
uses: actions/upload-artifact@v4
with:
name: sdcard-image-${{ inputs.board }}-${{ env.BOOTLOADER }}
path: |
output/images/*-sdcard.img
retention-days: 30
- name: Create checksums
run: |
cd output/images/
for file in *-sdcard.img; do
if [ -f "$file" ]; then
sha256sum "$file" > "$file.sha256"
fi
done
- name: Upload to release
uses: ncipollo/release-action@v1
with:
allowUpdates: true
omitName: true
omitBody: true
omitBodyDuringUpdate: true
prerelease: true
tag: "latest-boot"
token: ${{ secrets.GITHUB_TOKEN }}
artifacts: "output/images/*-sdcard.img*"
- name: Generate summary
run: |
cat <<EOF >> $GITHUB_STEP_SUMMARY
# SD Card Image Build Complete! 🚀
**Board:** ${{ inputs.board }}
**Target:** ${{ env.TARGET }}
**Bootloader:** ${{ env.BOOTLOADER }}
**Artifact Source:** ${{ inputs.use_latest_release && 'Latest Release' || 'Latest Workflow Run' }}
## Created Images
$(find output/images/ -name "*.img" -o -name "*.qcow2" -o -name "*.raw" | xargs ls -lh 2>/dev/null | sed 's/^/- /' || echo "- No images found")
## Download
The SD card image is available as a workflow artifact above.
EOF
+1 -1
View File
@@ -125,7 +125,7 @@ jobs:
if echo $ver | grep -qE 'v[0-9.]+(-alpha|-beta|-rc)[0-9]*'; then
echo "pre=true" >> $GITHUB_OUTPUT
echo "latest=false" >> $GITHUB_OUTPUT
elif echo $ver | grep -qE '^v[0-9.]+\.[0-9.]+(\.[0-9]+)?$'; then
elif echo $ver | grep -qE '^v[0-9]+\.[0-9]+(\.0)?$'; then
echo "pre=false" >> $GITHUB_OUTPUT
echo "latest=true" >> $GITHUB_OUTPUT
echo "cat=Releases" >> $GITHUB_OUTPUT
+1 -1
View File
@@ -37,6 +37,6 @@ jobs:
name: "infix"
test-publish-x86_64:
if: startsWith(github.repository, 'kernelkit/')
if: ${{ github.repository_owner == 'kernelkit' && github.ref_name == 'main' }}
needs: test-run-x86_64
uses: ./.github/workflows/publish.yml
+3 -7
View File
@@ -112,12 +112,10 @@ containers for any custom functionality you need.
### Supported Platforms
- **Raspberry Pi 4B** - Perfect for home labs, learning, and prototyping
- **Banana Pi-R3** - Your next home router and gateway
- **NanoPi R2S** - Compact dual-port router in a tiny package
- **NanoPi R2S** - Compact dual-port router in a tiny package
- **x86_64** - Run in VMs or on mini PCs for development and testing
- **Marvell CN9130 CRB, EspressoBIN** - High-performance ARM64 platforms
- **Microchip SparX-5i** - Enterprise switching capabilities
- **NXP i.MX8MP EVK** - Highly capable ARM64 SoC
- **Marvell CN9130 CRB, EspressoBIN** - High-performance ARM platforms
- **Microchip SparX-5i, NXP i.MX8MP EVK** - Enterprise switching capabilities
- **StarFive VisionFive2** - RISC-V architecture support
*Why start with Raspberry Pi?* It's affordable, widely available, has
@@ -150,8 +148,6 @@ high-throughput network appliances.
<br />Infix development is sponsored by <a href="https://wires.se">Wires</a>
</div>
![Alt](https://repobeats.axiom.co/api/embed/5ce7a2a67edc923823afa0f60c327a6e8575b6e9.svg "Repobeats analytics image")
[1]: https://buildroot.org/ "Buildroot Homepage"
[2]: https://www.sysrepo.org/ "Sysrepo Homepage"
[3]: https://kernelkit.org/infix/latest/cli/introduction/
+45 -2
View File
@@ -4,8 +4,51 @@ aarch64
Board Specific Documentation
----------------------------
- [Banana Pi R3](banana-pi-r3/)
- [Marvell CN9130-CRB](cn9130-crb/)
- [Microchip SparX-5i PCB135 (eMMC)](sparx5-pcb135/)
- [NanoPi R2S](r2s/)
- [Raspberry Pi 4 b](raspberry-pi-4/)
- [Raspberry Pi 4 b](#raspberry-pi-4-b)
# Raspberry Pi 4 b
## Support level
Full support for base board but not any extension board on the
GPIOs.
### Touch screen
The [Raspberry Pi touch display v1][RPI-TOUCH] is supported, including
touch functionality. There are multiple touchscreens on the market for
Raspberry Pi, but only the official (first version with 800x480
resolution) is currently supported. Infix supplies all drivers
required to utilize the hardware, but you need to add the actual
graphical application in a container.
There are some important considerations you need to know about when
using Infix for graphical applications. The container needs access to
/dev/dri/ to be able to access the graphics card, and it also needs
access to /run/udev to be able to find the input devices.
Example of running Doom in Infix:
```cli
admin@example:/> configure
admin@example:/config/> edit container doom
admin@example:/config/container/doom/> set image docker://mattiaswal/alpine-doom:latest
admin@example:/config/container/doom/> set privileged
admin@example:/config/container/doom/> edit mount udev
admin@example:/config/container/doom/mount/udev/> set type bind
admin@example:/config/container/doom/mount/udev/> set target /run/udev/
admin@example:/config/container/doom/mount/udev/> set source /run/udev/
admin@example:/config/container/doom/mount/udev/> end
admin@example:/config/container/doom/mount/xorg.conf/> set content U2VjdGlvbiAiT3V0cHV0Q2xhc3MiCiAgSWRlbnRpZmllciAidmM0IgogIE1hdGNoRHJpdmVyICJ2YzQiCiAgRHJpdmVyICJtb2Rlc2V0dGluZyIKICBPcHRpb24gIlByaW1hcnlHUFUiICJ0cnVlIgpFbmRTZWN0aW9uCg==
admin@example:/config/container/doom/mount/xorg.conf/> set target /etc/X11/xorg.conf
admin@example:/config/container/doom/mount/xorg.conf/> end
admin@example:/config/container/doom/> edit volume var
admin@example:/config/container/doom/volume/var/> set target /var
admin@example:/config/container/doom/volume/var/> leave
admin@example:/>
```
[RPI-TOUCH]: https://www.raspberrypi.com/products/raspberry-pi-touch-display/
-27
View File
@@ -1,27 +0,0 @@
# Banana Pi R3
## Support level
Full support for all Infix enabled features including switched ethernet ports, WiFi,
and SFP interfaces. The board includes comprehensive hardware support for
MediaTek MT7986 SoC features.
### Hardware features
The Banana Pi R3 is a high-performance networking board featuring:
- MediaTek MT7986 ARM Cortex-A53 quad-core processor
- 4x Gigabit LAN ports (lan1-lan4)
- 1x Gigabit WAN port
- 2x SFP ports (sfp1, sfp2) for fiber connectivity
- Dual WiFi interfaces (wifi0 for 2.4GHz, wifi1 for 5GHz)
- USB support
- SD card boot support
### Network configuration
The board comes preconfigured with:
- 4 switched LAN ports for internal networking
- Dedicated WAN port with DHCP client enabled
- SFP ports for high-speed fiber connections
- Dual WiFi interfaces for wireless connectivity
### Pre-built images
SD card image: [infix-bpi-r3-sdcard.img](https://github.com/kernelkit/infix/releases/download/latest-boot/infix-bpi-r3-sdcard.img)
+9 -1
View File
@@ -401,6 +401,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
@@ -549,7 +550,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
@@ -153,7 +153,7 @@
}
]
},
"infix-system:motd-banner": "Li0tLS0tLS0uCnwgIC4gLiAgfCBJbmZpeCBPUyDigJQgSW1tdXRhYmxlLkZyaWVuZGx5LlNlY3VyZQp8LS4gdiAuLXwgaHR0cHM6Ly9rZXJuZWxraXQub3JnCictJy0tLSctJwo="
"infix-system:motd-banner": "Li0tLS0tLS0uCnwgIC4gLiAgfCBJbmZpeCAtLSBhIE5ldHdvcmsgT3BlcmF0aW5nIFN5c3RlbQp8LS4gdiAuLXwgaHR0cHM6Ly9rZXJuZWxraXQuZ2l0aHViLmlvCictJy0tLSctJwo="
},
"infix-dhcp-client:dhcp-client": {
"client-if": [
-46
View File
@@ -1,46 +0,0 @@
# Raspberry Pi 4 b
## Support level
Full support for base board but not any extension board on the
GPIOs.
### Touch screen
The [Raspberry Pi touch display v1][RPI-TOUCH] is supported, including
touch functionality. There are multiple touchscreens on the market for
Raspberry Pi, but only the official (first version with 800x480
resolution) is currently supported. Infix supplies all drivers
required to utilize the hardware, but you need to add the actual
graphical application in a container.
There are some important considerations you need to know about when
using Infix for graphical applications. The container needs access to
/dev/dri/ to be able to access the graphics card, and it also needs
access to /run/udev to be able to find the input devices.
Example of running Doom in Infix:
```cli
admin@example:/> configure
admin@example:/config/> edit container doom
admin@example:/config/container/doom/> set image docker://mattiaswal/alpine-doom:latest
admin@example:/config/container/doom/> set privileged
admin@example:/config/container/doom/> edit mount udev
admin@example:/config/container/doom/mount/udev/> set type bind
admin@example:/config/container/doom/mount/udev/> set target /run/udev/
admin@example:/config/container/doom/mount/udev/> set source /run/udev/
admin@example:/config/container/doom/mount/udev/> end
admin@example:/config/container/doom/mount/xorg.conf/> set content U2VjdGlvbiAiT3V0cHV0Q2xhc3MiCiAgSWRlbnRpZmllciAidmM0IgogIE1hdGNoRHJpdmVyICJ2YzQiCiAgRHJpdmVyICJtb2Rlc2V0dGluZyIKICBPcHRpb24gIlByaW1hcnlHUFUiICJ0cnVlIgpFbmRTZWN0aW9uCg==
admin@example:/config/container/doom/mount/xorg.conf/> set target /etc/X11/xorg.conf
admin@example:/config/container/doom/mount/xorg.conf/> end
admin@example:/config/container/doom/> edit volume var
admin@example:/config/container/doom/volume/var/> set target /var
admin@example:/config/container/doom/volume/var/> leave
admin@example:/>
```
### Pre-built images
SD card image: [infix-rpi4-sdcard.img](https://github.com/kernelkit/infix/releases/download/latest-boot/infix-rpi4-sdcard.img)
[RPI-TOUCH]: https://www.raspberrypi.com/products/raspberry-pi-touch-display/
@@ -6,12 +6,6 @@ mountprefix=/var/lib/rauc/mnt
bundle-formats=-plain
max-bundle-download-size=1073741824
[log.event-log]
filename=/var/log/upgrade-json.log
format=json-pretty
max-size=1M
max-files=5
[keyring]
directory=/etc/rauc/keys
@@ -0,0 +1,161 @@
#!/bin/sh
set -e
# Build a map of phandle -> device tree node path for all PHY nodes
build_phandle_map()
{
for phy_node in /sys/firmware/devicetree/base/cp*/config-space*/mdio*/switch*/mdio/ethernet-phy@* \
/sys/firmware/devicetree/base/cp*/config-space*/mdio*/ethernet-phy@*; do
[ -f "$phy_node/phandle" ] || continue
phandle=$(od -An -t x4 -N 4 "$phy_node/phandle" 2>/dev/null | tr -d ' ')
if [ -n "$phandle" ]; then
echo "$phandle:$phy_node"
fi
done
}
build_phy_map()
{
phandle_map=$(build_phandle_map)
# Build a mapping of PHY of_node path -> interface name
for iface in /sys/class/net/*; do
[ -d "$iface" ] || continue
iface_name=$(basename "$iface")
# Try regular phydev approach first (for non-DSA interfaces)
if [ -L "$iface/phydev/of_node" ]; then
phy_of_node=$(readlink -f "$iface/phydev/of_node" 2>/dev/null)
if [ -n "$phy_of_node" ]; then
echo "$phy_of_node:$iface_name"
continue
fi
fi
# For DSA interfaces, resolve via of_node's phy-handle
if [ -L "$iface/of_node" ]; then
iface_of_node=$(readlink -f "$iface/of_node" 2>/dev/null)
[ -n "$iface_of_node" ] || continue
# Try to read phy-handle property (4-byte phandle)
if [ -f "$iface_of_node/phy-handle" ]; then
phy_phandle=$(od -An -t x4 -N 4 "$iface_of_node/phy-handle" 2>/dev/null | tr -d ' ')
if [ -n "$phy_phandle" ]; then
# Look up the PHY node path from our phandle map
phy_of_node=$(echo "$phandle_map" | grep "^$phy_phandle:" | cut -d: -f2)
if [ -n "$phy_of_node" ]; then
echo "$phy_of_node:$iface_name"
fi
fi
fi
fi
done
}
zone_map()
{
type="$1"
case "$type" in
ap-ic-thermal)
echo "Application processor interconnect"
;;
ap-cpu[0-9]*-thermal)
cpu=${type#ap-cpu}
cpu=${cpu%-thermal}
echo "Application processor core $cpu"
;;
cp[0-9]*-ic-thermal)
cp=${type%%-*}
cp=${cp#cp}
echo "Communication processor $cp interconnect"
;;
*)
echo "$type"
;;
esac
}
thermal_zones()
{
echo "Thermal Zones"
echo "============="
echo
for zone in /sys/class/thermal/thermal_zone*; do
[ -d "$zone" ] || continue
name=$(basename "$zone")
type=$(cat "$zone/type" 2>/dev/null || echo "unknown")
data=$(cat "$zone/temp" 2>/dev/null)
desc=$(zone_map "$type")
if [ -n "$data" ] && [ "$data" != "N/A" ]; then
# Convert millidegrees to degrees Celsius
temp_c=$(awk "BEGIN {printf \"%.1f\", $data / 1000}")
printf "%-20s %8s°C %s\n" "$name" "$temp_c" "$desc"
else
printf "%-20s %8s %s\n" "$name" "N/A" "$desc"
fi
done
echo
}
hwmon()
{
tmpfile=$(mktemp)
echo "Hardware Monitors"
echo "================="
echo
phy_map=$(build_phy_map)
for hwmon in /sys/class/hwmon/hwmon*; do
[ -d "$hwmon" ] || continue
name=$(basename "$hwmon")
data=$(cat "$hwmon/temp1_input" 2>/dev/null)
# Try to find the associated network interface
iface=
if [ -L "$hwmon/of_node" ]; then
hwmon_of_node=$(readlink -f "$hwmon/of_node" 2>/dev/null)
if [ -n "$hwmon_of_node" ]; then
iface=$(echo "$phy_map" | grep "^$hwmon_of_node:" | cut -d: -f2)
fi
fi
if [ -n "$iface" ]; then
description="Phy $iface temperature"
else
description="N/A"
fi
if [ -n "$data" ] && [ "$data" != "N/A" ]; then
# Convert millidegrees to degrees Celsius
temp_c=$(awk "BEGIN {printf \"%.1f\", $data / 1000}")
# Format: sortkey|hwmon|temp|description (sortkey for natural sort by interface)
printf "%s|%-20s %8s°C %s\n" "$iface" "$name" "$temp_c" "$description" >> "$tmpfile"
else
printf "%s|%-20s %8s %s\n" "$iface" "$name" "N/A" "$description" >> "$tmpfile"
fi
done
# Sort by interface name naturally (e2 before e10), with N/A entries at the end
# Then strip the sort key before displaying
sort -V -t'|' -k1,1 "$tmpfile" | cut -d'|' -f2-
rm -f "$tmpfile"
echo
}
[ -n "$1" ] || { echo "usage: $0 OUT-DIR"; exit 1; }
work="$1"/system
mkdir -p "${work}"
thermal_zones > "${work}"/temperature.txt
hwmon >> "${work}"/temperature.txt
@@ -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
+2 -18
View File
@@ -15,34 +15,18 @@ export PROMPT_COMMAND=prompt_command
# update the values of LINES and COLUMNS.
shopt -s checkwinsize
# don't put duplicate lines or lines starting with space in the history.
export HISTCONTROL=ignoreboth
# append to the history file, don't overwrite it
shopt -s histappend
# for setting history length see HISTSIZE and HISTFILESIZE in bash(1)
export HISTSIZE=1000
export HISTFILESIZE=2000
# case-insensitive filename completion
bind "set completion-ignore-case on"
# show all completions immediately instead of ringing bell
bind "set show-all-if-ambiguous on"
log()
{
local fn="/var/log/syslog"
[ -n "$1" ] && fn="/var/log/$1"
less +G -r "$fn"
less +G "$fn"
}
follow()
{
local fn="/var/log/syslog"
[ -n "$1" ] && fn="/var/log/$1"
less +F -r "$fn"
tail -F "$fn"
}
_logfile_completions()
@@ -1,3 +0,0 @@
service name:netopeer notify:none log env:/etc/default/confd \
[12345] <pid/confd> netopeer2-server -F -t $CONFD_TIMEOUT -v 1 \
-- NETCONF server
@@ -1,5 +1,5 @@
set G_MESSAGES_DEBUG=nocolor
service [2345] <service/dbus/running> \
env:-/etc/default/rauc \
env:-/etc/default/rauc log:prio:user.notice \
rauc service $RAUC_ARGS -- Software update service
@@ -1,4 +1,4 @@
# Use <pid/syslogd> as barrier for other system tasks and service that
# rely on modules, firmware, and device nodes to be ready.
service if:udevd nowarn env:-/etc/default/sysklogd <run/udevadm:post/success> \
[S0123456789] syslogd -8 -F $SYSLOGD_ARGS -- System log daemon
[S0123456789] syslogd -F $SYSLOGD_ARGS -- System log daemon
@@ -1 +0,0 @@
../available/netconf.conf
-66
View File
@@ -1,66 +0,0 @@
# /etc/inputrc - global inputrc for libreadline
# Be 8 bit clean.
set input-meta on
set output-meta on
# To allow the use of 8bit-characters like the german umlauts, uncomment
# the line below. However this makes the meta key not work as a meta key,
# which is annoying to those which don't need to type in 8-bit characters.
# set convert-meta off
# try to enable the application keypad when it is called. Some systems
# need this to enable the arrow keys.
# set enable-keypad on
# see /usr/share/doc/bash/inputrc.arrows for other codes of arrow keys
# do not bell on tab-completion
# set bell-style none
# set bell-style visible
# some defaults / modifications for the emacs mode
$if mode=emacs
# allow the use of the Home/End keys
"\e[1~": beginning-of-line
"\e[4~": end-of-line
# allow the use of the Delete/Insert keys
"\e[3~": delete-char
"\e[2~": quoted-insert
# mappings for "page up" and "page down" to step to the beginning/end
# of the history
# "\e[5~": beginning-of-history
# "\e[6~": end-of-history
# alternate mappings for "page up" and "page down" to search the history
"\e[5~": history-search-backward
"\e[6~": history-search-forward
# mappings for Ctrl-left-arrow and Ctrl-right-arrow for word moving
"\e[1;5C": forward-word
"\e[1;5D": backward-word
"\e[5C": forward-word
"\e[5D": backward-word
"\e\e[C": forward-word
"\e\e[D": backward-word
$if term=rxvt
"\e[7~": beginning-of-line
"\e[8~": end-of-line
"\eOc": forward-word
"\eOd": backward-word
$endif
# for non RH/Debian xterm, can't hurt for RH/Debian xterm
# "\eOH": beginning-of-line
# "\eOF": end-of-line
# for freebsd console
# "\e[H": beginning-of-line
# "\e[F": end-of-line
$endif
+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"
}
+1 -2
View File
@@ -5,11 +5,10 @@
# -K :: exit immediately when an interrupt character (usually ^C) is typed
# -R :: Almost raw control charachters, only ANSI color escape sequences and
# OSC 8 hyperlink sequences are output. Allows veritcal scrolling
# -r :: Causes "raw" control characters to be displayed, including unicode.
# -X :: No termcap initialization and deinitialization set to the terminal.
# This is what leaves the contents of the output on screen.
export LESS="-P %f (press h for help or q to quit)"
export LANG=en_US.UTF-8
less -rIKd -FX "$@"
less -RIKd -FX "$@"
@@ -1,49 +1,12 @@
#!/bin/bash
IFQUIRKSFILE=${IFQUIRKSFILE:-/etc/product/interface-quirks.json}
#!/bin/sh
if [ $# -lt 2 ]; then
echo "usage: $0 <quirk-name> <ifname>"
exit 1
fi
quirk=$1
ifname=$2
[ -f "$IFQUIRKSFILE" ] || { echo false && exit; }
match()
{
jq -e \
--arg quirk "$quirk" --arg pattern "$1" \
'.[$pattern][$quirk]' "$IFQUIRKSFILE" >/dev/null || return
echo true
exit 0
}
ethtoolmatch()
{
local pattern="${1#@ethtool:}"
grep -qFxvf \
<(ethtool -i "$ifname") \
<(echo -n "$pattern" | awk -v FS="=" -v RS=";" '{ printf("%s: %s\n", $1, $2); }') \
&& return
match "@ethtool:$pattern"
}
for pattern in $(jq -r 'keys[]' "$IFQUIRKSFILE"); do
case "$pattern" in
@ethtool:*)
ethtoolmatch "$pattern"
;;
"$ifname")
match "$ifname"
;;
esac
done
echo "false"
if [ -f "/etc/product/interface-quirks.json" ]; then
echo "$(jq -r --arg iface "$ifname" --arg quirk "$quirk" '.[$iface][$quirk] // "false"' /etc/product/interface-quirks.json)"
else
echo "false"
fi
@@ -34,12 +34,6 @@ ports=$(devlink -j port | jq -r '.port[]
| select(.flavour == "physical")
| .netdev')
for iface in $ports; do
# On systems with multiple switch trees, a port may be _both_
# a physical port, registered with devlink, _and_ a DSA
# port. In those cases, hold on to our initial "internal"
# classification.
[ $(ip -j link show dev "$iface" | jq -r '.[0].group') = internal ] && continue
ip link set "$iface" group port
done
@@ -23,22 +23,12 @@ fi
log()
{
logger -I $$ -t udhcpc -p user.notice "${interface}: $*"
}
dbg()
{
logger -I $$ -t udhcpc -p user.debug "${interface}: $*"
}
err()
{
logger -I $$ -t udhcpc -p user.err "${interface}: $*"
logger -I $$ -t udhcpc -p user.notice "$*"
}
wait_for_ipv6_default_route()
{
dbg "waiting for IPv6 default route to be installed."
log "waiting for IPv6 default route to be installed."
while [ $IF_WAIT_DELAY -gt 0 ]; do
if ip -6 route list proto dhcp dev $interface | grep -q default; then
return
@@ -47,7 +37,7 @@ wait_for_ipv6_default_route()
printf "."
: $((IF_WAIT_DELAY -= 1))
done
err "Timed out waiting for IPv6 default route!"
log "Timed out witing for IPv6 default route!"
}
# RFC3442: If the DHCP server returns both a Classless
@@ -60,7 +50,7 @@ set_dhcp_routes()
# format: dest1/mask gw1 ... destn/mask gwn
set -- $staticroutes
while [ -n "$1" -a -n "$2" ]; do
dbg "adding route $1 via $2 metric $metric tag 100"
log "adding route $1 via $2 metric $metric tag 100"
echo "ip route $1 $2 $metric tag 100" >> "$NEXT"
shift 2
done
@@ -79,7 +69,7 @@ set_dhcp_routes()
clr_dhcp_routes()
{
log "deleting DHCP routes"
log "deleting DHCP routes from $interface"
[ -f "$NAME" ] || return
rm "$NAME"
@@ -94,7 +84,7 @@ clr_dhcp_addresses()
for addr in $addrs; do
ip="$(echo "$addr" | jq -r '."local"')"
prefix="$(echo "$addr" | jq -r '."prefixlen"')"
log "removing $ip/$prefix"
log "removing $ip/$prefix from $interface"
ip addr del "$ip/$prefix" dev "$interface"
done
}
@@ -152,12 +142,12 @@ case "$ACTION" in
fi
if [ -n "$search_list" ]; then
dbg "adding search $search_list"
log "adding search $search_list"
echo "search $search_list # $interface" >> $RESOLV_CONF
fi
for i in $dns ; do
dbg "adding dns $i"
log "adding dns $i"
echo "nameserver $i # $interface" >> $RESOLV_CONF
resolvconf -u
done
@@ -165,7 +155,7 @@ case "$ACTION" in
if [ -n "$ntpsrv" ]; then
truncate -s 0 "$NTPFILE"
for srv in $ntpsrv; do
dbg "got NTP server $srv"
log "got NTP server $srv"
echo "server $srv iburst" >> "$NTPFILE"
done
chronyc reload sources >/dev/null
+1 -1
View File
@@ -33,7 +33,7 @@ for tgt in "${boot_targets}"; do
setexpr ixmenu_n ${ixmenu_n} + 1
if load ${devtype} ${devnum}:${auxpart} ${loadaddr} /uboot.env; then
env import -c ${loadaddr} ${filesize} BOOT_ORDER DEBUG ethact
env import -b ${loadaddr} ${filesize} BOOT_ORDER DEBUG ethact
fi
if test -n "${DEBUG}"; then
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

+9 -1
View File
@@ -461,8 +461,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
@@ -6,12 +6,6 @@ mountprefix=/var/lib/rauc/mnt
bundle-formats=-plain
max-bundle-download-size=1073741824
[log.event-log]
filename=/var/log/upgrade-json.log
format=json-pretty
max-size=1M
max-files=5
[keyring]
directory=/etc/rauc/keys
+7 -1
View File
@@ -266,7 +266,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
@@ -7,12 +7,6 @@ mountprefix=/var/lib/rauc/mnt
bundle-formats=-plain
max-bundle-download-size=1073741824
[log.event-log]
filename=/var/log/upgrade-json.log
format=json-pretty
max-size=1M
max-files=5
[keyring]
directory=/etc/rauc/keys
+16 -14
View File
@@ -8,7 +8,6 @@ BR2_CCACHE=y
BR2_CCACHE_DIR="${BR2_EXTERNAL_INFIX_PATH}/.ccache"
BR2_ENABLE_DEBUG=y
BR2_GLOBAL_PATCH_DIR="${BR2_EXTERNAL_INFIX_PATH}/patches"
BR2_DOWNLOAD_FORCE_CHECK_HASHES=y
BR2_TARGET_GENERIC_HOSTNAME="infix"
BR2_TARGET_GENERIC_ISSUE="Infix by KernelKit"
BR2_INIT_FINIT=y
@@ -28,16 +27,16 @@ BR2_ROOTFS_POST_BUILD_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-build
BR2_ROOTFS_POST_IMAGE_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-image.sh"
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.49"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.63"
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
BR2_PACKAGE_BUSYBOX_CONFIG="${BR2_EXTERNAL_INFIX_PATH}/board/common/busybox_defconfig"
BR2_PACKAGE_STRACE=y
BR2_PACKAGE_STRESS_NG=y
BR2_PACKAGE_SYSREPO_GROUP="sys-cli"
BR2_PACKAGE_JQ=y
BR2_PACKAGE_E2FSPROGS=y
BR2_PACKAGE_E2FSPROGS_RESIZE2FS=y
BR2_PACKAGE_DBUS_CXX=y
BR2_PACKAGE_DBUS_GLIB=y
BR2_PACKAGE_DBUS_TRIGGERD=y
@@ -62,7 +61,6 @@ BR2_PACKAGE_LIBINPUT=y
BR2_PACKAGE_LIBCURL_CURL=y
BR2_PACKAGE_NETOPEER2_CLI=y
BR2_PACKAGE_NSS_MDNS=y
BR2_PACKAGE_SYSREPO_GROUP="sys-cli"
BR2_PACKAGE_LINUX_PAM=y
BR2_PACKAGE_LIBPAM_RADIUS_AUTH=y
BR2_PACKAGE_ONIGURUMA=y
@@ -75,13 +73,11 @@ BR2_PACKAGE_ETHTOOL=y
BR2_PACKAGE_FPING=y
BR2_PACKAGE_FRR=y
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
BR2_PACKAGE_IPERF3=y
BR2_PACKAGE_IPROUTE2=y
BR2_PACKAGE_IPTABLES_NFTABLES=y
BR2_PACKAGE_IPUTILS=y
BR2_PACKAGE_LLDPD=y
BR2_PACKAGE_MSTPD=y
BR2_PACKAGE_MTR=y
BR2_PACKAGE_NETCALC=y
BR2_PACKAGE_NETCAT_OPENBSD=y
BR2_PACKAGE_NETSNMP=y
@@ -101,10 +97,8 @@ BR2_PACKAGE_TRACEROUTE=y
BR2_PACKAGE_ULOGD=y
BR2_PACKAGE_WHOIS=y
BR2_PACKAGE_BASH_COMPLETION=y
BR2_PACKAGE_NEOFETCH=y
BR2_PACKAGE_SUDO=y
BR2_PACKAGE_TTYD=y
BR2_PACKAGE_GETENT=y
BR2_PACKAGE_HTOP=y
BR2_PACKAGE_IRQBALANCE=y
BR2_PACKAGE_KMOD_TOOLS=y
@@ -117,6 +111,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
@@ -131,24 +131,23 @@ BR2_PACKAGE_HOST_RAUC=y
BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
INFIX_VENDOR_HOME="https://kernelkit.org"
INFIX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers."
INFIX_VENDOR_HOME="https://github.com/kernelkit"
INFIX_DESC="Infix is an operating system based on Linux and modeled with YANG. It can be set up both as a switch, with offloading using switchdev, a router with firewalling, or a secure end device. All while supporting advanced networking scenarios and running Docker containers."
INFIX_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://kernelkit.org/infix/"
INFIX_DOC="https://github.com/kernelkit/infix/tree/main/doc"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_ALDER_ALDER=y
BR2_PACKAGE_MARVELL_CN9130_CRB=y
BR2_PACKAGE_MARVELL_ESPRESSOBIN=y
BR2_PACKAGE_STYX_DCP_SC_28P=y
BR2_PACKAGE_RASPBERRY_PI_4=y
BR2_PACKAGE_BANANA_PI_R3=y
BR2_PACKAGE_FEATURE_WIFI_DONGLE_REALTEK=y
BR2_PACKAGE_CONFD=y
BR2_PACKAGE_CONFD_TEST_MODE=y
BR2_PACKAGE_CURIOS_HTTPD=y
BR2_PACKAGE_CURIOS_NFTABLES=y
BR2_PACKAGE_GENCERT=y
BR2_PACKAGE_STATD=y
BR2_PACKAGE_SHOW=y
BR2_PACKAGE_FACTORY=y
BR2_PACKAGE_FINIT_PLUGIN_HOTPLUG=y
BR2_PACKAGE_FINIT_PLUGIN_HOOK_SCRIPTS=y
@@ -170,11 +169,14 @@ BR2_PACKAGE_PODMAN=y
BR2_PACKAGE_PODMAN_DRIVER_BTRFS=y
BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
BR2_PACKAGE_SHOW=y
BR2_PACKAGE_TETRIS=y
BR2_PACKAGE_ROUSETTE=y
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
BR2_PACKAGE_FEATURE_WIFI=y
BR2_PACKAGE_FEATURE_WIFI_DONGLE_REALTEK=y
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
BR2_DOWNLOAD_FORCE_CHECK_HASHES=y
BR2_PACKAGE_GETENT=y
TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y
DISK_IMAGE_BOOT_BIN=y
+10 -4
View File
@@ -27,7 +27,7 @@ BR2_ROOTFS_POST_BUILD_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-build
BR2_ROOTFS_POST_IMAGE_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-image.sh"
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.49"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.63"
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
@@ -94,6 +94,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
@@ -107,10 +113,10 @@ BR2_PACKAGE_HOST_RAUC=y
BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
INFIX_VENDOR_HOME="https://kernelkit.org"
INFIX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers."
INFIX_VENDOR_HOME="https://github.com/kernelkit"
INFIX_DESC="Infix is an operating system based on Linux and modeled with YANG. It can be set up both as a switch, with offloading using switchdev, a router with firewalling, or a secure end device. All while supporting advanced networking scenarios and running Docker containers."
INFIX_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://kernelkit.org/infix/"
INFIX_DOC="https://github.com/kernelkit/infix/tree/main/doc"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_ALDER_ALDER=y
BR2_PACKAGE_MARVELL_CN9130_CRB=y
-43
View File
@@ -1,43 +0,0 @@
BR2_aarch64=y
BR2_TOOLCHAIN_EXTERNAL=y
BR2_TOOLCHAIN_EXTERNAL_BOOTLIN=y
BR2_DL_DIR="$(BR2_EXTERNAL_INFIX_PATH)/dl"
BR2_CCACHE=y
BR2_CCACHE_DIR="$(BR2_EXTERNAL_INFIX_PATH)/.ccache"
BR2_ENABLE_DEBUG=y
BR2_PACKAGE_OVERRIDE_FILE="$(BR2_EXTERNAL_INFIX_PATH)/local.mk"
BR2_GLOBAL_PATCH_DIR="$(BR2_EXTERNAL_INFIX_PATH)/patches"
BR2_SSP_NONE=y
BR2_INIT_NONE=y
BR2_SYSTEM_BIN_SH_NONE=y
# BR2_PACKAGE_BUSYBOX is not set
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
# BR2_TARGET_ROOTFS_TAR is not set
BR2_TARGET_ARM_TRUSTED_FIRMWARE=y
BR2_TARGET_ARM_TRUSTED_FIRMWARE_CUSTOM_GIT=y
BR2_TARGET_ARM_TRUSTED_FIRMWARE_CUSTOM_REPO_URL="https://github.com/mtk-openwrt/arm-trusted-firmware-mtk.git"
BR2_TARGET_ARM_TRUSTED_FIRMWARE_CUSTOM_REPO_VERSION="78a0dfd927bb00ce973a1f8eb4079df0f755887a"
BR2_TARGET_ARM_TRUSTED_FIRMWARE_PLATFORM="mt7986"
BR2_TARGET_ARM_TRUSTED_FIRMWARE_FIP=y
BR2_TARGET_ARM_TRUSTED_FIRMWARE_UBOOT_AS_BL33=y
BR2_TARGET_ARM_TRUSTED_FIRMWARE_ADDITIONAL_VARIABLES="BOOT_DEVICE=sdmmc DRAM_USE_DDR4=1 USE_MKIMAGE=1 MKIMAGE=$(HOST_DIR)/bin/mkimage"
BR2_TARGET_ARM_TRUSTED_FIRMWARE_IMAGES="*.img *.bin"
BR2_TARGET_UBOOT=y
BR2_TARGET_UBOOT_BUILD_SYSTEM_KCONFIG=y
BR2_TARGET_UBOOT_CUSTOM_VERSION=y
BR2_TARGET_UBOOT_CUSTOM_VERSION_VALUE="2025.01"
BR2_TARGET_UBOOT_BOARD_DEFCONFIG="mt7986a_bpir3_sd"
BR2_TARGET_UBOOT_CONFIG_FRAGMENT_FILES="${BR2_EXTERNAL_INFIX_PATH}/board/common/uboot/extras.config ${BR2_EXTERNAL_INFIX_PATH}/src/board/banana-pi-r3/uboot/extras.config ${BR2_EXTERNAL_INFIX_PATH}/src/board/banana-pi-r3/uboot/sd-extras.config"
BR2_TARGET_UBOOT_NEEDS_DTC=y
BR2_TARGET_UBOOT_FORMAT_DTB=y
BR2_TARGET_UBOOT_CUSTOM_DTS_PATH="${BR2_EXTERNAL_INFIX_PATH}/src/board/banana-pi-r3/uboot/*.dtsi"
BR2_PACKAGE_HOST_GENIMAGE=y
BR2_PACKAGE_HOST_RAUC=y
BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y
DISK_IMAGE_BOOT_BIN=y
DISK_IMAGE_BOOT_DATA="${BINARIES_DIR}/flash-image.bin"
DISK_IMAGE_BOOT_OFFSET=0x00200000
+4 -9
View File
@@ -29,7 +29,7 @@ BR2_ROOTFS_POST_IMAGE_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-image
BR2_ROOTFS_POST_SCRIPT_ARGS="-c $(BR2_EXTERNAL_INFIX_PATH)/board/aarch64/r2s/genimage.cfg"
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.49"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.44"
BR2_LINUX_KERNEL_PATCH="$(BR2_EXTERNAL_INFIX_PATH)/board/aarch64/r2s/rk3328-nanopi-r2s-dts.patch"
BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/aarch64/r2s/linux_defconfig"
@@ -44,7 +44,6 @@ BR2_PACKAGE_STRESS_NG=y
BR2_PACKAGE_SYSREPO_GROUP="sys-cli"
BR2_PACKAGE_JQ=y
BR2_PACKAGE_E2FSPROGS=y
BR2_PACKAGE_E2FSPROGS_RESIZE2FS=y
BR2_PACKAGE_LINUX_FIRMWARE=y
BR2_PACKAGE_LINUX_FIRMWARE_RALINK_RT61=y
BR2_PACKAGE_LINUX_FIRMWARE_RALINK_RT73=y
@@ -66,7 +65,6 @@ BR2_PACKAGE_GPTFDISK_SGDISK=y
BR2_PACKAGE_INPUT_EVENT_DAEMON=y
BR2_PACKAGE_MDIO_TOOLS=y
BR2_PACKAGE_RNG_TOOLS=y
BR2_PACKAGE_UBOOT_TOOLS=y
BR2_PACKAGE_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_UBOOT_TOOLS_FIT_CHECK_SIGN=y
@@ -93,12 +91,10 @@ BR2_PACKAGE_ETHTOOL=y
BR2_PACKAGE_FPING=y
BR2_PACKAGE_FRR=y
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
BR2_PACKAGE_IPERF3=y
BR2_PACKAGE_IPROUTE2=y
BR2_PACKAGE_IPTABLES_NFTABLES=y
BR2_PACKAGE_IPUTILS=y
BR2_PACKAGE_LLDPD=y
BR2_PACKAGE_MTR=y
BR2_PACKAGE_NETCALC=y
BR2_PACKAGE_NETCAT_OPENBSD=y
BR2_PACKAGE_NETSNMP=y
@@ -121,7 +117,6 @@ BR2_PACKAGE_WIRELESS_REGDB=y
BR2_PACKAGE_WIRELESS_TOOLS=y
BR2_PACKAGE_WPA_SUPPLICANT=y
BR2_PACKAGE_BASH_COMPLETION=y
BR2_PACKAGE_NEOFETCH=y
BR2_PACKAGE_SUDO=y
BR2_PACKAGE_TTYD=y
BR2_PACKAGE_HTOP=y
@@ -173,11 +168,11 @@ BR2_PACKAGE_HOST_RAUC=y
BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
INFIX_VENDOR_HOME="https://kernelkit.org"
INFIX_VENDOR_HOME="https://github.com/kernelkit"
INFIX_IMAGE_ID="${INFIX_ID}-r2s"
INFIX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers."
INFIX_DESC="Infix is an operating system based on Linux and modeled with YANG. It can be set up both as a switch, with offloading using switchdev, a router with firewalling, or a secure end device. All while supporting advanced networking scenarios and running Docker containers."
INFIX_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://kernelkit.org/infix/"
INFIX_DOC="https://github.com/kernelkit/infix/tree/main/doc"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_CONFD=y
BR2_PACKAGE_GENCERT=y
+17 -15
View File
@@ -11,8 +11,8 @@ BR2_TARGET_GENERIC_ISSUE="Infix by KernelKit"
BR2_INIT_FINIT=y
BR2_ROOTFS_DEVICE_CREATION_DYNAMIC_EUDEV=y
BR2_ROOTFS_DEVICE_TABLE="system/device_table.txt ${BR2_EXTERNAL_INFIX_PATH}/board/common/xattrs"
BR2_ROOTFS_MERGED_USR=y
# BR2_TARGET_ENABLE_ROOT_LOGIN is not set
BR2_ROOTFS_MERGED_USR=y
BR2_SYSTEM_BIN_SH_BASH=y
BR2_TARGET_GENERIC_GETTY_PORT="@console"
BR2_TARGET_GENERIC_GETTY_TERM="xterm"
@@ -39,9 +39,9 @@ BR2_LINUX_KERNEL_NEEDS_HOST_OPENSSL=y
BR2_PACKAGE_BUSYBOX_CONFIG="$(BR2_EXTERNAL_INFIX_PATH)/board/common/busybox_defconfig"
BR2_PACKAGE_STRACE=y
BR2_PACKAGE_STRESS_NG=y
BR2_PACKAGE_SYSREPO_GROUP="sys-cli"
BR2_PACKAGE_JQ=y
BR2_PACKAGE_E2FSPROGS=y
BR2_PACKAGE_E2FSPROGS_RESIZE2FS=y
BR2_PACKAGE_LINUX_FIRMWARE=y
BR2_PACKAGE_LINUX_FIRMWARE_RALINK_RT61=y
BR2_PACKAGE_LINUX_FIRMWARE_RALINK_RT73=y
@@ -60,22 +60,19 @@ BR2_PACKAGE_GPTFDISK=y
BR2_PACKAGE_GPTFDISK_SGDISK=y
BR2_PACKAGE_MDIO_TOOLS=y
BR2_PACKAGE_RNG_TOOLS=y
BR2_PACKAGE_UBOOT_TOOLS=y
BR2_PACKAGE_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_UBOOT_TOOLS_FIT_CHECK_SIGN=y
BR2_PACKAGE_UBOOT_TOOLS_MKENVIMAGE=y
BR2_PACKAGE_PYTHON3=y
BR2_PACKAGE_PYTHON_GUNICORN=y
BR2_PACKAGE_LIBSSH_OPENSSL=y
BR2_PACKAGE_LIBSSH2=y
BR2_PACKAGE_LIBSSH2_OPENSSL=y
BR2_PACKAGE_LIBXCRYPT=y
BR2_PACKAGE_LIBOPENSSL_BIN=y
BR2_PACKAGE_LIBINPUT=y
BR2_PACKAGE_LIBCURL_CURL=y
BR2_PACKAGE_NETOPEER2_CLI=y
BR2_PACKAGE_NSS_MDNS=y
BR2_PACKAGE_SYSREPO_GROUP="sys-cli"
BR2_PACKAGE_LINUX_PAM=y
BR2_PACKAGE_LIBPAM_RADIUS_AUTH=y
BR2_PACKAGE_ONIGURUMA=y
@@ -88,13 +85,11 @@ BR2_PACKAGE_ETHTOOL=y
BR2_PACKAGE_FPING=y
BR2_PACKAGE_FRR=y
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
BR2_PACKAGE_IPERF3=y
BR2_PACKAGE_IPROUTE2=y
BR2_PACKAGE_IPTABLES_NFTABLES=y
BR2_PACKAGE_IPUTILS=y
BR2_PACKAGE_LLDPD=y
BR2_PACKAGE_MSTPD=y
BR2_PACKAGE_MTR=y
BR2_PACKAGE_NETCALC=y
BR2_PACKAGE_NETCAT_OPENBSD=y
BR2_PACKAGE_NETSNMP=y
@@ -114,10 +109,8 @@ BR2_PACKAGE_TRACEROUTE=y
BR2_PACKAGE_ULOGD=y
BR2_PACKAGE_WHOIS=y
BR2_PACKAGE_BASH_COMPLETION=y
BR2_PACKAGE_NEOFETCH=y
BR2_PACKAGE_SUDO=y
BR2_PACKAGE_TTYD=y
BR2_PACKAGE_GETENT=y
BR2_PACKAGE_HTOP=y
BR2_PACKAGE_IRQBALANCE=y
BR2_PACKAGE_KMOD_TOOLS=y
@@ -130,6 +123,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
@@ -168,14 +167,16 @@ BR2_PACKAGE_HOST_RAUC=y
BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
INFIX_VENDOR_HOME="https://kernelkit.org"
INFIX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers."
INFIX_VENDOR_HOME="https://github.com/kernelkit"
INFIX_DESC="Infix is an operating system based on Linux and modeled with YANG. It can be set up both as a switch, with offloading using switchdev, a router with firewalling, or a secure end device. All while supporting advanced networking scenarios and running Docker containers."
INFIX_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://kernelkit.org/infix/"
INFIX_DOC="https://github.com/kernelkit/infix/tree/main/doc"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_CONFD=y
# BR2_PACKAGE_CONFD_TEST_MODE is not set
BR2_PACKAGE_GENCERT=y
BR2_PACKAGE_STATD=y
BR2_PACKAGE_SHOW=y
BR2_PACKAGE_FACTORY=y
BR2_PACKAGE_FINIT_PLUGIN_HOTPLUG=y
BR2_PACKAGE_FINIT_PLUGIN_HOOK_SCRIPTS=y
@@ -197,11 +198,12 @@ BR2_PACKAGE_PODMAN=y
BR2_PACKAGE_PODMAN_DRIVER_BTRFS=y
BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
BR2_PACKAGE_SHOW=y
BR2_PACKAGE_TETRIS=y
BR2_PACKAGE_ROUSETTE=y
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
BR2_DOWNLOAD_FORCE_CHECK_HASHES=y
BR2_PACKAGE_GETENT=y
TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y
# GNS3_APPLIANCE is not set
-3
View File
@@ -14,8 +14,6 @@ BR2_SYSTEM_BIN_SH_NONE=y
BR2_ROOTFS_POST_IMAGE_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-image.sh"
# BR2_PACKAGE_BUSYBOX is not set
BR2_PACKAGE_RPI_FIRMWARE=y
BR2_PACKAGE_RPI_FIRMWARE_VARIANT_PI4=y
BR2_PACKAGE_RPI_FIRMWARE_VARIANT_PI4_X=y
BR2_PACKAGE_RPI_FIRMWARE_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/src/board/raspberry-pi-4/config.txt"
BR2_PACKAGE_RPI_FIRMWARE_CMDLINE_FILE="${BR2_EXTERNAL_INFIX_PATH}/src/board/raspberry-pi-4/cmdline.txt"
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
@@ -36,5 +34,4 @@ BR2_PACKAGE_HOST_RAUC=y
BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_BOOTLOADER_SPLASHSCREEN=y
# GNS3_APPLIANCE is not set
+16 -14
View File
@@ -7,7 +7,6 @@ BR2_CCACHE=y
BR2_CCACHE_DIR="${BR2_EXTERNAL_INFIX_PATH}/.ccache"
BR2_ENABLE_DEBUG=y
BR2_GLOBAL_PATCH_DIR="${BR2_EXTERNAL_INFIX_PATH}/patches"
BR2_DOWNLOAD_FORCE_CHECK_HASHES=y
BR2_TARGET_GENERIC_HOSTNAME="infix"
BR2_TARGET_GENERIC_ISSUE="Infix by KernelKit"
BR2_INIT_FINIT=y
@@ -27,7 +26,7 @@ BR2_ROOTFS_POST_BUILD_SCRIPT="board/qemu/x86_64/post-build.sh ${BR2_EXTERNAL_INF
BR2_ROOTFS_POST_IMAGE_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-image.sh"
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.49"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.63"
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
@@ -35,9 +34,9 @@ BR2_LINUX_KERNEL_NEEDS_HOST_LIBELF=y
BR2_PACKAGE_BUSYBOX_CONFIG="${BR2_EXTERNAL_INFIX_PATH}/board/common/busybox_defconfig"
BR2_PACKAGE_STRACE=y
BR2_PACKAGE_STRESS_NG=y
BR2_PACKAGE_SYSREPO_GROUP="sys-cli"
BR2_PACKAGE_JQ=y
BR2_PACKAGE_E2FSPROGS=y
BR2_PACKAGE_E2FSPROGS_RESIZE2FS=y
BR2_PACKAGE_DBUS_CXX=y
BR2_PACKAGE_DBUS_GLIB=y
BR2_PACKAGE_DBUS_TRIGGERD=y
@@ -60,7 +59,6 @@ BR2_PACKAGE_LIBOPENSSL_BIN=y
BR2_PACKAGE_LIBCURL_CURL=y
BR2_PACKAGE_NETOPEER2_CLI=y
BR2_PACKAGE_NSS_MDNS=y
BR2_PACKAGE_SYSREPO_GROUP="sys-cli"
BR2_PACKAGE_LINUX_PAM=y
BR2_PACKAGE_LIBPAM_RADIUS_AUTH=y
BR2_PACKAGE_ONIGURUMA=y
@@ -73,13 +71,11 @@ BR2_PACKAGE_ETHTOOL=y
BR2_PACKAGE_FPING=y
BR2_PACKAGE_FRR=y
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
BR2_PACKAGE_IPERF3=y
BR2_PACKAGE_IPROUTE2=y
BR2_PACKAGE_IPTABLES_NFTABLES=y
BR2_PACKAGE_IPUTILS=y
BR2_PACKAGE_LLDPD=y
BR2_PACKAGE_MSTPD=y
BR2_PACKAGE_MTR=y
BR2_PACKAGE_NETCALC=y
BR2_PACKAGE_NETCAT_OPENBSD=y
BR2_PACKAGE_NETSNMP=y
@@ -99,10 +95,8 @@ BR2_PACKAGE_TRACEROUTE=y
BR2_PACKAGE_ULOGD=y
BR2_PACKAGE_WHOIS=y
BR2_PACKAGE_BASH_COMPLETION=y
BR2_PACKAGE_NEOFETCH=y
BR2_PACKAGE_SUDO=y
BR2_PACKAGE_TTYD=y
BR2_PACKAGE_GETENT=y
BR2_PACKAGE_HTOP=y
BR2_PACKAGE_IRQBALANCE=y
BR2_PACKAGE_KMOD_TOOLS=y
@@ -115,6 +109,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
@@ -137,19 +137,18 @@ BR2_PACKAGE_HOST_RAUC=y
BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
INFIX_VENDOR_HOME="https://kernelkit.org"
INFIX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers."
INFIX_VENDOR_HOME="https://github.com/kernelkit"
INFIX_DESC="Infix is an operating system based on Linux and modeled with YANG. It can be set up both as a switch, with offloading using switchdev, a router with firewalling, or a secure end device. All while supporting advanced networking scenarios and running Docker containers."
INFIX_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://kernelkit.org/infix/"
INFIX_DOC="https://github.com/kernelkit/infix/tree/main/doc"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_FEATURE_WIFI=y
BR2_PACKAGE_FEATURE_WIFI_DONGLE_REALTEK=y
BR2_PACKAGE_CONFD=y
BR2_PACKAGE_CONFD_TEST_MODE=y
BR2_PACKAGE_CURIOS_HTTPD=y
BR2_PACKAGE_CURIOS_NFTABLES=y
BR2_PACKAGE_GENCERT=y
BR2_PACKAGE_STATD=y
BR2_PACKAGE_SHOW=y
BR2_PACKAGE_FACTORY=y
BR2_PACKAGE_FINIT_PLUGIN_HOTPLUG=y
BR2_PACKAGE_FINIT_PLUGIN_HOOK_SCRIPTS=y
@@ -171,11 +170,14 @@ BR2_PACKAGE_PODMAN=y
BR2_PACKAGE_PODMAN_DRIVER_BTRFS=y
BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
BR2_PACKAGE_SHOW=y
BR2_PACKAGE_TETRIS=y
BR2_PACKAGE_ROUSETTE=y
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
BR2_PACKAGE_FEATURE_WIFI=y
BR2_PACKAGE_FEATURE_WIFI_DONGLE_REALTEK=y
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
BR2_DOWNLOAD_FORCE_CHECK_HASHES=y
BR2_PACKAGE_GETENT=y
TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y
GNS3_APPLIANCE_RAM=512
+10 -4
View File
@@ -27,7 +27,7 @@ BR2_ROOTFS_POST_BUILD_SCRIPT="board/qemu/x86_64/post-build.sh ${BR2_EXTERNAL_INF
BR2_ROOTFS_POST_IMAGE_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-image.sh"
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.49"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.63"
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
@@ -114,10 +120,10 @@ BR2_PACKAGE_HOST_RAUC=y
BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
INFIX_VENDOR_HOME="https://kernelkit.org"
INFIX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers."
INFIX_VENDOR_HOME="https://github.com/kernelkit"
INFIX_DESC="Infix is an operating system based on Linux and modeled with YANG. It can be set up both as a switch, with offloading using switchdev, a router with firewalling, or a secure end device. All while supporting advanced networking scenarios and running Docker containers."
INFIX_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://kernelkit.org/infix/"
INFIX_DOC="https://github.com/kernelkit/infix/tree/main/doc"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_CONFD=y
BR2_PACKAGE_CONFD_TEST_MODE=y
+32 -24
View File
@@ -3,36 +3,44 @@ Change Log
All notable changes to the project are documented in this file.
[v25.09.0][] - 2025-09-30
[v25.08.2][] - 2025-12-19
-------------------------
### Changes
- Upgrade Buildroot to 2025.02.6 (LTS)
- Upgrade Linux kernel to 6.12.49 (LTS)
- Upgrade libyang to 3.13.5
- Upgrade sysrepo to 3.7.11
- Upgrade netopeer2 (NETCONF) to 2.4.5
- Upgrade rousette (RESTCONF) to v2
- Add support for [Banana Pi R3][BPI-R3], a 7 port switch with 2 WiFi chip
- Add neofetch system information tool for system introspection, issue #1143
- Add mtr and iperf3 network diagnostic tools, issue #1144
- Improve default bash settings with better history handling and tab completion
- cli: new `terminal reset` and `terminal resize` convenience commands
- Upgrade Linux kernel to 6.12.63 (LTS)
- Enable workaround for issue #670 by disabling iitod on styx platform. This
prohibits software control of LEDs, leaving the default HW control, which
has proven more stable on this platform
- Add support for configurable OSPF debug logging, issue #1281. Debug options
can now be enabled per category (bfd, packet, ism, nsm, default-information,
nssa). All debug options are disabled by default to prevent log flooding in
production environments. See the documentation for usage examples
- Add support data collection script, useful when troubleshooting issues on
deployed systems. Gathers system information, logs, and more. Issue #1287
- Enable kernel panic on lockups + hung tasks => console log + reboot. Also,
enable watchdogd resource monitors, logs: memory/file system + descriptor
usage. Issue #1318
- Enable CN9130 HW watchdog, and kernel `test_lockup` module, issue #1320
### Fixes
- Fix #1080: Error message in log from rauc, deprecated 'Install' D-Bus method
- Fix #1100: Reduce DHCP client logging verbosity by 70% and include interface
names in log messages for easier troubleshooting
- Fix #1119: CLI UX regression, restore proper behavior for `no enabled` command
- Fix #1155: `show ospf` commands regression
- Fix #1150: `show-legacy` wrapper permissions
- Fix #1161: error in log during boot about unsupported command
- Fix #1169: Expected neighbors not shown in sysrepocfg
- Fixes for unicode translation in log and pager outputs as well as `syslogd`
- Fix #981: copying any file, including `running-config`, to the persistent
back-end store for `startup-config`, does not take
- Fix #1203: copying any file, including `startup-config`, to `running-config`
does not take
[BPI-R3]: https://wiki.banana-pi.org/Banana_Pi_BPI-R3
[v25.08.1][] - 2025-10-03
-------------------------
### Changes
- N/A
### Fixes
- Fix #1150: `show-legacy` wrapper permissions
- Fix #1155: `show ospf` commands regression
- Fix #1169: Expected OSPF neighbors not shown in `sysrepocfg` when the
system has at least one non-OSPF interface
[v25.08.0][] - 2025-09-01
-------------------------
@@ -1643,8 +1651,8 @@ Supported YANG models in addition to those used by sysrepo and netopeer:
- N/A
[buildroot]: https://buildroot.org/
[UNRELEASED]: https://github.com/kernelkit/infix/compare/v25.09.0...HEAD
[v25.09.0]: https://github.com/kernelkit/infix/compare/v25.08.0...v26.09.0
[UNRELEASED]: https://github.com/kernelkit/infix/compare/v25.08.0...HEAD
[v25.08.1]: https://github.com/kernelkit/infix/compare/v25.08.0...v26.08.1
[v25.08.0]: https://github.com/kernelkit/infix/compare/v25.06.1...v26.08.0
[v25.06.0]: https://github.com/kernelkit/infix/compare/v25.05.1...v26.06.0
[v25.05.1]: https://github.com/kernelkit/infix/compare/v25.05.0...v25.05.1
-312
View File
@@ -1,312 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="489.27203mm"
height="159.49489mm"
viewBox="0 0 489.27202 159.49489"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="logo-large.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.57493546"
inkscape:cx="104.35954"
inkscape:cy="264.3775"
inkscape:window-width="2560"
inkscape:window-height="1385"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer6" />
<defs
id="defs2" />
<g
inkscape:groupmode="layer"
id="layer6"
inkscape:label="bg"
style="display:none;fill:#ff0000;fill-opacity:1"
transform="matrix(1.2831201,0,0,3.2117929,118.15852,-263.46763)">
<rect
style="fill:#22272e;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1"
id="rect10264"
width="163.66356"
height="92.471725"
x="20.800945"
y="69.866936"
inkscape:label="bg" />
</g>
<g
inkscape:groupmode="layer"
id="layer5"
inkscape:label="tagline"
style="display:inline"
transform="translate(144.84863,-39.0695)">
<g
aria-label="Immutable . Friendly . Secure"
transform="matrix(5.7077268,0,0,6.3072396,-498.93695,-594.0847)"
id="text9503"
style="font-size:6.44339px;font-family:Laksaman;-inkscape-font-specification:'Laksaman, Normal';stroke:#5c5f5c;stroke-width:0;stroke-miterlimit:0"
inkscape:label="linux-netconf"
inkscape:export-filename="../../../Pictures/logo-medium.svg"
inkscape:export-xdpi="191.95932"
inkscape:export-ydpi="191.95932">
<path
d="m 62.472956,124.15871 h -0.386604 v -4.61347 h 0.386604 z"
id="path2380" />
<path
d="m 67.144418,120.79526 q 0.573462,0 0.869857,0.4317 0.296396,0.43171 0.296396,1.10182 v 1.82993 h -0.386603 v -1.82348 q 0,-0.54125 -0.219075,-0.8763 -0.212632,-0.3415 -0.663669,-0.3415 -0.367274,0 -0.657226,0.28995 -0.289953,0.28995 -0.289953,0.65722 v 2.09411 h -0.386603 v -1.99745 q 0,-1.04383 -0.831197,-1.04383 -0.38016,0 -0.689443,0.32861 -0.309283,0.32861 -0.309283,0.71522 v 1.99745 H 63.491016 V 121.678 q 0,-0.28995 -0.06443,-0.81187 h 0.354386 q 0.06443,0.19975 0.07732,0.58635 0.373717,-0.65722 1.108263,-0.65722 0.670113,0 0.998725,0.73454 0.386604,-0.73454 1.179141,-0.73454 z"
id="path2382" />
<path
d="m 72.969238,120.79526 q 0.573462,0 0.869858,0.4317 0.296396,0.43171 0.296396,1.10182 v 1.82993 h -0.386604 v -1.82348 q 0,-0.54125 -0.219075,-0.8763 -0.212632,-0.3415 -0.663669,-0.3415 -0.367273,0 -0.657226,0.28995 -0.289953,0.28995 -0.289953,0.65722 v 2.09411 h -0.386603 v -1.99745 q 0,-1.04383 -0.831197,-1.04383 -0.38016,0 -0.689443,0.32861 -0.309283,0.32861 -0.309283,0.71522 v 1.99745 H 69.315836 V 121.678 q 0,-0.28995 -0.06443,-0.81187 h 0.354386 q 0.06443,0.19975 0.07732,0.58635 0.373717,-0.65722 1.108263,-0.65722 0.670113,0 0.998726,0.73454 0.386603,-0.73454 1.17914,-0.73454 z"
id="path2384" />
<path
d="m 78.078841,124.14582 -0.36083,0.0838 q -0.115981,-0.32861 -0.154641,-0.64434 -0.431707,0.66367 -1.237131,0.66367 -1.185584,0 -1.185584,-1.53352 v -1.84926 h 0.386604 v 1.77838 q 0,1.28223 0.882744,1.28223 0.457481,0 0.786094,-0.30928 0.328613,-0.31572 0.328613,-0.76032 v -1.99101 h 0.386603 v 2.48715 q 0,0.34795 0.167528,0.79254 z"
id="path2386" />
<path
d="m 79.973201,124.24891 q -0.39949,0 -0.625009,-0.29639 -0.219075,-0.30284 -0.219075,-0.76032 v -2.03611 h -0.657226 v -0.28996 h 0.657226 v -0.97939 l 0.386603,-0.17397 v 1.15336 h 1.050273 v 0.28996 H 79.51572 v 2.03611 q 0,0.73454 0.489698,0.73454 0.302839,0 0.444594,-0.058 l 0.03866,0.28996 q -0.199745,0.0902 -0.515471,0.0902 z"
id="path2388" />
<path
d="m 82.015758,124.24891 q -0.43815,0 -0.747433,-0.25129 -0.302839,-0.25773 -0.302839,-0.66367 0,-1.23068 2.190752,-1.23068 0,-0.47037 -0.225519,-0.72811 -0.225518,-0.25773 -0.663669,-0.25773 -0.354386,0 -1.050272,0.21907 l -0.0451,-0.36727 q 0.541245,-0.17397 1.12115,-0.17397 1.250018,0 1.250018,1.37888 v 1.17914 q 0,0.40594 0.115981,0.80543 l -0.335057,0.0709 -0.135311,-0.5348 q -0.444594,0.55413 -1.172697,0.55413 z m -0.663669,-0.97939 q 0,0.30284 0.186858,0.48325 0.186859,0.17397 0.489698,0.17397 0.386603,0 0.753876,-0.2384 0.373717,-0.24485 0.373717,-0.59279 v -0.67012 q -1.804149,0 -1.804149,0.84409 z"
id="path2390" />
<path
d="m 86.178189,120.79526 q 0.683,0 1.08249,0.47681 0.39949,0.47037 0.39949,1.19847 0,0.77965 -0.412377,1.28223 -0.412377,0.49614 -1.127593,0.49614 -0.766764,0 -1.224244,-0.67011 0,0.32861 -0.01933,0.57991 h -0.354387 q 0.02577,-0.22552 0.02577,-0.80543 v -4.09155 h 0.386603 v 2.22941 q 0.425264,-0.69588 1.243574,-0.69588 z m -1.243574,2.04255 q 0,0.46393 0.328613,0.77965 0.328613,0.30928 0.805424,0.30928 1.204914,0 1.204914,-1.44331 0,-0.61213 -0.30284,-0.98584 -0.302839,-0.38016 -0.863414,-0.38016 -0.489698,0 -0.831197,0.3415 -0.3415,0.3415 -0.3415,0.83119 z"
id="path2392" />
<path
d="m 89.696285,123.92674 -0.03866,0.32217 q -1.224244,-0.058 -1.224244,-1.74615 v -3.24103 h 0.386603 v 3.10571 q 0,0.36728 0.03222,0.61857 0.03222,0.25129 0.115981,0.48325 0.09021,0.23197 0.270623,0.34795 0.180415,0.10953 0.457481,0.10953 z"
id="path2394" />
<path
d="m 90.51459,122.54142 q 0.01933,0.625 0.373717,1.00516 0.36083,0.38016 0.960065,0.38016 0.560575,0 1.153367,-0.23196 l 0.03222,0.31573 q -0.573462,0.2384 -1.224245,0.2384 -0.740989,0 -1.211357,-0.45748 -0.470367,-0.46392 -0.470367,-1.19202 0,-0.77965 0.451037,-1.28868 0.457481,-0.51547 1.192027,-0.51547 1.385329,0 1.417546,1.74616 z m 2.287403,-0.32217 q -0.01289,-0.50259 -0.309282,-0.79898 -0.289953,-0.30284 -0.766764,-0.30284 -0.47681,0 -0.79898,0.31572 -0.322169,0.30929 -0.393047,0.7861 z"
id="path2396" />
<path
d="m 96.345854,123.54658 q 0.135311,0 0.238405,0.1031 0.103095,0.10309 0.103095,0.2384 0,0.13532 -0.103095,0.23197 -0.103094,0.0966 -0.238405,0.0966 -0.135311,0 -0.231962,-0.0966 -0.09665,-0.0967 -0.09665,-0.23197 0,-0.13531 0.09665,-0.2384 0.09665,-0.1031 0.231962,-0.1031 z"
id="path2398" />
<path
d="m 102.20934,121.94218 h -2.07477 v 2.21653 h -0.386608 v -4.61347 h 2.609578 v 0.32217 h -2.22297 v 1.7526 h 2.07477 z"
id="path2400" />
<path
d="m 104.6385,121.13031 q -0.0387,-0.0129 -0.11598,-0.0129 -0.42527,0 -0.72166,0.34794 -0.28996,0.3415 -0.28996,0.9214 v 1.77194 h -0.3866 v -2.21653 q 0,-0.7281 -0.0967,-1.07605 h 0.32861 q 0.1031,0.20619 0.1031,0.65723 0.30928,-0.7281 0.99228,-0.7281 0.0902,0 0.18686,0.0193 z"
id="path2402" />
<path
d="m 105.60501,124.15871 h -0.38661 v -3.29258 h 0.38661 z m -0.20619,-4.60058 q 0.13531,0 0.21907,0.0902 0.0902,0.0838 0.0902,0.21908 0,0.13531 -0.0902,0.21907 -0.0838,0.0773 -0.21907,0.0773 -0.13531,0 -0.21908,-0.0773 -0.0773,-0.0838 -0.0773,-0.21907 0,-0.13531 0.0773,-0.21908 0.0838,-0.0902 0.21908,-0.0902 z"
id="path2404" />
<path
d="m 106.75838,122.54142 q 0.0193,0.625 0.37371,1.00516 0.36083,0.38016 0.96007,0.38016 0.56057,0 1.15336,-0.23196 l 0.0322,0.31573 q -0.57346,0.2384 -1.22424,0.2384 -0.74099,0 -1.21136,-0.45748 -0.47037,-0.46392 -0.47037,-1.19202 0,-0.77965 0.45104,-1.28868 0.45748,-0.51547 1.19203,-0.51547 1.38533,0 1.41754,1.74616 z m 2.2874,-0.32217 q -0.0129,-0.50259 -0.30928,-0.79898 -0.28996,-0.30284 -0.76677,-0.30284 -0.47681,0 -0.79898,0.31572 -0.32217,0.30929 -0.39304,0.7861 z"
id="path2406" />
<path
d="m 113.00202,124.15871 h -0.3866 v -1.86859 q 0,-1.17269 -0.9794,-1.17269 -0.3866,0 -0.70233,0.32861 -0.31572,0.32861 -0.31572,0.71522 v 1.99745 h -0.38661 V 121.678 q 0,-0.28995 -0.0644,-0.81187 h 0.36083 q 0.0644,0.19975 0.0773,0.58635 0.37372,-0.65722 1.10826,-0.65722 1.28868,0 1.28868,1.49486 z"
id="path2408" />
<path
d="m 113.77523,122.47054 q 0,-0.72166 0.39949,-1.19847 0.40593,-0.47681 1.08249,-0.47681 0.81831,0 1.24357,0.69588 v -2.22941 h 0.38661 v 4.09155 q 0,0.57991 0.0258,0.80543 h -0.35439 q -0.0193,-0.2513 -0.0193,-0.57991 -0.45748,0.67011 -1.22424,0.67011 -0.72166,0 -1.13404,-0.49614 -0.40593,-0.50258 -0.40593,-1.28223 z m 1.59152,1.4562 q 0.47681,0 0.80542,-0.30928 0.32861,-0.31572 0.32861,-0.77965 v -0.54769 q 0,-0.49614 -0.3415,-0.83119 -0.33505,-0.3415 -0.83119,-0.3415 -0.56058,0 -0.86342,0.38016 -0.30284,0.38016 -0.30284,0.98584 0,1.44331 1.20492,1.44331 z"
id="path2410" />
<path
d="m 119.15547,123.92674 -0.0387,0.32217 q -1.22424,-0.058 -1.22424,-1.74615 v -3.24103 h 0.3866 v 3.10571 q 0,0.36728 0.0322,0.61857 0.0322,0.25129 0.11598,0.48325 0.0902,0.23197 0.27062,0.34795 0.18042,0.10953 0.45749,0.10953 z"
id="path2412" />
<path
d="m 121.10781,124.15871 q -0.57346,1.28867 -1.52064,1.51419 l -0.0902,-0.30284 q 0.82476,-0.18685 1.25002,-1.32089 0,-0.058 -0.0258,-0.11598 l -1.366,-3.06706 h 0.40593 l 1.23069,2.78999 1.15337,-2.78999 h 0.41237 z"
id="path2414" />
<path
d="m 125.44421,123.54658 q 0.13531,0 0.2384,0.1031 0.1031,0.10309 0.1031,0.2384 0,0.13532 -0.1031,0.23197 -0.10309,0.0966 -0.2384,0.0966 -0.13531,0 -0.23196,-0.0966 -0.0966,-0.0967 -0.0966,-0.23197 0,-0.13531 0.0966,-0.2384 0.0966,-0.1031 0.23196,-0.1031 z"
id="path2416" />
<path
d="m 131.42367,122.95379 q 0,0.59924 -0.45103,0.94718 -0.4446,0.34794 -1.11471,0.34794 -0.76032,0 -1.26935,-0.35438 l 0.15464,-0.32862 q 0.47681,0.36083 1.15337,0.36083 0.50903,0 0.82475,-0.24484 0.31573,-0.24485 0.31573,-0.69589 0,-0.43815 -0.25129,-0.65723 -0.2513,-0.22551 -0.75388,-0.38016 -1.28868,-0.41882 -1.28868,-1.30156 0,-0.54125 0.41238,-0.85697 0.41238,-0.32217 1.01805,-0.32217 0.64434,0 1.05672,0.24485 -0.0129,0.0258 -0.0773,0.14175 -0.058,0.10954 -0.0838,0.16109 -0.41882,-0.22552 -0.90208,-0.22552 -0.45104,0 -0.74743,0.21263 -0.28995,0.21263 -0.28995,0.59924 0,0.63789 0.99872,0.98583 0.32217,0.11599 0.51547,0.21264 0.19975,0.0902 0.39949,0.24484 0.19975,0.15465 0.28995,0.38016 0.0902,0.22552 0.0902,0.52836 z"
id="path2418" />
<path
d="m 132.38373,122.54142 q 0.0193,0.625 0.37372,1.00516 0.36083,0.38016 0.96006,0.38016 0.56058,0 1.15337,-0.23196 l 0.0322,0.31573 q -0.57347,0.2384 -1.22425,0.2384 -0.74099,0 -1.21136,-0.45748 -0.47036,-0.46392 -0.47036,-1.19202 0,-0.77965 0.45103,-1.28868 0.45749,-0.51547 1.19203,-0.51547 1.38533,0 1.41755,1.74616 z m 2.2874,-0.32217 q -0.0129,-0.50259 -0.30928,-0.79898 -0.28995,-0.30284 -0.76676,-0.30284 -0.47681,0 -0.79898,0.31572 -0.32217,0.30929 -0.39305,0.7861 z"
id="path2420" />
<path
d="m 137.3387,121.11743 q -0.61212,0 -0.97295,0.3866 -0.36083,0.3866 -0.36083,1.01806 0,0.625 0.36727,1.01805 0.36727,0.3866 0.96651,0.3866 0.54124,0 0.90207,-0.23196 l 0.13531,0.31573 q -0.42526,0.2384 -1.01805,0.2384 -0.79898,0 -1.26935,-0.46392 -0.47037,-0.47037 -0.47037,-1.2629 0,-0.79254 0.47037,-1.25647 0.47037,-0.47036 1.26935,-0.47036 0.59923,0 1.01805,0.23196 l -0.13531,0.32217 q -0.36083,-0.23196 -0.90207,-0.23196 z"
id="path2422" />
<path
d="m 141.99727,124.14582 -0.36083,0.0838 q -0.11598,-0.32861 -0.15464,-0.64434 -0.4317,0.66367 -1.23713,0.66367 -1.18558,0 -1.18558,-1.53352 v -1.84926 h 0.3866 v 1.77838 q 0,1.28223 0.88275,1.28223 0.45748,0 0.78609,-0.30928 0.32861,-0.31572 0.32861,-0.76032 v -1.99101 h 0.38661 v 2.48715 q 0,0.34795 0.16752,0.79254 z"
id="path2424" />
<path
d="m 144.34911,121.13031 q -0.0387,-0.0129 -0.11598,-0.0129 -0.42526,0 -0.72166,0.34794 -0.28995,0.3415 -0.28995,0.9214 v 1.77194 h -0.38661 v -2.21653 q 0,-0.7281 -0.0967,-1.07605 h 0.32862 q 0.10309,0.20619 0.10309,0.65723 0.30928,-0.7281 0.99228,-0.7281 0.0902,0 0.18686,0.0193 z"
id="path2426" />
<path
d="m 145.08366,122.54142 q 0.0193,0.625 0.37372,1.00516 0.36083,0.38016 0.96006,0.38016 0.56058,0 1.15337,-0.23196 l 0.0322,0.31573 q -0.57346,0.2384 -1.22424,0.2384 -0.74099,0 -1.21136,-0.45748 -0.47036,-0.46392 -0.47036,-1.19202 0,-0.77965 0.45103,-1.28868 0.45748,-0.51547 1.19203,-0.51547 1.38533,0 1.41755,1.74616 z m 2.2874,-0.32217 q -0.0129,-0.50259 -0.30928,-0.79898 -0.28995,-0.30284 -0.76676,-0.30284 -0.47681,0 -0.79898,0.31572 -0.32217,0.30929 -0.39305,0.7861 z"
id="path2428" />
</g>
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.308367;stroke-linejoin:bevel"
d="m -144.31598,174.51773 v -14.33904 h 0.9251 0.9251 v 14.33904 14.33901 h -0.9251 -0.9251 z"
id="path1834" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.308367;stroke-linejoin:bevel"
d="m -136.30031,181.22465 c -0.001,-4.1976 -0.0919,-8.78814 -0.20181,-10.20114 l -0.19998,-2.56908 0.89565,0.1023 c 0.8478,0.0966 0.90062,0.1887 0.98885,1.7211 0.0513,0.8904 0.20586,1.61892 0.34352,1.61892 0.13764,0 0.618,-0.53058 1.0674,-1.17906 1.45087,-2.09358 4.23746,-3.17568 6.56441,-2.5491 1.36671,0.36801 3.24026,2.05989 3.81345,3.44367 0.20532,0.49566 0.46053,0.89748 0.56716,0.89292 0.10668,-0.006 0.53985,-0.56889 0.96272,-1.25409 2.58453,-4.18785 8.33854,-4.38975 10.69125,-0.37515 1.35685,2.31528 1.63536,4.25529 1.64063,11.42817 l 0.005,6.55278 h -0.92514 -0.9251 v -6.12522 c 0,-6.42789 -0.25833,-8.71464 -1.18804,-10.51704 -1.54819,-3.00141 -5.32325,-3.35445 -7.81531,-0.73086 -1.64902,1.73601 -1.78946,2.54217 -1.78946,10.2708 v 7.10232 h -0.8975 -0.89747 l -0.11478,-7.63206 c -0.12399,-8.24403 -0.26391,-9.02502 -1.88595,-10.52016 -0.8482,-0.78183 -2.9732,-1.19196 -4.3655,-0.84252 -1.15129,0.2889 -3.25337,2.29635 -3.90658,3.73062 -0.49008,1.07607 -0.56263,2.12859 -0.56854,8.2488 l -0.006,7.01532 h -0.92514 -0.9251 z"
id="path1836" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.308367;stroke-linejoin:bevel"
d="m -103.02389,181.53305 c -0.0159,-4.02804 -0.12003,-8.6073 -0.23127,-10.17615 l -0.20232,-2.85249 0.84214,9e-5 c 0.91226,9e-5 1.08223,0.35814 1.23423,2.59995 l 0.0822,1.21224 0.90345,-1.30884 c 1.592469,-2.30697 4.454556,-3.48627 6.877557,-2.83383 1.285035,0.34605 3.171906,2.10468 3.846369,3.58494 0.18903,0.4149 0.425514,0.74997 0.525495,0.74463 0.10002,-0.006 0.549228,-0.61569 0.998349,-1.35624 1.190781,-1.9635 3.276012,-3.10278 5.681709,-3.10422 2.125218,-0.001 3.530028,0.73824 4.730481,2.49021 1.425795,2.0808 1.662396,3.54492 1.802493,11.15388 l 0.13191,7.16952 h -0.953946 -0.953958 l -0.0096,-5.4735 c -0.0111,-6.4119 -0.345123,-9.45564 -1.227219,-11.18469 -1.697829,-3.32802 -6.20544,-3.26967 -8.549655,0.1107 l -0.852249,1.22895 -0.09222,7.65927 -0.09222,7.6593 h -0.872124 -0.872127 l -0.13461,-7.47789 c -0.12207,-6.78261 -0.19539,-7.61328 -0.78783,-8.93439 -1.868358,-4.16616 -7.021416,-3.50571 -9.439975,1.20993 -0.42611,0.83082 -0.50616,2.11092 -0.50616,8.0946 v 7.10775 h -0.9251 -0.92509 z"
id="path1838" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.308367;stroke-linejoin:bevel"
d="m -65.528455,189.01025 c -0.678402,-0.24 -1.67559,-0.91593 -2.215989,-1.50231 -1.721088,-1.86762 -2.021112,-3.47595 -2.178735,-11.67969 l -0.14064,-7.32369 h 0.957168 0.957168 l 0.0048,5.01096 c 0.0063,6.21369 0.397179,10.10931 1.161543,11.59083 1.796637,3.48225 6.830331,3.36933 9.425844,-0.2115 l 0.970869,-1.33941 0.0924,-7.52544 0.0924,-7.52547 h 1.063956 1.063959 l 0.0078,8.71134 c 0.0048,4.79124 0.14874,9.324 0.321357,10.07277 0.24207,1.05027 0.22275,1.4202 -0.0846,1.61853 -0.877587,0.56631 -1.320246,0.0885 -1.745649,-1.88487 -0.390237,-1.81014 -0.463677,-1.92957 -0.851025,-1.3839 -2.309766,3.2538 -5.654358,4.52049 -8.902956,3.37182 z"
id="path1840" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="m -42.840682,189.54167 c -0.830898,-0.0516 -1.586373,-0.32814 -2.21226,-0.80976 -0.11286,-0.0867 -0.337599,-0.2976 -0.49935,-0.46863 -0.813387,-0.85929 -1.347882,-2.09901 -1.613787,-3.7431 l -0.08223,-0.50814 -0.0111,-6.97791 -0.0111,-6.97791 h -1.868187 -1.868187 v -0.88926 -0.88926 h 1.869252 1.869249 v -3.09453 -3.09453 l 1.070895,-0.53508 c 0.588993,-0.2943 1.078992,-0.53508 1.088883,-0.53508 0.0096,0 0.018,1.63332 0.018,3.62961 v 3.62961 h 2.994423 2.99442 v 0.88926 0.88926 h -2.995308 -2.995296 l 0.0111,7.03233 c 0.0126,7.55964 0.0048,7.20882 0.18975,8.02035 0.25452,1.11204 0.752538,1.8687 1.452594,2.20695 0.397431,0.192 0.639018,0.234 1.352916,0.2346 0.894777,7.5e-4 1.608678,-0.0864 2.103723,-0.2568 0.11145,-0.0381 0.20673,-0.0654 0.21195,-0.0603 0.0048,0.006 0.05667,0.40134 0.1143,0.88029 l 0.10479,0.87081 -0.07494,0.0441 c -0.12462,0.0735 -0.530289,0.2211 -0.818958,0.2979 -0.672816,0.1791 -1.614678,0.2637 -2.395539,0.2151 z"
id="path1842" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="m -31.281523,189.54041 c -2.117439,-0.1116 -4.0266,-1.30272 -4.90974,-3.0633 -0.382494,-0.76251 -0.552753,-1.49451 -0.57882,-2.48841 -0.0258,-0.98481 0.08034,-1.78161 0.342393,-2.57181 0.550053,-1.65828 1.703355,-2.91345 3.498849,-3.80796 1.8933,-0.94317 4.422204,-1.44672 7.615539,-1.51629 l 1.030845,-0.024 -0.0231,-0.58419 c -0.12114,-3.08307 -1.437594,-5.03694 -3.728196,-5.53323 -0.18975,-0.0411 -0.568755,-0.0933 -0.842232,-0.1161 -1.32177,-0.1101 -3.283998,0.2748 -5.922663,1.16184 -0.29799,0.1002 -0.545955,0.1779 -0.550989,0.1731 -0.0048,-0.006 -0.05937,-0.46629 -0.12081,-1.02495 -0.06129,-0.55869 -0.11985,-1.06599 -0.13002,-1.12737 l -0.0183,-0.1116 0.590262,-0.1863 c 1.911207,-0.60366 3.694686,-0.88557 5.635404,-0.89073 1.226661,-0.003 1.966944,0.0957 2.823465,0.37677 2.327178,0.76416 3.733878,2.60757 4.236099,5.55114 0.19122,1.12077 0.18897,1.05471 0.21954,6.47901 0.0159,2.7948 0.03777,5.13861 0.0489,5.20848 0.0111,0.0699 0.0462,0.37203 0.07794,0.67149 0.07383,0.69654 0.23166,1.62993 0.392748,2.32266 0.06954,0.2994 0.12129,0.54936 0.11493,0.55569 -0.0159,0.015 -1.847232,0.44229 -1.854438,0.4323 -0.0033,-0.006 -0.17214,-0.74271 -0.375747,-1.64103 -0.20358,-0.89835 -0.378585,-1.65783 -0.388872,-1.68777 -0.0144,-0.0411 -0.03825,-0.027 -0.0978,0.054 -0.396462,0.54423 -0.982011,1.17393 -1.476264,1.58757 -1.569006,1.31304 -3.44154,1.91388 -5.609082,1.7997 z m 1.47774,-2.04291 c 1.043211,-0.1572 2.05602,-0.55764 3.024108,-1.19553 1.290702,-0.85047 2.04543,-1.78962 2.365311,-2.94327 l 0.10065,-0.36297 0.0126,-2.46459 0.0126,-2.46462 -1.009095,0.024 c -5.651721,0.1266 -8.751711,1.62918 -9.249408,4.48323 -0.0708,0.40623 -0.07002,1.35084 0.0016,1.77069 0.29403,1.72404 1.366362,2.86329 2.968689,3.15396 0.360186,0.0654 1.342119,0.0657 1.774029,5.1e-4 z"
id="path1844" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="m -7.8875825,189.54161 c -1.538988,-0.0744 -2.9374805,-0.60105 -4.2103355,-1.58514 -0.65556,-0.50682 -1.496838,-1.44732 -2.046417,-2.2878 l -0.22209,-0.33963 -0.0078,0.2307 c -0.0048,0.1269 -0.0252,0.79425 -0.04539,1.48296 -0.0204,0.68871 -0.04764,1.41147 -0.06063,1.60611 l -0.0234,0.35388 h -0.994473 -0.994473 l 0.0216,-0.2088 c 0.09669,-0.93147 0.1035,-1.8627 0.11637,-15.98841 l 0.0126,-14.65452 h 1.087533 1.087548 l 0.0011,7.03233 c 9.21e-4,5.53779 0.0111,7.01928 0.04539,6.97074 0.0243,-0.0339 0.15033,-0.2355 0.28014,-0.44802 0.379557,-0.62154 0.777366,-1.12458 1.322769,-1.67265 1.260618,-1.26678 2.5565885,-1.90101 4.4120105,-2.15913 0.469743,-0.0654 1.713279,-0.0552 2.232201,0.018 2.097834,0.2973 3.715656,1.25979 5.01145196,2.98143 1.20858,1.60575 1.89528304,3.44268 2.16369904,5.78775 0.06396,0.55932 0.06462,2.90985 9.51e-4,3.50256 -0.19008,1.76865 -0.59969404,3.26412 -1.26256804,4.60959 -1.64027096,3.32937 -4.32204896,4.94199 -7.92795896,4.76736 z m 1.368312,-2.0466 c 2.796684,-0.36504 4.538139,-2.03373 5.306523,-5.08476 0.47731196,-1.89534 0.56147996,-4.59102 0.20358,-6.52143 -0.47136,-2.54259 -1.745583,-4.51794 -3.485817,-5.4039 -0.920607,-0.46869 -1.741026,-0.65262 -2.920761,-0.65481 -1.00611,-0.002 -1.613154,0.1107 -2.474007,0.45876 -0.9240365,0.37362 -1.9277015,1.15236 -2.6473175,2.05398 -0.710628,0.8904 -1.236075,2.02467 -1.4628,3.15777 -0.15747,0.78702 -0.17418,1.18113 -0.15843,3.73848 0.0159,2.6016 0.0165,2.60625 0.24939,3.48444 0.568038,2.14122 2.215881,3.91269 4.2321965,4.54965 0.25773,0.0813 0.876282,0.2127 1.190181,0.2529 0.313497,0.0399 1.575021,0.021 1.967196,-0.0312 z"
id="path1846" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="m 12.038954,189.48929 c -1.380489,-0.2007 -2.5289035,-0.74685 -3.4593055,-1.64538 -1.650744,-1.59426 -2.5149,-4.11801 -2.710227,-7.91517 -0.0225,-0.43971 -0.03651,-4.69791 -0.03666,-11.24271 l -3.48e-4,-10.53492 h 1.087644 1.087644 l 0.0144,10.91604 c 0.0144,11.15352 0.0126,11.12724 0.15351,12.40416 0.16794,1.5306 0.555705,3.06945 0.985725,3.91164 0.709041,1.38867 1.7957965,2.07525 3.4244605,2.16342 l 0.41886,0.024 -0.10701,0.98907 -0.10701,0.98907 -0.20292,-0.003 c -0.1116,-0.002 -0.358092,-0.027 -0.547737,-0.054 z"
id="path1848" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="m 24.488495,189.5423 c -1.316022,-0.0591 -2.562162,-0.366 -3.65757,-0.90096 -0.600471,-0.2931 -0.872235,-0.45927 -1.423875,-0.86988 -1.613886,-1.20126 -2.83002,-3.00558 -3.445749,-5.11236 -0.12336,-0.42213 -0.26172,-1.10259 -0.348948,-1.71612 -0.09144,-0.64314 -0.11256,-2.46267 -0.03777,-3.24603 0.18384,-1.92444 0.568215,-3.31806 1.321434,-4.79121 0.980265,-1.91715 2.435433,-3.40968 4.126182,-4.2321 1.230918,-0.59871 2.366169,-0.84465 3.883677,-0.84129 0.897063,0.002 1.51638,0.0747 2.251233,0.264 2.647791,0.68271 4.409805,2.70723 5.225415,6.00387 0.30423,1.22967 0.462249,2.45031 0.546609,4.22232 l 0.0228,0.48093 h -7.644861 -7.644861 l 0.0183,0.1725 c 0.0096,0.0948 0.0279,0.33576 0.03969,0.53538 0.10095,1.71342 0.604836,3.41064 1.388331,4.67724 0.498126,0.80532 1.268001,1.64076 1.96539,2.13282 0.885237,0.62463 1.959198,1.02819 3.134232,1.17774 0.593217,0.0756 2.155158,0.0561 2.848515,-0.0357 1.538922,-0.2034 3.239802,-0.66696 4.594113,-1.25232 0.09651,-0.0417 0.18762,-0.0759 0.20256,-0.0759 0.0285,0 0.20805,1.91424 0.18165,1.9362 -0.0078,0.006 -0.1785,0.0822 -0.378141,0.1671 -1.84893,0.78693 -3.835227,1.23453 -5.734782,1.29228 -0.339375,0.012 -0.674193,0.024 -0.744075,0.027 -0.06984,0.006 -0.38019,-0.003 -0.689628,-0.015 z m 6.279102,-12.88929 c -3.96e-4,-0.49158 -0.13002,-1.44957 -0.27408,-2.02635 -0.337248,-1.34979 -0.923781,-2.373 -1.893666,-3.30354 -0.946179,-0.9078 -2.048889,-1.37718 -3.490149,-1.48563 -2.248362,-0.1692 -4.144344,0.70395 -5.586063,2.5725 -0.829104,1.07454 -1.417035,2.45454 -1.657095,3.88956 -0.03174,0.1893 -0.06588,0.38112 -0.07587,0.42606 l -0.0183,0.0816 h 6.497685 6.497685 l -1.11e-4,-0.1542 z"
id="path1850" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="M 70.403009,174.46616 V 159.9296 h 7.440693 7.440693 v 0.99816 0.99813 h -6.35181 -6.35181 v 5.5533 5.5533 h 5.916261 5.916264 v 0.99813 0.99813 h -5.916264 -5.916261 v 6.987 6.987 h -1.088883 -1.088883 z"
id="path1852" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="m 89.675846,181.22633 c -3.96e-4,-8.42712 -0.0159,-9.27867 -0.20184,-10.95234 -0.06081,-0.54813 -0.20655,-1.46937 -0.28287,-1.78818 l -0.05001,-0.2094 0.929625,0.009 0.929622,0.009 0.11667,0.34482 c 0.24405,0.72153 0.38751,1.73133 0.438294,3.08502 l 0.0264,0.70767 0.17553,-0.4173 c 0.582486,-1.38471 1.378806,-2.47692 2.332371,-3.1989 0.355965,-0.2694 1.123425,-0.6519 1.556829,-0.77565 0.713328,-0.2037 1.640634,-0.2709 2.314305,-0.1677 l 0.31758,0.0486 v 0.97788 0.97788 l -0.13605,-0.03 c -0.22089,-0.0468 -1.025748,-0.021 -1.369377,0.0426 -0.976185,0.183 -1.774746,0.60066 -2.534016,1.32558 -1.109838,1.05963 -1.80732,2.38722 -2.146092,4.08492 -0.22383,1.12158 -0.21426,0.80616 -0.2286,7.5393 l -0.0126,6.16125 h -1.087404 -1.087407 z"
id="path1854" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="M 101.65393,178.64024 V 168.2777 h 1.07073 1.07073 v 10.36254 10.36251 h -1.07073 -1.07073 z"
id="path1856" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="m 117.20677,189.54065 c -1.9962,-0.0849 -3.77313,-0.72921 -5.28687,-1.91715 -0.39069,-0.3066 -1.11243,-1.02423 -1.42371,-1.41555 -1.60191,-2.01393 -2.34033,-4.45113 -2.25987,-7.45884 0.024,-0.84651 0.0549,-1.25475 0.1539,-1.94184 0.36663,-2.54832 1.4064,-4.76052 3.02607,-6.43812 1.344,-1.39212 2.97423,-2.20983 4.93761,-2.47665 0.61383,-0.0834 1.84362,-0.0825 2.48049,0.002 3.47037,0.4608 5.57949,2.73192 6.4251,6.91866 0.2106,1.04241 0.38616,2.72055 0.38616,3.69009 v 0.30006 h -7.62216 -7.62216 l 7.2e-4,0.2268 c 10e-4,0.30822 0.0912,1.21647 0.1638,1.65045 0.58707,3.50589 2.59434,5.95074 5.46024,6.65055 0.76488,0.1869 1.27044,0.2316 2.36031,0.2085 1.92048,-0.0402 3.57471,-0.38781 5.58582,-1.1736 0.3273,-0.1281 0.60087,-0.2268 0.60792,-0.2196 0.03,0.03 0.1878,1.89675 0.1626,1.92225 -0.0591,0.0594 -1.1994,0.50613 -1.73637,0.67998 -1.89084,0.61224 -3.82146,0.87579 -5.79948,0.79173 z m 6.24924,-13.21431 c -0.0957,-2.00139 -0.78849,-3.6687 -2.03955,-4.90902 -0.9795,-0.97116 -2.09058,-1.46088 -3.58356,-1.57962 -1.31247,-0.1044 -2.65083,0.192 -3.67557,0.81426 -1.09476,0.66462 -2.09601,1.78734 -2.742,3.07458 -0.42441,0.8457 -0.76491,1.95288 -0.88953,2.89242 l -0.024,0.1884 h 6.48912 6.48909 z"
id="path1858" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="m 130.246,180.50039 c -0.012,-7.70118 -0.018,-8.57418 -0.0753,-9.26457 -0.0666,-0.80139 -0.2277,-2.49213 -0.2658,-2.78571 l -0.021,-0.1725 h 1.01796 1.01793 l 0.0744,0.31758 c 0.1575,0.67284 0.2772,1.6569 0.31812,2.61228 0.0345,0.80928 0.0366,0.81345 0.2283,0.46161 0.93858,-1.72203 2.27916,-2.93382 3.8508,-3.48087 1.75359,-0.61038 4.23234,-0.44265 5.8749,0.39753 1.41144,0.72198 2.43792,1.968 3.05133,3.70395 0.2595,0.7341 0.4899,1.80171 0.59922,2.7753 0.1071,0.95442 0.1269,2.20143 0.1275,8.04864 l 6.3e-4,5.88903 h -1.08726 -1.0872 l -0.015,-6.48792 c -0.015,-6.91569 -0.012,-6.74199 -0.1899,-7.78422 -0.44652,-2.59383 -1.69596,-4.18083 -3.70875,-4.71075 -0.79704,-0.2097 -1.92897,-0.258 -2.62404,-0.1113 -0.96021,0.2022 -1.78407,0.66915 -2.62653,1.48845 -1.14552,1.11399 -1.87053,2.382 -2.17077,3.79641 l -0.0843,0.39816 -0.009,6.70569 -0.009,6.70572 h -1.08687 -1.08687 z"
id="path1860" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="m 158.71132,189.5417 c -2.91762,-0.1311 -5.18709,-1.59285 -6.66876,-4.29561 -0.60435,-1.10238 -1.08093,-2.50629 -1.31427,-3.8715 -0.51462,-3.01113 -0.2292,-6.08682 0.77988,-8.40225 1.14759,-2.63334 3.06033,-4.36614 5.44281,-4.93071 1.55871,-0.36936 3.38505,-0.27 4.80921,0.2616 1.45209,0.54207 2.69157,1.55238 3.68406,3.00291 0.1398,0.2043 0.31938,0.48798 0.39924,0.63057 0.0798,0.1425 0.1575,0.2598 0.1725,0.2601 0.015,4.5e-4 0.027,-3.15966 0.027,-7.02246 v -7.02327 h 1.08888 1.08888 l 9e-5,14.16453 c 9e-5,13.49286 0.012,15.33414 0.1038,16.38768 l 0.027,0.2994 h -0.98556 -0.98556 l -0.021,-0.1905 c -0.027,-0.2265 -0.096,-2.13246 -0.1071,-2.93091 l -0.009,-0.56259 -0.1773,0.2721 c -0.53577,0.82308 -1.42497,1.82859 -2.05977,2.32926 -1.32408,1.04424 -2.78742,1.57869 -4.47777,1.63542 -0.1596,0.006 -0.52722,-8.7e-4 -0.81666,-0.015 z m 2.15235,-2.09568 c 0.57033,-0.1161 0.99816,-0.2643 1.53171,-0.53058 0.59409,-0.2964 1.05009,-0.62085 1.55505,-1.10673 0.84591,-0.81402 1.46733,-1.83672 1.78131,-2.93163 0.2856,-0.99594 0.3213,-1.50216 0.30141,-4.27485 -0.018,-2.44896 -0.03,-2.61489 -0.2607,-3.51579 -0.35535,-1.38651 -1.02678,-2.50746 -2.13348,-3.56184 -0.95904,-0.91368 -2.00778,-1.43454 -3.29436,-1.63623 -0.50643,-0.0795 -1.58565,-0.0792 -2.10177,4.8e-4 -0.73497,0.1134 -1.47864,0.35451 -2.01039,0.6519 -1.78428,0.9978 -3.04128,3.1326 -3.43863,5.83986 -0.246,1.67523 -0.1632,3.98358 0.2037,5.68635 0.57291,2.6589 1.91361,4.38552 3.95682,5.09574 0.85908,0.2985 1.49043,0.38781 2.6463,0.37377 0.72558,-0.009 0.9366,-0.024 1.26309,-0.0906 z"
id="path1862" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="m 180.18046,189.48815 c -2.2323,-0.3471 -3.81351,-1.50858 -4.84362,-3.55797 -0.64728,-1.28769 -1.01874,-2.74482 -1.26159,-4.94886 -0.057,-0.51579 -0.063,-1.48887 -0.0735,-11.6964 l -0.012,-11.13381 h 1.06932 1.06935 l 0.015,10.64382 c 0.015,10.83192 0.018,11.11626 0.1545,12.45861 0.2028,2.0022 0.65679,3.64941 1.26459,4.58895 0.2085,0.32232 0.69909,0.83706 0.97977,1.02795 0.60663,0.41262 1.51659,0.67965 2.31819,0.68025 0.198,1.5e-4 0.2814,0.012 0.2814,0.0447 0,0.0795 -0.1842,1.78593 -0.2019,1.87011 -0.015,0.0738 -0.0372,0.0813 -0.2253,0.0777 -0.1146,-0.002 -0.35499,-0.027 -0.53466,-0.0549 z"
id="path1864" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="m 183.62242,198.44585 c -0.012,-0.0534 -0.1263,-0.47253 -0.2508,-0.93168 -0.1245,-0.45915 -0.2238,-0.8361 -0.2208,-0.83769 0.003,-0.002 0.2115,-0.0693 0.46311,-0.1503 1.79742,-0.57957 3.34176,-1.85958 4.61625,-3.82614 0.6873,-1.06047 1.37181,-2.48631 1.89687,-3.95127 0.1506,-0.42048 0.1638,-0.60156 0.0675,-0.93942 -0.0306,-0.1074 -1.7352,-4.36221 -3.78795,-9.4551 -2.05275,-5.09289 -3.80616,-9.44355 -3.89643,-9.66813 l -0.1641,-0.40833 h 1.13391 1.13394 l 1.88088,4.7094 c 1.03452,2.5902 2.61192,6.53874 3.50532,8.77458 0.89343,2.23584 1.6383,4.06152 1.65528,4.05705 0.018,-0.006 1.50456,-3.94716 3.3057,-8.76156 1.80114,-4.81443 3.28092,-8.75955 3.28833,-8.76699 0.009,-0.009 0.52716,-0.009 1.15491,-0.003 l 1.14138,0.009 -2.37141,5.95254 c -5.33205,13.38411 -6.05598,15.19422 -6.2784,15.69807 -2.06613,4.68051 -4.71939,7.51905 -7.94979,8.50488 l -0.2994,0.0915 z"
id="path1866" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="m 241.59337,189.54203 c -2.30475,-0.0822 -4.35306,-0.70485 -6.02514,-1.83138 -0.2196,-0.1479 -0.4287,-0.2919 -0.46467,-0.31959 -0.0615,-0.0477 -0.0405,-0.1092 0.35664,-1.04307 0.2322,-0.54588 0.4281,-0.99852 0.43548,-1.00587 0.009,-0.009 0.1653,0.1008 0.35112,0.24 1.06728,0.80076 2.1348,1.31283 3.42627,1.64358 0.86898,0.2226 1.41873,0.2925 2.47128,0.31497 1.00587,0.021 1.46865,-0.012 2.17122,-0.1536 2.10912,-0.4272 3.73479,-1.73637 4.3308,-3.48762 0.2931,-0.86106 0.41376,-1.90122 0.33672,-2.90118 -0.1395,-1.80786 -0.6534,-2.93001 -1.76901,-3.86127 -0.97362,-0.81276 -2.03499,-1.38099 -3.75489,-2.01033 -2.0091,-0.73512 -3.25644,-1.37757 -4.4781,-2.30646 -0.44967,-0.34188 -1.30902,-1.19166 -1.6125,-1.59453 -1.03554,-1.37463 -1.51131,-2.91504 -1.44294,-4.67202 0.0846,-2.17602 0.82695,-3.77448 2.36478,-5.09244 1.02237,-0.87618 2.27838,-1.50102 3.58158,-1.78173 0.73059,-0.1572 1.05438,-0.1914 2.00802,-0.2106 1.70271,-0.0342 3.16122,0.1812 4.46778,0.65937 0.3816,0.1398 1.26171,0.5628 1.54623,0.74331 l 0.1704,0.108 -0.438,0.89952 c -0.2409,0.49476 -0.44949,0.91224 -0.46353,0.92778 -0.015,0.015 -0.1632,-0.048 -0.33123,-0.1413 -1.14852,-0.63687 -2.49195,-1.047 -3.86232,-1.17903 -0.56211,-0.054 -1.84353,-0.024 -2.3052,0.0564 -1.36629,0.2346 -2.43981,0.74907 -3.25305,1.55919 -0.66813,0.66558 -1.03581,1.36992 -1.23555,2.36694 -0.093,0.46467 -0.0933,1.6863 -2.7e-4,2.11788 0.30807,1.42977 1.06047,2.5218 2.45406,3.56166 0.80493,0.6006 1.77321,1.10781 3.28704,1.72188 2.76288,1.12068 4.2393,1.98141 5.40978,3.15372 0.69783,0.69894 1.08957,1.33416 1.39341,2.25948 0.33486,1.01973 0.45378,1.93737 0.42714,3.29625 -0.021,0.99504 -0.06,1.35537 -0.2304,2.05074 -0.44691,1.82274 -1.54683,3.32724 -3.24318,4.4361 -1.68516,1.10154 -3.59061,1.56384 -6.07959,1.47498 z"
id="path1868" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="m 263.46175,189.54323 c -2.80899,-0.1308 -5.22276,-1.38135 -6.87114,-3.56007 -0.4245,-0.56109 -0.6738,-0.96681 -0.98814,-1.60818 -0.52932,-1.08 -0.84054,-2.14656 -1.0221,-3.50256 -0.0939,-0.70014 -0.0927,-2.75589 0.002,-3.53886 0.2937,-2.43396 1.01424,-4.32531 2.2989,-6.03567 0.36348,-0.48396 1.13139,-1.28982 1.55358,-1.63047 1.20624,-0.97314 2.58915,-1.55871 4.1922,-1.77507 0.54591,-0.0738 1.80054,-0.0825 2.34111,-0.015 2.5482,0.31149 4.38213,1.57836 5.51166,3.80742 0.85362,1.68453 1.29828,3.73545 1.42377,6.56667 l 0.024,0.5535 h -7.63599 -7.63602 l 0.027,0.49665 c 0.1689,3.18186 1.43247,5.72484 3.54882,7.14141 0.83019,0.55566 1.90509,0.93372 3.04812,1.07208 0.39846,0.0483 1.71624,0.0477 2.25129,-9.6e-4 1.60254,-0.1455 3.34935,-0.58182 4.92957,-1.23093 0.2058,-0.0846 0.3795,-0.1482 0.38568,-0.141 0.006,0.006 0.051,0.44307 0.0996,0.96903 l 0.0882,0.95631 -0.2019,0.0894 c -1.84176,0.81639 -3.96888,1.31205 -5.90028,1.37487 -0.31938,0.012 -0.65421,0.024 -0.74406,0.03 -0.0897,0.006 -0.41649,-0.002 -0.72591,-0.015 z m 6.27882,-12.94467 c -6e-4,-0.32358 -0.0996,-1.1841 -0.1848,-1.60608 -0.31341,-1.55172 -1.01139,-2.81103 -2.10477,-3.79752 -1.10712,-0.99891 -2.66208,-1.48497 -4.34538,-1.35828 -0.90084,0.0678 -1.52766,0.2337 -2.2752,0.60285 -0.63606,0.31395 -1.07013,0.63144 -1.63392,1.19508 -0.95667,0.95637 -1.56321,1.9479 -1.99737,3.2649 -0.1857,0.56301 -0.41574,1.55532 -0.41574,1.79244 v 0.1155 h 6.47883 6.47886 l -3.9e-4,-0.2088 z"
id="path1870" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="m 284.44087,189.54212 c -3.24438,-0.1257 -5.8536,-1.54668 -7.52163,-4.09638 -0.30882,-0.47208 -0.76221,-1.37259 -0.96384,-1.91436 -0.57444,-1.54371 -0.85107,-3.38343 -0.79269,-5.27226 0.099,-3.20727 0.94287,-5.55774 2.68761,-7.48674 1.27536,-1.41003 2.92155,-2.3352 4.84833,-2.72475 2.16588,-0.43788 4.818,-0.2328 6.71478,0.51963 0.37071,0.147 1.10307,0.49998 1.32684,0.63945 l 0.111,0.069 -0.37239,0.97527 c -0.2049,0.5364 -0.37752,0.98043 -0.38382,0.98673 -0.006,0.006 -0.1254,-0.0627 -0.2649,-0.1539 -0.36555,-0.2385 -1.13112,-0.60219 -1.61448,-0.76683 -1.88151,-0.64089 -4.23801,-0.66975 -5.95125,-0.0729 -2.21205,0.77064 -3.92331,2.79354 -4.58943,5.42523 -0.2493,0.98457 -0.33003,1.73625 -0.32727,3.04332 0.003,1.14081 0.0474,1.64979 0.219,2.484 0.66186,3.21606 2.73378,5.52453 5.53848,6.17091 1.08297,0.2496 2.75193,0.2487 4.0035,-0.002 1.00905,-0.2022 2.0469,-0.61467 2.75373,-1.09416 0.1632,-0.1107 0.2199,-0.1335 0.2427,-0.0966 0.078,0.126 0.72657,1.86156 0.71097,1.90221 -0.0303,0.0783 -1.32156,0.69663 -1.78362,0.85386 -1.44879,0.49299 -2.84556,0.67896 -4.59144,0.61131 z"
id="path1872" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="m 301.06447,189.54269 c -3.29796,-0.1524 -5.33844,-2.26452 -6.00708,-6.21795 -0.2553,-1.50936 -0.2715,-2.04396 -0.2718,-9.01284 l -3.6e-4,-6.0342 h 1.08729 1.08729 l 0.015,6.34275 c 0.015,6.81669 0.009,6.6264 0.2052,7.83087 0.33183,2.04864 1.04052,3.47316 2.14131,4.3041 0.35019,0.2643 0.91161,0.52767 1.40709,0.65994 0.42285,0.1128 0.43758,0.114 1.29882,0.1149 0.95967,6e-4 1.26414,-0.042 1.95999,-0.2739 1.55616,-0.5187 2.95113,-1.77549 3.72168,-3.35301 0.35256,-0.72171 0.55941,-1.45686 0.65067,-2.31246 0.024,-0.2295 0.0378,-2.61342 0.0378,-6.83271 v -6.4803 h 1.08804 1.08807 l 0.012,8.33904 c 0.012,7.66665 0.018,8.37378 0.0744,8.77035 0.1428,0.98406 0.39552,2.082 0.69543,3.02172 0.0879,0.2754 0.1566,0.50394 0.1527,0.5076 -0.003,0.003 -0.43185,0.1152 -0.95088,0.2481 -0.51903,0.1329 -0.96993,0.249 -1.002,0.2586 -0.1329,0.0384 -0.64872,-2.03064 -0.84747,-3.39957 -0.0498,-0.34338 -0.0975,-0.63111 -0.1056,-0.63945 -0.009,-0.009 -0.0678,0.0768 -0.132,0.1893 -0.30909,0.54027 -1.0674,1.49451 -1.52232,1.91568 -1.16817,1.08153 -2.46564,1.71837 -4.01253,1.96953 -0.47538,0.0771 -1.25811,0.1125 -1.86924,0.0843 z"
id="path1874" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="m 316.34506,181.51667 c -3e-5,-8.6226 -0.024,-9.77421 -0.255,-11.5875 -0.0678,-0.53586 -0.2301,-1.47663 -0.2745,-1.59234 -0.021,-0.0537 0.0627,-0.0591 0.90414,-0.0591 h 0.92682 l 0.0792,0.2088 c 0.1203,0.31725 0.2739,0.95907 0.34128,1.42461 0.0558,0.38475 0.0864,0.78735 0.1572,2.05071 l 0.027,0.47187 0.105,-0.2721 c 0.1659,-0.43044 0.62172,-1.30971 0.8892,-1.71555 0.95484,-1.44867 2.16645,-2.28351 3.70614,-2.55366 0.42108,-0.0738 1.39458,-0.0777 1.74222,-0.006 l 0.2358,0.048 0.009,0.97368 0.009,0.97368 -0.1005,-0.021 c -0.41964,-0.0825 -1.16352,-0.0432 -1.70343,0.09 -1.25616,0.30921 -2.35041,1.12935 -3.21057,2.4063 -0.59457,0.88263 -0.99009,1.89357 -1.21293,3.10002 -0.1968,1.06509 -0.1983,1.1235 -0.1983,7.58067 v 5.9649 h -1.08888 -1.08888 l -3e-5,-7.48608 z"
id="path1876" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0362961;stroke-linejoin:bevel"
d="m 335.98129,189.54323 c -2.62656,-0.1134 -4.88073,-1.18362 -6.53217,-3.10116 -1.32438,-1.53783 -2.06556,-3.27531 -2.38359,-5.5878 -0.0924,-0.67071 -0.0936,-2.70963 -0.002,-3.46626 0.1236,-1.02396 0.2679,-1.73994 0.51606,-2.55945 0.8235,-2.72073 2.61351,-4.99845 4.80111,-6.1092 1.33455,-0.67758 2.90184,-0.98193 4.5321,-0.88008 1.7079,0.1068 3.00651,0.5583 4.1319,1.43682 0.32757,0.2559 0.8991,0.83427 1.14711,1.16121 1.1439,1.50795 1.8381,3.59547 2.1003,6.31554 0.0504,0.52383 0.1098,1.49682 0.1098,1.8057 l 1.2e-4,0.2451 h -7.6278 -7.6278 l 0.024,0.48375 c 0.075,1.56153 0.44094,3.03714 1.05954,4.27104 0.37419,0.74634 0.79491,1.35066 1.32336,1.90071 1.01376,1.05522 2.19021,1.68849 3.65853,1.96929 0.42336,0.081 0.62955,0.0978 1.37457,0.1134 0.52479,0.012 1.07097,0.001 1.3611,-0.024 1.61388,-0.1401 3.16101,-0.51303 4.7805,-1.15224 0.32652,-0.129 0.59898,-0.2292 0.60546,-0.2226 0.006,0.006 0.0453,0.38718 0.0864,0.84603 0.0411,0.45885 0.0822,0.88797 0.0912,0.95361 0.015,0.1149 0.009,0.1227 -0.1902,0.2121 -1.7916,0.80517 -3.98541,1.31487 -5.92326,1.37613 -0.3294,0.012 -0.65604,0.024 -0.72594,0.027 -0.0699,0.006 -0.38019,-0.002 -0.68961,-0.015 z m 6.23133,-13.18554 c -0.0591,-1.16901 -0.35121,-2.34747 -0.80586,-3.2526 -0.45792,-0.9117 -1.23573,-1.81122 -2.03418,-2.35245 -1.01961,-0.69114 -2.37819,-1.02192 -3.76728,-0.91722 -0.9042,0.0681 -1.58085,0.2487 -2.32809,0.62076 -0.58911,0.2934 -1.0392,0.62271 -1.5618,1.14261 -1.04076,1.03533 -1.72257,2.19609 -2.13825,3.64038 -0.1272,0.44184 -0.31065,1.31451 -0.31065,1.47759 v 0.0906 h 6.48444 6.48444 z"
id="path1878" />
<path
style="fill:#ff7f2a;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.00620118;stroke-linejoin:bevel"
d="m 50.862044,189.3734 c -0.303291,-0.024 -0.554784,-0.1056 -0.78756,-0.252 -0.11526,-0.0726 -0.21627,-0.1548 -0.33231,-0.2706 -0.22431,-0.2238 -0.366474,-0.43287 -0.476934,-0.70161 -0.18369,-0.44685 -0.20955,-1.00887 -0.06888,-1.4958 0.1251,-0.43269 0.40821,-0.85758 0.757158,-1.13604 0.412605,-0.32928 0.936354,-0.43947 1.454181,-0.30603 0.21978,0.0567 0.4365,0.1665 0.641811,0.32499 0.09129,0.0705 0.301623,0.2802 0.381936,0.38106 0.27891,0.34977 0.429213,0.70716 0.48114,1.14414 0.0111,0.0903 0.0111,0.39948 0,0.48987 -0.06159,0.51813 -0.26865,0.92478 -0.661704,1.29948 -0.326436,0.31116 -0.709485,0.48696 -1.133997,0.52047 -0.07446,0.006 -0.19752,0.006 -0.25488,0.002 z"
id="path2003" />
<path
style="fill:#ff7f2a;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0109968;stroke-linejoin:bevel"
d="m 216.94123,189.36752 c -0.51726,-0.0315 -0.97512,-0.2949 -1.32675,-0.76323 -0.2478,-0.32988 -0.37818,-0.68661 -0.4131,-1.12965 -0.0519,-0.65604 0.1791,-1.2891 0.64977,-1.78191 0.2568,-0.2688 0.57717,-0.44529 0.92586,-0.5097 0.1413,-0.027 0.42816,-0.027 0.57111,-6e-4 0.38352,0.0693 0.71907,0.2577 1.02084,0.5739 0.40176,0.42084 0.61104,0.9003 0.63585,1.45689 0.027,0.5913 -0.1734,1.12062 -0.58497,1.54935 -0.42063,0.43821 -0.91278,0.63957 -1.47861,0.60495 z"
id="path2005" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0225439;stroke-linejoin:bevel"
d="m 102.26473,163.77071 c -0.48993,-0.096 -0.86955,-0.37644 -1.06938,-0.7896 -0.174,-0.35997 -0.2061,-0.52104 -0.2055,-1.03449 4.5e-4,-0.38628 0.006,-0.46284 0.0522,-0.63123 0.1728,-0.64161 0.60519,-1.10346 1.17597,-1.2561 0.183,-0.0489 0.65316,-0.054 0.83415,-0.009 0.1815,0.045 0.46689,0.1851 0.6063,0.2973 0.31089,0.2505 0.5793,0.66225 0.68235,1.04703 0.0705,0.2631 0.0708,0.84519 5.4e-4,1.10466 -0.1764,0.65178 -0.62106,1.10604 -1.22382,1.25055 -0.2031,0.0486 -0.65523,0.06 -0.85272,0.021 z"
id="path2161" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="heading"
transform="translate(144.84863,-39.0695)">
<g
aria-label="Infix"
id="text1160"
style="font-size:25.4px;font-family:'URW Bookman';-inkscape-font-specification:'URW Bookman, Normal';fill:#5c5f5c;stroke-width:1.029"
inkscape:label="infix"
transform="matrix(6,0,0,6,-498.93695,-594.0847)">
<path
d="m 85.532731,107.6593 0.508,0.0762 c 1.3462,0.1778 1.6256,0.635 1.651,2.794 v 9.9822 c -0.0254,2.159 -0.3048,2.6162 -1.651,2.794 l -0.508,0.0762 v 0.7874 h 6.858 v -0.7874 l -0.508,-0.0762 c -1.3462,-0.1778 -1.6256,-0.635 -1.651,-2.794 v -9.9822 c 0.0254,-2.159 0.3048,-2.6162 1.651,-2.794 l 0.508,-0.0762 v -0.7874 h -6.858 z"
id="path2431" />
<path
d="m 98.588334,111.8503 h -0.8636 c -1.5748,0.3048 -2.413,0.4064 -3.8354,0.508 v 0.762 l 0.6096,0.0508 c 1.3462,0.1524 1.6764,0.5588 1.6764,2.1082 v 5.969 c 0,1.397 -0.4572,2.0066 -1.5748,2.1082 l -0.635,0.0508 v 0.762 h 6.832596 v -0.762 l -0.635,-0.0508 c -1.117596,-0.1016 -1.574796,-0.7112 -1.574796,-2.1082 v -6.2738 c 1.574796,-1.4224 3.073396,-2.1336 4.495796,-2.1336 1.397,0 2.159,0.889 2.159,2.54 v 5.8674 c 0,1.397 -0.4826,2.0066 -1.5748,2.1082 l -0.635,0.0508 v 0.762 h 6.8326 v -0.762 l -0.635,-0.0508 c -1.1176,-0.1016 -1.5748,-0.7112 -1.5748,-2.1082 v -6.2738 c 0,-2.1082 -1.4224,-3.3528 -3.8354,-3.3528 -1.9558,0 -3.429,0.635 -5.232396,2.2352 z"
id="path2433" />
<path
d="m 115.27613,111.8503 v -1.5748 c 0,-1.651 0.0508,-1.9812 0.4064,-2.5908 0.4826,-0.8382 1.4986,-1.3462 2.6416,-1.3462 1.27,0 2.4638,0.762 2.4638,1.6256 0.0254,1.0414 0.0254,1.0414 0.2286,1.397 0.2286,0.4064 0.635,0.635 1.143,0.635 0.762,0 1.27,-0.5334 1.27,-1.27 0,-0.5842 -0.2286,-1.0668 -0.762,-1.6256 -1.016,-1.0414 -2.413,-1.5748 -4.191,-1.5748 -1.9558,0 -3.7338,0.6604 -4.6482,1.7272 -0.6858,0.8128 -0.9652,1.8542 -0.9652,3.6322 v 0.9652 h -2.159 v 0.8382 h 2.159 v 8.5598 c 0,1.397 -0.4572,2.0066 -1.5748,2.1082 l -0.635,0.0508 v 0.762 h 6.8326 v -0.762 l -0.635,-0.0508 c -1.1176,-0.1016 -1.5748,-0.7112 -1.5748,-2.1082 v -8.5598 h 3.3782 c 1.8288,0 2.3368,0.5842 2.3114,2.667 v 5.8928 c 0,1.397 -0.4572,2.0066 -1.5748,2.1082 l -0.635,0.0508 v 0.762 h 6.8326 v -0.762 l -0.635,-0.0508 c -1.1176,-0.1016 -1.5748,-0.7112 -1.5748,-2.1082 v -9.398 z"
id="path2435" />
<path
d="m 134.09755,117.0827 2.2098,-2.3622 c 1.397,-1.3716 2.1082,-1.778 3.81,-2.1082 v -0.762 h -5.3594 v 0.762 c 0.9144,0.1016 1.143,0.2286 1.143,0.6604 0,0.2032 -0.0762,0.4064 -0.254,0.6096 l -2.159,2.4384 -2.0066,-2.4384 c -0.1778,-0.254 -0.3048,-0.4826 -0.3048,-0.635 0,-0.3556 0.3048,-0.5334 1.0414,-0.6096 l 0.254,-0.0254 v -0.762 h -6.2992 v 0.762 c 1.2192,0.1778 1.7018,0.4826 2.5908,1.5494 l 2.9718,3.7338 -3.2258,3.6322 c -1.143,1.2954 -1.8034,1.7018 -3.0226,1.8796 v 0.762 h 5.6896 v -0.762 c -0.889,-0.1524 -1.0922,-0.2032 -1.27,-0.3048 -0.254,-0.1524 -0.4318,-0.4064 -0.4318,-0.6604 0,-0.254 0.1778,-0.5842 0.4572,-0.9144 l 2.54,-2.8448 2.286,3.2004 c 0.1524,0.2286 0.254,0.4826 0.254,0.6858 0,0.4826 -0.3048,0.6604 -1.524,0.8382 v 0.762 h 7.0358 v -0.762 c -1.3716,-0.2286 -1.8288,-0.5334 -2.9464,-1.8796 z"
id="path2437" />
</g>
</g>
<g
inkscape:label="tux"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(144.84863,-39.0695)">
<path
id="rect1698"
style="fill:#f9f9f9;stroke:#5c5f5c;stroke-width:6.174;stroke-linejoin:round"
inkscape:label="body"
d="M -141.76163,48.090612 H -6.9369515 V 149.9153 H -141.76163 Z" />
<path
id="rect184-3"
style="fill:#ff7f2a;stroke:#5c5f5c;stroke-width:6;stroke-linejoin:round"
inkscape:label="foot-r"
d="m -42.936952,125.9153 h 36.0000005 v 24 H -42.936952 Z" />
<path
id="rect184"
style="fill:#ff7f2a;stroke:#5c5f5c;stroke-width:6;stroke-linejoin:round"
inkscape:label="foot-l"
d="m -141.76163,125.9153 h 36 v 24 h -36 z" />
<path
id="path11801"
style="fill:#ff7f2a;stroke-width:6"
inkscape:label="nose"
inkscape:transform-center-y="2.9490877"
d="M -74.349298,107.85026 -84.565276,90.155655 h 20.43195 z" />
<path
id="path6519-5"
style="display:inline;fill:#5c5f5c;stroke-width:29.9999"
inkscape:label="eye-r"
d="m -56.67046,72.046755 a 3,3 0 0 1 -3,3 3,3 0 0 1 -3,-3 3,3 0 0 1 3,-3 3,3 0 0 1 3,3 z" />
<path
id="path6519"
style="fill:#5c5f5c;stroke-width:29.9999"
inkscape:label="eye-l"
d="m -86.028172,72.046755 a 3,3 0 0 1 -3,3 3,3 0 0 1 -3,-3 3,3 0 0 1 3,-3 3,3 0 0 1 3,3 z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 45 KiB

-312
View File
@@ -1,312 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="163.09068mm"
height="53.164967mm"
viewBox="0 0 163.09068 53.164966"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="logo-medium.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="1.4929971"
inkscape:cx="304.08633"
inkscape:cy="286.0019"
inkscape:window-width="2560"
inkscape:window-height="1385"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer6" />
<defs
id="defs2" />
<g
inkscape:groupmode="layer"
id="layer6"
inkscape:label="bg"
style="display:none;fill:#ff0000;fill-opacity:1"
transform="matrix(1.2831201,0,0,3.2117929,-44.932163,-316.63259)">
<rect
style="fill:#22272e;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1"
id="rect10264"
width="163.66356"
height="92.471725"
x="20.800945"
y="69.866936"
inkscape:label="bg" />
</g>
<g
inkscape:groupmode="layer"
id="layer5"
inkscape:label="tagline"
style="display:inline"
transform="translate(-18.242052,-92.23446)">
<g
aria-label="Immutable . Friendly . Secure"
transform="matrix(1.9025756,0,0,2.1024132,-99.78739,-118.81694)"
id="text9503"
style="font-size:6.44339px;font-family:Laksaman;-inkscape-font-specification:'Laksaman, Normal';stroke:#5c5f5c;stroke-width:0;stroke-miterlimit:0"
inkscape:label="linux-netconf"
inkscape:export-filename="../../../Pictures/logo-medium.svg"
inkscape:export-xdpi="191.95932"
inkscape:export-ydpi="191.95932">
<path
d="m 62.472956,124.15871 h -0.386604 v -4.61347 h 0.386604 z"
id="path2380" />
<path
d="m 67.144418,120.79526 q 0.573462,0 0.869857,0.4317 0.296396,0.43171 0.296396,1.10182 v 1.82993 h -0.386603 v -1.82348 q 0,-0.54125 -0.219075,-0.8763 -0.212632,-0.3415 -0.663669,-0.3415 -0.367274,0 -0.657226,0.28995 -0.289953,0.28995 -0.289953,0.65722 v 2.09411 h -0.386603 v -1.99745 q 0,-1.04383 -0.831197,-1.04383 -0.38016,0 -0.689443,0.32861 -0.309283,0.32861 -0.309283,0.71522 v 1.99745 H 63.491016 V 121.678 q 0,-0.28995 -0.06443,-0.81187 h 0.354386 q 0.06443,0.19975 0.07732,0.58635 0.373717,-0.65722 1.108263,-0.65722 0.670113,0 0.998725,0.73454 0.386604,-0.73454 1.179141,-0.73454 z"
id="path2382" />
<path
d="m 72.969238,120.79526 q 0.573462,0 0.869858,0.4317 0.296396,0.43171 0.296396,1.10182 v 1.82993 h -0.386604 v -1.82348 q 0,-0.54125 -0.219075,-0.8763 -0.212632,-0.3415 -0.663669,-0.3415 -0.367273,0 -0.657226,0.28995 -0.289953,0.28995 -0.289953,0.65722 v 2.09411 h -0.386603 v -1.99745 q 0,-1.04383 -0.831197,-1.04383 -0.38016,0 -0.689443,0.32861 -0.309283,0.32861 -0.309283,0.71522 v 1.99745 H 69.315836 V 121.678 q 0,-0.28995 -0.06443,-0.81187 h 0.354386 q 0.06443,0.19975 0.07732,0.58635 0.373717,-0.65722 1.108263,-0.65722 0.670113,0 0.998726,0.73454 0.386603,-0.73454 1.17914,-0.73454 z"
id="path2384" />
<path
d="m 78.078841,124.14582 -0.36083,0.0838 q -0.115981,-0.32861 -0.154641,-0.64434 -0.431707,0.66367 -1.237131,0.66367 -1.185584,0 -1.185584,-1.53352 v -1.84926 h 0.386604 v 1.77838 q 0,1.28223 0.882744,1.28223 0.457481,0 0.786094,-0.30928 0.328613,-0.31572 0.328613,-0.76032 v -1.99101 h 0.386603 v 2.48715 q 0,0.34795 0.167528,0.79254 z"
id="path2386" />
<path
d="m 79.973201,124.24891 q -0.39949,0 -0.625009,-0.29639 -0.219075,-0.30284 -0.219075,-0.76032 v -2.03611 h -0.657226 v -0.28996 h 0.657226 v -0.97939 l 0.386603,-0.17397 v 1.15336 h 1.050273 v 0.28996 H 79.51572 v 2.03611 q 0,0.73454 0.489698,0.73454 0.302839,0 0.444594,-0.058 l 0.03866,0.28996 q -0.199745,0.0902 -0.515471,0.0902 z"
id="path2388" />
<path
d="m 82.015758,124.24891 q -0.43815,0 -0.747433,-0.25129 -0.302839,-0.25773 -0.302839,-0.66367 0,-1.23068 2.190752,-1.23068 0,-0.47037 -0.225519,-0.72811 -0.225518,-0.25773 -0.663669,-0.25773 -0.354386,0 -1.050272,0.21907 l -0.0451,-0.36727 q 0.541245,-0.17397 1.12115,-0.17397 1.250018,0 1.250018,1.37888 v 1.17914 q 0,0.40594 0.115981,0.80543 l -0.335057,0.0709 -0.135311,-0.5348 q -0.444594,0.55413 -1.172697,0.55413 z m -0.663669,-0.97939 q 0,0.30284 0.186858,0.48325 0.186859,0.17397 0.489698,0.17397 0.386603,0 0.753876,-0.2384 0.373717,-0.24485 0.373717,-0.59279 v -0.67012 q -1.804149,0 -1.804149,0.84409 z"
id="path2390" />
<path
d="m 86.178189,120.79526 q 0.683,0 1.08249,0.47681 0.39949,0.47037 0.39949,1.19847 0,0.77965 -0.412377,1.28223 -0.412377,0.49614 -1.127593,0.49614 -0.766764,0 -1.224244,-0.67011 0,0.32861 -0.01933,0.57991 h -0.354387 q 0.02577,-0.22552 0.02577,-0.80543 v -4.09155 h 0.386603 v 2.22941 q 0.425264,-0.69588 1.243574,-0.69588 z m -1.243574,2.04255 q 0,0.46393 0.328613,0.77965 0.328613,0.30928 0.805424,0.30928 1.204914,0 1.204914,-1.44331 0,-0.61213 -0.30284,-0.98584 -0.302839,-0.38016 -0.863414,-0.38016 -0.489698,0 -0.831197,0.3415 -0.3415,0.3415 -0.3415,0.83119 z"
id="path2392" />
<path
d="m 89.696285,123.92674 -0.03866,0.32217 q -1.224244,-0.058 -1.224244,-1.74615 v -3.24103 h 0.386603 v 3.10571 q 0,0.36728 0.03222,0.61857 0.03222,0.25129 0.115981,0.48325 0.09021,0.23197 0.270623,0.34795 0.180415,0.10953 0.457481,0.10953 z"
id="path2394" />
<path
d="m 90.51459,122.54142 q 0.01933,0.625 0.373717,1.00516 0.36083,0.38016 0.960065,0.38016 0.560575,0 1.153367,-0.23196 l 0.03222,0.31573 q -0.573462,0.2384 -1.224245,0.2384 -0.740989,0 -1.211357,-0.45748 -0.470367,-0.46392 -0.470367,-1.19202 0,-0.77965 0.451037,-1.28868 0.457481,-0.51547 1.192027,-0.51547 1.385329,0 1.417546,1.74616 z m 2.287403,-0.32217 q -0.01289,-0.50259 -0.309282,-0.79898 -0.289953,-0.30284 -0.766764,-0.30284 -0.47681,0 -0.79898,0.31572 -0.322169,0.30929 -0.393047,0.7861 z"
id="path2396" />
<path
d="m 96.345854,123.54658 q 0.135311,0 0.238405,0.1031 0.103095,0.10309 0.103095,0.2384 0,0.13532 -0.103095,0.23197 -0.103094,0.0966 -0.238405,0.0966 -0.135311,0 -0.231962,-0.0966 -0.09665,-0.0967 -0.09665,-0.23197 0,-0.13531 0.09665,-0.2384 0.09665,-0.1031 0.231962,-0.1031 z"
id="path2398" />
<path
d="m 102.20934,121.94218 h -2.07477 v 2.21653 h -0.386608 v -4.61347 h 2.609578 v 0.32217 h -2.22297 v 1.7526 h 2.07477 z"
id="path2400" />
<path
d="m 104.6385,121.13031 q -0.0387,-0.0129 -0.11598,-0.0129 -0.42527,0 -0.72166,0.34794 -0.28996,0.3415 -0.28996,0.9214 v 1.77194 h -0.3866 v -2.21653 q 0,-0.7281 -0.0967,-1.07605 h 0.32861 q 0.1031,0.20619 0.1031,0.65723 0.30928,-0.7281 0.99228,-0.7281 0.0902,0 0.18686,0.0193 z"
id="path2402" />
<path
d="m 105.60501,124.15871 h -0.38661 v -3.29258 h 0.38661 z m -0.20619,-4.60058 q 0.13531,0 0.21907,0.0902 0.0902,0.0838 0.0902,0.21908 0,0.13531 -0.0902,0.21907 -0.0838,0.0773 -0.21907,0.0773 -0.13531,0 -0.21908,-0.0773 -0.0773,-0.0838 -0.0773,-0.21907 0,-0.13531 0.0773,-0.21908 0.0838,-0.0902 0.21908,-0.0902 z"
id="path2404" />
<path
d="m 106.75838,122.54142 q 0.0193,0.625 0.37371,1.00516 0.36083,0.38016 0.96007,0.38016 0.56057,0 1.15336,-0.23196 l 0.0322,0.31573 q -0.57346,0.2384 -1.22424,0.2384 -0.74099,0 -1.21136,-0.45748 -0.47037,-0.46392 -0.47037,-1.19202 0,-0.77965 0.45104,-1.28868 0.45748,-0.51547 1.19203,-0.51547 1.38533,0 1.41754,1.74616 z m 2.2874,-0.32217 q -0.0129,-0.50259 -0.30928,-0.79898 -0.28996,-0.30284 -0.76677,-0.30284 -0.47681,0 -0.79898,0.31572 -0.32217,0.30929 -0.39304,0.7861 z"
id="path2406" />
<path
d="m 113.00202,124.15871 h -0.3866 v -1.86859 q 0,-1.17269 -0.9794,-1.17269 -0.3866,0 -0.70233,0.32861 -0.31572,0.32861 -0.31572,0.71522 v 1.99745 h -0.38661 V 121.678 q 0,-0.28995 -0.0644,-0.81187 h 0.36083 q 0.0644,0.19975 0.0773,0.58635 0.37372,-0.65722 1.10826,-0.65722 1.28868,0 1.28868,1.49486 z"
id="path2408" />
<path
d="m 113.77523,122.47054 q 0,-0.72166 0.39949,-1.19847 0.40593,-0.47681 1.08249,-0.47681 0.81831,0 1.24357,0.69588 v -2.22941 h 0.38661 v 4.09155 q 0,0.57991 0.0258,0.80543 h -0.35439 q -0.0193,-0.2513 -0.0193,-0.57991 -0.45748,0.67011 -1.22424,0.67011 -0.72166,0 -1.13404,-0.49614 -0.40593,-0.50258 -0.40593,-1.28223 z m 1.59152,1.4562 q 0.47681,0 0.80542,-0.30928 0.32861,-0.31572 0.32861,-0.77965 v -0.54769 q 0,-0.49614 -0.3415,-0.83119 -0.33505,-0.3415 -0.83119,-0.3415 -0.56058,0 -0.86342,0.38016 -0.30284,0.38016 -0.30284,0.98584 0,1.44331 1.20492,1.44331 z"
id="path2410" />
<path
d="m 119.15547,123.92674 -0.0387,0.32217 q -1.22424,-0.058 -1.22424,-1.74615 v -3.24103 h 0.3866 v 3.10571 q 0,0.36728 0.0322,0.61857 0.0322,0.25129 0.11598,0.48325 0.0902,0.23197 0.27062,0.34795 0.18042,0.10953 0.45749,0.10953 z"
id="path2412" />
<path
d="m 121.10781,124.15871 q -0.57346,1.28867 -1.52064,1.51419 l -0.0902,-0.30284 q 0.82476,-0.18685 1.25002,-1.32089 0,-0.058 -0.0258,-0.11598 l -1.366,-3.06706 h 0.40593 l 1.23069,2.78999 1.15337,-2.78999 h 0.41237 z"
id="path2414" />
<path
d="m 125.44421,123.54658 q 0.13531,0 0.2384,0.1031 0.1031,0.10309 0.1031,0.2384 0,0.13532 -0.1031,0.23197 -0.10309,0.0966 -0.2384,0.0966 -0.13531,0 -0.23196,-0.0966 -0.0966,-0.0967 -0.0966,-0.23197 0,-0.13531 0.0966,-0.2384 0.0966,-0.1031 0.23196,-0.1031 z"
id="path2416" />
<path
d="m 131.42367,122.95379 q 0,0.59924 -0.45103,0.94718 -0.4446,0.34794 -1.11471,0.34794 -0.76032,0 -1.26935,-0.35438 l 0.15464,-0.32862 q 0.47681,0.36083 1.15337,0.36083 0.50903,0 0.82475,-0.24484 0.31573,-0.24485 0.31573,-0.69589 0,-0.43815 -0.25129,-0.65723 -0.2513,-0.22551 -0.75388,-0.38016 -1.28868,-0.41882 -1.28868,-1.30156 0,-0.54125 0.41238,-0.85697 0.41238,-0.32217 1.01805,-0.32217 0.64434,0 1.05672,0.24485 -0.0129,0.0258 -0.0773,0.14175 -0.058,0.10954 -0.0838,0.16109 -0.41882,-0.22552 -0.90208,-0.22552 -0.45104,0 -0.74743,0.21263 -0.28995,0.21263 -0.28995,0.59924 0,0.63789 0.99872,0.98583 0.32217,0.11599 0.51547,0.21264 0.19975,0.0902 0.39949,0.24484 0.19975,0.15465 0.28995,0.38016 0.0902,0.22552 0.0902,0.52836 z"
id="path2418" />
<path
d="m 132.38373,122.54142 q 0.0193,0.625 0.37372,1.00516 0.36083,0.38016 0.96006,0.38016 0.56058,0 1.15337,-0.23196 l 0.0322,0.31573 q -0.57347,0.2384 -1.22425,0.2384 -0.74099,0 -1.21136,-0.45748 -0.47036,-0.46392 -0.47036,-1.19202 0,-0.77965 0.45103,-1.28868 0.45749,-0.51547 1.19203,-0.51547 1.38533,0 1.41755,1.74616 z m 2.2874,-0.32217 q -0.0129,-0.50259 -0.30928,-0.79898 -0.28995,-0.30284 -0.76676,-0.30284 -0.47681,0 -0.79898,0.31572 -0.32217,0.30929 -0.39305,0.7861 z"
id="path2420" />
<path
d="m 137.3387,121.11743 q -0.61212,0 -0.97295,0.3866 -0.36083,0.3866 -0.36083,1.01806 0,0.625 0.36727,1.01805 0.36727,0.3866 0.96651,0.3866 0.54124,0 0.90207,-0.23196 l 0.13531,0.31573 q -0.42526,0.2384 -1.01805,0.2384 -0.79898,0 -1.26935,-0.46392 -0.47037,-0.47037 -0.47037,-1.2629 0,-0.79254 0.47037,-1.25647 0.47037,-0.47036 1.26935,-0.47036 0.59923,0 1.01805,0.23196 l -0.13531,0.32217 q -0.36083,-0.23196 -0.90207,-0.23196 z"
id="path2422" />
<path
d="m 141.99727,124.14582 -0.36083,0.0838 q -0.11598,-0.32861 -0.15464,-0.64434 -0.4317,0.66367 -1.23713,0.66367 -1.18558,0 -1.18558,-1.53352 v -1.84926 h 0.3866 v 1.77838 q 0,1.28223 0.88275,1.28223 0.45748,0 0.78609,-0.30928 0.32861,-0.31572 0.32861,-0.76032 v -1.99101 h 0.38661 v 2.48715 q 0,0.34795 0.16752,0.79254 z"
id="path2424" />
<path
d="m 144.34911,121.13031 q -0.0387,-0.0129 -0.11598,-0.0129 -0.42526,0 -0.72166,0.34794 -0.28995,0.3415 -0.28995,0.9214 v 1.77194 h -0.38661 v -2.21653 q 0,-0.7281 -0.0967,-1.07605 h 0.32862 q 0.10309,0.20619 0.10309,0.65723 0.30928,-0.7281 0.99228,-0.7281 0.0902,0 0.18686,0.0193 z"
id="path2426" />
<path
d="m 145.08366,122.54142 q 0.0193,0.625 0.37372,1.00516 0.36083,0.38016 0.96006,0.38016 0.56058,0 1.15337,-0.23196 l 0.0322,0.31573 q -0.57346,0.2384 -1.22424,0.2384 -0.74099,0 -1.21136,-0.45748 -0.47036,-0.46392 -0.47036,-1.19202 0,-0.77965 0.45103,-1.28868 0.45748,-0.51547 1.19203,-0.51547 1.38533,0 1.41755,1.74616 z m 2.2874,-0.32217 q -0.0129,-0.50259 -0.30928,-0.79898 -0.28995,-0.30284 -0.76676,-0.30284 -0.47681,0 -0.79898,0.31572 -0.32217,0.30929 -0.39305,0.7861 z"
id="path2428" />
</g>
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.102789;stroke-linejoin:bevel"
d="m 18.4196,137.38387 v -4.77968 h 0.308366 0.308367 v 4.77968 4.77967 H 18.727966 18.4196 Z"
id="path1834" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.102789;stroke-linejoin:bevel"
d="m 21.091489,139.61951 c -3.38e-4,-1.3992 -0.03064,-2.92938 -0.06727,-3.40038 l -0.06666,-0.85636 0.298551,0.0341 c 0.282601,0.0322 0.300207,0.0629 0.329618,0.5737 0.01709,0.2968 0.06862,0.53964 0.114506,0.53964 0.04588,0 0.205999,-0.17686 0.355801,-0.39302 0.483621,-0.69786 1.412484,-1.05856 2.188136,-0.8497 0.45557,0.12267 1.080087,0.68663 1.271148,1.14789 0.06844,0.16522 0.153512,0.29916 0.189056,0.29764 0.03556,-0.002 0.179948,-0.18963 0.320907,-0.41803 0.86151,-1.39595 2.779512,-1.46325 3.563747,-0.12505 0.452284,0.77176 0.545121,1.41843 0.546878,3.80939 l 0.0016,2.18426 H 29.829126 29.52076 v -2.04174 c 0,-2.14263 -0.08611,-2.90488 -0.396013,-3.50568 -0.516064,-1.00047 -1.774417,-1.11815 -2.605103,-0.24362 -0.549672,0.57867 -0.596487,0.84739 -0.596487,3.4236 v 2.36744 h -0.299165 -0.299159 l -0.03826,-2.54402 c -0.04133,-2.74801 -0.08797,-3.00834 -0.62865,-3.50672 -0.282733,-0.26061 -0.991065,-0.39732 -1.455166,-0.28084 -0.383762,0.0963 -1.084458,0.76545 -1.302194,1.24354 -0.163359,0.35869 -0.187542,0.70953 -0.189511,2.7496 l -0.0021,2.33844 H 21.40057 21.092204 Z"
id="path1836" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.102789;stroke-linejoin:bevel"
d="m 32.183632,139.72231 c -0.0053,-1.34268 -0.04001,-2.8691 -0.07709,-3.39205 l -0.06744,-0.95083 0.280712,3e-5 c 0.304086,3e-5 0.360744,0.11938 0.411411,0.86665 l 0.02741,0.40408 0.301149,-0.43628 c 0.530823,-0.76899 1.484852,-1.16209 2.292519,-0.94461 0.428345,0.11535 1.057302,0.70156 1.282123,1.19498 0.06301,0.1383 0.141838,0.24999 0.175165,0.24821 0.03334,-0.002 0.183076,-0.20523 0.332783,-0.45208 0.396927,-0.6545 1.092004,-1.03426 1.893903,-1.03474 0.708406,-4.2e-4 1.176676,0.24608 1.576827,0.83007 0.475265,0.6936 0.554132,1.18164 0.600831,3.71796 l 0.04397,2.38984 h -0.317982 -0.317986 l -0.0032,-1.8245 c -0.0037,-2.1373 -0.115041,-3.15188 -0.409073,-3.72823 -0.565943,-1.10934 -2.06848,-1.08989 -2.849885,0.0369 l -0.284083,0.40965 -0.03074,2.55309 -0.03074,2.5531 h -0.290708 -0.290709 l -0.04487,-2.49263 c -0.04069,-2.26087 -0.06513,-2.53776 -0.26261,-2.97813 -0.622786,-1.38872 -2.340472,-1.16857 -3.146657,0.40331 -0.142039,0.27694 -0.16872,0.70364 -0.16872,2.6982 v 2.36925 h -0.308367 -0.308366 z"
id="path1838" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.102789;stroke-linejoin:bevel"
d="m 44.682109,142.21471 c -0.226134,-0.08 -0.55853,-0.30531 -0.738663,-0.50077 -0.573696,-0.62254 -0.673704,-1.15865 -0.726245,-3.89323 l -0.04688,-2.44123 h 0.319056 0.319056 l 0.0016,1.67032 c 0.0021,2.07123 0.132393,3.36977 0.387181,3.86361 0.598879,1.16075 2.276777,1.12311 3.141948,-0.0705 l 0.323623,-0.44647 0.0308,-2.50848 0.0308,-2.50849 h 0.354652 0.354653 l 0.0026,2.90378 c 0.0016,1.59708 0.04958,3.108 0.107119,3.35759 0.08069,0.35009 0.07425,0.4734 -0.0282,0.53951 -0.292529,0.18877 -0.440082,0.0295 -0.581883,-0.62829 -0.130079,-0.60338 -0.154559,-0.64319 -0.283675,-0.4613 -0.769922,1.0846 -1.884786,1.50683 -2.967652,1.12394 z"
id="path1840" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 52.2447,142.39185 c -0.276966,-0.0172 -0.528791,-0.10938 -0.73742,-0.26992 -0.03762,-0.0289 -0.112533,-0.0992 -0.16645,-0.15621 -0.271129,-0.28643 -0.449294,-0.69967 -0.537929,-1.2477 l -0.02741,-0.16938 -0.0037,-2.32597 -0.0037,-2.32597 h -0.622729 -0.622729 v -0.29642 -0.29642 h 0.623084 0.623083 v -1.03151 -1.03151 l 0.356965,-0.17836 c 0.196331,-0.0981 0.359664,-0.17836 0.362961,-0.17836 0.0032,0 0.006,0.54444 0.006,1.20987 v 1.20987 h 0.998141 0.99814 v 0.29642 0.29642 h -0.998436 -0.998432 l 0.0037,2.34411 c 0.0042,2.51988 0.0016,2.40294 0.06325,2.67345 0.08484,0.37068 0.250846,0.6229 0.484198,0.73565 0.132477,0.064 0.213006,0.078 0.450972,0.0782 0.298259,2.5e-4 0.536226,-0.0288 0.701241,-0.0856 0.03715,-0.0127 0.06891,-0.0218 0.07065,-0.0201 0.0016,0.002 0.01889,0.13378 0.0381,0.29343 l 0.03493,0.29027 -0.02498,0.0147 c -0.04154,0.0245 -0.176763,0.0737 -0.272986,0.0993 -0.224272,0.0597 -0.538226,0.0879 -0.798513,0.0717 z"
id="path1842" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 56.097753,142.39143 c -0.705813,-0.0372 -1.3422,-0.43424 -1.63658,-1.0211 -0.127498,-0.25417 -0.184251,-0.49817 -0.19294,-0.82947 -0.0086,-0.32827 0.02678,-0.59387 0.114131,-0.85727 0.183351,-0.55276 0.567785,-0.97115 1.166283,-1.26932 0.6311,-0.31439 1.474068,-0.48224 2.538513,-0.50543 l 0.343615,-0.008 -0.0077,-0.19473 c -0.04038,-1.02769 -0.479198,-1.67898 -1.242732,-1.84441 -0.06325,-0.0137 -0.189585,-0.0311 -0.280744,-0.0387 -0.44059,-0.0367 -1.094666,0.0916 -1.974221,0.38728 -0.09933,0.0334 -0.181985,0.0593 -0.183663,0.0577 -0.0016,-0.002 -0.01979,-0.15543 -0.04027,-0.34165 -0.02043,-0.18623 -0.03995,-0.35533 -0.04334,-0.37579 l -0.0061,-0.0372 0.196754,-0.0621 c 0.637069,-0.20122 1.231562,-0.29519 1.878468,-0.29691 0.408887,-10e-4 0.655648,0.0319 0.941155,0.12559 0.775726,0.25472 1.244626,0.86919 1.412033,1.85038 0.06374,0.37359 0.06299,0.35157 0.07318,2.15967 0.0053,0.9316 0.01259,1.71287 0.0163,1.73616 0.0037,0.0233 0.0154,0.12401 0.02598,0.22383 0.02461,0.23218 0.07722,0.54331 0.130916,0.77422 0.02318,0.0998 0.04043,0.18312 0.03831,0.18523 -0.0053,0.005 -0.615744,0.14743 -0.618146,0.1441 -0.0011,-0.002 -0.05738,-0.24757 -0.125249,-0.54701 -0.06786,-0.29945 -0.126195,-0.55261 -0.129624,-0.56259 -0.0048,-0.0137 -0.01275,-0.009 -0.0326,0.018 -0.132154,0.18141 -0.327337,0.39131 -0.492088,0.52919 -0.523002,0.43768 -1.14718,0.63796 -1.869694,0.5999 z m 0.49258,-0.68097 c 0.347737,-0.0524 0.68534,-0.18588 1.008036,-0.39851 0.430234,-0.28349 0.68181,-0.59654 0.788437,-0.98109 l 0.03355,-0.12099 0.0042,-0.82153 0.0042,-0.82154 -0.336365,0.008 c -1.883907,0.0422 -2.917237,0.54306 -3.083136,1.49441 -0.0236,0.13541 -0.02334,0.45028 5.29e-4,0.59023 0.09801,0.57468 0.455454,0.95443 0.989563,1.05132 0.120062,0.0218 0.447373,0.0219 0.591343,1.7e-4 z"
id="path1844" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 63.895733,142.39183 c -0.512996,-0.0248 -0.97916,-0.20035 -1.403445,-0.52838 -0.21852,-0.16894 -0.498946,-0.48244 -0.682139,-0.7626 l -0.07403,-0.11321 -0.0026,0.0769 c -0.0016,0.0423 -0.0084,0.26475 -0.01513,0.49432 -0.0068,0.22957 -0.01588,0.47049 -0.02021,0.53537 l -0.0078,0.11796 h -0.331491 -0.331491 l 0.0072,-0.0696 c 0.03223,-0.31049 0.0345,-0.6209 0.03879,-5.32947 l 0.0042,-4.88484 h 0.362511 0.362516 l 3.81e-4,2.34411 c 3.07e-4,1.84593 0.0037,2.33976 0.01513,2.32358 0.0081,-0.0113 0.05011,-0.0785 0.09338,-0.14934 0.126519,-0.20718 0.259122,-0.37486 0.440923,-0.55755 0.420206,-0.42226 0.852196,-0.63367 1.47067,-0.71971 0.156581,-0.0218 0.571093,-0.0184 0.744067,0.006 0.699278,0.0991 1.238552,0.41993 1.670484,0.99381 0.40286,0.53525 0.631761,1.14756 0.721233,1.92925 0.02132,0.18644 0.02154,0.96995 3.17e-4,1.16752 -0.06336,0.58955 -0.199898,1.08804 -0.420856,1.53653 -0.546757,1.10979 -1.440683,1.64733 -2.642653,1.58912 z m 0.456104,-0.6822 c 0.932228,-0.12168 1.512713,-0.67791 1.768841,-1.69492 0.159104,-0.63178 0.18716,-1.53034 0.06786,-2.17381 -0.15712,-0.84753 -0.581861,-1.50598 -1.161939,-1.8013 -0.306869,-0.15623 -0.580342,-0.21754 -0.973587,-0.21827 -0.33537,-5.3e-4 -0.537718,0.0369 -0.824669,0.15292 -0.308012,0.12454 -0.642567,0.38412 -0.882439,0.68466 -0.236876,0.2968 -0.412025,0.67489 -0.4876,1.05259 -0.05249,0.26234 -0.05806,0.39371 -0.05281,1.24616 0.0053,0.8672 0.0055,0.86875 0.08313,1.16148 0.189346,0.71374 0.738627,1.30423 1.410732,1.51655 0.08591,0.0271 0.292094,0.0709 0.396727,0.0843 0.104499,0.0133 0.525007,0.007 0.655732,-0.0104 z"
id="path1846" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 70.537912,142.37439 c -0.460163,-0.0669 -0.842968,-0.24895 -1.153102,-0.54846 -0.550248,-0.53142 -0.8383,-1.37267 -0.903409,-2.63839 -0.0075,-0.14657 -0.01217,-1.56597 -0.01222,-3.74757 l -1.16e-4,-3.51164 h 0.362548 0.362548 l 0.0048,3.63868 c 0.0048,3.71784 0.0042,3.70908 0.05117,4.13472 0.05598,0.5102 0.185235,1.02315 0.328575,1.30388 0.236347,0.46289 0.598599,0.69175 1.141487,0.72114 l 0.13962,0.008 -0.03567,0.32969 -0.03567,0.32969 -0.06764,-10e-4 c -0.0372,-5.3e-4 -0.119364,-0.009 -0.182579,-0.018 z"
id="path1848" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 74.687759,142.39206 c -0.438674,-0.0197 -0.854054,-0.122 -1.21919,-0.30032 -0.200157,-0.0977 -0.290745,-0.15309 -0.474625,-0.28996 -0.537962,-0.40042 -0.94334,-1.00186 -1.148583,-1.70412 -0.04112,-0.14071 -0.08724,-0.36753 -0.116316,-0.57204 -0.03048,-0.21438 -0.03752,-0.82089 -0.01259,-1.08201 0.06128,-0.64148 0.189405,-1.10602 0.440478,-1.59707 0.326755,-0.63905 0.811811,-1.13656 1.375394,-1.4107 0.410306,-0.19957 0.788723,-0.28155 1.294559,-0.28043 0.299021,5.3e-4 0.50546,0.0249 0.750411,0.088 0.882597,0.22757 1.469935,0.90241 1.741805,2.00129 0.10141,0.40989 0.154083,0.81677 0.182203,1.40744 l 0.0076,0.16031 h -2.548287 -2.548287 l 0.0061,0.0575 c 0.0032,0.0316 0.0093,0.11192 0.01323,0.17846 0.03365,0.57114 0.201612,1.13688 0.462777,1.55908 0.166042,0.26844 0.422667,0.54692 0.65513,0.71094 0.295079,0.20821 0.653066,0.34273 1.044744,0.39258 0.197739,0.0252 0.718386,0.0187 0.949505,-0.0119 0.512974,-0.0678 1.079934,-0.22232 1.531371,-0.41744 0.03217,-0.0139 0.06254,-0.0253 0.06752,-0.0253 0.0095,0 0.06935,0.63808 0.06055,0.6454 -0.0026,0.002 -0.0595,0.0274 -0.126047,0.0557 -0.61631,0.26231 -1.278409,0.41151 -1.911594,0.43076 -0.113125,0.004 -0.224731,0.008 -0.248025,0.009 -0.02328,0.002 -0.12673,-0.001 -0.229876,-0.005 z m 2.093034,-4.29643 c -1.32e-4,-0.16386 -0.04334,-0.48319 -0.09136,-0.67545 -0.112416,-0.44993 -0.307927,-0.791 -0.631222,-1.10118 -0.315393,-0.3026 -0.682963,-0.45906 -1.163383,-0.49521 -0.749454,-0.0564 -1.381448,0.23465 -1.862021,0.8575 -0.276368,0.35818 -0.472345,0.81818 -0.552365,1.29652 -0.01058,0.0631 -0.02196,0.12704 -0.02529,0.14202 l -0.0061,0.0272 h 2.165895 2.165895 l -3.7e-5,-0.0514 z"
id="path1850" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 89.992597,137.36668 v -4.84552 h 2.480231 2.480231 v 0.33272 0.33271 h -2.11727 -2.11727 v 1.8511 1.8511 h 1.972087 1.972088 v 0.33271 0.33271 h -1.972088 -1.972087 v 2.329 2.329 h -0.362961 -0.362961 z"
id="path1852" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 96.416876,139.62007 c -1.32e-4,-2.80904 -0.0053,-3.09289 -0.06728,-3.65078 -0.02027,-0.18271 -0.06885,-0.48979 -0.09429,-0.59606 l -0.01667,-0.0698 0.309875,0.003 0.309874,0.003 0.03889,0.11494 c 0.08135,0.24051 0.12917,0.57711 0.146098,1.02834 l 0.0088,0.23589 0.05851,-0.1391 c 0.194162,-0.46157 0.459602,-0.82564 0.777457,-1.0663 0.118655,-0.0898 0.374475,-0.2173 0.518943,-0.25855 0.237776,-0.0679 0.546878,-0.0903 0.771435,-0.0559 l 0.10586,0.0162 v 0.32596 0.32596 l -0.04535,-0.01 c -0.07363,-0.0156 -0.341916,-0.007 -0.456459,0.0142 -0.325395,0.061 -0.591582,0.20022 -0.844672,0.44186 -0.369946,0.35321 -0.60244,0.79574 -0.715364,1.36164 -0.07461,0.37386 -0.07142,0.26872 -0.0762,2.5131 l -0.0042,2.05375 h -0.362468 -0.362469 z"
id="path1854" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 100.40957,138.75804 v -3.45418 h 0.35691 0.35691 v 3.45418 3.45417 h -0.35691 -0.35691 z"
id="path1856" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 105.59385,142.39151 c -0.6654,-0.0283 -1.25771,-0.24307 -1.76229,-0.63905 -0.13023,-0.1022 -0.37081,-0.34141 -0.47457,-0.47185 -0.53397,-0.67131 -0.78011,-1.48371 -0.75329,-2.48628 0.008,-0.28217 0.0183,-0.41825 0.0513,-0.64728 0.12221,-0.84944 0.4688,-1.58684 1.00869,-2.14604 0.448,-0.46404 0.99141,-0.73661 1.64587,-0.82555 0.20461,-0.0278 0.61454,-0.0275 0.82683,5.3e-4 1.15679,0.1536 1.85983,0.91064 2.1417,2.30622 0.0702,0.34747 0.12872,0.90685 0.12872,1.23003 v 0.10002 h -2.54072 -2.54072 l 2.4e-4,0.0756 c 3.4e-4,0.10274 0.0304,0.40549 0.0546,0.55015 0.19569,1.16863 0.86478,1.98358 1.82008,2.21685 0.25496,0.0623 0.42348,0.0772 0.78677,0.0695 0.64016,-0.0134 1.19157,-0.12927 1.86194,-0.3912 0.1091,-0.0427 0.20029,-0.0756 0.20264,-0.0732 0.01,0.01 0.0626,0.63225 0.0542,0.64075 -0.0197,0.0198 -0.3998,0.16871 -0.57879,0.22666 -0.63028,0.20408 -1.27382,0.29193 -1.93316,0.26391 z m 2.08308,-4.40477 c -0.0319,-0.66713 -0.26283,-1.2229 -0.67985,-1.63634 -0.3265,-0.32372 -0.69686,-0.48696 -1.19452,-0.52654 -0.43749,-0.0348 -0.88361,0.064 -1.22519,0.27142 -0.36492,0.22154 -0.69867,0.59578 -0.914,1.02486 -0.14147,0.2819 -0.25497,0.65096 -0.29651,0.96414 l -0.008,0.0628 h 2.16304 2.16303 z"
id="path1858" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 109.94026,139.37809 c -0.004,-2.56706 -0.006,-2.85806 -0.0251,-3.08819 -0.0222,-0.26713 -0.0759,-0.83071 -0.0886,-0.92857 l -0.007,-0.0575 h 0.33932 0.33931 l 0.0248,0.10586 c 0.0525,0.22428 0.0924,0.5523 0.10604,0.87076 0.0115,0.26976 0.0122,0.27115 0.0761,0.15387 0.31286,-0.57401 0.75972,-0.97794 1.2836,-1.16029 0.58453,-0.20346 1.41078,-0.14755 1.9583,0.13251 0.47048,0.24066 0.81264,0.656 1.01711,1.23465 0.0865,0.2447 0.1633,0.60057 0.19974,0.9251 0.0357,0.31814 0.0423,0.73381 0.0425,2.68288 l 2.1e-4,1.96301 h -0.36242 -0.3624 l -0.005,-2.16264 c -0.005,-2.30523 -0.004,-2.24733 -0.0633,-2.59474 -0.14884,-0.86461 -0.56532,-1.39361 -1.23625,-1.57025 -0.26568,-0.0699 -0.64299,-0.086 -0.87468,-0.0371 -0.32007,0.0674 -0.59469,0.22305 -0.87551,0.49615 -0.38184,0.37133 -0.62351,0.794 -0.72359,1.26547 l -0.0281,0.13272 -0.003,2.23523 -0.003,2.23524 h -0.36229 -0.36229 z"
id="path1860" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 119.4287,142.39186 c -0.97254,-0.0437 -1.72903,-0.53095 -2.22292,-1.43187 -0.20145,-0.36746 -0.36031,-0.83543 -0.43809,-1.2905 -0.17154,-1.00371 -0.0764,-2.02894 0.25996,-2.80075 0.38253,-0.87778 1.02011,-1.45538 1.81427,-1.64357 0.51957,-0.12312 1.12835,-0.09 1.60307,0.0872 0.48403,0.18069 0.89719,0.51746 1.22802,1.00097 0.0466,0.0681 0.10646,0.16266 0.13308,0.21019 0.0266,0.0475 0.0525,0.0866 0.0575,0.0867 0.005,1.5e-4 0.009,-1.05322 0.009,-2.34082 v -2.34109 h 0.36296 0.36296 l 3e-5,4.72151 c 3e-5,4.49762 0.004,5.11138 0.0346,5.46256 l 0.009,0.0998 h -0.32852 -0.32852 l -0.007,-0.0635 c -0.009,-0.0755 -0.032,-0.71082 -0.0357,-0.97697 l -0.003,-0.18753 -0.0591,0.0907 c -0.17859,0.27436 -0.47499,0.60953 -0.68659,0.77642 -0.44136,0.34808 -0.92914,0.52623 -1.49259,0.54514 -0.0532,0.002 -0.17574,-2.9e-4 -0.27222,-0.005 z m 0.71745,-0.69856 c 0.19011,-0.0387 0.33272,-0.0881 0.51057,-0.17686 0.19803,-0.0988 0.35003,-0.20695 0.51835,-0.36891 0.28197,-0.27134 0.48911,-0.61224 0.59377,-0.97721 0.0952,-0.33198 0.1071,-0.50072 0.10047,-1.42495 -0.006,-0.81632 -0.01,-0.87163 -0.0869,-1.17193 -0.11845,-0.46217 -0.34226,-0.83582 -0.71116,-1.18728 -0.31968,-0.30456 -0.66926,-0.47818 -1.09812,-0.54541 -0.16881,-0.0265 -0.52855,-0.0264 -0.70059,1.6e-4 -0.24499,0.0378 -0.49288,0.11817 -0.67013,0.2173 -0.59476,0.3326 -1.01376,1.0442 -1.14621,1.94662 -0.082,0.55841 -0.0544,1.32786 0.0679,1.89545 0.19097,0.8863 0.63787,1.46184 1.31894,1.69858 0.28636,0.0995 0.49681,0.12927 0.8821,0.12459 0.24186,-0.003 0.3122,-0.008 0.42103,-0.0302 z"
id="path1862" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 126.58508,142.37401 c -0.7441,-0.1157 -1.27117,-0.50286 -1.61454,-1.18599 -0.21576,-0.42923 -0.33958,-0.91494 -0.42053,-1.64962 -0.019,-0.17193 -0.021,-0.49629 -0.0245,-3.8988 l -0.004,-3.71127 h 0.35644 0.35645 l 0.005,3.54794 c 0.005,3.61064 0.006,3.70542 0.0515,4.15287 0.0676,0.6674 0.21893,1.21647 0.42153,1.52965 0.0695,0.10744 0.23303,0.27902 0.32659,0.34265 0.20221,0.13754 0.50553,0.22655 0.77273,0.22675 0.066,5e-5 0.0938,0.004 0.0938,0.0149 0,0.0265 -0.0614,0.59531 -0.0673,0.62337 -0.005,0.0246 -0.0124,0.0271 -0.0751,0.0259 -0.0382,-5.3e-4 -0.11833,-0.009 -0.17822,-0.0183 z"
id="path1864" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 127.7324,145.35991 c -0.004,-0.0178 -0.0421,-0.15751 -0.0836,-0.31056 -0.0415,-0.15305 -0.0746,-0.2787 -0.0736,-0.27923 0.001,-5.3e-4 0.0705,-0.0231 0.15437,-0.0501 0.59914,-0.19319 1.11392,-0.61986 1.53875,-1.27538 0.2291,-0.35349 0.45727,-0.82877 0.63229,-1.31709 0.0502,-0.14016 0.0546,-0.20052 0.0225,-0.31314 -0.0102,-0.0358 -0.5784,-1.45407 -1.26265,-3.1517 -0.68425,-1.69763 -1.26872,-3.14785 -1.29881,-3.22271 l -0.0547,-0.13611 h 0.37797 0.37798 l 0.62696,1.5698 c 0.34484,0.8634 0.87064,2.17958 1.16844,2.92486 0.29781,0.74528 0.5461,1.35384 0.55176,1.35235 0.006,-0.002 0.50152,-1.31572 1.1019,-2.92052 0.60038,-1.60481 1.09364,-2.91985 1.09611,-2.92233 0.003,-0.003 0.17572,-0.003 0.38497,-0.001 l 0.38046,0.003 -0.79047,1.98418 c -1.77735,4.46137 -2.01866,5.06474 -2.0928,5.23269 -0.68871,1.56017 -1.57313,2.50635 -2.64993,2.83496 l -0.0998,0.0305 z"
id="path1866" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 147.05605,142.39197 c -0.76825,-0.0274 -1.45102,-0.23495 -2.00838,-0.61046 -0.0732,-0.0493 -0.1429,-0.0973 -0.15489,-0.10653 -0.0205,-0.0159 -0.0135,-0.0364 0.11888,-0.34769 0.0774,-0.18196 0.1427,-0.33284 0.14516,-0.33529 0.003,-0.003 0.0551,0.0336 0.11704,0.08 0.35576,0.26692 0.7116,0.43761 1.14209,0.54786 0.28966,0.0742 0.47291,0.0975 0.82376,0.10499 0.33529,0.007 0.48955,-0.004 0.72374,-0.0512 0.70304,-0.1424 1.24493,-0.57879 1.4436,-1.16254 0.0977,-0.28702 0.13792,-0.63374 0.11224,-0.96706 -0.0465,-0.60262 -0.2178,-0.97667 -0.58967,-1.28709 -0.32454,-0.27092 -0.67833,-0.46033 -1.25163,-0.67011 -0.6697,-0.24504 -1.08548,-0.45919 -1.4927,-0.76882 -0.14989,-0.11396 -0.43634,-0.39722 -0.5375,-0.53151 -0.34518,-0.45821 -0.50377,-0.97168 -0.48098,-1.55734 0.0282,-0.72534 0.27565,-1.25816 0.78826,-1.69748 0.34079,-0.29206 0.75946,-0.50034 1.19386,-0.59391 0.24353,-0.0524 0.35146,-0.0638 0.66934,-0.0702 0.56757,-0.0114 1.05374,0.0604 1.48926,0.21979 0.1272,0.0466 0.42057,0.1876 0.51541,0.24777 l 0.0568,0.036 -0.146,0.29984 c -0.0803,0.16492 -0.14983,0.30408 -0.15451,0.30926 -0.005,0.005 -0.0544,-0.016 -0.11041,-0.0471 -0.38284,-0.21229 -0.83065,-0.349 -1.28744,-0.39301 -0.18737,-0.018 -0.61451,-0.008 -0.7684,0.0188 -0.45543,0.0782 -0.81327,0.24969 -1.08435,0.51973 -0.22271,0.22186 -0.34527,0.45664 -0.41185,0.78898 -0.031,0.15489 -0.0311,0.5621 -9e-5,0.70596 0.10269,0.47659 0.35349,0.8406 0.81802,1.18722 0.26831,0.2002 0.59107,0.36927 1.09568,0.57396 0.92096,0.37356 1.4131,0.66047 1.80326,1.05124 0.23261,0.23298 0.36319,0.44472 0.46447,0.75316 0.11162,0.33991 0.15126,0.64579 0.14238,1.09875 -0.007,0.33168 -0.02,0.45179 -0.0768,0.68358 -0.14897,0.60758 -0.51561,1.10908 -1.08106,1.4787 -0.56172,0.36718 -1.19687,0.52128 -2.02653,0.49166 z"
id="path1868" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 154.34551,142.39237 c -0.93633,-0.0436 -1.74092,-0.46045 -2.29038,-1.18669 -0.1415,-0.18703 -0.2246,-0.32227 -0.32938,-0.53606 -0.17644,-0.36 -0.28018,-0.71552 -0.3407,-1.16752 -0.0313,-0.23338 -0.0309,-0.91863 5.3e-4,-1.17962 0.0979,-0.81132 0.33808,-1.44177 0.7663,-2.01189 0.12116,-0.16132 0.37713,-0.42994 0.51786,-0.54349 0.40208,-0.32438 0.86305,-0.51957 1.3974,-0.59169 0.18197,-0.0246 0.60018,-0.0275 0.78037,-0.005 0.8494,0.10383 1.46071,0.52612 1.83722,1.26914 0.28454,0.56151 0.43276,1.24515 0.47459,2.18889 l 0.008,0.1845 h -2.54533 -2.54534 l 0.009,0.16555 c 0.0563,1.06062 0.47749,1.90828 1.18294,2.38047 0.27673,0.18522 0.63503,0.31124 1.01604,0.35736 0.13282,0.0161 0.57208,0.0159 0.75043,-3.2e-4 0.53418,-0.0485 1.11645,-0.19394 1.64319,-0.41031 0.0686,-0.0282 0.1265,-0.0494 0.12856,-0.047 0.002,0.002 0.017,0.14769 0.0332,0.32301 l 0.0294,0.31877 -0.0673,0.0298 c -0.61392,0.27213 -1.32296,0.43735 -1.96676,0.45829 -0.10646,0.004 -0.21807,0.008 -0.24802,0.01 -0.0299,0.002 -0.13883,-5.3e-4 -0.24197,-0.005 z m 2.09294,-4.31489 c -2e-4,-0.10786 -0.0332,-0.3947 -0.0616,-0.53536 -0.10447,-0.51724 -0.33713,-0.93701 -0.70159,-1.26584 -0.36904,-0.33297 -0.88736,-0.49499 -1.44846,-0.45276 -0.30028,0.0226 -0.50922,0.0779 -0.7584,0.20095 -0.21202,0.10465 -0.35671,0.21048 -0.54464,0.39836 -0.31889,0.31879 -0.52107,0.6493 -0.66579,1.0883 -0.0619,0.18767 -0.13858,0.51844 -0.13858,0.59748 v 0.0385 h 2.15961 2.15962 l -1.3e-4,-0.0696 z"
id="path1870" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 161.33855,142.392 c -1.08146,-0.0419 -1.9512,-0.51556 -2.50721,-1.36546 -0.10294,-0.15736 -0.25407,-0.45753 -0.32128,-0.63812 -0.19148,-0.51457 -0.28369,-1.12781 -0.26423,-1.75742 0.033,-1.06909 0.31429,-1.85258 0.89587,-2.49558 0.42512,-0.47001 0.97385,-0.7784 1.61611,-0.90825 0.72196,-0.14596 1.606,-0.0776 2.23826,0.17321 0.12357,0.049 0.36769,0.16666 0.44228,0.21315 l 0.037,0.023 -0.12413,0.32509 c -0.0683,0.1788 -0.12584,0.32681 -0.12794,0.32891 -0.002,0.002 -0.0418,-0.0209 -0.0883,-0.0513 -0.12185,-0.0795 -0.37704,-0.20073 -0.53816,-0.25561 -0.62717,-0.21363 -1.41267,-0.22325 -1.98375,-0.0243 -0.73735,0.25688 -1.30777,0.93118 -1.52981,1.80841 -0.0831,0.32819 -0.11001,0.57875 -0.10909,1.01444 0.001,0.38027 0.0158,0.54993 0.073,0.828 0.22062,1.07202 0.91126,1.84151 1.84616,2.05697 0.36099,0.0832 0.91731,0.0829 1.3345,-5.3e-4 0.33635,-0.0674 0.6823,-0.20489 0.91791,-0.36472 0.0544,-0.0369 0.0733,-0.0445 0.0809,-0.0322 0.026,0.042 0.24219,0.62052 0.23699,0.63407 -0.0101,0.0261 -0.44052,0.23221 -0.59454,0.28462 -0.48293,0.16433 -0.94852,0.22632 -1.53048,0.20377 z"
id="path1872" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 166.87975,142.39219 c -1.09932,-0.0508 -1.77948,-0.75484 -2.00236,-2.07265 -0.0851,-0.50312 -0.0905,-0.68132 -0.0906,-3.00428 l -1.2e-4,-2.0114 h 0.36243 0.36243 l 0.005,2.11425 c 0.005,2.27223 0.003,2.2088 0.0684,2.61029 0.11061,0.68288 0.34684,1.15772 0.71377,1.4347 0.11673,0.0881 0.30387,0.17589 0.46903,0.21998 0.14095,0.0376 0.14586,0.038 0.43294,0.0383 0.31989,2e-4 0.42138,-0.014 0.65333,-0.0913 0.51872,-0.1729 0.98371,-0.59183 1.24056,-1.11767 0.11752,-0.24057 0.18647,-0.48562 0.21689,-0.77082 0.008,-0.0765 0.0126,-0.87114 0.0126,-2.27757 v -2.1601 h 0.36268 0.36269 l 0.004,2.77968 c 0.004,2.55555 0.006,2.79126 0.0248,2.92345 0.0476,0.32802 0.13184,0.694 0.23181,1.00724 0.0293,0.0918 0.0522,0.16798 0.0509,0.1692 -0.001,0.001 -0.14395,0.0384 -0.31696,0.0827 -0.17301,0.0443 -0.32331,0.083 -0.334,0.0862 -0.0443,0.0128 -0.21624,-0.67688 -0.28249,-1.13319 -0.0166,-0.11446 -0.0325,-0.21037 -0.0352,-0.21315 -0.003,-0.003 -0.0226,0.0256 -0.044,0.0631 -0.10303,0.18009 -0.3558,0.49817 -0.50744,0.63856 -0.38939,0.36051 -0.82188,0.57279 -1.33751,0.65651 -0.15846,0.0257 -0.41937,0.0375 -0.62308,0.0281 z"
id="path1874" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 171.97328,139.71685 c -10e-6,-2.8742 -0.008,-3.25807 -0.085,-3.8625 -0.0226,-0.17862 -0.0767,-0.49221 -0.0915,-0.53078 -0.007,-0.0179 0.0209,-0.0197 0.30138,-0.0197 h 0.30894 l 0.0264,0.0696 c 0.0401,0.10575 0.0913,0.31969 0.11376,0.47487 0.0186,0.12825 0.0288,0.26245 0.0524,0.68357 l 0.009,0.15729 0.035,-0.0907 c 0.0553,-0.14348 0.20724,-0.43657 0.2964,-0.57185 0.31828,-0.48289 0.72215,-0.76117 1.23538,-0.85122 0.14036,-0.0246 0.46486,-0.0259 0.58074,-0.002 l 0.0786,0.016 0.003,0.32456 0.003,0.32456 -0.0335,-0.007 c -0.13988,-0.0275 -0.38784,-0.0144 -0.56781,0.03 -0.41872,0.10307 -0.78347,0.37645 -1.07019,0.8021 -0.19819,0.29421 -0.33003,0.63119 -0.40431,1.03334 -0.0656,0.35503 -0.0661,0.3745 -0.0661,2.52689 v 1.9883 h -0.36296 -0.36296 l -1e-5,-2.49536 z"
id="path1876" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.0120987;stroke-linejoin:bevel"
d="m 178.51869,142.39237 c -0.87552,-0.0378 -1.62691,-0.39454 -2.17739,-1.03372 -0.44146,-0.51261 -0.68852,-1.09177 -0.79453,-1.8626 -0.0308,-0.22357 -0.0312,-0.90321 -5.3e-4,-1.15542 0.0412,-0.34132 0.0893,-0.57998 0.17202,-0.85315 0.2745,-0.90691 0.87117,-1.66615 1.60037,-2.0364 0.44485,-0.22586 0.96728,-0.32731 1.5107,-0.29336 0.5693,0.0356 1.00217,0.1861 1.3773,0.47894 0.10919,0.0853 0.2997,0.27809 0.38237,0.38707 0.3813,0.50265 0.6127,1.19849 0.7001,2.10518 0.0168,0.17461 0.0366,0.49894 0.0366,0.6019 l 4e-5,0.0817 h -2.5426 -2.5426 l 0.008,0.16125 c 0.025,0.52051 0.14698,1.01238 0.35318,1.42368 0.12473,0.24878 0.26497,0.45022 0.44112,0.63357 0.33792,0.35174 0.73007,0.56283 1.21951,0.65643 0.14112,0.027 0.20985,0.0326 0.45819,0.0378 0.17493,0.004 0.35699,3.9e-4 0.4537,-0.008 0.53796,-0.0467 1.05367,-0.17101 1.5935,-0.38408 0.10884,-0.043 0.19966,-0.0764 0.20182,-0.0742 0.002,0.002 0.0151,0.12906 0.0288,0.28201 0.0137,0.15295 0.0274,0.29599 0.0304,0.31787 0.005,0.0383 0.003,0.0409 -0.0634,0.0707 -0.5972,0.26839 -1.32847,0.43829 -1.97442,0.45871 -0.1098,0.004 -0.21868,0.008 -0.24198,0.009 -0.0233,0.002 -0.12673,-5.3e-4 -0.22987,-0.005 z m 2.07711,-4.39518 c -0.0197,-0.38967 -0.11707,-0.78249 -0.26862,-1.0842 -0.15264,-0.3039 -0.41191,-0.60374 -0.67806,-0.78415 -0.33987,-0.23038 -0.79273,-0.34064 -1.25576,-0.30574 -0.3014,0.0227 -0.52695,0.0829 -0.77603,0.20692 -0.19637,0.0978 -0.3464,0.20757 -0.5206,0.38087 -0.34692,0.34511 -0.57419,0.73203 -0.71275,1.21346 -0.0424,0.14728 -0.10355,0.43817 -0.10355,0.49253 v 0.0302 h 2.16148 2.16148 z"
id="path1878" />
<path
style="fill:#ff7f2a;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.00206706;stroke-linejoin:bevel"
d="m 83.478942,142.33576 c -0.101097,-0.008 -0.184928,-0.0352 -0.26252,-0.084 -0.03842,-0.0242 -0.07209,-0.0516 -0.11077,-0.0902 -0.07477,-0.0746 -0.122158,-0.14429 -0.158978,-0.23387 -0.06123,-0.14895 -0.06985,-0.33629 -0.02296,-0.4986 0.0417,-0.14423 0.13607,-0.28586 0.252386,-0.37868 0.137535,-0.10976 0.312118,-0.14649 0.484727,-0.10201 0.07326,0.0189 0.1455,0.0555 0.213937,0.10833 0.03043,0.0235 0.100541,0.0934 0.127312,0.12702 0.09297,0.11659 0.143071,0.23572 0.16038,0.38138 0.0037,0.0301 0.0037,0.13316 0,0.16329 -0.02053,0.17271 -0.08955,0.30826 -0.220568,0.43316 -0.108812,0.10372 -0.236495,0.16232 -0.377999,0.17349 -0.02482,0.002 -0.06584,0.002 -0.08496,5.3e-4 z"
id="path2003" />
<path
style="fill:#ff7f2a;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.00366559;stroke-linejoin:bevel"
d="m 138.83867,142.3338 c -0.17242,-0.0105 -0.32504,-0.0983 -0.44225,-0.25441 -0.0826,-0.10996 -0.12606,-0.22887 -0.1377,-0.37655 -0.0173,-0.21868 0.0597,-0.4297 0.21659,-0.59397 0.0856,-0.0896 0.19239,-0.14843 0.30862,-0.1699 0.0471,-0.009 0.14272,-0.009 0.19037,-2e-4 0.12784,0.0231 0.23969,0.0859 0.34028,0.1913 0.13392,0.14028 0.20368,0.3001 0.21195,0.48563 0.009,0.1971 -0.0578,0.37354 -0.19499,0.51645 -0.14021,0.14607 -0.30426,0.21319 -0.49287,0.20165 z"
id="path2005" />
<path
style="fill:#5c5f5c;fill-opacity:1;stroke:#5c5f5c;stroke-width:0.00751464;stroke-linejoin:bevel"
d="m 100.61317,133.80153 c -0.16331,-0.032 -0.28985,-0.12548 -0.35646,-0.2632 -0.058,-0.11999 -0.0687,-0.17368 -0.0685,-0.34483 1.5e-4,-0.12876 0.002,-0.15428 0.0174,-0.21041 0.0576,-0.21387 0.20173,-0.36782 0.39199,-0.4187 0.061,-0.0163 0.21772,-0.018 0.27805,-0.003 0.0605,0.015 0.15563,0.0617 0.2021,0.0991 0.10363,0.0835 0.1931,0.22075 0.22745,0.34901 0.0235,0.0877 0.0236,0.28173 1.8e-4,0.36822 -0.0588,0.21726 -0.20702,0.36868 -0.40794,0.41685 -0.0677,0.0162 -0.21841,0.02 -0.28424,0.007 z"
id="path2161" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="heading"
transform="translate(-18.242052,-92.23446)">
<g
aria-label="Infix"
id="text1160"
style="font-size:25.4px;font-family:'URW Bookman';-inkscape-font-specification:'URW Bookman, Normal';fill:#5c5f5c;stroke-width:1.029"
inkscape:label="infix"
transform="matrix(2,0,0,2,-99.78739,-118.81694)">
<path
d="m 85.532731,107.6593 0.508,0.0762 c 1.3462,0.1778 1.6256,0.635 1.651,2.794 v 9.9822 c -0.0254,2.159 -0.3048,2.6162 -1.651,2.794 l -0.508,0.0762 v 0.7874 h 6.858 v -0.7874 l -0.508,-0.0762 c -1.3462,-0.1778 -1.6256,-0.635 -1.651,-2.794 v -9.9822 c 0.0254,-2.159 0.3048,-2.6162 1.651,-2.794 l 0.508,-0.0762 v -0.7874 h -6.858 z"
id="path2431" />
<path
d="m 98.588334,111.8503 h -0.8636 c -1.5748,0.3048 -2.413,0.4064 -3.8354,0.508 v 0.762 l 0.6096,0.0508 c 1.3462,0.1524 1.6764,0.5588 1.6764,2.1082 v 5.969 c 0,1.397 -0.4572,2.0066 -1.5748,2.1082 l -0.635,0.0508 v 0.762 h 6.832596 v -0.762 l -0.635,-0.0508 c -1.117596,-0.1016 -1.574796,-0.7112 -1.574796,-2.1082 v -6.2738 c 1.574796,-1.4224 3.073396,-2.1336 4.495796,-2.1336 1.397,0 2.159,0.889 2.159,2.54 v 5.8674 c 0,1.397 -0.4826,2.0066 -1.5748,2.1082 l -0.635,0.0508 v 0.762 h 6.8326 v -0.762 l -0.635,-0.0508 c -1.1176,-0.1016 -1.5748,-0.7112 -1.5748,-2.1082 v -6.2738 c 0,-2.1082 -1.4224,-3.3528 -3.8354,-3.3528 -1.9558,0 -3.429,0.635 -5.232396,2.2352 z"
id="path2433" />
<path
d="m 115.27613,111.8503 v -1.5748 c 0,-1.651 0.0508,-1.9812 0.4064,-2.5908 0.4826,-0.8382 1.4986,-1.3462 2.6416,-1.3462 1.27,0 2.4638,0.762 2.4638,1.6256 0.0254,1.0414 0.0254,1.0414 0.2286,1.397 0.2286,0.4064 0.635,0.635 1.143,0.635 0.762,0 1.27,-0.5334 1.27,-1.27 0,-0.5842 -0.2286,-1.0668 -0.762,-1.6256 -1.016,-1.0414 -2.413,-1.5748 -4.191,-1.5748 -1.9558,0 -3.7338,0.6604 -4.6482,1.7272 -0.6858,0.8128 -0.9652,1.8542 -0.9652,3.6322 v 0.9652 h -2.159 v 0.8382 h 2.159 v 8.5598 c 0,1.397 -0.4572,2.0066 -1.5748,2.1082 l -0.635,0.0508 v 0.762 h 6.8326 v -0.762 l -0.635,-0.0508 c -1.1176,-0.1016 -1.5748,-0.7112 -1.5748,-2.1082 v -8.5598 h 3.3782 c 1.8288,0 2.3368,0.5842 2.3114,2.667 v 5.8928 c 0,1.397 -0.4572,2.0066 -1.5748,2.1082 l -0.635,0.0508 v 0.762 h 6.8326 v -0.762 l -0.635,-0.0508 c -1.1176,-0.1016 -1.5748,-0.7112 -1.5748,-2.1082 v -9.398 z"
id="path2435" />
<path
d="m 134.09755,117.0827 2.2098,-2.3622 c 1.397,-1.3716 2.1082,-1.778 3.81,-2.1082 v -0.762 h -5.3594 v 0.762 c 0.9144,0.1016 1.143,0.2286 1.143,0.6604 0,0.2032 -0.0762,0.4064 -0.254,0.6096 l -2.159,2.4384 -2.0066,-2.4384 c -0.1778,-0.254 -0.3048,-0.4826 -0.3048,-0.635 0,-0.3556 0.3048,-0.5334 1.0414,-0.6096 l 0.254,-0.0254 v -0.762 h -6.2992 v 0.762 c 1.2192,0.1778 1.7018,0.4826 2.5908,1.5494 l 2.9718,3.7338 -3.2258,3.6322 c -1.143,1.2954 -1.8034,1.7018 -3.0226,1.8796 v 0.762 h 5.6896 v -0.762 c -0.889,-0.1524 -1.0922,-0.2032 -1.27,-0.3048 -0.254,-0.1524 -0.4318,-0.4064 -0.4318,-0.6604 0,-0.254 0.1778,-0.5842 0.4572,-0.9144 l 2.54,-2.8448 2.286,3.2004 c 0.1524,0.2286 0.254,0.4826 0.254,0.6858 0,0.4826 -0.3048,0.6604 -1.524,0.8382 v 0.762 h 7.0358 v -0.762 c -1.3716,-0.2286 -1.8288,-0.5334 -2.9464,-1.8796 z"
id="path2437" />
</g>
</g>
<g
inkscape:label="tux"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(-18.242052,-92.23446)">
<path
id="rect1698"
style="fill:#f9f9f9;stroke:#5c5f5c;stroke-width:2.058;stroke-linejoin:round"
inkscape:label="body"
d="M 19.271052,95.241499 H 64.21261 V 129.18306 H 19.271052 Z" />
<path
id="rect184-3"
style="fill:#ff7f2a;stroke:#5c5f5c;stroke-width:2;stroke-linejoin:round"
inkscape:label="foot-r"
d="m 52.21261,121.18306 h 12 v 8 h -12 z" />
<path
id="rect184"
style="fill:#ff7f2a;stroke:#5c5f5c;stroke-width:2;stroke-linejoin:round"
inkscape:label="foot-l"
d="m 19.271052,121.18306 h 12 v 8 h -12 z" />
<path
id="path11801"
style="fill:#ff7f2a;stroke-width:2"
inkscape:label="nose"
inkscape:transform-center-y="0.98302844"
d="m 41.741828,115.16138 -3.405326,-5.8982 h 6.81065 z" />
<path
id="path6519-5"
style="display:inline;fill:#5c5f5c;stroke-width:9.99998"
inkscape:label="eye-r"
d="m 47.634774,103.22688 a 1,1 0 0 1 -1,1 1,1 0 0 1 -1,-1 1,1 0 0 1 1,-1 1,1 0 0 1 1,1 z" />
<path
id="path6519"
style="fill:#5c5f5c;stroke-width:9.99998"
inkscape:label="eye-l"
d="m 37.84887,103.22688 a 1,1 0 0 1 -1,1 1,1 0 0 1 -1,-1 1,1 0 0 1 1,-1 1,1 0 0 1 1,1 z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 45 KiB

-1
View File
@@ -42,5 +42,4 @@ source "$BR2_EXTERNAL_INFIX_PATH/package/rousette/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/nghttp2-asio/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/date-cpp/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/rauc-installation-status/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/bootloader-splashscreen/Config.in"
endmenu
+1 -1
View File
@@ -11,7 +11,7 @@ BIN_LICENSE = BSD-3-Clause
BIN_LICENSE_FILES = LICENSE
BIN_REDISTRIBUTE = NO
BIN_DEPENDENCIES = sysrepo libite
BIN_CONF_OPTS = --prefix= --disable-silent-rules
BIN_CONF_OPTS = --disable-silent-rules
BIN_AUTORECONF = YES
define BIN_CONF_ENV
-2
View File
@@ -7,7 +7,5 @@ source "$BR2_EXTERNAL_INFIX_PATH/package/board/marvell-espressobin/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/board/microchip-sparx5-pcb135/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/board/styx-dcp-sc-28p/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/board/raspberry-pi-4/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/board/banana-pi-r3/Config.in"
endmenu
-10
View File
@@ -1,10 +0,0 @@
config BR2_PACKAGE_BANANA_PI_R3
bool "Banana Pi R3"
depends on BR2_aarch64
select BR2_PACKAGE_FEATURE_WIFI
select BR2_PACKAGE_LINUX_FIRMWARE
select BR2_PACKAGE_LINUX_FIRMWARE_MEDIATEK
select BR2_PACKAGE_LINUX_FIRMWARE_MEDIATEK_MT7986
select SDCARD_AUX
help
Build Banana PI R3 support
@@ -1,40 +0,0 @@
GENIMAGE_CFG="$(BUILD_DIR)/genimage.cfg"
GENIMAGE_TMP="$(BUILD_DIR)/genimage.tmp"
BOARD_DIR="$(BR2_EXTERNAL_INFIX_PATH)/src/board/banana-pi-r3"
define BANANA_PI_R3_LINUX_CONFIG_FIXUPS
$(call KCONFIG_ENABLE_OPT,CONFIG_ARCH_MEDIATEK)
$(call KCONFIG_ENABLE_OPT,CONFIG_MACH_MT7986)
$(call KCONFIG_ENABLE_OPT,CONFIG_PINCTRL_MT7986)
$(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_8250_MT6577)
$(call KCONFIG_SET_OPT,CONFIG_I2C_GPIO,y)
$(call KCONFIG_SET_OPT,CONFIG_MTK_THERMAL,m)
$(call KCONFIG_ENABLE_OPT,CONFIG_MTK_UART)
$(call KCONFIG_ENABLE_OPT,CONFIG_MTK_WATCHDOG)
$(call KCONFIG_ENABLE_OPT,CONFIG_MEDIATEK_GE_PHY)
$(call KCONFIG_ENABLE_OPT,CONFIG_REALTEK_PHY)
$(call KCONFIG_ENABLE_OPT,CONFIG_NET_VENDOR_MEDIATEK)
$(call KCONFIG_ENABLE_OPT,CONFIG_NET_MEDIATEK_SOC)
$(call KCONFIG_SET_OPT,CONFIG_NET_DSA_MT7530,m)
$(call KCONFIG_SET_OPT,CONFIG_MT7915E,m)
$(call KCONFIG_SET_OPT,CONFIG_PCIE_MEDIATEK_GEN3,m)
$(call KCONFIG_ENABLE_OPT,CONFIG_MTK_SCPSYS)
$(call KCONFIG_ENABLE_OPT,CONFIG_MMC_MTK)
$(call KCONFIG_ENABLE_OPT,CONFIG_MTK_HSDMA)
$(call KCONFIG_SET_OPT,CONFIG_MT7915E,m)
$(call KCONFIG_ENABLE_OPT,CONFIG_MT798X_WMAC)
$(call KCONFIG_SET_OPT,CONFIG_MTK_SOC_THERMAL,m)
$(call KCONFIG_SET_OPT,CONFIG_I2C_MT65XX,m)
$(call KCONFIG_SET_OPT,CONFIG_USB_XHCI_MTK,m)
$(call KCONFIG_SET_OPT,CONFIG_PHY_MTK_TPHY,m)
$(call KCONFIG_SET_OPT,CONFIG_PHY_MTK_XSPHY,m)
$(call KCONFIG_SET_OPT,CONFIG_REGULATOR_GPIO,m)
$(call KCONFIG_SET_OPT,CONFIG_REGULATOR_MT6380,m)
$(call KCONFIG_SET_OPT,CONFIG_PWM_MEDIATEK,m)
$(call KCONFIG_SET_OPT,CONFIG_SENSORS_PWM_FAN,m)
$(call KCONFIG_SET_OPT,CONFIG_NVMEM_MTK_EFUSE,m)
$(call KCONFIG_SET_OPT,CONFIG_CRYPTO_DEV_SAFEXCEL,m)
endef
$(eval $(ix-board))
$(eval $(generic-package))
-1
View File
@@ -1,7 +1,6 @@
config BR2_PACKAGE_RASPBERRY_PI_4
bool "Raspberry Pi 4"
depends on BR2_aarch64
select SDCARD_AUX
select BR2_PACKAGE_FEATURE_WIFI
select BR2_PACKAGE_BRCMFMAC_SDIO_FIRMWARE_RPI
select BR2_PACKAGE_BRCMFMAC_SDIO_FIRMWARE_RPI_WIFI
-12
View File
@@ -1,12 +0,0 @@
config BR2_PACKAGE_BOOTLOADER_SPLASHSCREEN
bool "bootloader-splashscreen"
help
Install a BMP splash screen image for bootloader.
config BR2_PACKAGE_BOOTLOADER_SPLASHSCREEN_PATH
string "Path to BMP image"
default "$(BR2_EXTERNAL_INFIX_PATH)/board/common/uboot/splash.bmp"
depends on BR2_PACKAGE_BOOTLOADER_SPLASHSCREEN
help
Path to the BMP image file to be used as bootloader splash screen.
The image will be installed to output/images/.
@@ -1,19 +0,0 @@
################################################################################
#
# bootloader-splashscreen
#
################################################################################
BOOTLOADER_SPLASHSCREEN_VERSION = 1.0
BOOTLOADER_SPLASHSCREEN_SOURCE =
BOOTLOADER_SPLASHSCREEN_SITE =
define BOOTLOADER_SPLASHSCREEN_BUILD_CMDS
# Nothing to build
endef
define BOOTLOADER_SPLASHSCREEN_INSTALL_TARGET_CMDS
$(INSTALL) -D -m 0644 $(call qstrip,$(BR2_PACKAGE_BOOTLOADER_SPLASHSCREEN_PATH)) $(BINARIES_DIR)/splash.bmp
endef
$(eval $(generic-package))
+4
View File
@@ -25,3 +25,7 @@ run name:failure log:prio:user.crit norestart env:/etc/default/confd \
run name:error :2 log:console norestart \
if:<run/failure/failure> \
[S] /usr/libexec/confd/error --
service name:netopeer notify:none log env:/etc/default/confd \
[12345] <pid/confd> netopeer2-server -F -t $CONFD_TIMEOUT -v 1 \
-- NETCONF server
@@ -1,3 +1,3 @@
# Locally calculated
sha256 9d9d33b873917ca5d0bdcc47a36d2fd385971ab0c045d1472fcadf95ee5bcf5b LICENCE
sha256 a6fe769a8f24a065b274f4e0f5f1fec634b2aae5bdd586aa6f71fe60ca6b8a64 klish-plugin-sysrepo-30bc6294489807ea026abed20f94d1ad1583cc6b-git4.tar.gz
sha256 598089ad964594bbbaf2b7d7214d8761c828174eef53b7cfb1cd98517f407d01 klish-plugin-sysrepo-b693714a1ff5f8021651d7619556afb19945e5e6-git4.tar.gz
@@ -4,7 +4,7 @@
#
################################################################################
KLISH_PLUGIN_SYSREPO_VERSION = 30bc6294489807ea026abed20f94d1ad1583cc6b
KLISH_PLUGIN_SYSREPO_VERSION = b693714a1ff5f8021651d7619556afb19945e5e6
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,135 +0,0 @@
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()`
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
This function allows to access list of actions
within schema node.
It is implemented same as `Module::actionRpcs`
to follow same approach, so there is no need to use
collections.
Change-Id: Ie99908cfdd334433fd9ae6f1a909f196e61139fd
Signed-off-by: Jan Kundrát <jan.kundrat@cesnet.cz>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
include/libyang-cpp/SchemaNode.hpp | 1 +
src/SchemaNode.cpp | 12 ++++++++++++
tests/example_schema.hpp | 28 ++++++++++++++++++++++++++++
tests/schema_node.cpp | 21 +++++++++++++++++++++
4 files changed, 62 insertions(+)
diff --git a/include/libyang-cpp/SchemaNode.hpp b/include/libyang-cpp/SchemaNode.hpp
index 0f1a4c4..9f49fd7 100644
--- a/include/libyang-cpp/SchemaNode.hpp
+++ b/include/libyang-cpp/SchemaNode.hpp
@@ -79,6 +79,7 @@ public:
Collection<SchemaNode, IterationType::Sibling> siblings() const;
Collection<SchemaNode, IterationType::Sibling> immediateChildren() const;
std::vector<ExtensionInstance> extensionInstances() const;
+ std::vector<SchemaNode> actionRpcs() const;
std::vector<When> when() const;
diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp
index 26b5099..ce24203 100644
--- a/src/SchemaNode.cpp
+++ b/src/SchemaNode.cpp
@@ -117,6 +117,18 @@ Collection<SchemaNode, IterationType::Sibling> SchemaNode::immediateChildren() c
return c ? c->siblings() : Collection<SchemaNode, IterationType::Sibling>{nullptr, nullptr};
}
+/**
+ * @brief Returns a collection of action nodes (not RPC nodes) as SchemaNode
+ */
+std::vector<SchemaNode> SchemaNode::actionRpcs() const
+{
+ std::vector<SchemaNode> res;
+ for (auto action = reinterpret_cast<const struct lysc_node*>(lysc_node_actions(m_node)); action; action = action->next) {
+ res.emplace_back(SchemaNode{action, m_ctx});
+ }
+ return res;
+}
+
/**
* Returns the YANG description of the node.
*
diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp
index 0d8acb9..02d3d74 100644
--- a/tests/example_schema.hpp
+++ b/tests/example_schema.hpp
@@ -214,6 +214,34 @@ module example-schema {
}
container bigTree {
+ action firstAction {
+ input {
+ leaf inputLeaf1 {
+ type string;
+ }
+ }
+
+ output {
+ leaf outputLeaf1 {
+ type string;
+ }
+ }
+ }
+
+ action secondAction {
+ input {
+ leaf inputLeaf2 {
+ type string;
+ }
+ }
+
+ output {
+ leaf outputLeaf2 {
+ type string;
+ }
+ }
+ }
+
container one {
leaf myLeaf {
type string;
diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp
index 0001377..65ef462 100644
--- a/tests/schema_node.cpp
+++ b/tests/schema_node.cpp
@@ -544,6 +544,27 @@ TEST_CASE("SchemaNode")
REQUIRE(elem.extensionInstances()[2].argument() == "last-modified");
}
+ DOCTEST_SUBCASE("SchemaNode::actionRpcs")
+ {
+ DOCTEST_SUBCASE("no actions")
+ {
+ auto actions = ctx->findPath("/example-schema:presenceContainer").actionRpcs();
+ REQUIRE(actions.size() == 0);
+ }
+
+ DOCTEST_SUBCASE("two actions")
+ {
+ auto actions = ctx->findPath("/example-schema:bigTree").actionRpcs();
+ REQUIRE(actions.size() == 2);
+ REQUIRE(actions[0].asActionRpc().name() == "firstAction");
+ REQUIRE(actions[0].asActionRpc().input().child()->name() == "inputLeaf1");
+ REQUIRE(actions[0].asActionRpc().output().child()->name() == "outputLeaf1");
+ REQUIRE(actions[1].asActionRpc().name() == "secondAction");
+ REQUIRE(actions[1].asActionRpc().input().child()->name() == "inputLeaf2");
+ REQUIRE(actions[1].asActionRpc().output().child()->name() == "outputLeaf2");
+ }
+ }
+
DOCTEST_SUBCASE("SchemaNode::operator==")
{
auto a = ctx->findPath("/type_module:leafString");
--
2.43.0
@@ -0,0 +1,82 @@
From d0f6422fee7a46fcb7445c88f499f61b3eb0ead0 Mon Sep 17 00:00:00 2001
From: Adam Piecek <Adam.Piecek@cesnet.cz>
Date: Wed, 23 Oct 2024 14:37:09 +0200
Subject: [PATCH 01/18] added support for RpcYang in Context::parseOp
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: I25182ea2d042be1e6e4246e18aee260cc032e547
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/Context.cpp | 6 ++++--
tests/context.cpp | 21 +++++++++++++++++++++
2 files changed, 25 insertions(+), 2 deletions(-)
diff --git a/src/Context.cpp b/src/Context.cpp
index b844bd9..287f8c8 100644
--- a/src/Context.cpp
+++ b/src/Context.cpp
@@ -221,7 +221,8 @@ std::optional<DataNode> Context::parseExtData(
* - a NETCONF RPC,
* - a NETCONF notification,
* - a RESTCONF notification,
- * - a YANG notification.
+ * - a YANG notification,
+ * - a YANG RPC.
*
* Parsing any of these requires just the schema (which is available through the Context), and the textual payload.
* All the other information are encoded in the textual payload as per the standard.
@@ -243,6 +244,7 @@ ParsedOp Context::parseOp(const std::string& input, const DataFormat format, con
auto in = wrap_ly_in_new_memory(input);
switch (opType) {
+ case OperationType::RpcYang:
case OperationType::RpcNetconf:
case OperationType::NotificationNetconf:
case OperationType::NotificationRestconf:
@@ -254,7 +256,7 @@ ParsedOp Context::parseOp(const std::string& input, const DataFormat format, con
ParsedOp res;
res.tree = tree ? std::optional{libyang::wrapRawNode(tree)} : std::nullopt;
- if (opType == OperationType::NotificationYang) {
+ if ((opType == OperationType::NotificationYang) || (opType == OperationType::RpcYang)) {
res.op = op && tree ? std::optional{DataNode(op, res.tree->m_refs)} : std::nullopt;
} else {
res.op = op ? std::optional{libyang::wrapRawNode(op)} : std::nullopt;
diff --git a/tests/context.cpp b/tests/context.cpp
index 71ae873..11019eb 100644
--- a/tests/context.cpp
+++ b/tests/context.cpp
@@ -464,6 +464,27 @@ TEST_CASE("context")
REQUIRE(data->findPath("/example-schema:leafInt8")->asTerm().valueStr() == "-43");
}
+ DOCTEST_SUBCASE("Context::parseOp")
+ {
+ DOCTEST_SUBCASE("RPC")
+ {
+ ctx->parseModule(example_schema, libyang::SchemaFormat::YANG);
+ std::string dataJson = R"({"example-schema:myRpc":{"inputLeaf":"str"}})";
+ auto pop = ctx->parseOp(dataJson, libyang::DataFormat::JSON, libyang::OperationType::RpcYang);
+ REQUIRE(pop.op->schema().name() == "myRpc");
+ REQUIRE(pop.tree->findPath("/example-schema:myRpc/inputLeaf")->asTerm().valueStr() == "str");
+ }
+ DOCTEST_SUBCASE("action")
+ {
+ ctx->parseModule(example_schema, libyang::SchemaFormat::YANG);
+ std::string datajson = R"({"example-schema:person":[{"name":"john", "poke":{}}]})";
+ auto pop = ctx->parseOp(datajson, libyang::DataFormat::JSON, libyang::OperationType::RpcYang);
+ REQUIRE(pop.op->schema().name() == "poke");
+ REQUIRE(pop.tree->findPath("/example-schema:person[name='john']/poke")->schema().nodeType() == libyang::NodeType::Action);
+ }
+ }
+
+
DOCTEST_SUBCASE("Context::parseExt")
{
ctx->setSearchDir(TESTS_DIR / "yang");
--
2.43.0
@@ -0,0 +1,53 @@
From 7e015f3486bdbb54f1dcc2e2ce51102b1d623081 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Wed, 23 Oct 2024 12:52:24 +0200
Subject: [PATCH 02/18] throw when lyd_validate_all returns error
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Bug: https://github.com/CESNET/libyang-cpp/issues/20
Change-Id: I005a2f1b057978573a4046e7b4cc31d77e36fde3
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/DataNode.cpp | 4 +++-
tests/data_node.cpp | 7 +++++++
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/DataNode.cpp b/src/DataNode.cpp
index 51c86c8..2ef17f2 100644
--- a/src/DataNode.cpp
+++ b/src/DataNode.cpp
@@ -1170,7 +1170,9 @@ void validateAll(std::optional<libyang::DataNode>& node, const std::optional<Val
}
// TODO: support the `diff` argument
- lyd_validate_all(node ? &node->m_node : nullptr, nullptr, opts ? utils::toValidationOptions(*opts) : 0, nullptr);
+ auto ret = lyd_validate_all(node ? &node->m_node : nullptr, nullptr, opts ? utils::toValidationOptions(*opts) : 0, nullptr);
+ throwIfError(ret, "libyang:validateAll: lyd_validate_all failed");
+
if (!node->m_node) {
node = std::nullopt;
}
diff --git a/tests/data_node.cpp b/tests/data_node.cpp
index a23e4c2..8a2610e 100644
--- a/tests/data_node.cpp
+++ b/tests/data_node.cpp
@@ -489,6 +489,13 @@ TEST_CASE("Data Node manipulation")
REQUIRE_THROWS_WITH_AS(libyang::validateAll(node, libyang::ValidationOptions::NoState), "validateAll: Node is not a unique reference", libyang::Error);
}
+ DOCTEST_SUBCASE("validateAll throws on validation failure")
+ {
+ ctx.parseModule(type_module, libyang::SchemaFormat::YANG);
+ auto node = std::optional{ctx.newPath("/type_module:leafWithConfigFalse", "hi")};
+ REQUIRE_THROWS_WITH_AS(libyang::validateAll(node, libyang::ValidationOptions::NoState), "libyang:validateAll: lyd_validate_all failed: LY_EVALID", libyang::ErrorWithCode);
+ }
+
DOCTEST_SUBCASE("unlink")
{
auto root = ctx.parseData(data2, libyang::DataFormat::JSON);
--
2.43.0
@@ -0,0 +1,38 @@
From 490d8bb242d33213b948485f5b94c55e22cf86a6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 21 Nov 2024 11:32:44 +0100
Subject: [PATCH 03/18] remove a misleading comment
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
The whole intention within action's input/output handling here was to
put some emphasis on the fact that we aren't tracking the input/output
nodes directly. However, looking at all the other classes this is a bit
redundant, we're using a pattern like this all the time. Just drop the
comment.
Change-Id: Ibd9bf9f1e83c650dda3bc43ef48e61dd6d95da5a
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/SchemaNode.cpp | 3 ---
1 file changed, 3 deletions(-)
diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp
index 81e938f..9934cea 100644
--- a/src/SchemaNode.cpp
+++ b/src/SchemaNode.cpp
@@ -640,9 +640,6 @@ bool List::isUserOrdered() const
*/
ActionRpcInput ActionRpc::input() const
{
- // I need a lysc_node* for ActionRpcInput, but m_node->input is a lysp_node_action_inout. lysp_node_action_inout is
- // still just a lysc_node, so I'll just convert to lysc_node.
- // This is not very pretty, but I don't want to introduce another member for ActionRpcInput and ActionRpcOutput.
return ActionRpcInput{reinterpret_cast<const lysc_node*>(&reinterpret_cast<const lysc_node_action*>(m_node)->input), m_ctx};
}
--
2.43.0
@@ -0,0 +1,35 @@
From e1b17386cf61048d2fe27fffb3b763981a225f52 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= <bedrich.schindler@gmail.com>
Date: Wed, 27 Nov 2024 09:47:47 +0100
Subject: [PATCH 04/18] schema: improve `List::keys()` not to use `std::move`
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
`List::keys()` used `std::move` while iterating over array of leafs.
This was solved without using `std::move`.
Change-Id: I8cbf8780ecd8848e46c1de5d4123a08624536bba
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/SchemaNode.cpp | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp
index 9934cea..20e2aff 100644
--- a/src/SchemaNode.cpp
+++ b/src/SchemaNode.cpp
@@ -593,8 +593,7 @@ std::vector<Leaf> List::keys() const
LY_LIST_FOR(list->child, elem)
{
if (lysc_is_key(elem)) {
- Leaf leaf(elem, m_ctx);
- res.emplace_back(std::move(leaf));
+ res.emplace_back(Leaf(elem, m_ctx));
}
}
--
2.43.0
@@ -0,0 +1,124 @@
From 1102ecdcafbc9206f59b383769687e418557838e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= <bedrich.schindler@gmail.com>
Date: Mon, 25 Nov 2024 15:54:02 +0100
Subject: [PATCH 05/18] schema: make leaf-list's `default` statement available
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Make leaf-list's `default` statement available so that it can be
accessed if end-user requires reading schema nodes.
`LeafList::defaultValuesStr()` returns array of canonized string default
values.
Change-Id: Idc42cd877f1fd3d717d491d09c46b59492527bff
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
include/libyang-cpp/SchemaNode.hpp | 1 +
src/SchemaNode.cpp | 17 +++++++++++++++++
tests/context.cpp | 1 +
tests/example_schema.hpp | 8 ++++++++
tests/schema_node.cpp | 7 +++++++
5 files changed, 34 insertions(+)
diff --git a/include/libyang-cpp/SchemaNode.hpp b/include/libyang-cpp/SchemaNode.hpp
index 83afc06..8ddf9be 100644
--- a/include/libyang-cpp/SchemaNode.hpp
+++ b/include/libyang-cpp/SchemaNode.hpp
@@ -169,6 +169,7 @@ class LIBYANG_CPP_EXPORT LeafList : public SchemaNode {
public:
bool isMandatory() const;
types::Type valueType() const;
+ std::vector<std::string> defaultValuesStr() const;
libyang::types::constraints::ListSize maxElements() const;
libyang::types::constraints::ListSize minElements() const;
std::optional<std::string> units() const;
diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp
index 9934cea..95bc09b 100644
--- a/src/SchemaNode.cpp
+++ b/src/SchemaNode.cpp
@@ -472,6 +472,23 @@ types::Type LeafList::valueType() const
return types::Type{reinterpret_cast<const lysc_node_leaflist*>(m_node)->type, typeParsed, m_ctx};
}
+/**
+ * @brief Retrieves the default string values for this leaf-list.
+ *
+ * @return The default values, or an empty vector if the leaf-list does not have default values.
+ *
+ * Wraps `lysc_node_leaflist::dflts`.
+ */
+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));
+ }
+ return res;
+}
+
/**
* @brief Retrieves the units for this leaf.
* @return The units, or std::nullopt if no units are available.
diff --git a/tests/context.cpp b/tests/context.cpp
index 11019eb..902faf6 100644
--- a/tests/context.cpp
+++ b/tests/context.cpp
@@ -733,6 +733,7 @@ TEST_CASE("context")
+--rw iid-valid? instance-identifier
+--rw iid-relaxed? instance-identifier
+--rw leafListBasic* string
+ +--rw leafListWithDefault* int32
+--rw leafListWithMinMaxElements* int32
+--rw leafListWithUnits* int32
+--rw listBasic* [primary-key]
diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp
index c093f50..2861b1a 100644
--- a/tests/example_schema.hpp
+++ b/tests/example_schema.hpp
@@ -525,6 +525,14 @@ module type_module {
ordered-by user;
}
+ leaf-list leafListWithDefault {
+ type int32;
+ default -1;
+ default +512;
+ default 0x400;
+ default 04000;
+ }
+
leaf-list leafListWithMinMaxElements {
type int32;
min-elements 1;
diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp
index a86900d..80c7407 100644
--- a/tests/schema_node.cpp
+++ b/tests/schema_node.cpp
@@ -200,6 +200,7 @@ TEST_CASE("SchemaNode")
"/type_module:iid-valid",
"/type_module:iid-relaxed",
"/type_module:leafListBasic",
+ "/type_module:leafListWithDefault",
"/type_module:leafListWithMinMaxElements",
"/type_module:leafListWithUnits",
"/type_module:listBasic",
@@ -606,6 +607,12 @@ TEST_CASE("SchemaNode")
REQUIRE(!ctx->findPath("/type_module:leafListBasic").asLeafList().isMandatory());
}
+ 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:leafListBasic").asLeafList().defaultValuesStr().size() == 0);
+ }
+
DOCTEST_SUBCASE("LeafList::maxElements")
{
REQUIRE(ctx->findPath("/type_module:leafListWithMinMaxElements").asLeafList().maxElements() == 5);
--
2.43.0
@@ -0,0 +1,434 @@
From 01f2633cef60495d5cafc4b4b1f25273b03ab3cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= <bedrich.schindler@gmail.com>
Date: Tue, 22 Oct 2024 15:11:30 +0200
Subject: [PATCH 06/18] schema: Make choice and case statements available
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Make choice and case statements available so that they can be accessed
if end-user requires reading schema nodes.
By design, choice and case statements do not exist in data tree directly.
Only children of one case can be present in the data tree at one time.
That means that choice and case children are not instantiable, thus
`SchemaNode::immediateChildren` must be used (instead of
`SchemaNode::childInstantibles`) if end-user wants to access choice
and case substatements.
Change-Id: Ib089672ad21dda8a0344895835d92d3432fcccb8
Co-authored-by: Jan Kundrát <jan.kundrat@cesnet.cz>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
include/libyang-cpp/SchemaNode.hpp | 34 +++++++++++++
src/SchemaNode.cpp | 68 ++++++++++++++++++++++++++
tests/context.cpp | 77 +++++++++++++++++++-----------
tests/example_schema.hpp | 60 +++++++++++++++++++++++
tests/schema_node.cpp | 72 ++++++++++++++++++++++++++++
5 files changed, 284 insertions(+), 27 deletions(-)
diff --git a/include/libyang-cpp/SchemaNode.hpp b/include/libyang-cpp/SchemaNode.hpp
index 8ddf9be..0f1a4c4 100644
--- a/include/libyang-cpp/SchemaNode.hpp
+++ b/include/libyang-cpp/SchemaNode.hpp
@@ -22,6 +22,8 @@ class AnyDataAnyXML;
class ActionRpc;
class ActionRpcInput;
class ActionRpcOutput;
+class Case;
+class Choice;
class Container;
class Leaf;
class LeafList;
@@ -62,6 +64,8 @@ public:
// drectly by the user.
// TODO: turn these into a templated `as<>` method.
AnyDataAnyXML asAnyDataAnyXML() const;
+ Case asCase() const;
+ Choice asChoice() const;
Container asContainer() const;
Leaf asLeaf() const;
LeafList asLeafList() const;
@@ -129,6 +133,36 @@ private:
using SchemaNode::SchemaNode;
};
+/**
+ * @brief Class representing a schema definition of a `case` node.
+ *
+ * Wraps `lysc_node_case`.
+ */
+class LIBYANG_CPP_EXPORT Case : public SchemaNode {
+public:
+ friend SchemaNode;
+ friend Choice;
+
+private:
+ using SchemaNode::SchemaNode;
+};
+
+/**
+ * @brief Class representing a schema definition of a `choice` node.
+ *
+ * Wraps `lysc_node_choice`.
+ */
+class LIBYANG_CPP_EXPORT Choice : public SchemaNode {
+public:
+ bool isMandatory() const;
+ std::vector<Case> cases() const;
+ std::optional<Case> defaultCase() const;
+ friend SchemaNode;
+
+private:
+ using SchemaNode::SchemaNode;
+};
+
/**
* @brief Class representing a schema definition of a `container` node.
*/
diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp
index bd20402..26b5099 100644
--- a/src/SchemaNode.cpp
+++ b/src/SchemaNode.cpp
@@ -191,6 +191,32 @@ NodeType SchemaNode::nodeType() const
return utils::toNodeType(m_node->nodetype);
}
+/**
+ * @brief Try to cast this SchemaNode to a Case node.
+ * @throws Error If this node is not a case.
+ */
+Case SchemaNode::asCase() const
+{
+ if (nodeType() != NodeType::Case) {
+ throw Error("Schema node is not a case: " + path());
+ }
+
+ return Case{m_node, m_ctx};
+}
+
+/**
+ * @brief Try to cast this SchemaNode to a Choice node.
+ * @throws Error If this node is not a choice.
+ */
+Choice SchemaNode::asChoice() const
+{
+ if (nodeType() != NodeType::Choice) {
+ throw Error("Schema node is not a choice: " + path());
+ }
+
+ return Choice{m_node, m_ctx};
+}
+
/**
* @brief Try to cast this SchemaNode to a Container node.
* @throws Error If this node is not a container.
@@ -401,6 +427,48 @@ bool AnyDataAnyXML::isMandatory() const
return m_node->flags & LYS_MAND_TRUE;
}
+/**
+ * @brief Checks whether this choice is mandatory.
+ *
+ * Wraps flag `LYS_MAND_TRUE`.
+ */
+bool Choice::isMandatory() const
+{
+ return m_node->flags & LYS_MAND_TRUE;
+}
+
+/**
+ * @brief Retrieves the list of cases for this choice.
+ *
+ * Wraps `lysc_node_choice::cases`.
+ */
+std::vector<Case> Choice::cases() const
+{
+ auto choice = reinterpret_cast<const lysc_node_choice*>(m_node);
+ auto cases = reinterpret_cast<lysc_node*>(choice->cases);
+ std::vector<Case> res;
+ lysc_node* elem;
+ LY_LIST_FOR(cases, elem)
+ {
+ res.emplace_back(Case(elem, m_ctx));
+ }
+ return res;
+}
+
+/**
+ * @brief Retrieves the default case for this choice.
+ *
+ * Wraps `lysc_node_choice::dflt`.
+ */
+std::optional<Case> Choice::defaultCase() const
+{
+ auto choice = reinterpret_cast<const lysc_node_choice*>(m_node);
+ if (!choice->dflt) {
+ return std::nullopt;
+ }
+ return Case{reinterpret_cast<lysc_node*>(choice->dflt), m_ctx};
+}
+
/**
* @brief Checks whether this container is mandatory.
*
diff --git a/tests/context.cpp b/tests/context.cpp
index 902faf6..5929b75 100644
--- a/tests/context.cpp
+++ b/tests/context.cpp
@@ -709,33 +709,56 @@ TEST_CASE("context")
auto mod = ctx_pp->parseModule(type_module, libyang::SchemaFormat::YANG);
REQUIRE(mod.printStr(libyang::SchemaOutputFormat::Tree) == R"(module: type_module
- +--rw anydataBasic? anydata
- +--rw anydataWithMandatoryChild anydata
- +--rw anyxmlBasic? anyxml
- +--rw anyxmlWithMandatoryChild anyxml
- +--rw leafBinary? binary
- +--rw leafBits? bits
- +--rw leafEnum? enumeration
- +--rw leafEnum2? enumeration
- +--rw leafNumber? int32
- +--rw leafRef? -> /custom-prefix:listAdvancedWithOneKey/lol
- +--rw leafRefRelaxed? -> /custom-prefix:listAdvancedWithOneKey/lol
- +--rw leafString? string
- +--rw leafUnion? union
- +--rw meal? identityref
- +--ro leafWithConfigFalse? string
- +--rw leafWithDefaultValue? string
- +--rw leafWithDescription? string
- +--rw leafWithMandatoryTrue string
- x--rw leafWithStatusDeprecated? string
- o--rw leafWithStatusObsolete? string
- +--rw leafWithUnits? int32
- +--rw iid-valid? instance-identifier
- +--rw iid-relaxed? instance-identifier
- +--rw leafListBasic* string
- +--rw leafListWithDefault* int32
- +--rw leafListWithMinMaxElements* int32
- +--rw leafListWithUnits* int32
+ +--rw anydataBasic? anydata
+ +--rw anydataWithMandatoryChild anydata
+ +--rw anyxmlBasic? anyxml
+ +--rw anyxmlWithMandatoryChild anyxml
+ +--rw choiceBasicContainer
+ | +--rw (choiceBasic)?
+ | +--:(case1)
+ | | +--rw l? string
+ | | +--rw ll* string
+ | +--:(case2)
+ | +--rw l2? string
+ +--rw choiceWithMandatoryContainer
+ | +--rw (choiceWithMandatory)
+ | +--:(case3)
+ | | +--rw l3? string
+ | +--:(case4)
+ | +--rw l4? string
+ +--rw choiceWithDefaultContainer
+ | +--rw (choiceWithDefault)?
+ | +--:(case5)
+ | | +--rw l5? string
+ | +--:(case6)
+ | +--rw l6? string
+ +--rw implicitCaseContainer
+ | +--rw (implicitCase)?
+ | +--:(implicitLeaf)
+ | +--rw implicitLeaf? string
+ +--rw leafBinary? binary
+ +--rw leafBits? bits
+ +--rw leafEnum? enumeration
+ +--rw leafEnum2? enumeration
+ +--rw leafNumber? int32
+ +--rw leafRef? -> /custom-prefix:listAdvancedWithOneKey/lol
+ +--rw leafRefRelaxed? -> /custom-prefix:listAdvancedWithOneKey/lol
+ +--rw leafString? string
+ +--rw leafUnion? union
+ +--rw meal? identityref
+ +--ro leafWithConfigFalse? string
+ +--rw leafWithDefaultValue? string
+ +--rw leafWithDescription? string
+ +--rw leafWithMandatoryTrue string
+ x--rw leafWithStatusDeprecated? string
+ o--rw leafWithStatusObsolete? string
+ +--rw leafWithUnits? int32
+ +--rw iid-valid? instance-identifier
+ +--rw iid-relaxed? instance-identifier
+ +--rw leafListBasic* string
+ +--rw leafListWithDefault* int32
+ +--rw leafListWithMinMaxElements* int32
+ +--rw leafListWithUnits* int32
+--rw listBasic* [primary-key]
| +--rw primary-key string
+--rw listAdvancedWithOneKey* [lol]
diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp
index 2861b1a..ae3b4de 100644
--- a/tests/example_schema.hpp
+++ b/tests/example_schema.hpp
@@ -390,6 +390,66 @@ module type_module {
mandatory true;
}
+ container choiceBasicContainer {
+ choice choiceBasic {
+ case case1 {
+ leaf l {
+ type string;
+ }
+ leaf-list ll {
+ type string;
+ ordered-by user;
+ }
+ }
+ case case2 {
+ leaf l2 {
+ type string;
+ }
+ }
+ }
+ }
+
+ container choiceWithMandatoryContainer {
+ choice choiceWithMandatory {
+ mandatory true;
+ case case3 {
+ leaf l3 {
+ type string;
+ }
+ }
+ case case4 {
+ leaf l4 {
+ type string;
+ }
+ }
+ }
+ }
+
+ container choiceWithDefaultContainer {
+ choice choiceWithDefault {
+ default case5;
+ case case5 {
+ leaf l5 {
+ type string;
+ }
+ }
+ case case6 {
+ leaf l6 {
+ type string;
+ }
+ }
+ }
+ }
+
+ container implicitCaseContainer {
+ choice implicitCase {
+ leaf implicitLeaf {
+ type string;
+ }
+ }
+ }
+
+
leaf leafBinary {
type binary;
}
diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp
index 80c7407..8d74bd2 100644
--- a/tests/schema_node.cpp
+++ b/tests/schema_node.cpp
@@ -58,6 +58,9 @@ TEST_CASE("SchemaNode")
],
"type_module:anydataWithMandatoryChild": {"content": "test-string"},
"type_module:anyxmlWithMandatoryChild": {"content": "test-string"},
+ "type_module:choiceWithMandatoryContainer": {
+ "l4": "test-string"
+ },
"type_module:containerWithMandatoryChild": {
"leafWithMandatoryTrue": "test-string"
},
@@ -180,6 +183,10 @@ TEST_CASE("SchemaNode")
"/type_module:anydataWithMandatoryChild",
"/type_module:anyxmlBasic",
"/type_module:anyxmlWithMandatoryChild",
+ "/type_module:choiceBasicContainer",
+ "/type_module:choiceWithMandatoryContainer",
+ "/type_module:choiceWithDefaultContainer",
+ "/type_module:implicitCaseContainer",
"/type_module:leafBinary",
"/type_module:leafBits",
"/type_module:leafEnum",
@@ -417,6 +424,71 @@ TEST_CASE("SchemaNode")
REQUIRE(!ctx->findPath("/type_module:anyxmlBasic").asAnyDataAnyXML().isMandatory());
}
+ DOCTEST_SUBCASE("Choice and Case")
+ {
+ std::string xpath;
+ bool isMandatory = false;
+ std::optional<std::string> defaultCase;
+ std::vector<std::string> caseNames;
+ std::optional<libyang::SchemaNode> root;
+
+ DOCTEST_SUBCASE("two cases with nothing fancy")
+ {
+ root = ctx->findPath("/type_module:choiceBasicContainer");
+ caseNames = {"case1", "case2"};
+ }
+
+ DOCTEST_SUBCASE("mandatory choice") {
+ root = ctx->findPath("/type_module:choiceWithMandatoryContainer");
+ isMandatory = true;
+ caseNames = {"case3", "case4"};
+ }
+
+ DOCTEST_SUBCASE("default choice") {
+ root = ctx->findPath("/type_module:choiceWithDefaultContainer");
+ defaultCase = "case5";
+ caseNames = {"case5", "case6"};
+ }
+
+ DOCTEST_SUBCASE("implicit case") {
+ root = ctx->findPath("/type_module:implicitCaseContainer");
+ caseNames = {"implicitLeaf"};
+ }
+
+ // For testing purposes, we have each choice in its own container. As choice and case are not directly instantiable,
+ // we wrap them in a container to simplify the testing process. It allows us to simply address the choice by its
+ // container and then get the choice from it. It also prevents polluting the test schema with unnecessary nodes
+ // and isolates the choice from other nodes.
+ auto container = root->asContainer();
+ auto choice = container.immediateChildren().begin()->asChoice();
+ REQUIRE(choice.isMandatory() == isMandatory);
+ REQUIRE(!!choice.defaultCase() == !!defaultCase);
+ if (defaultCase) {
+ REQUIRE(choice.defaultCase()->name() == *defaultCase);
+ }
+ std::vector<std::string> actualCaseNames;
+ for (const auto& case_ : choice.cases()) {
+ actualCaseNames.push_back(case_.name());
+ }
+ REQUIRE(actualCaseNames == caseNames);
+
+ // Also test child node access for one arbitrary choice/case combination
+ if (root->path() == "/type_module:choiceBasicContainer") {
+ REQUIRE(choice.cases().size() == 2);
+ auto case1 = choice.cases()[0];
+ auto children = case1.immediateChildren();
+ auto it = children.begin();
+ REQUIRE(it->asLeaf().name() == "l");
+ ++it;
+ REQUIRE(it->asLeafList().name() == "ll");
+
+ auto case2 = choice.cases()[1];
+ children = case2.immediateChildren();
+ it = children.begin();
+ REQUIRE(it->asLeaf().name() == "l2");
+ }
+ }
+
DOCTEST_SUBCASE("Container::isMandatory")
{
REQUIRE(ctx->findPath("/type_module:containerWithMandatoryChild").asContainer().isMandatory());
--
2.43.0
@@ -0,0 +1,129 @@
From a1acdc794facf8cbf113f73274ecebd5898c81a1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Tue, 17 Dec 2024 15:08:43 +0100
Subject: [PATCH 07/18] Wrap lyd_change_term for changing the value for a
terminal node
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Previously, the code would require a newPath(...,
libyang::CreationOptions::Update), which is quite a mouthful.
Change-Id: I8a908c0fdd3e48dda830819758522a511adedd3b
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
include/libyang-cpp/DataNode.hpp | 8 ++++++
src/DataNode.cpp | 21 ++++++++++++++++
tests/data_node.cpp | 42 ++++++++++++++++++++++++++------
3 files changed, 63 insertions(+), 8 deletions(-)
diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp
index 2211415..851681b 100644
--- a/include/libyang-cpp/DataNode.hpp
+++ b/include/libyang-cpp/DataNode.hpp
@@ -212,6 +212,14 @@ public:
Value value() const;
types::Type valueType() const;
+ /** @brief Was the value changed? */
+ enum class ValueChange {
+ Changed, /**< Yes, this is an actual change of the stored value */
+ ExplicitNonDefault, /**< It still holds the default value, but it's been set explicitly now */
+ EqualValueNotChanged, /**< No change, the previous value is the same as the new one, and it isn't an implicit default */
+ };
+ ValueChange changeValue(const std::string value);
+
private:
using DataNode::DataNode;
};
diff --git a/src/DataNode.cpp b/src/DataNode.cpp
index 2ef17f2..84591e5 100644
--- a/src/DataNode.cpp
+++ b/src/DataNode.cpp
@@ -903,6 +903,27 @@ types::Type DataNodeTerm::valueType() const
return impl(reinterpret_cast<const lyd_node_term*>(m_node)->value);
}
+/** @short Change the term's value
+ *
+ * Wraps `lyd_change_term`.
+ * */
+DataNodeTerm::ValueChange DataNodeTerm::changeValue(const std::string value)
+{
+ auto ret = lyd_change_term(m_node, value.c_str());
+
+ switch (ret) {
+ case LY_SUCCESS:
+ return ValueChange::Changed;
+ case LY_EEXIST:
+ return ValueChange::ExplicitNonDefault;
+ case LY_ENOT:
+ return ValueChange::EqualValueNotChanged;
+ default:
+ throwIfError(ret, "DataNodeTerm::changeValue failed");
+ __builtin_unreachable();
+ }
+}
+
/**
* @brief Returns a collection for iterating depth-first over the subtree this instance points to.
*
diff --git a/tests/data_node.cpp b/tests/data_node.cpp
index 8a2610e..45fd6c1 100644
--- a/tests/data_node.cpp
+++ b/tests/data_node.cpp
@@ -456,15 +456,41 @@ TEST_CASE("Data Node manipulation")
REQUIRE(node.hasDefaultValue());
REQUIRE(node.isImplicitDefault());
- data->newPath("/example-schema3:leafWithDefault", "not-default-value", libyang::CreationOptions::Update);
- node = data->findPath("/example-schema3:leafWithDefault")->asTerm();
- REQUIRE(!node.hasDefaultValue());
- REQUIRE(!node.isImplicitDefault());
+ DOCTEST_SUBCASE("newPath")
+ {
+ data->newPath("/example-schema3:leafWithDefault", "not-default-value", libyang::CreationOptions::Update);
+ node = data->findPath("/example-schema3:leafWithDefault")->asTerm();
+ REQUIRE(!node.hasDefaultValue());
+ REQUIRE(!node.isImplicitDefault());
- data->newPath("/example-schema3:leafWithDefault", "AHOJ", libyang::CreationOptions::Update);
- node = data->findPath("/example-schema3:leafWithDefault")->asTerm();
- REQUIRE(node.hasDefaultValue());
- REQUIRE(!node.isImplicitDefault());
+ data->newPath("/example-schema3:leafWithDefault", "AHOJ", libyang::CreationOptions::Update);
+ node = data->findPath("/example-schema3:leafWithDefault")->asTerm();
+ REQUIRE(node.hasDefaultValue());
+ REQUIRE(!node.isImplicitDefault());
+ }
+
+ DOCTEST_SUBCASE("changing values")
+ {
+ auto node = data->findPath("/example-schema3:leafWithDefault");
+ REQUIRE(!!node);
+ auto term = node->asTerm();
+
+ DOCTEST_SUBCASE("to an arbitrary value") {
+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::Changed);
+ }
+
+ DOCTEST_SUBCASE("from an implicit default to an explicit default") {
+ REQUIRE(term.changeValue("AHOJ") == libyang::DataNodeTerm::ValueChange::ExplicitNonDefault);
+ REQUIRE(term.changeValue("AHOJ") == libyang::DataNodeTerm::ValueChange::EqualValueNotChanged);
+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::Changed);
+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::EqualValueNotChanged);
+ }
+
+ DOCTEST_SUBCASE("from an implicit default to something else") {
+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::Changed);
+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::EqualValueNotChanged);
+ }
+ }
}
DOCTEST_SUBCASE("isTerm")
--
2.43.0
@@ -0,0 +1,652 @@
From 32b200ed06e9adb44a8d4ce6771f18812a54d06e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= <bedrich.schindler@gmail.com>
Date: Wed, 20 Nov 2024 10:20:19 +0100
Subject: [PATCH 08/18] Add `Module::child()`, `Module::childrenDfs()` and
`Module::immediateChildren()`
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Those functions are implemented in the same manner as in `SchemaNode`
and allows to walk through modules children. This is counterpart to
already implemented `Module::childInstantiables()` that returns
instantiables schema nodes. These return all nodes, including
the schema-only nodes such as choice and case if end-user needs
to read its schema.
While the implementation is inspired by functions in `SchemaNode`,
imlementation of `Module::parent()` and `Module::siblings()` was omitted
as those do no make sense on `Module`.
Change-Id: I38c8374304f859d65343d04d08302e07deb05f27
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
include/libyang-cpp/Collection.hpp | 1 +
include/libyang-cpp/Module.hpp | 5 +
src/Module.cpp | 40 +++
tests/context.cpp | 5 +
tests/example_schema.hpp | 21 ++
tests/schema_node.cpp | 409 +++++++++++++++++++----------
6 files changed, 346 insertions(+), 135 deletions(-)
diff --git a/include/libyang-cpp/Collection.hpp b/include/libyang-cpp/Collection.hpp
index 557a0a2..4324791 100644
--- a/include/libyang-cpp/Collection.hpp
+++ b/include/libyang-cpp/Collection.hpp
@@ -98,6 +98,7 @@ class LIBYANG_CPP_EXPORT Collection {
public:
friend DataNode;
friend Iterator<NodeType, ITER_TYPE>;
+ friend Module;
friend SchemaNode;
~Collection();
Collection(const Collection<NodeType, ITER_TYPE>&);
diff --git a/include/libyang-cpp/Module.hpp b/include/libyang-cpp/Module.hpp
index f10c36f..ab20d36 100644
--- a/include/libyang-cpp/Module.hpp
+++ b/include/libyang-cpp/Module.hpp
@@ -34,6 +34,8 @@ class ChildInstanstiables;
class Identity;
class SchemaNode;
class SubmoduleParsed;
+template <typename NodeType, IterationType ITER_TYPE>
+class Collection;
namespace types {
class IdentityRef;
@@ -86,7 +88,10 @@ public:
std::vector<Identity> identities() const;
+ std::optional<SchemaNode> child() const;
ChildInstanstiables childInstantiables() const;
+ libyang::Collection<SchemaNode, IterationType::Dfs> childrenDfs() const;
+ Collection<SchemaNode, IterationType::Sibling> immediateChildren() const;
std::vector<SchemaNode> actionRpcs() const;
std::string printStr(const SchemaOutputFormat format, const std::optional<SchemaPrintFlags> flags = std::nullopt, std::optional<size_t> lineLength = std::nullopt) const;
diff --git a/src/Module.cpp b/src/Module.cpp
index 4dc9e3b..d6d4023 100644
--- a/src/Module.cpp
+++ b/src/Module.cpp
@@ -8,6 +8,7 @@
#include <algorithm>
#include <libyang-cpp/ChildInstantiables.hpp>
+#include <libyang-cpp/Collection.hpp>
#include <libyang-cpp/Module.hpp>
#include <libyang-cpp/Utils.hpp>
#include <libyang/libyang.h>
@@ -178,6 +179,23 @@ std::vector<Identity> Module::identities() const
return res;
}
+/**
+ * @brief Returns the first child node of this module.
+ * @return The child, or std::nullopt if there are no children.
+ */
+std::optional<SchemaNode> Module::child() const
+{
+ if (!m_module->implemented) {
+ throw Error{"Module::child: module is not implemented"};
+ }
+
+ if (!m_module->compiled->data) {
+ return std::nullopt;
+ }
+
+ return SchemaNode{m_module->compiled->data, m_ctx};
+}
+
/**
* @brief Returns a collection of data instantiable top-level nodes of this module.
*
@@ -191,6 +209,28 @@ ChildInstanstiables Module::childInstantiables() const
return ChildInstanstiables{nullptr, m_module->compiled, m_ctx};
}
+/**
+ * @brief Returns a collection for iterating depth-first over the subtree this module points to.
+ */
+Collection<SchemaNode, IterationType::Dfs> Module::childrenDfs() const
+{
+ if (!m_module->implemented) {
+ throw Error{"Module::childrenDfs: module is not implemented"};
+ }
+ return Collection<SchemaNode, IterationType::Dfs>{m_module->compiled->data, m_ctx};
+}
+
+/**
+ * @brief Returns a collection for iterating over the immediate children of where this module points to.
+ *
+ * This is a convenience function for iterating over this->child().siblings() which does not throw even when module has no children.
+ */
+Collection<SchemaNode, IterationType::Sibling> Module::immediateChildren() const
+{
+ auto c = child();
+ return c ? c->siblings() : Collection<SchemaNode, IterationType::Sibling>{nullptr, nullptr};
+}
+
/**
* @brief Returns a collection of RPC nodes (not action nodes) as SchemaNode
*
diff --git a/tests/context.cpp b/tests/context.cpp
index 5929b75..25343db 100644
--- a/tests/context.cpp
+++ b/tests/context.cpp
@@ -713,6 +713,11 @@ TEST_CASE("context")
+--rw anydataWithMandatoryChild anydata
+--rw anyxmlBasic? anyxml
+--rw anyxmlWithMandatoryChild anyxml
+ +--rw (choiceOnModule)?
+ | +--:(case1)
+ | | +--rw choiceOnModuleLeaf1? string
+ | +--:(case2)
+ | +--rw choiceOnModuleLeaf2? string
+--rw choiceBasicContainer
| +--rw (choiceBasic)?
| +--:(case1)
diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp
index ae3b4de..0d8acb9 100644
--- a/tests/example_schema.hpp
+++ b/tests/example_schema.hpp
@@ -390,6 +390,19 @@ module type_module {
mandatory true;
}
+ choice choiceOnModule {
+ case case1 {
+ leaf choiceOnModuleLeaf1 {
+ type string;
+ }
+ }
+ case case2 {
+ leaf choiceOnModuleLeaf2 {
+ type string;
+ }
+ }
+ }
+
container choiceBasicContainer {
choice choiceBasic {
case case1 {
@@ -787,6 +800,14 @@ module type_module {
}
)"s;
+const auto empty_module = R"(
+module empty_module {
+ yang-version 1.1;
+ namespace "e";
+ prefix "e";
+}
+)"s;
+
const auto with_inet_types_module = R"(
module with-inet-types {
yang-version 1.1;
diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp
index 8d74bd2..0001377 100644
--- a/tests/schema_node.cpp
+++ b/tests/schema_node.cpp
@@ -24,8 +24,10 @@ TEST_CASE("SchemaNode")
libyang::ContextOptions::SetPrivParsed | libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd};
ctx->parseModule(example_schema, libyang::SchemaFormat::YANG);
ctx->parseModule(type_module, libyang::SchemaFormat::YANG);
+ ctx->parseModule(empty_module, libyang::SchemaFormat::YANG);
ctxWithParsed->parseModule(example_schema, libyang::SchemaFormat::YANG);
ctxWithParsed->parseModule(type_module, libyang::SchemaFormat::YANG);
+ ctxWithParsed->parseModule(empty_module, libyang::SchemaFormat::YANG);
DOCTEST_SUBCASE("context lifetime")
{
@@ -74,10 +76,34 @@ TEST_CASE("SchemaNode")
REQUIRE(node->schema().path() == "/example-schema:person");
}
- DOCTEST_SUBCASE("SchemaNode::child")
+ DOCTEST_SUBCASE("child")
{
- REQUIRE(ctx->findPath("/type_module:listAdvancedWithTwoKey").child()->name() == "first");
- REQUIRE(!ctx->findPath("/type_module:leafString").child().has_value());
+ DOCTEST_SUBCASE("implemented module")
+ {
+ DOCTEST_SUBCASE("SchemaNode::child")
+ {
+ REQUIRE(ctx->findPath("/type_module:listAdvancedWithTwoKey").child()->name() == "first");
+ REQUIRE(!ctx->findPath("/type_module:leafString").child().has_value());
+ }
+
+ DOCTEST_SUBCASE("Module::child")
+ {
+ REQUIRE(ctx->getModule("type_module", std::nullopt)->child()->name() == "anydataBasic");
+ REQUIRE(!ctx->getModule("empty_module", std::nullopt)->child());
+ }
+ }
+
+ DOCTEST_SUBCASE("unimplemented module")
+ {
+ DOCTEST_SUBCASE("Module::child")
+ {
+ ctx->setSearchDir(TESTS_DIR / "yang");
+ auto modYangPatch = ctx->loadModule("ietf-yang-patch", std::nullopt);
+ auto modRestconf = ctx->getModule("ietf-restconf", "2017-01-26");
+ REQUIRE(!modRestconf->implemented());
+ REQUIRE_THROWS_WITH_AS(modRestconf->child(), "Module::child: module is not implemented", libyang::Error);
+ }
+ }
}
DOCTEST_SUBCASE("SchemaNode::config")
@@ -160,162 +186,275 @@ TEST_CASE("SchemaNode")
DOCTEST_SUBCASE("childInstantiables")
{
- std::vector<std::string> expectedPaths;
- std::optional<libyang::ChildInstanstiables> children;
-
- DOCTEST_SUBCASE("SchemaNode::childInstantiables")
+ DOCTEST_SUBCASE("implemented module")
{
- expectedPaths = {
- "/type_module:listAdvancedWithOneKey/lol",
- "/type_module:listAdvancedWithOneKey/notKey1",
- "/type_module:listAdvancedWithOneKey/notKey2",
- "/type_module:listAdvancedWithOneKey/notKey3",
- "/type_module:listAdvancedWithOneKey/notKey4",
- };
+ std::vector<std::string> expectedPaths;
+ std::optional<libyang::ChildInstanstiables> children;
+
+ DOCTEST_SUBCASE("SchemaNode::childInstantiables")
+ {
+ expectedPaths = {
+ "/type_module:listAdvancedWithOneKey/lol",
+ "/type_module:listAdvancedWithOneKey/notKey1",
+ "/type_module:listAdvancedWithOneKey/notKey2",
+ "/type_module:listAdvancedWithOneKey/notKey3",
+ "/type_module:listAdvancedWithOneKey/notKey4",
+ };
+
+ children = ctx->findPath("/type_module:listAdvancedWithOneKey").childInstantiables();
+ }
- children = ctx->findPath("/type_module:listAdvancedWithOneKey").childInstantiables();
- }
+ DOCTEST_SUBCASE("Module::childInstantiables")
+ {
+ expectedPaths = {
+ "/type_module:anydataBasic",
+ "/type_module:anydataWithMandatoryChild",
+ "/type_module:anyxmlBasic",
+ "/type_module:anyxmlWithMandatoryChild",
+ "/type_module:choiceOnModuleLeaf1",
+ "/type_module:choiceOnModuleLeaf2",
+ "/type_module:choiceBasicContainer",
+ "/type_module:choiceWithMandatoryContainer",
+ "/type_module:choiceWithDefaultContainer",
+ "/type_module:implicitCaseContainer",
+ "/type_module:leafBinary",
+ "/type_module:leafBits",
+ "/type_module:leafEnum",
+ "/type_module:leafEnum2",
+ "/type_module:leafNumber",
+ "/type_module:leafRef",
+ "/type_module:leafRefRelaxed",
+ "/type_module:leafString",
+ "/type_module:leafUnion",
+ "/type_module:meal",
+ "/type_module:leafWithConfigFalse",
+ "/type_module:leafWithDefaultValue",
+ "/type_module:leafWithDescription",
+ "/type_module:leafWithMandatoryTrue",
+ "/type_module:leafWithStatusDeprecated",
+ "/type_module:leafWithStatusObsolete",
+ "/type_module:leafWithUnits",
+ "/type_module:iid-valid",
+ "/type_module:iid-relaxed",
+ "/type_module:leafListBasic",
+ "/type_module:leafListWithDefault",
+ "/type_module:leafListWithMinMaxElements",
+ "/type_module:leafListWithUnits",
+ "/type_module:listBasic",
+ "/type_module:listAdvancedWithOneKey",
+ "/type_module:listAdvancedWithTwoKey",
+ "/type_module:listWithMinMaxElements",
+ "/type_module:numeric",
+ "/type_module:container",
+ "/type_module:containerWithMandatoryChild",
+ };
+ children = ctx->getModule("type_module", std::nullopt)->childInstantiables();
+ }
- DOCTEST_SUBCASE("Module::childInstantiables")
- {
- expectedPaths = {
- "/type_module:anydataBasic",
- "/type_module:anydataWithMandatoryChild",
- "/type_module:anyxmlBasic",
- "/type_module:anyxmlWithMandatoryChild",
- "/type_module:choiceBasicContainer",
- "/type_module:choiceWithMandatoryContainer",
- "/type_module:choiceWithDefaultContainer",
- "/type_module:implicitCaseContainer",
- "/type_module:leafBinary",
- "/type_module:leafBits",
- "/type_module:leafEnum",
- "/type_module:leafEnum2",
- "/type_module:leafNumber",
- "/type_module:leafRef",
- "/type_module:leafRefRelaxed",
- "/type_module:leafString",
- "/type_module:leafUnion",
- "/type_module:meal",
- "/type_module:leafWithConfigFalse",
- "/type_module:leafWithDefaultValue",
- "/type_module:leafWithDescription",
- "/type_module:leafWithMandatoryTrue",
- "/type_module:leafWithStatusDeprecated",
- "/type_module:leafWithStatusObsolete",
- "/type_module:leafWithUnits",
- "/type_module:iid-valid",
- "/type_module:iid-relaxed",
- "/type_module:leafListBasic",
- "/type_module:leafListWithDefault",
- "/type_module:leafListWithMinMaxElements",
- "/type_module:leafListWithUnits",
- "/type_module:listBasic",
- "/type_module:listAdvancedWithOneKey",
- "/type_module:listAdvancedWithTwoKey",
- "/type_module:listWithMinMaxElements",
- "/type_module:numeric",
- "/type_module:container",
- "/type_module:containerWithMandatoryChild",
- };
- children = ctx->getModule("type_module", std::nullopt)->childInstantiables();
- }
+ std::vector<std::string> actualPaths;
+ for (const auto& child : *children) {
+ actualPaths.emplace_back(child.path());
+ }
- std::vector<std::string> actualPaths;
- for (const auto& child : *children) {
- actualPaths.emplace_back(child.path());
+ REQUIRE(expectedPaths == actualPaths);
}
- REQUIRE(expectedPaths == actualPaths);
+ DOCTEST_SUBCASE("unimplemented module")
+ {
+ DOCTEST_SUBCASE("Module::childInstantiables")
+ {
+ ctx->setSearchDir(TESTS_DIR / "yang");
+ auto modYangPatch = ctx->loadModule("ietf-yang-patch", std::nullopt);
+ auto modRestconf = ctx->getModule("ietf-restconf", "2017-01-26");
+ REQUIRE(!modRestconf->implemented());
+ REQUIRE_THROWS_WITH_AS(modRestconf->childInstantiables(), "Module::childInstantiables: module is not implemented", libyang::Error);
+ }
+ }
}
- DOCTEST_SUBCASE("SchemaNode::childrenDfs")
+ DOCTEST_SUBCASE("childrenDfs")
{
- std::vector<std::string> expectedPaths;
+ DOCTEST_SUBCASE("implemented module")
+ {
+ std::vector<std::string> expectedPaths;
+ std::optional<libyang::Collection<libyang::SchemaNode, libyang::IterationType::Dfs>> children;
- const char* path;
+ DOCTEST_SUBCASE("SchemaNode::childrenDfs")
+ {
+ DOCTEST_SUBCASE("listAdvancedWithTwoKey")
+ {
+ expectedPaths = {
+ "/type_module:listAdvancedWithTwoKey",
+ "/type_module:listAdvancedWithTwoKey/first",
+ "/type_module:listAdvancedWithTwoKey/second",
+ };
+ children = ctx->findPath("/type_module:listAdvancedWithTwoKey").childrenDfs();
+ }
- DOCTEST_SUBCASE("listAdvancedWithTwoKey")
- {
- expectedPaths = {
- "/type_module:listAdvancedWithTwoKey",
- "/type_module:listAdvancedWithTwoKey/first",
- "/type_module:listAdvancedWithTwoKey/second",
- };
+ DOCTEST_SUBCASE("DFS on a leaf")
+ {
+ expectedPaths = {
+ "/type_module:leafString",
+ };
+ children = ctx->findPath("/type_module:leafString").childrenDfs();
+ }
+ }
- path = "/type_module:listAdvancedWithTwoKey";
- }
+ DOCTEST_SUBCASE("Module::childrenDfs")
+ {
+ expectedPaths = {
+ "/type_module:anydataBasic",
+ };
+ children = ctx->getModule("type_module", std::nullopt)->childrenDfs();
+ }
- DOCTEST_SUBCASE("DFS on a leaf")
- {
- expectedPaths = {
- "/type_module:leafString",
- };
+ std::vector<std::string> actualPaths;
+ for (const auto& it : *children) {
+ actualPaths.emplace_back(it.path());
+ }
- path = "/type_module:leafString";
+ REQUIRE(actualPaths == expectedPaths);
}
- std::vector<std::string> actualPaths;
- for (const auto& it : ctx->findPath(path).childrenDfs()) {
- actualPaths.emplace_back(it.path());
+ DOCTEST_SUBCASE("unimplemented module")
+ {
+ DOCTEST_SUBCASE("Module::childrenDfs")
+ {
+ ctx->setSearchDir(TESTS_DIR / "yang");
+ auto modYangPatch = ctx->loadModule("ietf-yang-patch", std::nullopt);
+ auto modRestconf = ctx->getModule("ietf-restconf", "2017-01-26");
+ REQUIRE(!modRestconf->implemented());
+ REQUIRE_THROWS_WITH_AS(modRestconf->childrenDfs(), "Module::childrenDfs: module is not implemented", libyang::Error);
+ }
}
-
- REQUIRE(actualPaths == expectedPaths);
}
- DOCTEST_SUBCASE("SchemaNode::immediateChildren")
+ DOCTEST_SUBCASE("immediateChildren")
{
- std::vector<std::string> expectedPaths;
- const char* path;
- DOCTEST_SUBCASE("listAdvancedWithTwoKey")
- {
- expectedPaths = {
- "/type_module:listAdvancedWithTwoKey/first",
- "/type_module:listAdvancedWithTwoKey/second",
- };
- path = "/type_module:listAdvancedWithTwoKey";
- }
- DOCTEST_SUBCASE("leaf")
+ DOCTEST_SUBCASE("implemented module")
{
- expectedPaths = {
- };
- path = "/type_module:leafString";
- }
- DOCTEST_SUBCASE("no recursion")
- {
- expectedPaths = {
- "/type_module:container/x",
- "/type_module:container/y",
- "/type_module:container/z",
- };
- path = "/type_module:container";
- }
- DOCTEST_SUBCASE("empty container")
- {
- expectedPaths = {
- };
- path = "/type_module:container/y";
- }
- DOCTEST_SUBCASE("one item")
- {
- expectedPaths = {
- "/type_module:container/z/z1",
- };
- path = "/type_module:container/z";
+ std::vector<std::string> expectedPaths;
+ std::optional<libyang::Collection<libyang::SchemaNode, libyang::IterationType::Sibling>> children;
+
+ DOCTEST_SUBCASE("SchemaNode::immediateChildren")
+ {
+ DOCTEST_SUBCASE("listAdvancedWithTwoKey")
+ {
+ expectedPaths = {
+ "/type_module:listAdvancedWithTwoKey/first",
+ "/type_module:listAdvancedWithTwoKey/second",
+ };
+ children = ctx->findPath("/type_module:listAdvancedWithTwoKey").immediateChildren();
+ }
+ DOCTEST_SUBCASE("leaf")
+ {
+ expectedPaths = {};
+ children = ctx->findPath("/type_module:leafString").immediateChildren();
+ }
+ DOCTEST_SUBCASE("no recursion")
+ {
+ expectedPaths = {
+ "/type_module:container/x",
+ "/type_module:container/y",
+ "/type_module:container/z",
+ };
+ children = ctx->findPath("/type_module:container").immediateChildren();
+ }
+ DOCTEST_SUBCASE("empty container")
+ {
+ expectedPaths = {};
+ children = ctx->findPath("/type_module:container/y").immediateChildren();
+ }
+ DOCTEST_SUBCASE("one item")
+ {
+ expectedPaths = {
+ "/type_module:container/z/z1",
+ };
+ children = ctx->findPath("/type_module:container/z").immediateChildren();
+ }
+ DOCTEST_SUBCASE("two items")
+ {
+ expectedPaths = {
+ "/type_module:container/x/x1",
+ "/type_module:container/x/x2",
+ };
+ children = ctx->findPath("/type_module:container/x").immediateChildren();
+ }
+ }
+
+ DOCTEST_SUBCASE("Module::immediateChildren")
+ {
+ expectedPaths = {
+ "/type_module:anydataBasic",
+ "/type_module:anydataWithMandatoryChild",
+ "/type_module:anyxmlBasic",
+ "/type_module:anyxmlWithMandatoryChild",
+ // choiceOnModule is a choice, so it doesn't have path "/type_module:choiceOnModule".
+ // This node is tested at the end of the test subcase.
+ "/",
+ "/type_module:choiceBasicContainer",
+ "/type_module:choiceWithMandatoryContainer",
+ "/type_module:choiceWithDefaultContainer",
+ "/type_module:implicitCaseContainer",
+ "/type_module:leafBinary",
+ "/type_module:leafBits",
+ "/type_module:leafEnum",
+ "/type_module:leafEnum2",
+ "/type_module:leafNumber",
+ "/type_module:leafRef",
+ "/type_module:leafRefRelaxed",
+ "/type_module:leafString",
+ "/type_module:leafUnion",
+ "/type_module:meal",
+ "/type_module:leafWithConfigFalse",
+ "/type_module:leafWithDefaultValue",
+ "/type_module:leafWithDescription",
+ "/type_module:leafWithMandatoryTrue",
+ "/type_module:leafWithStatusDeprecated",
+ "/type_module:leafWithStatusObsolete",
+ "/type_module:leafWithUnits",
+ "/type_module:iid-valid",
+ "/type_module:iid-relaxed",
+ "/type_module:leafListBasic",
+ "/type_module:leafListWithDefault",
+ "/type_module:leafListWithMinMaxElements",
+ "/type_module:leafListWithUnits",
+ "/type_module:listBasic",
+ "/type_module:listAdvancedWithOneKey",
+ "/type_module:listAdvancedWithTwoKey",
+ "/type_module:listWithMinMaxElements",
+ "/type_module:numeric",
+ "/type_module:container",
+ "/type_module:containerWithMandatoryChild",
+ };
+ children = ctx->getModule("type_module", std::nullopt)->immediateChildren();
+
+ std::vector<std::string> actualNames;
+ for (auto it : children.value()) {
+ actualNames.emplace_back(it.name());
+ }
+ // choiceOnModule is a choice, so it doesn't have path, just name.
+ REQUIRE(actualNames[4] == "choiceOnModule");
+ }
+
+ std::vector<std::string> actualPaths;
+ for (const auto& it : *children) {
+ actualPaths.emplace_back(it.path());
+ }
+ REQUIRE(actualPaths == expectedPaths);
}
- DOCTEST_SUBCASE("two items")
+
+ DOCTEST_SUBCASE("unimplemented module")
{
- expectedPaths = {
- "/type_module:container/x/x1",
- "/type_module:container/x/x2",
- };
- path = "/type_module:container/x";
- }
- std::vector<std::string> actualPaths;
- for (const auto& it : ctx->findPath(path).immediateChildren()) {
- actualPaths.emplace_back(it.path());
+ DOCTEST_SUBCASE("Module::immediateChildren")
+ {
+ ctx->setSearchDir(TESTS_DIR / "yang");
+ auto modYangPatch = ctx->loadModule("ietf-yang-patch", std::nullopt);
+ auto modRestconf = ctx->getModule("ietf-restconf", "2017-01-26");
+ REQUIRE(!modRestconf->implemented());
+ REQUIRE_THROWS_WITH_AS(modRestconf->immediateChildren(), "Module::child: module is not implemented", libyang::Error);
+ }
}
- REQUIRE(actualPaths == expectedPaths);
}
DOCTEST_SUBCASE("SchemaNode::siblings")
--
2.43.0
@@ -0,0 +1,46 @@
From 39c7530caa510144c17521278b721ba1e6d8ff40 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Thu, 9 Jan 2025 15:31:37 +0100
Subject: [PATCH 09/18] upstream stopped reporting schema-mounts node
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: I940769d38d56fcfda3e1408c92331fdb00c161e9
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 2 +-
tests/context.cpp | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3d86809..732f52b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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.4.2 IMPORTED_TARGET)
+pkg_check_modules(LIBYANG REQUIRED libyang>=3.7.8 IMPORTED_TARGET)
set(LIBYANG_CPP_PKG_VERSION "3")
# FIXME from gcc 14.1 on we should be able to use the calendar/time from libstdc++ and thus remove the date dependency
diff --git a/tests/context.cpp b/tests/context.cpp
index 5929b75..9d38fea 100644
--- a/tests/context.cpp
+++ b/tests/context.cpp
@@ -509,8 +509,7 @@ TEST_CASE("context")
"error-message": "hi"
}
]
- },
- "ietf-yang-schema-mount:schema-mounts": {}
+ }
}
)");
}
--
2.43.0
@@ -0,0 +1,39 @@
From 82c963766ad4e4a802db7be656acbedb640745e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 30 Jan 2025 16:34:12 +0100
Subject: [PATCH 10/18] CI: temporarily pin version due to anyxml behavior
changes upstream
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: Id977f4d045098c1b93656c0efb871c9a1b650e2d
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 b41c490..19f0def 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-01-29
- name: github/onqtam/doctest
override-checkout: v2.3.6
- f38-clang-asan-ubsan:
required-projects: &projects
- name: github/CESNET/libyang
- override-checkout: devel
+ override-checkout: cesnet/2025-01-29
- name: github/onqtam/doctest
override-checkout: v2.4.11
- f38-clang-tsan:
--
2.43.0
@@ -0,0 +1,40 @@
From 50a216e35a555961f94a32a71bb2d45ac611d0aa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Wed, 29 Jan 2025 22:49:08 +0100
Subject: [PATCH 11/18] build: a single place to define package version
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: I2cd7397895ed4852f852e99b97543dde76eaff8f
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 732f52b..c518ca8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -21,7 +21,8 @@ add_custom_target(libyang-cpp-version-cmake
cmake/ProjectGitVersionRunner.cmake
)
include(cmake/ProjectGitVersion.cmake)
-prepare_git_version(LIBYANG_CPP_VERSION "3")
+set(LIBYANG_CPP_PKG_VERSION "3")
+prepare_git_version(LIBYANG_CPP_VERSION ${LIBYANG_CPP_PKG_VERSION})
find_package(Doxygen)
option(WITH_DOCS "Create and install internal documentation (needs Doxygen)" ${DOXYGEN_FOUND})
@@ -29,7 +30,6 @@ option(BUILD_SHARED_LIBS "By default, shared libs are enabled. Turn off for a st
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBYANG REQUIRED libyang>=3.7.8 IMPORTED_TARGET)
-set(LIBYANG_CPP_PKG_VERSION "3")
# 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)
--
2.43.0
@@ -0,0 +1,173 @@
From 1533458346b4f395b1187c646b61bbcb1fddc615 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Mon, 4 Nov 2024 14:52:12 +0100
Subject: [PATCH 12/18] YANG-flavored regular expressions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: I93b2756d0f470585280c076308df3f384bd7765d
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 3 +++
include/libyang-cpp/Regex.hpp | 28 ++++++++++++++++++++++
src/Regex.cpp | 45 +++++++++++++++++++++++++++++++++++
tests/regex.cpp | 30 +++++++++++++++++++++++
4 files changed, 106 insertions(+)
create mode 100644 include/libyang-cpp/Regex.hpp
create mode 100644 src/Regex.cpp
create mode 100644 tests/regex.cpp
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c518ca8..512af8c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -54,6 +54,7 @@ add_library(yang-cpp
src/Enum.cpp
src/Collection.cpp
src/Module.cpp
+ src/Regex.cpp
src/SchemaNode.cpp
src/Set.cpp
src/Type.cpp
@@ -83,6 +84,7 @@ if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.24")
libyang-cpp/Enum.hpp
libyang-cpp/ChildInstantiables.hpp
libyang-cpp/Module.hpp
+ libyang-cpp/Regex.hpp
libyang-cpp/Set.hpp
libyang-cpp/SchemaNode.hpp
libyang-cpp/Time.hpp
@@ -119,6 +121,7 @@ if(BUILD_TESTING)
libyang_cpp_test(schema_node)
libyang_cpp_test(unsafe)
target_link_libraries(test_unsafe PkgConfig::LIBYANG)
+ libyang_cpp_test(regex)
if(date_FOUND)
add_executable(test_time-stl-hhdate tests/time.cpp)
diff --git a/include/libyang-cpp/Regex.hpp b/include/libyang-cpp/Regex.hpp
new file mode 100644
index 0000000..31935f2
--- /dev/null
+++ b/include/libyang-cpp/Regex.hpp
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Jan Kundrát <jan.kundrat@cesnet.cz>
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+#pragma once
+#include <libyang-cpp/export.h>
+#include <string>
+
+namespace libyang {
+class Context;
+
+/**
+ * @brief A regular expression pattern which uses the YANG-flavored regex engine
+ */
+class LIBYANG_CPP_EXPORT Regex {
+public:
+ Regex(const std::string& pattern);
+ ~Regex();
+ bool matches(const std::string& input);
+
+private:
+ void* code;
+};
+
+}
diff --git a/src/Regex.cpp b/src/Regex.cpp
new file mode 100644
index 0000000..a34fcd5
--- /dev/null
+++ b/src/Regex.cpp
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Jan Kundrát <jan.kundrat@cesnet.cz>
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+// The following header MUST be included before anything else which "might" use PCRE2
+// because that library uses the preprocessor to prepare the "correct" versions of symbols.
+#include <libyang/tree_data.h>
+
+#include <libyang-cpp/Regex.hpp>
+#include <pcre2.h>
+#include "utils/exception.hpp"
+
+#define THE_PCRE2_CODE_P reinterpret_cast<pcre2_code*>(this->code)
+#define THE_PCRE2_CODE_P_P reinterpret_cast<pcre2_code**>(&this->code)
+
+namespace libyang {
+
+Regex::Regex(const std::string& pattern)
+ : code(nullptr)
+{
+ auto res = ly_pattern_compile(nullptr, pattern.c_str(), THE_PCRE2_CODE_P_P);
+ throwIfError(res, ly_last_logmsg());
+}
+
+Regex::~Regex()
+{
+ pcre2_code_free(THE_PCRE2_CODE_P);
+}
+
+bool Regex::matches(const std::string& input)
+{
+ auto res = ly_pattern_match(nullptr, nullptr /* we have a precompiled pattern */, input.c_str(), input.size(), THE_PCRE2_CODE_P_P);
+ if (res == LY_SUCCESS) {
+ return true;
+ } else if (res == LY_ENOT) {
+ return false;
+ } else {
+ throwError(res, ly_last_logmsg());
+ }
+}
+}
diff --git a/tests/regex.cpp b/tests/regex.cpp
new file mode 100644
index 0000000..7594f43
--- /dev/null
+++ b/tests/regex.cpp
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Jan Kundrát <jan.kundrat@cesnet.cz>
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include <doctest/doctest.h>
+#include <libyang-cpp/Regex.hpp>
+#include <libyang-cpp/Utils.hpp>
+
+TEST_CASE("regex")
+{
+ using libyang::Regex;
+ using namespace std::string_literals;
+
+ REQUIRE_THROWS_WITH_AS(Regex{"\\"}, R"(Regular expression "\" is not valid ("": \ at end of pattern).: LY_EVALID)", libyang::ErrorWithCode);
+
+ Regex re{"ahoj"};
+ REQUIRE(re.matches("ahoj"));
+ REQUIRE(!re.matches("cau"));
+ REQUIRE(re.matches("ahoj")); // test repeated calls as well
+ REQUIRE(!re.matches("oj"));
+ REQUIRE(!re.matches("aho"));
+
+ // Testing runtime errors during pattern *matching* is tricky. There's a limit on backtracking,
+ // so testing a pattern like x+x+y on an obscenely long string of "x" characters *will* do the trick, eventually,
+ // but the PCRE2 library has a default limit of 10M attempts. That's a VERY big number to hit during a test :(.
+}
--
2.43.0
@@ -0,0 +1,67 @@
From 8d406728a53c2e77a4fe7393b7e30d42b8f9b9bb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 30 Jan 2025 15:40:22 +0100
Subject: [PATCH 13/18] Adapt to upstream changes in anyxml JSON printing
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: I5f6de28cebc95a446549017c2768b450f4fd6526
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
.zuul.yaml | 4 ++--
CMakeLists.txt | 3 ++-
tests/data_node.cpp | 2 +-
3 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/.zuul.yaml b/.zuul.yaml
index 19f0def..b41c490 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -4,13 +4,13 @@
- f38-gcc-cover:
required-projects:
- name: github/CESNET/libyang
- override-checkout: cesnet/2025-01-29
+ override-checkout: devel
- name: github/onqtam/doctest
override-checkout: v2.3.6
- f38-clang-asan-ubsan:
required-projects: &projects
- name: github/CESNET/libyang
- override-checkout: cesnet/2025-01-29
+ override-checkout: devel
- name: github/onqtam/doctest
override-checkout: v2.4.11
- f38-clang-tsan:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 512af8c..a40fd52 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -29,7 +29,8 @@ 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.7.8 IMPORTED_TARGET)
+# FIXME: it's actually 3.7.12, but that hasn't been released yet
+pkg_check_modules(LIBYANG REQUIRED libyang>=3.7.11 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/tests/data_node.cpp b/tests/data_node.cpp
index 45fd6c1..14470dd 100644
--- a/tests/data_node.cpp
+++ b/tests/data_node.cpp
@@ -1568,7 +1568,7 @@ TEST_CASE("Data Node manipulation")
REQUIRE(*jsonAnyXmlNode.createdNode->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings)
== R"|({"example-schema:ax":[1,2,3]})|"s);
REQUIRE(*jsonAnyXmlNode.createdNode->printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings)
- == R"|(<ax xmlns="http://example.com/coze"/>)|"s);
+ == R"|(<ax xmlns="http://example.com/coze">)|"s + origJSON + "</ax>");
}
REQUIRE(!!val);
--
2.43.0
@@ -0,0 +1,57 @@
From f050e7e4a17ef2e221ca000a544042c33c9541fc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 13 Mar 2025 19:21:26 +0100
Subject: [PATCH 14/18] fix DataNode::insertSibling() return value
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
When such an insert happens, the C library returns the node which is now
the first sibling among all of the siblings of the node which was used
as a reference during the insert. Our API was also documented this way,
but we were not doing that.
Reported-by: Irfan <irfan.haslanded@gmail.com>
Bug: https://github.com/CESNET/libyang-cpp/issues/29
Change-Id: Id7f84a31e50212d6e2cbce9ab03a351a3721f767
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/DataNode.cpp | 2 +-
tests/data_node.cpp | 7 +++++++
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/DataNode.cpp b/src/DataNode.cpp
index 84591e5..b899b18 100644
--- a/src/DataNode.cpp
+++ b/src/DataNode.cpp
@@ -711,7 +711,7 @@ DataNode DataNode::insertSibling(DataNode toInsert)
lyd_insert_sibling(this->m_node, toInsert.m_node, &firstSibling);
}, toInsert.parent() ? OperationScope::JustThisNode : OperationScope::AffectsFollowingSiblings, m_refs);
- return DataNode{m_node, m_refs};
+ return DataNode{firstSibling, m_refs};
}
/**
diff --git a/tests/data_node.cpp b/tests/data_node.cpp
index 14470dd..b6ee455 100644
--- a/tests/data_node.cpp
+++ b/tests/data_node.cpp
@@ -973,6 +973,13 @@ TEST_CASE("Data Node manipulation")
REQUIRE(getNumberOrder() == expected);
}
+ DOCTEST_SUBCASE("DataNode::insertSibling")
+ {
+ auto node = ctx.newPath("/example-schema:leafUInt8", "10");
+ REQUIRE(node.insertSibling(ctx.newPath("/example-schema:leafUInt16", "10")).path() == "/example-schema:leafUInt8");
+ REQUIRE(node.insertSibling(ctx.newPath("/example-schema:dummy", "10")).path() == "/example-schema:dummy");
+ }
+
DOCTEST_SUBCASE("DataNode::duplicate")
{
auto root = ctx.parseData(data2, libyang::DataFormat::JSON);
--
2.43.0
@@ -0,0 +1,260 @@
From f958af42bf5d9fbd901ed59ebc1359ac0ddcc00f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Fri, 14 Mar 2025 11:36:50 +0100
Subject: [PATCH 15/18] API/ABI change: opaque node naming
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Our C++ API would ignore the "module name or XML prefix", which turns
out to be *the* relevant part when it comes to opaque node naming. The
prefix is, instead, just that string that might have been inherited from
the parent node when parsing the serialized data; it's an optional
thingy which, if not set explicitly, is implicitly inherited.
Adapt the API for this, and since this *will* break the build, let's
bump the package version.
Change-Id: I199afe5fa7a571034b744531c63b93b9c656563a
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 5 ++---
include/libyang-cpp/Context.hpp | 4 ++--
include/libyang-cpp/DataNode.hpp | 11 ++++++++--
src/Context.cpp | 32 ++++++++++++++++++++--------
src/DataNode.cpp | 19 ++++++++++++++---
tests/data_node.cpp | 36 ++++++++++++++++++++++++++------
6 files changed, 82 insertions(+), 25 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index a40fd52..c5fec45 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -21,7 +21,7 @@ add_custom_target(libyang-cpp-version-cmake
cmake/ProjectGitVersionRunner.cmake
)
include(cmake/ProjectGitVersion.cmake)
-set(LIBYANG_CPP_PKG_VERSION "3")
+set(LIBYANG_CPP_PKG_VERSION "4")
prepare_git_version(LIBYANG_CPP_VERSION ${LIBYANG_CPP_PKG_VERSION})
find_package(Doxygen)
@@ -29,8 +29,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)
-# FIXME: it's actually 3.7.12, but that hasn't been released yet
-pkg_check_modules(LIBYANG REQUIRED libyang>=3.7.11 IMPORTED_TARGET)
+pkg_check_modules(LIBYANG REQUIRED libyang>=3.10.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/include/libyang-cpp/Context.hpp b/include/libyang-cpp/Context.hpp
index baa47b3..ca89063 100644
--- a/include/libyang-cpp/Context.hpp
+++ b/include/libyang-cpp/Context.hpp
@@ -115,8 +115,8 @@ public:
CreatedNodes newPath2(const std::string& path, libyang::JSON json, const std::optional<CreationOptions> options = std::nullopt) const;
CreatedNodes newPath2(const std::string& path, libyang::XML xml, const std::optional<CreationOptions> options = std::nullopt) const;
std::optional<DataNode> newExtPath(const ExtensionInstance& ext, const std::string& path, const std::optional<std::string>& value, const std::optional<CreationOptions> options = std::nullopt) const;
- std::optional<DataNode> newOpaqueJSON(const std::string& moduleName, const std::string& name, const std::optional<libyang::JSON>& value) const;
- std::optional<DataNode> newOpaqueXML(const std::string& moduleName, const std::string& name, const std::optional<libyang::XML>& value) const;
+ std::optional<DataNode> newOpaqueJSON(const OpaqueName& name, const std::optional<libyang::JSON>& value) const;
+ std::optional<DataNode> newOpaqueXML(const OpaqueName& name, const std::optional<libyang::XML>& value) const;
SchemaNode findPath(const std::string& dataPath, const InputOutputNodes inputOutputNodes = InputOutputNodes::Input) const;
Set<SchemaNode> findXPath(const std::string& path) const;
diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp
index 851681b..310b5e3 100644
--- a/include/libyang-cpp/DataNode.hpp
+++ b/include/libyang-cpp/DataNode.hpp
@@ -225,15 +225,22 @@ private:
};
/**
- * @brief Contains a (possibly module-qualified) name of an opaque node.
+ * @brief Contains a name of an opaque node.
*
- * This is generic container of a prefix/module string and a name string.
+ * An opaque node always has a name, and a module (or XML namespace) to which this node belongs.
+ * Sometimes, it also has a prefix.
+ *
+ * If the prefix is set *and* the underlying node is an opaque JSON one, then the prefix must be the same as the "module or namespace" name.
+ * If the underlying node is an opaque XML one, then the XML prefix might be something completely different, and in that case the real fun begins.
+ * Review the libayng C manual, this is something that the C++ wrapper doesn't really have under control.
*
* Wraps `ly_opaq_name`.
*/
struct LIBYANG_CPP_EXPORT OpaqueName {
+ std::string moduleOrNamespace;
std::optional<std::string> prefix;
std::string name;
+ std::string pretty() const;
};
/**
diff --git a/src/Context.cpp b/src/Context.cpp
index 287f8c8..fec2f27 100644
--- a/src/Context.cpp
+++ b/src/Context.cpp
@@ -378,17 +378,25 @@ std::optional<DataNode> Context::newExtPath(const ExtensionInstance& ext, const
*
* Wraps `lyd_new_opaq`.
*
- * @param moduleName Node module name, used as a prefix as well
* @param name Name of the created node
* @param value JSON data blob, if any
* @return Returns the newly created node (if created)
*/
-std::optional<DataNode> Context::newOpaqueJSON(const std::string& moduleName, const std::string& name, const std::optional<libyang::JSON>& value) const
+std::optional<DataNode> Context::newOpaqueJSON(const OpaqueName& name, const std::optional<libyang::JSON>& value) const
{
+ if (name.prefix && *name.prefix != name.moduleOrNamespace) {
+ throw Error{"invalid opaque JSON node: prefix \"" + *name.prefix + "\" doesn't match module name \"" + name.moduleOrNamespace + "\""};
+ }
lyd_node* out;
- auto err = lyd_new_opaq(nullptr, m_ctx.get(), name.c_str(), value ? value->content.c_str() : nullptr, nullptr, moduleName.c_str(), &out);
+ auto err = lyd_new_opaq(nullptr,
+ m_ctx.get(),
+ name.name.c_str(),
+ value ? value->content.c_str() : nullptr,
+ name.prefix ? name.prefix->c_str() : nullptr,
+ name.moduleOrNamespace.c_str(),
+ &out);
- throwIfError(err, "Couldn't create an opaque JSON node '"s + moduleName + ':' + name + "'");
+ throwIfError(err, "Couldn't create an opaque JSON node " + name.pretty());
if (out) {
return DataNode{out, std::make_shared<internal_refcount>(m_ctx)};
@@ -403,17 +411,23 @@ std::optional<DataNode> Context::newOpaqueJSON(const std::string& moduleName, co
*
* Wraps `lyd_new_opaq2`.
*
- * @param xmlNamespace Node module namespace
* @param name Name of the created node
* @param value XML data blob, if any
* @return Returns the newly created node (if created)
*/
-std::optional<DataNode> Context::newOpaqueXML(const std::string& xmlNamespace, const std::string& name, const std::optional<libyang::XML>& value) const
+std::optional<DataNode> Context::newOpaqueXML(const OpaqueName& name, const std::optional<libyang::XML>& value) const
{
+ // the XML node naming is "complex", we cannot really check the XML namespace for sanity here
lyd_node* out;
- auto err = lyd_new_opaq2(nullptr, m_ctx.get(), name.c_str(), value ? value->content.c_str() : nullptr, nullptr, xmlNamespace.c_str(), &out);
-
- throwIfError(err, "Couldn't create an opaque XML node '"s + name +"' from namespace '" + xmlNamespace + "'");
+ auto err = lyd_new_opaq2(nullptr,
+ m_ctx.get(),
+ name.name.c_str(),
+ value ? value->content.c_str() : nullptr,
+ name.prefix ? name.prefix->c_str() : nullptr,
+ name.moduleOrNamespace.c_str(),
+ &out);
+
+ throwIfError(err, "Couldn't create an opaque XML node " + name.pretty());
if (out) {
return DataNode{out, std::make_shared<internal_refcount>(m_ctx)};
diff --git a/src/DataNode.cpp b/src/DataNode.cpp
index b899b18..344f1b6 100644
--- a/src/DataNode.cpp
+++ b/src/DataNode.cpp
@@ -1112,9 +1112,9 @@ OpaqueName DataNodeOpaque::name() const
{
auto opaq = reinterpret_cast<lyd_node_opaq*>(m_node);
return OpaqueName{
- .prefix = opaq->name.prefix ? std::optional(opaq->name.prefix) : std::nullopt,
- .name = opaq->name.name
- };
+ .moduleOrNamespace = opaq->name.module_name,
+ .prefix = opaq->name.prefix ? std::optional{opaq->name.prefix} : std::nullopt,
+ .name = opaq->name.name};
}
std::string DataNodeOpaque::value() const
@@ -1122,6 +1122,19 @@ std::string DataNodeOpaque::value() const
return reinterpret_cast<lyd_node_opaq*>(m_node)->value;
}
+std::string OpaqueName::pretty() const
+{
+ if (prefix) {
+ if (*prefix == moduleOrNamespace) {
+ return *prefix + ':' + name;
+ } else {
+ return "{" + moduleOrNamespace + "}, " + *prefix + ':' + name;
+ }
+ } else {
+ return "{" + moduleOrNamespace + "}, " + name;
+ }
+}
+
/**
* Wraps a raw non-null lyd_node pointer.
* @param node The pointer to be wrapped. Must not be null.
diff --git a/tests/data_node.cpp b/tests/data_node.cpp
index b6ee455..a1096a6 100644
--- a/tests/data_node.cpp
+++ b/tests/data_node.cpp
@@ -1969,9 +1969,11 @@ TEST_CASE("Data Node manipulation")
DOCTEST_SUBCASE("opaque nodes for sysrepo ops data discard")
{
- auto discard1 = ctx.newOpaqueJSON("sysrepo", "discard-items", libyang::JSON{"/example-schema:a"});
+ // the "short" form with no prefix
+ auto discard1 = ctx.newOpaqueJSON(libyang::OpaqueName{"sysrepo", std::nullopt, "discard-items"}, libyang::JSON{"/example-schema:a"});
REQUIRE(!!discard1);
- auto discard2 = ctx.newOpaqueJSON("sysrepo", "discard-items", libyang::JSON{"/example-schema:b"});
+ // let's use a prefix form here
+ 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)
@@ -2001,16 +2003,38 @@ TEST_CASE("Data Node manipulation")
auto data = ctx.newPath2("/example-schema:myRpc/outputLeaf", "AHOJ", libyang::CreationOptions::Output).createdNode;
REQUIRE(data);
data->newPath("/example-schema:myRpc/another", "yay", libyang::CreationOptions::Output);
+ std::string prettyName;
- DOCTEST_SUBCASE("JSON") {
- out = ctx.newOpaqueJSON(data->schema().module().name(), "output", std::nullopt);
+ DOCTEST_SUBCASE("JSON no prefix") {
+ out = ctx.newOpaqueJSON({data->schema().module().name(), std::nullopt, "output"}, std::nullopt);
+ prettyName = "{example-schema}, output";
}
- DOCTEST_SUBCASE("XML") {
- out = ctx.newOpaqueXML(data->schema().module().ns(), "output", std::nullopt);
+ DOCTEST_SUBCASE("JSON with prefix") {
+ out = ctx.newOpaqueJSON({data->schema().module().name(), data->schema().module().name(), "output"}, std::nullopt);
+ prettyName = "example-schema:output";
+
+ // wrong prefix is detected
+ REQUIRE_THROWS_WITH_AS(ctx.newOpaqueJSON({data->schema().module().name(), "xxx", "output"}, std::nullopt),
+ R"(invalid opaque JSON node: prefix "xxx" doesn't match module name "example-schema")",
+ libyang::Error);
+ }
+
+ DOCTEST_SUBCASE("XML no prefix") {
+ out = ctx.newOpaqueXML({data->schema().module().ns(), std::nullopt, "output"}, std::nullopt);
+ prettyName = "{http://example.com/coze}, output";
+ }
+
+ DOCTEST_SUBCASE("XML with prefix") {
+ out = ctx.newOpaqueXML({data->schema().module().ns(),
+ data->schema().module().name() /* prefix is a module name, not the XML NS*/,
+ "output"},
+ std::nullopt);
+ prettyName = "{http://example.com/coze}, example-schema:output";
}
REQUIRE(out);
+ REQUIRE(prettyName == out->asOpaque().name().pretty());
data->unlinkWithSiblings();
out->insertChild(*data);
--
2.43.0
@@ -0,0 +1,462 @@
From 8c59ecc5c687f8ee2ce62825835a378b422185f2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Fri, 14 Mar 2025 13:41:01 +0100
Subject: [PATCH 16/18] Add a helper for finding opaque nodes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
This is needed for sysrepo where we want to search through the
sysrepo:discard-items top-level opaque nodes.
At first I wanted to simply wrap lyd_find_sibling_opaq_next(), but its
semantics is quite different to what is needed in the code which uses
sysrepo, so here's my attempt at a nice-ish API.
Change-Id: I2571961e42f6d7a121e27c881cacdcfec0e87762
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
include/libyang-cpp/DataNode.hpp | 2 +
src/DataNode.cpp | 49 ++++++
tests/data_node.cpp | 74 +++++++++
tests/yang/sysrepo@2024-10-25.yang | 257 +++++++++++++++++++++++++++++
4 files changed, 382 insertions(+)
create mode 100644 tests/yang/sysrepo@2024-10-25.yang
diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp
index 310b5e3..50d6c0e 100644
--- a/include/libyang-cpp/DataNode.hpp
+++ b/include/libyang-cpp/DataNode.hpp
@@ -102,6 +102,7 @@ public:
bool isOpaque() const;
DataNodeOpaque asOpaque() const;
+ std::optional<DataNodeOpaque> firstOpaqueSibling() const;
// TODO: allow setting the `parent` argument
DataNode duplicate(const std::optional<DuplicationOptions> opts = std::nullopt) const;
@@ -241,6 +242,7 @@ struct LIBYANG_CPP_EXPORT OpaqueName {
std::optional<std::string> prefix;
std::string name;
std::string pretty() const;
+ bool matches(const std::string& prefixIsh, const std::string& name) const;
};
/**
diff --git a/src/DataNode.cpp b/src/DataNode.cpp
index 344f1b6..7e87917 100644
--- a/src/DataNode.cpp
+++ b/src/DataNode.cpp
@@ -1135,6 +1135,20 @@ std::string OpaqueName::pretty() const
}
}
+/**
+ * @short Fuzzy-match a real-world name against a combination of "something like a prefix" and "unqualified name"
+ *
+ * Because libyang doesn't propagate inherited prefixes, and because opaque nodes are magic, we seem to require
+ * this "fuzzy matching". It won't properly report a match on opaque nodes with a prefix that's inherited when
+ * using XML namespaces, though.
+ * */
+bool OpaqueName::matches(const std::string& prefixIsh, const std::string& name) const
+{
+ return name == this->name
+ && (prefixIsh == moduleOrNamespace
+ || (!!prefix && prefixIsh == *prefix));
+}
+
/**
* Wraps a raw non-null lyd_node pointer.
* @param node The pointer to be wrapped. Must not be null.
@@ -1305,4 +1319,39 @@ bool DataNode::siblingsEqual(const libyang::DataNode& other, const DataCompare f
}
}
+/**
+ * @short Find the first opaque node among the siblings
+ *
+ * This function was inspired by `lyd_find_sibling_opaq_next()`.
+ * */
+std::optional<DataNodeOpaque> DataNode::firstOpaqueSibling() const
+{
+ struct lyd_node *candidate = m_node;
+
+ // Skip all non-opaque nodes; libyang guarantees to have them first, followed by a (possibly empty) set
+ // of opaque nodes. This is not documented anywhere, but it was explicitly confirmed by the maintainer:
+ //
+ // JK: can I rely on all non-opaque nodes being listed first among the siblings, and then all opaque nodes
+ // in one continuous sequence (but with an unspecified order among the opaque nodes themselves)?
+ //
+ // MV: yep
+ while (candidate && candidate->schema) {
+ candidate = candidate->next;
+ }
+
+ // walk back through the opaque nodes; however, libyang lists are not your regular linked lists
+ while (candidate
+ && !candidate->prev->schema // don't go from the first opaque node through the non-opaque ones
+ && candidate->prev->next // don't wrap from the first node to the last one in case all of them are opaque
+ ) {
+ candidate = candidate->prev;
+ }
+
+ if (candidate) {
+ return DataNode{candidate, m_refs}.asOpaque();
+ }
+
+ return std::nullopt;
+}
+
}
diff --git a/tests/data_node.cpp b/tests/data_node.cpp
index a1096a6..db5a28e 100644
--- a/tests/data_node.cpp
+++ b/tests/data_node.cpp
@@ -1982,6 +1982,80 @@ TEST_CASE("Data Node manipulation")
"sysrepo:discard-items": "/example-schema:b"
}
)");
+
+ // check that a list which only consists of opaque nodes doesn't break our iteration
+ REQUIRE(!!discard1->firstOpaqueSibling());
+ REQUIRE(*discard1->firstOpaqueSibling() == *discard1);
+ REQUIRE(!!discard2->firstOpaqueSibling());
+ REQUIRE(*discard2->firstOpaqueSibling() == *discard1);
+
+ auto leafInt16 = ctx.newPath("/example-schema:leafInt16", "666");
+ leafInt16.insertSibling(*discard1);
+ REQUIRE(*discard1->firstSibling().printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings)
+ == R"({
+ "example-schema:leafInt16": 666,
+ "sysrepo:discard-items": "/example-schema:a",
+ "sysrepo:discard-items": "/example-schema:b"
+}
+)");
+ REQUIRE(leafInt16.firstSibling().path() == "/example-schema:leafInt16");
+ REQUIRE(discard1->firstSibling().path() == "/example-schema:leafInt16");
+ REQUIRE(discard2->firstSibling().path() == "/example-schema:leafInt16");
+
+ auto dummy = ctx.newPath("/example-schema:dummy", "blah");
+ auto opaqueLeaf = ctx.newPath("/example-schema:leafInt32", std::nullopt, libyang::CreationOptions::Opaque);
+ opaqueLeaf.newAttrOpaqueJSON("ietf-netconf", "operation", "delete");
+ dummy.insertSibling(opaqueLeaf);
+
+ // FIXME reword this: this one might not be handled by sysrepo, but we want it for our fuzzy matcher testing anyway
+ auto discard3 = ctx.newOpaqueXML(libyang::OpaqueName{"http://www.sysrepo.org/yang/sysrepo", "sysrepo", "discard-items"}, libyang::XML{"/example-schema:c"});
+ REQUIRE(!!discard3);
+ // notice that it's printed without a proper prefix at first...
+ REQUIRE(*discard3->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink)
+ == R"({"discard-items":"/example-schema:c"})");
+ // ...but after loading the module, the proper module is added back
+ ctx.parseModule(TESTS_DIR / "yang" / "sysrepo@2024-10-25.yang", libyang::SchemaFormat::YANG);
+ REQUIRE(*discard3->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink)
+ == R"({"sysrepo:discard-items":"/example-schema:c"})");
+
+ dummy.insertSibling(*discard3);
+ leafInt16.insertSibling(dummy);
+ REQUIRE(*discard1->firstSibling().printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings)
+ == R"({
+ "example-schema:dummy": "blah",
+ "example-schema:leafInt16": 666,
+ "sysrepo:discard-items": "/example-schema:a",
+ "sysrepo:discard-items": "/example-schema:b",
+ "example-schema:leafInt32": "",
+ "@example-schema:leafInt32": {
+ "ietf-netconf:operation": "delete"
+ },
+ "sysrepo:discard-items": "/example-schema:c"
+}
+)");
+
+ REQUIRE(dummy.firstOpaqueSibling() == discard1);
+ REQUIRE(dummy.firstOpaqueSibling() != discard2);
+ REQUIRE(leafInt16.firstOpaqueSibling() == discard1);
+ REQUIRE(opaqueLeaf.firstOpaqueSibling() == discard1);
+ REQUIRE(discard1->firstOpaqueSibling() == discard1);
+ REQUIRE(discard2->firstOpaqueSibling() == discard1);
+ REQUIRE(discard3->firstOpaqueSibling() == discard1);
+ REQUIRE(discard1->asOpaque().name().matches("sysrepo", "discard-items"));
+ REQUIRE(!discard1->asOpaque().name().matches("http://www.sysrepo.org/yang/sysrepo", "discard-items"));
+ REQUIRE(discard2->asOpaque().name().matches("sysrepo", "discard-items"));
+ REQUIRE(!discard2->asOpaque().name().matches("http://www.sysrepo.org/yang/sysrepo", "discard-items"));
+ REQUIRE(discard3->asOpaque().name().matches("sysrepo", "discard-items"));
+ REQUIRE(discard3->asOpaque().name().matches("http://www.sysrepo.org/yang/sysrepo", "discard-items"));
+ REQUIRE(!opaqueLeaf.asOpaque().name().matches("sysrepo", "discard-items"));
+
+ REQUIRE(!!dummy.firstOpaqueSibling()->nextSibling());
+ REQUIRE(dummy.firstOpaqueSibling()->nextSibling() == discard2);
+ REQUIRE(!!dummy.firstOpaqueSibling()->nextSibling()->nextSibling());
+ REQUIRE(dummy.firstOpaqueSibling()->nextSibling()->nextSibling() == opaqueLeaf);
+ REQUIRE(!!dummy.firstOpaqueSibling()->nextSibling()->nextSibling()->nextSibling());
+ REQUIRE(dummy.firstOpaqueSibling()->nextSibling()->nextSibling()->nextSibling() == discard3);
+ REQUIRE(!dummy.firstOpaqueSibling()->nextSibling()->nextSibling()->nextSibling()->nextSibling());
}
DOCTEST_SUBCASE("RESTCONF RPC output")
diff --git a/tests/yang/sysrepo@2024-10-25.yang b/tests/yang/sysrepo@2024-10-25.yang
new file mode 100644
index 0000000..f5bc32c
--- /dev/null
+++ b/tests/yang/sysrepo@2024-10-25.yang
@@ -0,0 +1,257 @@
+module sysrepo {
+ namespace "http://www.sysrepo.org/yang/sysrepo";
+ prefix sr;
+
+ yang-version 1.1;
+
+ import ietf-yang-types {
+ prefix yang;
+ }
+
+ import ietf-datastores {
+ prefix ds;
+ }
+
+ import ietf-yang-metadata {
+ prefix md;
+ revision-date 2016-08-05;
+ }
+
+ organization
+ "CESNET";
+
+ contact
+ "Author: Michal Vasko
+ <mvasko@cesnet.cz>";
+
+ description
+ "Sysrepo YANG datastore internal attributes and information.";
+
+ revision "2024-10-25" {
+ description
+ "Removed redundant metadata used for push operational data.";
+ }
+
+ revision "2019-07-10" {
+ description
+ "Initial revision.";
+ }
+
+ typedef module-ref {
+ description
+ "Reference to a module.";
+ type leafref {
+ path "/sysrepo-modules/module/name";
+ }
+ }
+
+ md:annotation operation {
+ type enumeration {
+ enum none {
+ description
+ "Node with this operation must exist but does not affect the datastore in any way.";
+ reference
+ "RFC 6241 section 7.2.: default-operation";
+ }
+ enum ether {
+ description
+ "Node with this operation does not have to exist and does not affect the datastore in any way.";
+ }
+ enum purge {
+ description
+ "Node with this operation represents an arbitrary generic node instance and all
+ the instances will be deleted.";
+ }
+ }
+ description
+ "Additional proprietary <edit-config> operations used internally.";
+ reference
+ "RFC 6241 section 7.2.";
+ }
+
+ identity notification {
+ base ds:datastore;
+ description
+ "Special datastore for storing notifications for replay.";
+ }
+
+ grouping module-info-grp {
+ leaf name {
+ type string;
+ description
+ "Module name.";
+ }
+
+ leaf revision {
+ type string;
+ description
+ "Module revision.";
+ }
+
+ leaf-list enabled-feature {
+ type string;
+ description
+ "List of all the enabled features.";
+ }
+
+ list plugin {
+ key "datastore";
+ description
+ "Module datastore plugin handling specific datastore data.";
+
+ leaf datastore {
+ type identityref {
+ base ds:datastore;
+ }
+ description
+ "Datastore of this plugin.";
+ }
+
+ leaf name {
+ type string;
+ mandatory true;
+ description
+ "Specific plugin name as present in the plugin structures.";
+ }
+ }
+ }
+
+ grouping deps-grp {
+ list lref {
+ description
+ "Dependency of a leafref node.";
+
+ leaf target-path {
+ type yang:xpath1.0;
+ mandatory true;
+ description
+ "Path identifying the leafref target node.";
+ }
+
+ leaf target-module {
+ type module-ref;
+ mandatory true;
+ description
+ "Foreign target module of the leafref.";
+ }
+ }
+
+ list inst-id {
+ description
+ "Dependency of an instance-identifier node.";
+
+ leaf source-path {
+ type yang:xpath1.0;
+ mandatory true;
+ description
+ "Path identifying the instance-identifier node.";
+ }
+
+ leaf default-target-path {
+ type yang:xpath1.0;
+ description
+ "Default instance-identifier value.";
+ }
+ }
+
+ list xpath {
+ description
+ "Dependency of an XPath expression - must or when statement.";
+
+ leaf expression {
+ type yang:xpath1.0;
+ mandatory true;
+ description
+ "XPath expression of the dependency - must or when statement argument.";
+ }
+
+ leaf-list target-module {
+ type module-ref;
+ description
+ "Foreign modules with the data needed for evaluation of the XPath.";
+ }
+ }
+ }
+
+ container sysrepo-modules {
+ config false;
+ description
+ "All installed Sysrepo modules.";
+
+ leaf content-id {
+ type uint32;
+ mandatory true;
+ description
+ "Sysrepo module-set content-id to be used for its generated yang-library data.";
+ }
+
+ list module {
+ key "name";
+ description
+ "Sysrepo module.";
+
+ uses module-info-grp;
+
+ leaf replay-support {
+ type yang:date-and-time;
+ description
+ "Present only if the module supports replay. Means the earliest stored notification if any present.
+ Otherwise the time the replay support was switched on.";
+ }
+
+ container deps {
+ description
+ "Module data dependencies on other modules.";
+ uses deps-grp;
+ }
+
+ leaf-list inverse-deps {
+ type module-ref;
+ description
+ "List of modules that depend on this module.";
+ }
+
+ list rpc {
+ key "path";
+ description
+ "Module RPC/actions.";
+
+ leaf path {
+ type yang:xpath1.0;
+ description
+ "Path identifying the operation.";
+ }
+
+ container in {
+ description
+ "Operation input dependencies.";
+ uses deps-grp;
+ }
+
+ container out {
+ description
+ "Operation output dependencies.";
+ uses deps-grp;
+ }
+ }
+
+ list notification {
+ key "path";
+ description
+ "Module notifications.";
+
+ leaf path {
+ type yang:xpath1.0;
+ description
+ "Path identifying the notification.";
+ }
+
+ container deps {
+ description
+ "Notification dependencies.";
+ uses deps-grp;
+ }
+ }
+ }
+ }
+}
--
2.43.0
@@ -0,0 +1,56 @@
From dab8c9aac96063ccb8541d5d1795ee89b75faeee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Wed, 2 Apr 2025 11:50:24 -0700
Subject: [PATCH 17/18] build: link to libpcre2 explicitly
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
This is apparently a problem on Mac OS X builds where the transitive
dependency via libyang doesn't take effect:
Undefined symbols for architecture arm64:
"_pcre2_code_free_8", referenced from:
libyang::Regex::~Regex() in Regex.cpp.o
libyang::Regex::~Regex() in Regex.cpp.o
ld: symbol(s) not found for architecture arm64
Let's fix this by linking with libpcre2-8 directly.
We're using a different approach than libyang; they have their own CMake
wrapper. I find it easier to work with pkg-config modules, so let's try
it that way.
Change-Id: I1a64407d852161595b7dca654433190002cc3600
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c5fec45..4e009aa 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -34,6 +34,9 @@ pkg_check_modules(LIBYANG REQUIRED libyang>=3.10.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)
+# libyang::Regex::~Regex() bypasses libyang and calls out to libpcre directly
+pkg_check_modules(LIBPCRE2 REQUIRED libpcre2-8 IMPORTED_TARGET)
+
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
include(CheckIncludeFileCXX)
@@ -64,7 +67,7 @@ add_library(yang-cpp
src/utils/newPath.cpp
)
-target_link_libraries(yang-cpp PRIVATE PkgConfig::LIBYANG)
+target_link_libraries(yang-cpp PRIVATE PkgConfig::LIBYANG PkgConfig::LIBPCRE2)
# We do not offer any long-term API/ABI guarantees. To make stuff easier for downstream consumers,
# we will be bumping both API and ABI versions very deliberately.
# There will be no attempts at semver tracking, for example.
--
2.43.0
@@ -0,0 +1,66 @@
From 7519fcd35216072a6b1eebe2a79e19789be345a9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Wed, 9 Apr 2025 15:35:37 +0200
Subject: [PATCH 18/18] CI: renamed project upstream
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: I5447f243297fbfde7c364eb3919b00db239bd069
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
.zuul.yaml | 4 ++--
README.md | 2 +-
ci/build.sh | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.zuul.yaml b/.zuul.yaml
index b41c490..7b92766 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -5,13 +5,13 @@
required-projects:
- name: github/CESNET/libyang
override-checkout: devel
- - name: github/onqtam/doctest
+ - name: github/doctest/doctest
override-checkout: v2.3.6
- f38-clang-asan-ubsan:
required-projects: &projects
- name: github/CESNET/libyang
override-checkout: devel
- - name: github/onqtam/doctest
+ - name: github/doctest/doctest
override-checkout: v2.4.11
- f38-clang-tsan:
required-projects: *projects
diff --git a/README.md b/README.md
index d76975b..8291e00 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ Object lifetimes are managed automatically via RAII.
- [libyang v3](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/onqtam/doctest/) as a C++ unit test framework
+- optionally for built-in tests, [Doctest](https://github.com/doctest/doctest/) as a C++ unit test framework
- optionally for the docs, Doxygen
## Building
diff --git a/ci/build.sh b/ci/build.sh
index d06b646..0867f07 100755
--- a/ci/build.sh
+++ b/ci/build.sh
@@ -73,7 +73,7 @@ build_n_test() {
}
build_n_test github/CESNET/libyang -DENABLE_BUILD_TESTS=ON -DENABLE_VALGRIND_TESTS=OFF
-build_n_test github/onqtam/doctest -DDOCTEST_WITH_TESTS=OFF
+build_n_test github/doctest/doctest -DDOCTEST_WITH_TESTS=OFF
build_n_test ${ZUUL_PROJECT_NAME} -DBUILD_TESTING=ON
pushd ${BUILD_DIR}/${ZUUL_PROJECT_NAME}
--
2.43.0
+1 -1
View File
@@ -1,3 +1,3 @@
# Locally calculated
sha256 82e3758011ec44c78e98d0777799d6e12aec5b8a64b32ebb20d0fe50e32488bb LICENSE
sha256 70fd0df940026fb930d5407abe679e064caf4701bff35d79465b9ad2c0915808 libyang-cpp-v4.tar.gz
sha256 43c84317ba13470f421a90bca74c062cf63a70e488d2e90c432b662c4638a14e libyang-cpp-v3.tar.gz
+1 -1
View File
@@ -3,7 +3,7 @@
# CPP bindings for libyang
#
################################################################################
LIBYANG_CPP_VERSION = v4
LIBYANG_CPP_VERSION = v3
LIBYANG_CPP_SITE = $(call github,CESNET,libyang-cpp,$(LIBYANG_CPP_VERSION))
LIBYANG_CPP_LICENSE = BSD-3-Clause
LIBYANG_CPP_LICENSE_FILES = LICENSE
-5
View File
@@ -5,8 +5,3 @@ config BR2_PACKAGE_NGHTTP2_ASIO
select BR2_PACKAGE_BOOST
help
C++ bindings for nghttp2
This is the CESNET fork of nghttp2-asio, introduced here:
https://github.com/CESNET/rousette/commit/fff3429
https://github.com/CESNET/nghttp2-asio
+2 -1
View File
@@ -1,3 +1,4 @@
# Locally calculated
sha256 6b94f3abc1aabd0c72a7c7d92a77f79dda7c8a0cb3df839a97890b4116a2de2a COPYING
sha256 883af672c1d210398170749a8ccda30103b2894bcd4eccd80f36771aad0b15c3 nghttp2-asio-2173b82e6caa85950c769eecc5da6809faadf61a-git4.tar.gz
sha256 d971c538a31eae5714d2434d90f86794f267615e85e8d2bb0c383e83c300e1ce nghttp2-asio-e877868abe06a83ed0a6ac6e245c07f6f20866b5-git4.tar.gz
+2 -2
View File
@@ -4,8 +4,8 @@
#
################################################################################
NGHTTP2_ASIO_VERSION = 2173b82e6caa85950c769eecc5da6809faadf61a
NGHTTP2_ASIO_SITE = https://github.com/CESNET/nghttp2-asio
NGHTTP2_ASIO_VERSION = e877868abe06a83ed0a6ac6e245c07f6f20866b5
NGHTTP2_ASIO_SITE = https://github.com/kernelkit/nghttp2-asio.git
NGHTTP2_ASIO_SITE_METHOD = git
NGHTTP2_ASIO_LICENSE = MIT
NGHTTP2_ASIO_LICENSE_FILES = COPYING
@@ -1,64 +0,0 @@
From c0d13b8e426608a30b1eea382f908fb6447fd01d 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
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
...so that we can debug the results of a nasty lockup that Michal
Hažlinský just found.
Change-Id: I2a930a02c7d30c051390fe73e6af9849edd580b4
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
src/restconf/Server.cpp | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 272ecc1..9821495 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -44,6 +44,9 @@ void logRequest(const auto& request)
{
const auto& peer = http::peer_from_request(request);
spdlog::info("{}: {} {}", peer, request.method(), request.uri().raw_path);
+ for (const auto& hdr: request.header()) {
+ spdlog::trace("{}: header: {}: {}", peer, hdr.first, hdr.second.sensitive ? "<sensitive>"s : hdr.second.value);
+ }
}
template <typename T>
@@ -1079,12 +1082,14 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std::
auto requestCtx = std::make_shared<RequestContext>(req, res, dataFormat, sess, restconfRequest);
- req.on_data([requestCtx, restconfRequest /* intentional copy */, timeout](const uint8_t* data, std::size_t length) {
+ req.on_data([requestCtx, restconfRequest /* intentional copy */, timeout, peer=http::peer_from_request(req)](const uint8_t* data, std::size_t length) {
if (length > 0) { // there are still some data to be read
requestCtx->payload.append(reinterpret_cast<const char*>(data), length);
return;
}
+ spdlog::trace("{}: HTTP payload: {}", peer, requestCtx->payload);
+
if (restconfRequest.type == RestconfRequest::Type::CreateChildren) {
WITH_RESTCONF_EXCEPTIONS(processPost, rejectWithError)(requestCtx, timeout);
} else if (restconfRequest.type == RestconfRequest::Type::MergeData && isYangPatch(requestCtx->req)) {
@@ -1143,10 +1148,11 @@ 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](const uint8_t* data, std::size_t length) {
+ req.on_data([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);
}
});
--
2.43.0
@@ -0,0 +1,199 @@
From 9622a68eba4aeaa60619b4c33d050ce91b27653d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Tue, 8 Oct 2024 12:22:49 +0200
Subject: [PATCH 01/44] restconf: report the list key values when they differ
between URI and data
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
For creating (leaf-)list instances with PUT and PATCH methods one has
to specify the list key values in the URI and in the data as well.
Those values must be the same. In case they are not, it is an error.
We reported that the values mismatch in case this happens, but the error
message did not report what the values were.
Knowing that might be beneficial when one is about to insert key values
that can be namespace qualified (like identityrefs) and that are
sometimes manipulated by libyang (e.g., when the identity belongs to the
same namespace as the list node, it is not necessary for it to be
namespace qualified by the client, but libyang adds the namespace
internally).
Change-Id: Ie0d42511bde01ab4c39d61370b6601f8808e40c5
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/restconf/Server.cpp | 29 +++++++++++++++++++++--------
tests/restconf-plain-patch.cpp | 2 +-
tests/restconf-writing.cpp | 14 +++++++-------
tests/restconf-yang-patch.cpp | 2 +-
4 files changed, 30 insertions(+), 17 deletions(-)
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 53d6625..5f560ed 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -154,9 +154,22 @@ auto rejectYangPatch(const std::string& patchId, const std::string& editId)
};
}
+struct KeyMismatch {
+ libyang::DataNode offendingNode;
+ std::optional<std::string> uriKeyValue;
+
+ std::string message() const {
+ if (uriKeyValue) {
+ return "List key mismatch between URI path ('"s + *uriKeyValue + "') and data ('" + offendingNode.asTerm().valueStr() + "').";
+ } else {
+ return "List key mismatch (key missing in the data).";
+ }
+ }
+};
+
/** @brief In case node is a (leaf-)list check if the key values are the same as the keys specified in the lastPathSegment.
* @return The node where the mismatch occurs */
-std::optional<libyang::DataNode> checkKeysMismatch(const libyang::DataNode& node, const PathSegment& lastPathSegment)
+std::optional<KeyMismatch> checkKeysMismatch(const libyang::DataNode& node, const PathSegment& lastPathSegment)
{
if (node.schema().nodeType() == libyang::NodeType::List) {
const auto& listKeys = node.schema().asList().keys();
@@ -164,18 +177,18 @@ std::optional<libyang::DataNode> checkKeysMismatch(const libyang::DataNode& node
const auto& keyValueURI = lastPathSegment.keys[i];
auto keyNodeData = node.findPath(listKeys[i].module().name() + ':' + listKeys[i].name());
if (!keyNodeData) {
- return node;
+ return KeyMismatch{node, std::nullopt};
}
const auto& keyValueData = keyNodeData->asTerm().valueStr();
if (keyValueURI != keyValueData) {
- return keyNodeData;
+ return KeyMismatch{*keyNodeData, keyValueURI};
}
}
} else if (node.schema().nodeType() == libyang::NodeType::Leaflist) {
if (lastPathSegment.keys[0] != node.asTerm().valueStr()) {
- return node;
+ return KeyMismatch{node, lastPathSegment.keys[0]};
}
}
return std::nullopt;
@@ -350,8 +363,8 @@ libyang::CreatedNodes createEditForPutAndPatch(libyang::Context& ctx, const std:
if (isSameNode(child, lastPathSegment)) {
// 1) a single child that is created by parseSubtree(), its name is the same as `lastPathSegment`.
// It could be a list; then we need to check if the keys in provided data match the keys in URI.
- if (auto offendingNode = checkKeysMismatch(child, lastPathSegment)) {
- throw ErrorResponse(400, "protocol", "invalid-value", "List key mismatch between URI path and data.", offendingNode->path());
+ if (auto keyMismatch = checkKeysMismatch(child, lastPathSegment)) {
+ throw ErrorResponse(400, "protocol", "invalid-value", keyMismatch->message(), keyMismatch->offendingNode.path());
}
replacementNode = child;
} else if (isKeyNode(*node, child)) {
@@ -373,8 +386,8 @@ libyang::CreatedNodes createEditForPutAndPatch(libyang::Context& ctx, const std:
if (!isSameNode(*replacementNode, lastPathSegment)) {
throw ErrorResponse(400, "protocol", "invalid-value", "Data contains invalid node.", replacementNode->path());
}
- if (auto offendingNode = checkKeysMismatch(*parent, lastPathSegment)) {
- throw ErrorResponse(400, "protocol", "invalid-value", "List key mismatch between URI path and data.", offendingNode->path());
+ if (auto keyMismatch = checkKeysMismatch(*parent, lastPathSegment)) {
+ throw ErrorResponse(400, "protocol", "invalid-value", keyMismatch->message(), keyMismatch->offendingNode.path());
}
}
}
diff --git a/tests/restconf-plain-patch.cpp b/tests/restconf-plain-patch.cpp
index 10d653a..d4f3952 100644
--- a/tests/restconf-plain-patch.cpp
+++ b/tests/restconf-plain-patch.cpp
@@ -72,7 +72,7 @@ TEST_CASE("Plain patch")
"error-type": "protocol",
"error-tag": "invalid-value",
"error-path": "/example:tlc/list[name='blabla']/name",
- "error-message": "List key mismatch between URI path and data."
+ "error-message": "List key mismatch between URI path ('libyang') and data ('blabla')."
}
]
}
diff --git a/tests/restconf-writing.cpp b/tests/restconf-writing.cpp
index d46952f..96dbb25 100644
--- a/tests/restconf-writing.cpp
+++ b/tests/restconf-writing.cpp
@@ -432,7 +432,7 @@ TEST_CASE("writing data")
"error-type": "protocol",
"error-tag": "invalid-value",
"error-path": "/example:tlc/list[name='ahoj']/name",
- "error-message": "List key mismatch between URI path and data."
+ "error-message": "List key mismatch between URI path ('netconf') and data ('ahoj')."
}
]
}
@@ -447,7 +447,7 @@ TEST_CASE("writing data")
"error-type": "protocol",
"error-tag": "invalid-value",
"error-path": "/example:top-level-list[name='ahoj']/name",
- "error-message": "List key mismatch between URI path and data."
+ "error-message": "List key mismatch between URI path ('netconf') and data ('ahoj')."
}
]
}
@@ -505,7 +505,7 @@ TEST_CASE("writing data")
"error-type": "protocol",
"error-tag": "invalid-value",
"error-path": "/example:tlc/list[name='netconf']/collection[.='666']",
- "error-message": "List key mismatch between URI path and data."
+ "error-message": "List key mismatch between URI path ('667') and data ('666')."
}
]
}
@@ -520,7 +520,7 @@ TEST_CASE("writing data")
"error-type": "protocol",
"error-tag": "invalid-value",
"error-path": "/example:top-level-leaf-list[.='666']",
- "error-message": "List key mismatch between URI path and data."
+ "error-message": "List key mismatch between URI path ('667') and data ('666')."
}
]
}
@@ -535,7 +535,7 @@ TEST_CASE("writing data")
"error-type": "protocol",
"error-tag": "invalid-value",
"error-path": "/example:tlc/list[name='sysrepo']/name",
- "error-message": "List key mismatch between URI path and data."
+ "error-message": "List key mismatch between URI path ('netconf') and data ('sysrepo')."
}
]
}
@@ -550,7 +550,7 @@ TEST_CASE("writing data")
"error-type": "protocol",
"error-tag": "invalid-value",
"error-path": "/example:tlc/list[name='sysrepo']/name",
- "error-message": "List key mismatch between URI path and data."
+ "error-message": "List key mismatch between URI path ('netconf') and data ('sysrepo')."
}
]
}
@@ -565,7 +565,7 @@ TEST_CASE("writing data")
"error-type": "protocol",
"error-tag": "invalid-value",
"error-path": "/example:tlc/list[name='libyang']/collection[.='42']",
- "error-message": "List key mismatch between URI path and data."
+ "error-message": "List key mismatch between URI path ('5') and data ('42')."
}
]
}
diff --git a/tests/restconf-yang-patch.cpp b/tests/restconf-yang-patch.cpp
index 9d70912..7cc8946 100644
--- a/tests/restconf-yang-patch.cpp
+++ b/tests/restconf-yang-patch.cpp
@@ -436,7 +436,7 @@ TEST_CASE("YANG patch")
"error-type": "protocol",
"error-tag": "invalid-value",
"error-path": "/example:tlc/list[name='asdasdauisbdhaijbsdad']/name",
- "error-message": "List key mismatch between URI path and data."
+ "error-message": "List key mismatch between URI path ('libyang') and data ('asdasdauisbdhaijbsdad')."
}
]
}
--
2.43.0
@@ -1,137 +0,0 @@
From 027a47ac3ff6c369caab7c007a5a171a68270829 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
withRestconfExceptions wrapper
Organization: Wires
In case the catch block of withRestconfExceptions is reached and an
exception is thrown, rousette will start behaving in a strange way
where no new connections are accepted and already connected clients are
not receiving the content [1].
This patch prevents throwing the exception that would slip through our
catch handlers.
An uncaught exception is a problem though, so expect a followup patch to
handle such situations.
[1] https://github.com/CESNET/rousette/issues/19
Bug: https://github.com/CESNET/rousette/issues/19
Change-Id: Ifbd74b9bdc0ca66c4e5449a7673ef2f12ae9215e
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
src/restconf/Server.cpp | 2 +-
tests/restconf-plain-patch.cpp | 24 ++++++++++++++++++++++++
tests/restconf-reading.cpp | 3 +++
tests/yang/example.yang | 19 +++++++++++++++++++
4 files changed, 47 insertions(+), 1 deletion(-)
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 9821495..1c2123e 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -350,7 +350,7 @@ constexpr auto withRestconfExceptions(T func, U rejectWithError)
} else if (e.code() == sysrepo::ErrorCode::ItemAlreadyExists) {
rejectWithError(requestCtx->sess.getContext(), requestCtx->dataFormat.response, requestCtx->req, requestCtx->res, 409, "application", "resource-denied", "Resource already exists.", std::nullopt);
} else if (e.code() == sysrepo::ErrorCode::ValidationFailed) {
- bool isAction = requestCtx->sess.getContext().findPath(requestCtx->restconfRequest.path).nodeType() == libyang::NodeType::Action;
+ bool isAction = requestCtx->restconfRequest.path != "/" && requestCtx->sess.getContext().findPath(requestCtx->restconfRequest.path).nodeType() == libyang::NodeType::Action;
/*
* FIXME: This happens on invalid input data (e.g., missing mandatory nodes) or missing action data node.
* The former (invalid input data) should probably be validated by libyang's parseOp but it only parses.
diff --git a/tests/restconf-plain-patch.cpp b/tests/restconf-plain-patch.cpp
index b550f54..34813d1 100644
--- a/tests/restconf-plain-patch.cpp
+++ b/tests/restconf-plain-patch.cpp
@@ -179,5 +179,29 @@ TEST_CASE("Plain patch")
]
}
}
+)"});
+
+ REQUIRE(patch(RESTCONF_ROOT_DS("running"), {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({
+ "example:channel-plan": {
+ "channel": [
+ {
+ "name": "coriant",
+ "lower-frequency": 199999999,
+ "upper-frequency": 191500000
+ }
+ ]
+ }
+}
+)") == Response{400, jsonHeaders, R"({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "application",
+ "error-tag": "operation-failed",
+ "error-message": "Validation failed. Invalid input data."
+ }
+ ]
+ }
+}
)"});
}
diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp
index f87c4f5..4f0d2ee 100644
--- a/tests/restconf-reading.cpp
+++ b/tests/restconf-reading.cpp
@@ -62,6 +62,7 @@ TEST_CASE("reading data")
// this relies on a NACM rule for anonymous access that filters out "a lot of stuff"
REQUIRE(get(RESTCONF_DATA_ROOT, {}) == Response{200, jsonHeaders, R"({
"example:top-level-leaf": "moo",
+ "example:channel-plan": {},
"example:tlc": {},
"example:a": {
"b": {
@@ -119,6 +120,7 @@ TEST_CASE("reading data")
REQUIRE(get(RESTCONF_ROOT_DS("operational"), {}) == Response{200, jsonHeaders, R"({
"example:top-level-leaf": "moo",
+ "example:channel-plan": {},
"example:tlc": {},
"example:a": {
"b": {
@@ -176,6 +178,7 @@ TEST_CASE("reading data")
REQUIRE(get(RESTCONF_ROOT_DS("running"), {}) == Response{200, jsonHeaders, R"({
"example:top-level-leaf": "moo",
+ "example:channel-plan": {},
"example:tlc": {},
"example:a": {
"b": {
diff --git a/tests/yang/example.yang b/tests/yang/example.yang
index 5d586a0..1cd12e3 100644
--- a/tests/yang/example.yang
+++ b/tests/yang/example.yang
@@ -27,6 +27,25 @@ module example {
}
leaf-list top-level-leaf-list { type int32; }
+ container channel-plan {
+ list channel {
+ key "name";
+ leaf name { type string; }
+ leaf lower-frequency {
+ type int32;
+ units "Hz";
+ }
+ leaf upper-frequency {
+ type int32;
+ units "Hz";
+ }
+
+ must "lower-frequency < upper-frequency" {
+ description "The lower frequency must be less than the upper frequency.";
+ }
+ }
+ }
+
container tlc {
if-feature f1;
list list {
--
2.43.0
@@ -0,0 +1,50 @@
From 070cffb48fda789910581930265d4624a7213e1b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Wed, 16 Oct 2024 11:02:45 +0200
Subject: [PATCH 02/44] uri: rename url-encoding to percent-encoding
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
RFC 3986 [1] calls it the percent-encoding, let's be consistent.
[1] https://datatracker.ietf.org/doc/html/rfc3986
Change-Id: Iee8b76c980b2694b6643e627b462f8bfc2c21c45
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/restconf/uri.cpp | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp
index 6e27168..b144d92 100644
--- a/src/restconf/uri.cpp
+++ b/src/restconf/uri.cpp
@@ -29,12 +29,12 @@ auto add = [](auto& ctx) {
char c = std::tolower(_attr(ctx));
_val(ctx) = _val(ctx) * 16 + (c >= 'a' ? c - 'a' + 10 : c - '0');
};
-const auto urlEncodedChar = x3::rule<class urlEncodedChar, unsigned>{"urlEncodedChar"} = x3::lit('%')[set_zero] >> x3::xdigit[add] >> x3::xdigit[add];
+const auto percentEncodedChar = x3::rule<class percentEncodedChar, unsigned>{"percentEncodedChar"} = x3::lit('%')[set_zero] >> x3::xdigit[add] >> x3::xdigit[add];
/* reserved characters according to RFC 3986, sec. 2.2 with '%' added. The '%' character is not specified as reserved but it effectively is because
* "Percent sign serves as the indicator for percent-encoded octets, it must be percent-encoded (...)" [RFC 3986, sec. 2.4]. */
const auto reservedChars = x3::lit(':') | '/' | '?' | '#' | '[' | ']' | '@' | '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ',' | ';' | '=' | '%';
-const auto keyValue = x3::rule<class keyValue, std::string>{"keyValue"} = *(urlEncodedChar | (x3::char_ - reservedChars));
+const auto keyValue = x3::rule<class keyValue, std::string>{"keyValue"} = *(percentEncodedChar | (x3::char_ - reservedChars));
const auto keyList = x3::rule<class keyList, std::vector<std::string>>{"keyList"} = keyValue % ',';
const auto identifier = x3::rule<class apiIdentifier, std::string>{"identifier"} = (x3::alpha | x3::char_('_')) >> *(x3::alnum | x3::char_('_') | x3::char_('-') | x3::char_('.'));
@@ -117,7 +117,7 @@ const auto dateAndTime = x3::rule<class dateAndTime, std::string>{"dateAndTime"}
x3::repeat(4)[x3::digit] >> x3::char_('-') >> x3::repeat(2)[x3::digit] >> x3::char_('-') >> x3::repeat(2)[x3::digit] >> x3::char_('T') >>
x3::repeat(2)[x3::digit] >> x3::char_(':') >> x3::repeat(2)[x3::digit] >> x3::char_(':') >> x3::repeat(2)[x3::digit] >> -(x3::char_('.') >> +x3::digit) >>
(x3::char_('Z') | (-(x3::char_('+')|x3::char_('-')) >> x3::repeat(2)[x3::digit] >> x3::char_(':') >> x3::repeat(2)[x3::digit]));
-const auto filter = x3::rule<class filter, std::string>{"filter"} = +(urlEncodedChar | (x3::char_ - '&'));
+const auto filter = x3::rule<class filter, std::string>{"filter"} = +(percentEncodedChar | (x3::char_ - '&'));
const auto depthParam = x3::rule<class depthParam, queryParams::QueryParamValue>{"depthParam"} = x3::uint_[validDepthValues] | (x3::string("unbounded") >> x3::attr(queryParams::UnboundedDepth{}));
const auto queryParamPair = x3::rule<class queryParamPair, std::pair<std::string, queryParams::QueryParamValue>>{"queryParamPair"} =
(x3::string("depth") >> "=" >> depthParam) |
--
2.43.0
@@ -1,172 +0,0 @@
From d1c56f14b54b113df8fba759924bb47cfa27366c 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
throws
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
In case there is an exception thrown in the handler function and it is
not caught by our code, rousette starts to behave in a strange way where
no new connections are accepted and already connected clients are not
receiving the content [1].
It turns out, that nghttp2-asio starts the boost::asio::io_service
wrapped inside std::async [2]. This means the thread terminated, and is
waiting for somebody to get the result of the future [3]. But nobody
did that in our code.
Luckily, the (hot-)fix proposed by Jan is quite simple. Rousette uses
only one io_service, so we can just call server.join() (which itself
calls future::get) instead of the pause() in the main function. If the
handler throws, we pick the result right away and rousette crashes.
(The crash is intentional and in line with the behaviour of our other
daemons in our systems. Daemon crashes, we log it and systemd restarts
it).
Note that to enable graceful shutdown, it was necessary to guard against
double call to server->join(), because future::get does not like being
called twice [3].
[1] https://github.com/CESNET/rousette/issues/19
[2] https://github.com/nghttp2/nghttp2-asio/blob/a057ffaa207fbe9584565ed503c2ee5c7e609747/lib/asio_io_service_pool.cc#L57
[3] https://en.cppreference.com/w/cpp/thread/future/get
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>
---
src/restconf/Server.cpp | 28 ++++++++++++++++++++++++++++
src/restconf/Server.h | 4 ++++
src/restconf/main.cpp | 15 ++++++++++-----
3 files changed, 42 insertions(+), 5 deletions(-)
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 1c2123e..4255540 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -826,6 +826,15 @@ bool isYangPatch(const nghttp2::asio_http2::server::request& req)
}
Server::~Server()
+{
+ stop();
+
+ if (!joined) {
+ server->join();
+ }
+}
+
+void Server::stop()
{
// notification to stop has to go through the asio io_context
for (const auto& service : server->io_services()) {
@@ -836,8 +845,25 @@ Server::~Server()
});
t.cancel();
}
+}
+
+void Server::join()
+{
+ /* FIXME: nghttp2-asio is calling io.run() wrapped in std::async.
+ * In case the handler function throws, the exception is not propagated to the main thread *until* someone calls server.join() which calls future.get() on all io.run() wrappers.
+ * Thankfully, we have only one thread, so we can just call join() right away. Underlying future.get() blocks until io.run() finishes, either gracefully or upon uncaught exception.
+ *
+ * !!! Will not work server uses multiple threads !!!
+ */
+ // main thread waits here
server->join();
+ joined = true;
+}
+
+std::vector<std::shared_ptr<boost::asio::io_context>> Server::io_services() const
+{
+ return server->io_services();
}
Server::Server(sysrepo::Connection conn, const std::string& address, const std::string& port, const std::chrono::milliseconds timeout)
@@ -846,6 +872,8 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std::
, server{std::make_unique<nghttp2::asio_http2::server::http2>()}
, 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
+
for (const auto& [module, version, features] : {
std::tuple<std::string, std::string, std::vector<std::string>>{"ietf-restconf", "2017-01-26", {}},
{"ietf-restconf-monitoring", "2017-01-26", {}},
diff --git a/src/restconf/Server.h b/src/restconf/Server.h
index d1b2187..ad53d4c 100644
--- a/src/restconf/Server.h
+++ b/src/restconf/Server.h
@@ -30,6 +30,9 @@ class Server {
public:
explicit Server(sysrepo::Connection conn, const std::string& address, const std::string& port, const std::chrono::milliseconds timeout = std::chrono::milliseconds{0});
~Server();
+ void join();
+ void stop();
+ std::vector<std::shared_ptr<boost::asio::io_context>> io_services() const;
private:
sysrepo::Session m_monitoringSession;
@@ -39,6 +42,7 @@ private:
std::unique_ptr<sr::OpticalEvents> dwdmEvents;
using JsonDiffSignal = boost::signals2::signal<void(const std::string& json)>;
JsonDiffSignal opticsChange;
+ bool joined = false; // true if the server has been joined, join twice is an error
};
}
}
diff --git a/src/restconf/main.cpp b/src/restconf/main.cpp
index 6029e08..477e846 100644
--- a/src/restconf/main.cpp
+++ b/src/restconf/main.cpp
@@ -5,13 +5,13 @@
*
*/
-#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <inttypes.h>
#include "configure.cmake.h" /* Expose HAVE_SYSTEMD */
+#include <boost/asio.hpp>
#ifdef HAVE_SYSTEMD
#include <spdlog/sinks/systemd_sink.h>
#endif
@@ -19,7 +19,6 @@
#include <spdlog/sinks/ansicolor_sink.h>
#include <sys/types.h>
#include <sys/stat.h>
-#include <unistd.h>
#include <docopt.h>
#include <spdlog/spdlog.h>
#include <sysrepo-cpp/Session.hpp>
@@ -106,9 +105,15 @@ int main(int argc, char* argv [])
auto conn = sysrepo::Connection{};
auto server = rousette::restconf::Server{conn, "::1", "10080", timeout};
- signal(SIGTERM, [](int) {});
- signal(SIGINT, [](int) {});
- pause();
+
+ // allow graceful shutdown
+ boost::asio::signal_set signals(*server.io_services()[0], SIGTERM, SIGINT);
+ signals.async_wait([&](const boost::system::error_code& ec, int) {
+ if (!ec) {
+ server.stop();
+ }
+ });
+ server.join();
return 0;
}
--
2.43.0
@@ -0,0 +1,34 @@
From 8a233202647f24538f2bbb8fff1e38d52e3599a4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Wed, 16 Oct 2024 11:05:09 +0200
Subject: [PATCH 03/44] uri: correct x3 rule class names
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Fixes: e06d5bf ("server: parser for data resources in URI paths")
Change-Id: Ic953e568d841032113ede1c0e896574361c0ebe2
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/restconf/uri.cpp | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp
index b144d92..8e8dc23 100644
--- a/src/restconf/uri.cpp
+++ b/src/restconf/uri.cpp
@@ -37,8 +37,8 @@ const auto reservedChars = x3::lit(':') | '/' | '?' | '#' | '[' | ']' | '@' | '!
const auto keyValue = x3::rule<class keyValue, std::string>{"keyValue"} = *(percentEncodedChar | (x3::char_ - reservedChars));
const auto keyList = x3::rule<class keyList, std::vector<std::string>>{"keyList"} = keyValue % ',';
-const auto identifier = x3::rule<class apiIdentifier, std::string>{"identifier"} = (x3::alpha | x3::char_('_')) >> *(x3::alnum | x3::char_('_') | x3::char_('-') | x3::char_('.'));
-const auto apiIdentifier = x3::rule<class identifier, ApiIdentifier>{"apiIdentifier"} = -(identifier >> ':') >> identifier;
+const auto identifier = x3::rule<class identifier, std::string>{"identifier"} = (x3::alpha | x3::char_('_')) >> *(x3::alnum | x3::char_('_') | x3::char_('-') | x3::char_('.'));
+const auto apiIdentifier = x3::rule<class apiIdentifier, ApiIdentifier>{"apiIdentifier"} = -(identifier >> ':') >> identifier;
const auto listInstance = x3::rule<class keyList, PathSegment>{"listInstance"} = apiIdentifier >> -('=' >> keyList);
const auto fullyQualifiedApiIdentifier = x3::rule<class identifier, ApiIdentifier>{"apiIdentifier"} = identifier >> ':' >> identifier;
const auto fullyQualifiedListInstance = x3::rule<class keyList, PathSegment>{"listInstance"} = fullyQualifiedApiIdentifier >> -('=' >> keyList);
--
2.43.0
@@ -1,374 +0,0 @@
From db64d7b3fc0a0373067e6c5ac9e41b93351778a1 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
Organization: Wires
Without this patch, all the ongoing SSE streams would be left alive for
about 50s, and only then killed. Fix that by a proper signalling -- once
the intention to shutdown is known, all the EventStreams are
(asynchronously) switched to a new state, and the HTTP2 state machine is
restarted. Once the event loop gets around to processing them, the
callback will just say EOF.
The clock demo is different because it doesn't wrap the nghttp2 server
like the restconf/Server.cpp does.
I have no idea how well this works against a Slowloris attack, but I
think that stuff like that should be solved at the HTTP library level (I
hope it is).
Change-Id: If442134783ba1d699de47c51a9068378f53e8339
Co-authored-by: Tomas Pecka <tomas.pecka@cesnet.cz>
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
CMakeLists.txt | 1 +
src/clock.cpp | 7 +--
src/http/EventStream.cpp | 30 +++++++++++--
src/http/EventStream.h | 10 +++--
src/restconf/NotificationStream.cpp | 7 +--
src/restconf/NotificationStream.h | 5 ++-
src/restconf/Server.cpp | 5 ++-
src/restconf/Server.h | 1 +
tests/restconf-eventstream.cpp | 70 +++++++++++++++++++++++++++++
tests/restconf_utils.cpp | 6 +++
10 files changed, 124 insertions(+), 18 deletions(-)
create mode 100644 tests/restconf-eventstream.cpp
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b99c577..c0304af 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -227,6 +227,7 @@ if(BUILD_TESTING)
rousette_test(NAME restconf-notifications LIBRARIES rousette-restconf FIXTURE common-models WRAP_PAM)
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)
set(nested-models
${common-models}
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/root-mod.yang)
diff --git a/src/clock.cpp b/src/clock.cpp
index 7ac6c3e..c85be19 100644
--- a/src/clock.cpp
+++ b/src/clock.cpp
@@ -18,7 +18,8 @@ int main(int argc [[maybe_unused]], char** argv [[maybe_unused]])
{
spdlog::set_level(spdlog::level::trace);
- rousette::http::EventStream::Signal sig;
+ 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);
@@ -30,8 +31,8 @@ int main(int argc [[maybe_unused]], char** argv [[maybe_unused]])
nghttp2::asio_http2::server::http2 server;
server.num_threads(4);
- server.handle("/events", [&sig](const auto& req, const auto& res) {
- auto client = std::make_shared<rousette::http::EventStream>(req, res, sig);
+ server.handle("/events", [&shutdown, &sig](const auto& req, const auto& res) {
+ auto client = std::make_shared<rousette::http::EventStream>(req, res, shutdown, sig);
client->activate();
});
diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp
index 25dbf3f..77c282d 100644
--- a/src/http/EventStream.cpp
+++ b/src/http/EventStream.cpp
@@ -18,7 +18,7 @@ using namespace nghttp2::asio_http2;
namespace rousette::http {
/** @short After constructing, make sure to call activate() immediately. */
-EventStream::EventStream(const server::request& req, const server::response& res, Signal& signal, const std::optional<std::string>& initialEvent)
+EventStream::EventStream(const server::request& req, const server::response& res, Termination& termination, EventSignal& signal, const std::optional<std::string>& initialEvent)
: res{res}
, peer{peer_from_request(req)}
{
@@ -26,9 +26,27 @@ EventStream::EventStream(const server::request& req, const server::response& res
enqueue(*initialEvent);
}
- subscription = signal.connect([this](const auto& msg) {
+ eventSub = signal.connect([this](const auto& msg) {
enqueue(msg);
});
+
+ terminateSub = termination.connect([this]() {
+ spdlog::trace("{}: will terminate", peer);
+ std::lock_guard lock{mtx};
+ if (state == Closed) { // we are late to the party, res is already gone
+ return;
+ }
+
+ state = WantToClose;
+ boost::asio::post(this->res.io_service(), [maybeClient = std::weak_ptr<EventStream>{shared_from_this()}]() {
+ if (auto client = maybeClient.lock()) {
+ std::lock_guard lock{client->mtx};
+ if (client->state == WantToClose) { // resume unless somebody closed it before this was picked up by the event loop
+ client->res.resume();
+ }
+ }
+ });
+ });
}
/** @short Start event processing and data delivery
@@ -47,7 +65,8 @@ void EventStream::activate()
res.on_close([client](const auto ec) {
spdlog::debug("{}: closed ({})", client->peer, nghttp2_http2_strerror(ec));
std::lock_guard lock{client->mtx};
- client->subscription.disconnect();
+ client->eventSub.disconnect();
+ client->terminateSub.disconnect();
client->state = Closed;
});
@@ -85,6 +104,9 @@ ssize_t EventStream::process(uint8_t* destination, std::size_t len, uint32_t* da
case WaitingForEvents:
spdlog::trace("{}: sleeping", peer);
return NGHTTP2_ERR_DEFERRED;
+ case WantToClose:
+ *data_flags |= NGHTTP2_DATA_FLAG_EOF;
+ return 0;
case Closed:
throw std::logic_error{"response already closed"};
}
@@ -105,7 +127,7 @@ void EventStream::enqueue(const std::string& what)
buf += '\n';
std::lock_guard lock{mtx};
- if (state == Closed) {
+ if (state == Closed || state == WantToClose) {
spdlog::trace("{}: enqueue: already disconnected", peer);
return;
}
diff --git a/src/http/EventStream.h b/src/http/EventStream.h
index 735b69a..b427fb3 100644
--- a/src/http/EventStream.h
+++ b/src/http/EventStream.h
@@ -23,13 +23,14 @@ namespace rousette::http {
/** @short Event delivery via text/event-stream
-Recieve data from a Signal, and deliver them to an HTTP client via a text/event-stream streamed response.
+Recieve data from an EventSignal, and deliver them to an HTTP client via a text/event-stream streamed response.
*/
class EventStream : public std::enable_shared_from_this<EventStream> {
public:
- using Signal = boost::signals2::signal<void(const std::string& message)>;
+ using EventSignal = boost::signals2::signal<void(const std::string& message)>;
+ using Termination = boost::signals2::signal<void()>;
- EventStream(const nghttp2::asio_http2::server::request& req, const nghttp2::asio_http2::server::response& res, Signal& signal, const std::optional<std::string>& initialEvent = std::nullopt);
+ EventStream(const nghttp2::asio_http2::server::request& req, const nghttp2::asio_http2::server::response& res, Termination& terminate, EventSignal& signal, const std::optional<std::string>& initialEvent = std::nullopt);
void activate();
private:
@@ -37,13 +38,14 @@ private:
enum State {
HasEvents,
WaitingForEvents,
+ WantToClose,
Closed,
};
State state = WaitingForEvents;
std::list<std::string> queue;
mutable std::mutex mtx; // for `state` and `queue`
- boost::signals2::scoped_connection subscription;
+ boost::signals2::scoped_connection eventSub, terminateSub;
const std::string peer;
size_t send_chunk(uint8_t* destination, std::size_t len, uint32_t* data_flags);
diff --git a/src/restconf/NotificationStream.cpp b/src/restconf/NotificationStream.cpp
index e00c39d..d0ba938 100644
--- a/src/restconf/NotificationStream.cpp
+++ b/src/restconf/NotificationStream.cpp
@@ -24,7 +24,7 @@ void subscribe(
std::optional<sysrepo::Subscription>& sub,
sysrepo::Session& session,
const std::string& moduleName,
- rousette::http::EventStream::Signal& signal,
+ rousette::http::EventStream::EventSignal& signal,
libyang::DataFormat dataFormat,
const std::optional<std::string>& filter,
const std::optional<sysrepo::NotificationTimeStamp>& startTime,
@@ -87,13 +87,14 @@ namespace rousette::restconf {
NotificationStream::NotificationStream(
const nghttp2::asio_http2::server::request& req,
const nghttp2::asio_http2::server::response& res,
- std::shared_ptr<rousette::http::EventStream::Signal> signal,
+ rousette::http::EventStream::Termination& termination,
+ std::shared_ptr<rousette::http::EventStream::EventSignal> signal,
sysrepo::Session session,
libyang::DataFormat dataFormat,
const std::optional<std::string>& filter,
const std::optional<sysrepo::NotificationTimeStamp>& startTime,
const std::optional<sysrepo::NotificationTimeStamp>& stopTime)
- : EventStream(req, res, *signal)
+ : EventStream(req, res, termination, *signal)
, m_notificationSignal(signal)
, m_session(std::move(session))
, m_dataFormat(dataFormat)
diff --git a/src/restconf/NotificationStream.h b/src/restconf/NotificationStream.h
index 3029a0d..46f8416 100644
--- a/src/restconf/NotificationStream.h
+++ b/src/restconf/NotificationStream.h
@@ -30,7 +30,7 @@ namespace rousette::restconf {
* @see rousette::http::EventStream
* */
class NotificationStream : public rousette::http::EventStream {
- std::shared_ptr<rousette::http::EventStream::Signal> m_notificationSignal;
+ std::shared_ptr<rousette::http::EventStream::EventSignal> m_notificationSignal;
sysrepo::Session m_session;
libyang::DataFormat m_dataFormat;
std::optional<std::string> m_filter;
@@ -42,7 +42,8 @@ public:
NotificationStream(
const nghttp2::asio_http2::server::request& req,
const nghttp2::asio_http2::server::response& res,
- std::shared_ptr<rousette::http::EventStream::Signal> signal,
+ rousette::http::EventStream::Termination& termination,
+ std::shared_ptr<rousette::http::EventStream::EventSignal> signal,
sysrepo::Session sess,
libyang::DataFormat dataFormat,
const std::optional<std::string>& filter,
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 4255540..2f495f9 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -845,6 +845,7 @@ void Server::stop()
});
t.cancel();
}
+ shutdownRequested();
}
void Server::join()
@@ -935,7 +936,7 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std::
server->handle("/telemetry/optics", [this](const auto& req, const auto& res) {
logRequest(req);
- auto client = std::make_shared<http::EventStream>(req, res, opticsChange, as_restconf_push_update(dwdmEvents->currentData(), std::chrono::system_clock::now()));
+ auto client = std::make_shared<http::EventStream>(req, res, shutdownRequested, opticsChange, as_restconf_push_update(dwdmEvents->currentData(), std::chrono::system_clock::now()));
client->activate();
});
@@ -972,7 +973,7 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std::
// The signal is constructed outside NotificationStream class because it is required to be passed to
// NotificationStream's parent (EventStream) constructor where it already must be constructed
// Yes, this is a hack.
- auto client = std::make_shared<NotificationStream>(req, res, std::make_shared<rousette::http::EventStream::Signal>(), sess, streamRequest.type.encoding, xpathFilter, startTime, stopTime);
+ auto client = std::make_shared<NotificationStream>(req, res, shutdownRequested, std::make_shared<rousette::http::EventStream::EventSignal>(), sess, streamRequest.type.encoding, xpathFilter, startTime, stopTime);
client->activate();
} catch (const auth::Error& e) {
processAuthError(req, res, e, [&res]() {
diff --git a/src/restconf/Server.h b/src/restconf/Server.h
index ad53d4c..112c943 100644
--- a/src/restconf/Server.h
+++ b/src/restconf/Server.h
@@ -43,6 +43,7 @@ private:
using JsonDiffSignal = boost::signals2::signal<void(const std::string& json)>;
JsonDiffSignal opticsChange;
bool joined = false; // true if the server has been joined, join twice is an error
+ boost::signals2::signal<void()> shutdownRequested;
};
}
}
diff --git a/tests/restconf-eventstream.cpp b/tests/restconf-eventstream.cpp
new file mode 100644
index 0000000..3d7fc15
--- /dev/null
+++ b/tests/restconf-eventstream.cpp
@@ -0,0 +1,70 @@
+/*
+ * 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 = "10091";
+#include <latch>
+#include <libyang-cpp/Time.hpp>
+#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"
+#include "tests/pretty_printers.h"
+
+#define SEND_NOTIFICATION(DATA) notifSession.sendNotification(*ctx.parseOp(DATA, libyang::DataFormat::JSON, libyang::OperationType::NotificationYang).op, sysrepo::Wait::No);
+
+using namespace std::chrono_literals;
+
+TEST_CASE("Termination on server shutdown")
+{
+ trompeloeil::sequence seqMod1;
+ sysrepo::setLogLevelStderr(sysrepo::LogLevel::Information);
+ spdlog::set_level(spdlog::level::trace);
+
+ std::vector<std::unique_ptr<trompeloeil::expectation>> expectations;
+
+ 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());
+
+ const std::string notification(R"({"example:eventA":{"message":"blabla","progress":11}})");
+ EXPECT_NOTIFICATION(notification, seqMod1);
+ EXPECT_NOTIFICATION(notification, seqMod1);
+ EXPECT_NOTIFICATION(notification, seqMod1);
+
+ auto notifSession = sysrepo::Connection{}.sessionStart();
+ auto ctx = notifSession.getContext();
+
+ PREPARE_LOOP_WITH_EXCEPTIONS;
+ auto 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(notification);
+ SEND_NOTIFICATION(notification);
+ SEND_NOTIFICATION(notification);
+ waitForCompletionAndBitMore(seqMod1);
+
+ auto beforeShutdown = std::chrono::system_clock::now();
+ server.reset();
+ auto shutdownDuration = std::chrono::system_clock::now() - beforeShutdown;
+ REQUIRE(shutdownDuration < 5s);
+ }));
+
+ SSEClient client(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, "/streams/NETCONF/JSON", std::map<std::string, std::string>{AUTH_ROOT});
+
+ RUN_LOOP_WITH_EXCEPTIONS;
+}
diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp
index a137463..cbfce2c 100644
--- a/tests/restconf_utils.cpp
+++ b/tests/restconf_utils.cpp
@@ -205,6 +205,12 @@ SSEClient::SSEClient(
t.expires_from_now(silenceTimeout);
});
});
+
+ req->on_close([maybeClient = std::weak_ptr<ng_client::session>{client}](auto) {
+ if (auto client = maybeClient.lock()) {
+ client->shutdown();
+ }
+ });
});
client->on_error([&](const boost::system::error_code& ec) {
--
2.43.0
@@ -0,0 +1,153 @@
From 96cbf730010ee9539d05d0d72697dc960b3a938c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Mon, 7 Oct 2024 20:46:24 +0200
Subject: [PATCH 04/44] restconf: parser should work on raw percent-encoded
paths
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
The difference between nghttp2's uri_ref::path and raw_path is that the
raw_path keeps the percent encoding, while the path converts the percent
encoded chars to their "normal" form.
Our parser expects some parts of the URI to be percent encoded so we
have to use raw_paths everywhere.
I thought about rewriting the parser not to expect percent encoded
characters but that would bring some complications. For instance when
querying lists, the RESTCONF RFC specifies that every key value is
percent encoded and individual key values are delimited by commas [1].
So, when somebody sends a request like /restconf/data/list=a%2Cb,c the
"normal" form is /restconf/data/list=a,b,c and in that case we obtain
three keys, but the client sent only two, where the first one contained
comma.
I am adding few tests to check for percent encoded values.
We realized this while working on one of the reported bugs [1]. The
query sent by the client there is wrong, the ':' char should be
percent-encoded.
[1] https://github.com/CESNET/rousette/issues/12
Bug: https://github.com/CESNET/rousette/issues/12
Change-Id: I473501cef3c8eae9af0c5d0751393cdad647e23c
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/restconf/Server.cpp | 8 ++++----
src/restconf/uri.cpp | 4 ++--
tests/restconf-reading.cpp | 15 +++++++++++++++
tests/uri-parser.cpp | 5 +++--
4 files changed, 24 insertions(+), 8 deletions(-)
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 5f560ed..79d8ff6 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -416,7 +416,7 @@ void processActionOrRPC(std::shared_ptr<RequestContext> requestCtx, const std::c
* - The data node exists but might get deleted right after this check: Sysrepo throws an error when this happens.
* - The data node does not exist but might get created right after this check: The node was not there when the request was issues so it should not be a problem
*/
- auto [pathToParent, pathSegment] = asLibyangPathSplit(ctx, requestCtx->req.uri().path);
+ auto [pathToParent, pathSegment] = asLibyangPathSplit(ctx, requestCtx->req.uri().raw_path);
if (!requestCtx->sess.getData(pathToParent, 0, sysrepo::GetOptions::Default, timeout)) {
throw ErrorResponse(400, "application", "operation-failed", "Action data node '" + requestCtx->restconfRequest.path + "' does not exist.");
}
@@ -539,7 +539,7 @@ void processYangPatchEdit(const std::shared_ptr<RequestContext>& requestCtx, con
auto target = childLeafValue(editContainer, "target");
auto operation = childLeafValue(editContainer, "operation");
- auto [singleEdit, replacementNode] = createEditForPutAndPatch(ctx, requestCtx->req.uri().path + target, yangPatchValueAsJSON(editContainer), libyang::DataFormat::JSON);
+ auto [singleEdit, replacementNode] = createEditForPutAndPatch(ctx, requestCtx->req.uri().raw_path + target, yangPatchValueAsJSON(editContainer), libyang::DataFormat::JSON);
validateInputMetaAttributes(ctx, *singleEdit);
// insert and move are not defined in RFC6241. sec 7.3 and sysrepo does not support them directly
@@ -658,7 +658,7 @@ void processPutOrPlainPatch(std::shared_ptr<RequestContext> requestCtx, const st
throw ErrorResponse(400, "protocol", "invalid-value", "Target resource does not exist");
}
- auto [edit, replacementNode] = createEditForPutAndPatch(ctx, requestCtx->req.uri().path, requestCtx->payload, *requestCtx->dataFormat.request /* caller checks if the dataFormat.request is present */);
+ auto [edit, replacementNode] = createEditForPutAndPatch(ctx, requestCtx->req.uri().raw_path, requestCtx->payload, *requestCtx->dataFormat.request /* caller checks if the dataFormat.request is present */);
validateInputMetaAttributes(ctx, *edit);
if (requestCtx->req.method() == "PUT") {
@@ -954,7 +954,7 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std::
dataFormat = chooseDataEncoding(req.header());
authorizeRequest(nacm, sess, req);
- auto restconfRequest = asRestconfRequest(sess.getContext(), req.method(), req.uri().path, req.uri().raw_query);
+ auto restconfRequest = asRestconfRequest(sess.getContext(), req.method(), req.uri().raw_path, req.uri().raw_query);
switch (restconfRequest.type) {
case RestconfRequest::Type::RestconfRoot:
diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp
index 8e8dc23..ac399b7 100644
--- a/src/restconf/uri.cpp
+++ b/src/restconf/uri.cpp
@@ -34,9 +34,9 @@ const auto percentEncodedChar = x3::rule<class percentEncodedChar, unsigned>{"pe
/* reserved characters according to RFC 3986, sec. 2.2 with '%' added. The '%' character is not specified as reserved but it effectively is because
* "Percent sign serves as the indicator for percent-encoded octets, it must be percent-encoded (...)" [RFC 3986, sec. 2.4]. */
const auto reservedChars = x3::lit(':') | '/' | '?' | '#' | '[' | ']' | '@' | '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ',' | ';' | '=' | '%';
-const auto keyValue = x3::rule<class keyValue, std::string>{"keyValue"} = *(percentEncodedChar | (x3::char_ - reservedChars));
+const auto percentEncodedString = x3::rule<class percentEncodedString, std::string>{"percentEncodedString"} = *(percentEncodedChar | (x3::char_ - reservedChars));
-const auto keyList = x3::rule<class keyList, std::vector<std::string>>{"keyList"} = keyValue % ',';
+const auto keyList = x3::rule<class keyList, std::vector<std::string>>{"keyList"} = percentEncodedString % ',';
const auto identifier = x3::rule<class identifier, std::string>{"identifier"} = (x3::alpha | x3::char_('_')) >> *(x3::alnum | x3::char_('_') | x3::char_('-') | x3::char_('.'));
const auto apiIdentifier = x3::rule<class apiIdentifier, ApiIdentifier>{"apiIdentifier"} = -(identifier >> ':') >> identifier;
const auto listInstance = x3::rule<class keyList, PathSegment>{"listInstance"} = apiIdentifier >> -('=' >> keyList);
diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp
index fa1cbcc..2898839 100644
--- a/tests/restconf-reading.cpp
+++ b/tests/restconf-reading.cpp
@@ -261,6 +261,21 @@ TEST_CASE("reading data")
}
)"});
+ // percent-encoded comma is a part of the key value, it is not a delimiter
+ REQUIRE(get(RESTCONF_DATA_ROOT "/ietf-system:system/radius/server=a%2Cb", {AUTH_DWDM}) == Response{404, jsonHeaders, R"({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "application",
+ "error-tag": "invalid-value",
+ "error-message": "No data from sysrepo."
+ }
+ ]
+ }
+}
+)"});
+
+ // comma is a delimiter of list key values
REQUIRE(get(RESTCONF_DATA_ROOT "/ietf-system:system/radius/server=a,b", {AUTH_DWDM}) == Response{400, jsonHeaders, R"({
"ietf-restconf:errors": {
"error": [
diff --git a/tests/uri-parser.cpp b/tests/uri-parser.cpp
index a748e09..5977afc 100644
--- a/tests/uri-parser.cpp
+++ b/tests/uri-parser.cpp
@@ -158,9 +158,9 @@ TEST_CASE("URI path parser")
{{"prefix", "lst"}, {"key1"}},
{{"prefix", "leaf"}},
}}},
- {"/restconf/data/foo:bar/lst=key1,,key3", {{
+ {"/restconf/data/foo:bar/lst=module%3Akey1,,key3", {{
{{"foo", "bar"}},
- {{"lst"}, {"key1", "", "key3"}},
+ {{"lst"}, {"module:key1", "", "key3"}},
}}},
{"/restconf/data/foo:bar/lst=key%2CWithCommas,,key2C", {{
{{"foo", "bar"}},
@@ -240,6 +240,7 @@ TEST_CASE("URI path parser")
"/restconf/data/foo:list=A%2",
"/restconf/data/foo:list=A%2,",
"/restconf/data/foo:bar/list1=%%",
+ "/restconf/data/foo:bar/list1=module:smth",
"/restconf/data/foo:bar/",
"/restconf/data/ foo : bar",
"/rest conf/data / foo:bar",
--
2.43.0
@@ -0,0 +1,428 @@
From 8b13c1e4ccaa61a241674c27063439e257fa88de Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Wed, 2 Oct 2024 20:21:28 +0200
Subject: [PATCH 05/44] restconf: list key values checking must respect
libyang's canonicalization
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
While dealing with the issue that was fixed in the previous commit we
realized that the issue is deeper. Not only that our parser rejected
the input when someone used identityref with module prefix in the URI,
but also, our internal code for creating/modifying list entries was
wrong.
In PUT requests we have to check if user entered the same key value in
URI and yang-data payload. However, some values are canonicalized by
libyang (e.g. decimal64 type with fraction-digits=2 or even the
identityrefs) and so if the client entered two different but
canonically equivalent values, we would reject such request.
Bug: https://github.com/CESNET/rousette/issues/12
Change-Id: I44245d831e8de6d0e6f991fcd18319c095b49b1d
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 3 +-
src/restconf/Server.cpp | 49 +++++++++++++++++----
src/restconf/utils/yang.cpp | 5 +++
src/restconf/utils/yang.h | 1 +
tests/restconf-reading.cpp | 59 +++++++++++++++++++++++++
tests/restconf-writing.cpp | 82 +++++++++++++++++++++++++++++++++++
tests/uri-parser.cpp | 2 +
tests/yang/example-types.yang | 13 ++++++
tests/yang/example.yang | 25 +++++++++++
9 files changed, 229 insertions(+), 10 deletions(-)
create mode 100644 tests/yang/example-types.yang
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c563dcf..465bef9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -204,7 +204,8 @@ if(BUILD_TESTING)
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example.yang --enable-feature f1
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-delete.yang
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-augment.yang
- --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-notif.yang)
+ --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-notif.yang
+ --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-types.yang)
rousette_test(NAME restconf-reading LIBRARIES rousette-restconf FIXTURE common-models WRAP_PAM)
rousette_test(NAME restconf-writing LIBRARIES rousette-restconf FIXTURE common-models WRAP_PAM)
rousette_test(NAME restconf-delete LIBRARIES rousette-restconf FIXTURE common-models WRAP_PAM)
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 79d8ff6..b515d66 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -154,6 +154,15 @@ auto rejectYangPatch(const std::string& patchId, const std::string& editId)
};
}
+/** @brief Check if these two paths compare the same after path canonicalization */
+bool compareKeyValue(const libyang::Context& ctx, const std::string& pathA, const std::string& pathB)
+{
+ auto [parentA, nodeA] = ctx.newPath2(pathA, std::nullopt);
+ auto [parentB, nodeB] = ctx.newPath2(pathB, std::nullopt);
+
+ return nodeA->asTerm().valueStr() == nodeB->asTerm().valueStr();
+}
+
struct KeyMismatch {
libyang::DataNode offendingNode;
std::optional<std::string> uriKeyValue;
@@ -169,25 +178,46 @@ struct KeyMismatch {
/** @brief In case node is a (leaf-)list check if the key values are the same as the keys specified in the lastPathSegment.
* @return The node where the mismatch occurs */
-std::optional<KeyMismatch> checkKeysMismatch(const libyang::DataNode& node, const PathSegment& lastPathSegment)
+std::optional<KeyMismatch> checkKeysMismatch(libyang::Context& ctx, const libyang::DataNode& node, const std::string& lyParentPath, const PathSegment& lastPathSegment)
{
+ const auto pathPrefix = (lyParentPath.empty() ? "" : lyParentPath) + "/" + lastPathSegment.apiIdent.name();
+
if (node.schema().nodeType() == libyang::NodeType::List) {
const auto& listKeys = node.schema().asList().keys();
for (size_t i = 0; i < listKeys.size(); ++i) {
- const auto& keyValueURI = lastPathSegment.keys[i];
auto keyNodeData = node.findPath(listKeys[i].module().name() + ':' + listKeys[i].name());
if (!keyNodeData) {
return KeyMismatch{node, std::nullopt};
}
- const auto& keyValueData = keyNodeData->asTerm().valueStr();
-
- if (keyValueURI != keyValueData) {
- return KeyMismatch{*keyNodeData, keyValueURI};
+ /*
+ * If the key's value has a canonical form then libyang makes the value canonical
+ * but there is no guarantee that the client provided the value in the canonical form.
+ *
+ * Let libyang do the work. Create two data nodes, one with the key value from the data and the other
+ * with the key value from the URI. Then compare the values from the two nodes. If they are different,
+ * they certainly mismatch.
+ *
+ * This can happen in cases like
+ * * The key's type is identityref and the client provided the key value as a string without the module name. Libyang will canonicalize the value by adding the module name.
+ * * The key's type is decimal64 with fractional-digits 2; then the client can provide the value as 1.0 or 1.00 and they should be the same. Libyang will canonicalize the value.
+ */
+
+ auto keysWithValueFromData = lastPathSegment.keys;
+ keysWithValueFromData[i] = keyNodeData->asTerm().valueStr();
+
+ const auto suffix = "/" + listKeys[i].name();
+ const auto pathFromData = pathPrefix + listKeyPredicate(listKeys, keysWithValueFromData) + suffix;
+ const auto pathFromURI = pathPrefix + listKeyPredicate(listKeys, lastPathSegment.keys) + suffix;
+
+ if (!compareKeyValue(ctx, pathFromData, pathFromURI)) {
+ return KeyMismatch{*keyNodeData, lastPathSegment.keys[i]};
}
}
} else if (node.schema().nodeType() == libyang::NodeType::Leaflist) {
- if (lastPathSegment.keys[0] != node.asTerm().valueStr()) {
+ const auto pathFromData = pathPrefix + leaflistKeyPredicate(node.asTerm().valueStr());
+ const auto pathFromURI = pathPrefix + leaflistKeyPredicate(lastPathSegment.keys[0]);
+ if (!compareKeyValue(ctx, pathFromData, pathFromURI)) {
return KeyMismatch{node, lastPathSegment.keys[0]};
}
}
@@ -363,7 +393,7 @@ libyang::CreatedNodes createEditForPutAndPatch(libyang::Context& ctx, const std:
if (isSameNode(child, lastPathSegment)) {
// 1) a single child that is created by parseSubtree(), its name is the same as `lastPathSegment`.
// It could be a list; then we need to check if the keys in provided data match the keys in URI.
- if (auto keyMismatch = checkKeysMismatch(child, lastPathSegment)) {
+ if (auto keyMismatch = checkKeysMismatch(ctx, child, lyParentPath, lastPathSegment)) {
throw ErrorResponse(400, "protocol", "invalid-value", keyMismatch->message(), keyMismatch->offendingNode.path());
}
replacementNode = child;
@@ -386,7 +416,8 @@ libyang::CreatedNodes createEditForPutAndPatch(libyang::Context& ctx, const std:
if (!isSameNode(*replacementNode, lastPathSegment)) {
throw ErrorResponse(400, "protocol", "invalid-value", "Data contains invalid node.", replacementNode->path());
}
- if (auto keyMismatch = checkKeysMismatch(*parent, lastPathSegment)) {
+
+ if (auto keyMismatch = checkKeysMismatch(ctx, *parent, lyParentPath, lastPathSegment)) {
throw ErrorResponse(400, "protocol", "invalid-value", keyMismatch->message(), keyMismatch->offendingNode.path());
}
}
diff --git a/src/restconf/utils/yang.cpp b/src/restconf/utils/yang.cpp
index 15cceb6..4c4d619 100644
--- a/src/restconf/utils/yang.cpp
+++ b/src/restconf/utils/yang.cpp
@@ -50,6 +50,11 @@ std::string listKeyPredicate(const std::vector<libyang::Leaf>& listKeyLeafs, con
return res;
}
+std::string leaflistKeyPredicate(const std::string& keyValue)
+{
+ return "[.=" + escapeListKey(keyValue) + ']';
+}
+
bool isUserOrderedList(const libyang::DataNode& node)
{
if (node.schema().nodeType() == libyang::NodeType::List) {
diff --git a/src/restconf/utils/yang.h b/src/restconf/utils/yang.h
index 677d049..e91ba8a 100644
--- a/src/restconf/utils/yang.h
+++ b/src/restconf/utils/yang.h
@@ -16,6 +16,7 @@ namespace rousette::restconf {
std::string escapeListKey(const std::string& str);
std::string listKeyPredicate(const std::vector<libyang::Leaf>& listKeyLeafs, const std::vector<std::string>& keyValues);
+std::string leaflistKeyPredicate(const std::string& keyValue);
bool isUserOrderedList(const libyang::DataNode& node);
bool isKeyNode(const libyang::DataNode& maybeList, const libyang::DataNode& node);
}
diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp
index 2898839..96c38ab 100644
--- a/tests/restconf-reading.cpp
+++ b/tests/restconf-reading.cpp
@@ -287,6 +287,65 @@ TEST_CASE("reading data")
]
}
}
+)"});
+
+ srSess.setItem("/example:list-with-identity-key[type='example:derived-identity'][name='name']", std::nullopt);
+ srSess.setItem("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']", std::nullopt);
+ srSess.setItem("/example:tlc/decimal-list[.='1.00']", std::nullopt);
+ srSess.applyChanges();
+
+ // dealing with keys which can have prefixes (YANG identities)
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=derived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
+ "example:list-with-identity-key": [
+ {
+ "type": "derived-identity",
+ "name": "name"
+ }
+ ]
+}
+)"});
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example%3Aderived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
+ "example:list-with-identity-key": [
+ {
+ "type": "derived-identity",
+ "name": "name"
+ }
+ ]
+}
+)"});
+
+ // an identity from another module must be namespace-qualified
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=another-derived-identity,name", {AUTH_ROOT}) == Response{404, jsonHeaders, R"({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "application",
+ "error-tag": "invalid-value",
+ "error-message": "No data from sysrepo."
+ }
+ ]
+ }
+}
+)"});
+
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example-types%3Aanother-derived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
+ "example:list-with-identity-key": [
+ {
+ "type": "example-types:another-derived-identity",
+ "name": "name"
+ }
+ ]
+}
+)"});
+
+ // test canonicalization of list key values; the key value was inserted as "1.00"
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:tlc/decimal-list=1", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
+ "example:tlc": {
+ "decimal-list": [
+ "1.0"
+ ]
+ }
+}
)"});
}
diff --git a/tests/restconf-writing.cpp b/tests/restconf-writing.cpp
index 96dbb25..8418554 100644
--- a/tests/restconf-writing.cpp
+++ b/tests/restconf-writing.cpp
@@ -389,6 +389,88 @@ TEST_CASE("writing data")
REQUIRE(put(RESTCONF_DATA_ROOT "/example:tlc/list=libyang/nested=11,12,13", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:nested": [{"first": "11", "second": 12, "third": "13"}]}]})") == Response{201, noContentTypeHeaders, ""});
}
+ SECTION("Test canonicalization of keys")
+ {
+ EXPECT_CHANGE(
+ CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']", std::nullopt),
+ CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/type", "example:derived-identity"),
+ CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/name", "name"),
+ CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/text", "blabla"));
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""});
+
+ // prefixed in the URI, not prefixed in the data
+ EXPECT_CHANGE(
+ MODIFIED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/text", "hehe"));
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example%3Aderived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "derived-identity", "text": "hehe"}]}]})") == Response{204, noContentTypeHeaders, ""});
+
+
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=another-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "another-derived-identity", "text": "blabla"}]}]})") == Response{400, jsonHeaders, R"({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "protocol",
+ "error-tag": "invalid-value",
+ "error-message": "Validation failure: Can't parse data: LY_EVALID"
+ }
+ ]
+ }
+}
+)"});
+
+ EXPECT_CHANGE(
+ CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']", std::nullopt),
+ CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']/type", "example-types:another-derived-identity"),
+ CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']/name", "name"),
+ CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']/text", "blabla"));
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example-types%3Aanother-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "example-types:another-derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""});
+
+ // missing namespace in the data
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example-types%3Aanother-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "another-derived-identity", "text": "blabla"}]}]})") == Response{400, jsonHeaders, R"({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "protocol",
+ "error-tag": "invalid-value",
+ "error-message": "Validation failure: Can't parse data: LY_EVALID"
+ }
+ ]
+ }
+}
+)"});
+
+ EXPECT_CHANGE(CREATED("/example:leaf-list-with-identity-key[.='example-types:another-derived-identity']", "example-types:another-derived-identity"));
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:leaf-list-with-identity-key=example-types%3Aanother-derived-identity", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:leaf-list-with-identity-key": ["example-types:another-derived-identity"]})") == Response{201, noContentTypeHeaders, ""});
+
+ // missing namespace in the URI
+ EXPECT_CHANGE(CREATED("/example:leaf-list-with-identity-key[.='example:derived-identity']", "example:derived-identity"));
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:leaf-list-with-identity-key=derived-identity", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:leaf-list-with-identity-key": ["example:derived-identity"]})") == Response{201, noContentTypeHeaders, ""});
+
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:leaf-list-with-identity-key=example-types%3Aanother-derived-identity", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:leaf-list-with-identity-key": ["example:derived-identity"]})") == Response{400, jsonHeaders, R"({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "protocol",
+ "error-tag": "invalid-value",
+ "error-path": "/example:leaf-list-with-identity-key[.='example:derived-identity']",
+ "error-message": "List key mismatch between URI path ('example-types:another-derived-identity') and data ('example:derived-identity')."
+ }
+ ]
+ }
+}
+)"});
+
+ // value in the URI and in the data have the same canonical form
+ EXPECT_CHANGE(CREATED("/example:tlc/decimal-list[.='1.0']", "1.0"));
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:tlc/decimal-list=1.00", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:decimal-list": ["1.0"]})") == Response{201, noContentTypeHeaders, ""});
+
+ // nothing is changed, still the same value
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:tlc/decimal-list=1.000", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:decimal-list": ["1"]})") == Response{204, noContentTypeHeaders, ""});
+
+ // different value
+ EXPECT_CHANGE(CREATED("/example:tlc/decimal-list[.='1.01']", "1.01"));
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:tlc/decimal-list=1.010", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:decimal-list": ["1.01"]})") == Response{201, noContentTypeHeaders, ""});
+ }
+
SECTION("Modify a leaf in a list entry")
{
EXPECT_CHANGE(MODIFIED("/example:tlc/list[name='libyang']/choice1", "restconf"));
diff --git a/tests/uri-parser.cpp b/tests/uri-parser.cpp
index 5977afc..7320c3c 100644
--- a/tests/uri-parser.cpp
+++ b/tests/uri-parser.cpp
@@ -293,6 +293,7 @@ TEST_CASE("URI path parser")
{"/restconf/data/example:tlc/list=eth0/choice1", "/example:tlc/list[name='eth0']/choice1", std::nullopt},
{"/restconf/data/example:tlc/list=eth0/choice2", "/example:tlc/list[name='eth0']/choice2", std::nullopt},
{"/restconf/data/example:tlc/list=eth0/collection=val", "/example:tlc/list[name='eth0']/collection[.='val']", std::nullopt},
+ {"/restconf/data/example:list-with-identity-key=example-types%3Aanother-derived-identity,aaa", "/example:list-with-identity-key[type='example-types:another-derived-identity'][name='aaa']", std::nullopt},
{"/restconf/data/example:tlc/status", "/example:tlc/status", std::nullopt},
// container example:a has a container b inserted locally and also via an augment. Check that we return the correct one
{"/restconf/data/example:a/b", "/example:a/b", std::nullopt},
@@ -327,6 +328,7 @@ TEST_CASE("URI path parser")
{"/restconf/data/example:tlc/status", "/example:tlc", {{"example", "status"}}},
{"/restconf/data/example:a/example-augment:b/c", "/example:a/example-augment:b", {{"example-augment", "c"}}},
{"/restconf/ds/ietf-datastores:startup/example:a/example-augment:b/c", "/example:a/example-augment:b", {{"example-augment", "c"}}},
+ {"/restconf/data/example:list-with-identity-key=example-types%3Aanother-derived-identity,aaa", "", {{"example", "list-with-identity-key"}, {"example-types:another-derived-identity", "aaa"}}},
}) {
CAPTURE(httpMethod);
CAPTURE(expectedRequestType);
diff --git a/tests/yang/example-types.yang b/tests/yang/example-types.yang
new file mode 100644
index 0000000..5bc2fb0
--- /dev/null
+++ b/tests/yang/example-types.yang
@@ -0,0 +1,13 @@
+module example-types {
+ yang-version 1.1;
+ namespace "http://example.tld/example-types";
+ prefix ex-types;
+
+ import example {
+ prefix ex;
+ }
+
+ identity another-derived-identity {
+ base ex:base-identity;
+ }
+}
diff --git a/tests/yang/example.yang b/tests/yang/example.yang
index df1301f..c46273c 100644
--- a/tests/yang/example.yang
+++ b/tests/yang/example.yang
@@ -6,6 +6,13 @@ module example {
feature f1 { }
feature f2 { }
+ identity base-identity {
+ }
+
+ identity derived-identity {
+ base base-identity;
+ }
+
leaf top-level-leaf { type string; }
leaf top-level-leaf2 { type string; default "x"; }
@@ -50,6 +57,11 @@ module example {
config false;
leaf name { type string; }
}
+ leaf-list decimal-list {
+ type decimal64 {
+ fraction-digits 2;
+ }
+ }
leaf status {
type enumeration {
enum on { }
@@ -109,6 +121,19 @@ module example {
}
}
+ list list-with-identity-key {
+ key "type name";
+ leaf type {
+ type identityref { base base-identity; }
+ }
+ leaf name { type string; }
+ leaf text { type string; }
+ }
+
+ leaf-list leaf-list-with-identity-key {
+ type identityref { base base-identity; }
+ }
+
rpc test-rpc {
input {
leaf i {
--
2.43.0
@@ -1,51 +0,0 @@
From 5c8a139d04d9b3dfb28b8c0e1a4aa7b945a1e8fc 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
Organization: Wires
In 94b24795 ("server: simplify listening/stopping") we needed to call
server->stop() through event loop and it was proposed to use the
approach introduced in [1], i.e., construct a timer, then cancel it
right away which would invoke the timer handler.
I find that quite complicated, why not just simply call post()? [2]
(Also, by looking at the http2::asio_http2::server::http2::stop(), it
does not seem like it should be called from *every* io_service.)
[1] https://stackoverflow.com/questions/17005258/why-does-boost-asio-not-support-an-event-based-interface/17029022#17029022
[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>
---
src/restconf/Server.cpp | 13 +++++--------
1 file changed, 5 insertions(+), 8 deletions(-)
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 2f495f9..bd2ce0d 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -837,14 +837,11 @@ Server::~Server()
void Server::stop()
{
// notification to stop has to go through the asio io_context
- for (const auto& service : server->io_services()) {
- boost::asio::deadline_timer t{*service, boost::posix_time::pos_infin};
- t.async_wait([server = this->server.get()](const boost::system::error_code&) {
- spdlog::trace("Stoping HTTP/2 server");
- server->stop();
- });
- t.cancel();
- }
+ boost::asio::post(*server->io_services().front(), [server = this->server.get()]() {
+ spdlog::trace("Stoping HTTP/2 server");
+ server->stop();
+ });
+
shutdownRequested();
}
--
2.43.0
@@ -1,425 +0,0 @@
From 086cfe1847e406df90852dc3cb52ece3d0cbe22b 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
Organization: Wires
The nghttp2-asio server disconnects client after 60 seconds of no
activity in the stream [1]. In our use-case I can imagine it can be
quite common that no data will flow for a long period of time. If no
data are updated (i.e., no notifications, or no YANG data changes in
sysrepo when we have YANG push), then nothing flows forth and back.
The 60 second value is configurable [2], but I am not sure about the
value that should be used. 5 minutes? hours? days?
Let's approach this differently. We can instead send a comment into the
stream every few seconds which will make sure that the connection will
not be dropped by nghttp2-asio. This approach is described in the
HTML Living Standard [3].
For now, let's resort to sending a ping-alive every 55 seconds and
hardcode 60 seconds no activity timeout.
I have verified that using this method a curl client keeps connected
to the server when the server sends these "keep-alive comments".
[1] https://github.com/nghttp2/nghttp2/issues/555
[2] https://github.com/nghttp2/nghttp2-asio/blob/e877868abe06a83ed0a6ac6e245c07f6f20866b5/lib/asio_server_http2_impl.h#L51
[3] https://html.spec.whatwg.org/multipage/server-sent-events.html#authoring-notes
Change-Id: I57e510d0b61ac7ed032c582779780c64768b7d53
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
src/clock.cpp | 4 +-
src/http/EventStream.cpp | 44 +++++++++++++--
src/http/EventStream.h | 13 ++++-
src/restconf/NotificationStream.cpp | 3 +-
src/restconf/NotificationStream.h | 1 +
src/restconf/Server.cpp | 21 +++++--
src/restconf/Server.h | 6 +-
tests/restconf-eventstream.cpp | 85 +++++++++++++++++++++--------
8 files changed, 139 insertions(+), 38 deletions(-)
diff --git a/src/clock.cpp b/src/clock.cpp
index c85be19..24e9efb 100644
--- a/src/clock.cpp
+++ b/src/clock.cpp
@@ -14,6 +14,8 @@
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);
@@ -32,7 +34,7 @@ int main(int argc [[maybe_unused]], char** argv [[maybe_unused]])
server.num_threads(4);
server.handle("/events", [&shutdown, &sig](const auto& req, const auto& res) {
- auto client = std::make_shared<rousette::http::EventStream>(req, res, shutdown, sig);
+ auto client = std::make_shared<rousette::http::EventStream>(req, res, shutdown, sig, keepAlivePingInterval);
client->activate();
});
diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp
index 77c282d..82f8b38 100644
--- a/src/http/EventStream.cpp
+++ b/src/http/EventStream.cpp
@@ -17,17 +17,26 @@ using namespace nghttp2::asio_http2;
namespace rousette::http {
+constexpr auto FIELD_DATA = "data";
+
/** @short After constructing, make sure to call activate() immediately. */
-EventStream::EventStream(const server::request& req, const server::response& res, Termination& termination, EventSignal& signal, const std::optional<std::string>& initialEvent)
+EventStream::EventStream(const server::request& req,
+ const server::response& res,
+ Termination& termination,
+ EventSignal& signal,
+ const std::chrono::seconds keepAlivePingInterval,
+ const std::optional<std::string>& initialEvent)
: res{res}
+ , ping{res.io_service()}
, peer{peer_from_request(req)}
+ , m_keepAlivePingInterval(keepAlivePingInterval)
{
if (initialEvent) {
- enqueue(*initialEvent);
+ enqueue(FIELD_DATA, *initialEvent);
}
eventSub = signal.connect([this](const auto& msg) {
- enqueue(msg);
+ enqueue(FIELD_DATA, msg);
});
terminateSub = termination.connect([this]() {
@@ -56,6 +65,8 @@ shared_from_this() throws bad_weak_ptr, so we need a two-phase construction.
*/
void EventStream::activate()
{
+ start_ping();
+
auto client = shared_from_this();
res.write_head(200, {
{"content-type", {"text/event-stream", false}},
@@ -65,6 +76,7 @@ void EventStream::activate()
res.on_close([client](const auto ec) {
spdlog::debug("{}: closed ({})", client->peer, nghttp2_http2_strerror(ec));
std::lock_guard lock{client->mtx};
+ client->ping.cancel();
client->eventSub.disconnect();
client->terminateSub.disconnect();
client->state = Closed;
@@ -113,14 +125,15 @@ ssize_t EventStream::process(uint8_t* destination, std::size_t len, uint32_t* da
__builtin_unreachable();
}
-void EventStream::enqueue(const std::string& what)
+void EventStream::enqueue(const std::string& fieldName, const std::string& what)
{
std::string buf;
buf.reserve(what.size());
const std::regex newline{"\n"};
for (auto it = std::sregex_token_iterator{what.begin(), what.end(), newline, -1};
it != std::sregex_token_iterator{}; ++it) {
- buf += "data: ";
+ buf += fieldName;
+ buf += ": ";
buf += *it;
buf += '\n';
}
@@ -139,4 +152,25 @@ void EventStream::enqueue(const std::string& what)
state = HasEvents;
boost::asio::post(res.io_service(), [&res = this->res]() { res.resume(); });
}
+
+void EventStream::start_ping()
+{
+ ping.expires_from_now(boost::posix_time::seconds(m_keepAlivePingInterval.count()));
+ ping.async_wait([maybeClient = weak_from_this()](const boost::system::error_code& ec) {
+ auto client = maybeClient.lock();
+ if (!client) {
+ spdlog::trace("ping: client already gone");
+ return;
+ }
+
+ if (ec == boost::asio::error::operation_aborted) {
+ spdlog::trace("{}: ping scheduler cancelled", client->peer);
+ return;
+ }
+
+ client->enqueue("", "\n");
+ spdlog::trace("{}: keep-alive ping enqueued", client->peer);
+ client->start_ping();
+ });
+}
}
diff --git a/src/http/EventStream.h b/src/http/EventStream.h
index b427fb3..4b3578a 100644
--- a/src/http/EventStream.h
+++ b/src/http/EventStream.h
@@ -7,6 +7,7 @@
#pragma once
+#include <boost/asio/deadline_timer.hpp>
#include <boost/signals2.hpp>
#include <list>
#include <memory>
@@ -30,7 +31,12 @@ public:
using EventSignal = boost::signals2::signal<void(const std::string& message)>;
using Termination = boost::signals2::signal<void()>;
- EventStream(const nghttp2::asio_http2::server::request& req, const nghttp2::asio_http2::server::response& res, Termination& terminate, EventSignal& signal, const std::optional<std::string>& initialEvent = std::nullopt);
+ EventStream(const nghttp2::asio_http2::server::request& req,
+ const nghttp2::asio_http2::server::response& res,
+ Termination& terminate,
+ EventSignal& signal,
+ const std::chrono::seconds keepAlivePingInterval,
+ const std::optional<std::string>& initialEvent = std::nullopt);
void activate();
private:
@@ -43,13 +49,16 @@ private:
};
State state = WaitingForEvents;
+ boost::asio::deadline_timer ping;
std::list<std::string> queue;
mutable std::mutex mtx; // for `state` and `queue`
boost::signals2::scoped_connection eventSub, terminateSub;
const std::string peer;
+ const std::chrono::seconds m_keepAlivePingInterval;
size_t send_chunk(uint8_t* destination, std::size_t len, uint32_t* data_flags);
ssize_t process(uint8_t* destination, std::size_t len, uint32_t* data_flags);
- void enqueue(const std::string& what);
+ void enqueue(const std::string& fieldName, const std::string& what);
+ void start_ping();
};
}
diff --git a/src/restconf/NotificationStream.cpp b/src/restconf/NotificationStream.cpp
index d0ba938..5017f3e 100644
--- a/src/restconf/NotificationStream.cpp
+++ b/src/restconf/NotificationStream.cpp
@@ -89,12 +89,13 @@ NotificationStream::NotificationStream(
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,
sysrepo::Session session,
libyang::DataFormat dataFormat,
const std::optional<std::string>& filter,
const std::optional<sysrepo::NotificationTimeStamp>& startTime,
const std::optional<sysrepo::NotificationTimeStamp>& stopTime)
- : EventStream(req, res, termination, *signal)
+ : EventStream(req, res, termination, *signal, keepAlivePingInterval)
, m_notificationSignal(signal)
, m_session(std::move(session))
, m_dataFormat(dataFormat)
diff --git a/src/restconf/NotificationStream.h b/src/restconf/NotificationStream.h
index 46f8416..daa79d6 100644
--- a/src/restconf/NotificationStream.h
+++ b/src/restconf/NotificationStream.h
@@ -44,6 +44,7 @@ public:
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,
sysrepo::Session sess,
libyang::DataFormat dataFormat,
const std::optional<std::string>& filter,
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index bd2ce0d..31b3d0f 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -864,13 +864,14 @@ 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)
+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>()}
, 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
+ server->read_timeout(boost::posix_time::seconds{60}); // terminate connection after 60 seconds of inactivity (this is explicitly setting the default value)
for (const auto& [module, version, features] : {
std::tuple<std::string, std::string, std::vector<std::string>>{"ietf-restconf", "2017-01-26", {}},
@@ -930,14 +931,14 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std::
res.end("<XRD xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0'><Link rel='restconf' href='"s + restconfRoot + "'></XRD>"s);
});
- server->handle("/telemetry/optics", [this](const auto& req, const auto& res) {
+ server->handle("/telemetry/optics", [this, keepAlivePingInterval](const auto& req, const auto& res) {
logRequest(req);
- auto client = std::make_shared<http::EventStream>(req, res, shutdownRequested, opticsChange, as_restconf_push_update(dwdmEvents->currentData(), std::chrono::system_clock::now()));
+ auto client = std::make_shared<http::EventStream>(req, res, shutdownRequested, opticsChange, keepAlivePingInterval, as_restconf_push_update(dwdmEvents->currentData(), std::chrono::system_clock::now()));
client->activate();
});
- server->handle(netconfStreamRoot, [this, conn](const auto& req, const auto& res) mutable {
+ server->handle(netconfStreamRoot, [this, conn, keepAlivePingInterval](const auto& req, const auto& res) mutable {
logRequest(req);
std::optional<std::string> xpathFilter;
@@ -970,7 +971,17 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std::
// The signal is constructed outside NotificationStream class because it is required to be passed to
// NotificationStream's parent (EventStream) constructor where it already must be constructed
// Yes, this is a hack.
- auto client = std::make_shared<NotificationStream>(req, res, shutdownRequested, std::make_shared<rousette::http::EventStream::EventSignal>(), sess, streamRequest.type.encoding, xpathFilter, startTime, stopTime);
+ auto client = std::make_shared<NotificationStream>(
+ req,
+ res,
+ shutdownRequested,
+ std::make_shared<rousette::http::EventStream::EventSignal>(),
+ keepAlivePingInterval,
+ sess,
+ streamRequest.type.encoding,
+ xpathFilter,
+ startTime,
+ stopTime);
client->activate();
} catch (const auth::Error& e) {
processAuthError(req, res, e, [&res]() {
diff --git a/src/restconf/Server.h b/src/restconf/Server.h
index 112c943..0720c8d 100644
--- a/src/restconf/Server.h
+++ b/src/restconf/Server.h
@@ -28,7 +28,11 @@ std::optional<std::string> as_subtree_path(const std::string& path);
/** @short A RESTCONF-ish server */
class Server {
public:
- explicit Server(sysrepo::Connection conn, const std::string& address, const std::string& port, const std::chrono::milliseconds timeout = std::chrono::milliseconds{0});
+ explicit Server(sysrepo::Connection conn,
+ 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});
~Server();
void join();
void stop();
diff --git a/tests/restconf-eventstream.cpp b/tests/restconf-eventstream.cpp
index 3d7fc15..ad1040d 100644
--- a/tests/restconf-eventstream.cpp
+++ b/tests/restconf-eventstream.cpp
@@ -21,9 +21,9 @@ static const auto SERVER_PORT = "10091";
using namespace std::chrono_literals;
-TEST_CASE("Termination on server shutdown")
+TEST_CASE("Event stream tests")
{
- trompeloeil::sequence seqMod1;
+ trompeloeil::sequence seq1, seq2;
sysrepo::setLogLevelStderr(sysrepo::LogLevel::Information);
spdlog::set_level(spdlog::level::trace);
@@ -34,37 +34,76 @@ TEST_CASE("Termination on server shutdown")
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);
+ const std::string notification(R"({"example:eventA":{"message":"blabla","progress":11}})");
RestconfNotificationWatcher netconfWatcher(srConn.sessionStart().getContext());
- const std::string notification(R"({"example:eventA":{"message":"blabla","progress":11}})");
- EXPECT_NOTIFICATION(notification, seqMod1);
- EXPECT_NOTIFICATION(notification, seqMod1);
- EXPECT_NOTIFICATION(notification, seqMod1);
+ SECTION("Termination on server shutdown")
+ {
+ auto server = std::make_unique<rousette::restconf::Server>(srConn, SERVER_ADDRESS, SERVER_PORT);
+
+ EXPECT_NOTIFICATION(notification, seq1);
+ EXPECT_NOTIFICATION(notification, seq1);
+ EXPECT_NOTIFICATION(notification, seq1);
+
+ auto notifSession = sysrepo::Connection{}.sessionStart();
+ auto ctx = notifSession.getContext();
+
+ PREPARE_LOOP_WITH_EXCEPTIONS;
+ auto 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(notification);
+ SEND_NOTIFICATION(notification);
+ SEND_NOTIFICATION(notification);
+ waitForCompletionAndBitMore(seq1);
+
+ auto beforeShutdown = std::chrono::system_clock::now();
+ server.reset();
+ auto shutdownDuration = std::chrono::system_clock::now() - beforeShutdown;
+ REQUIRE(shutdownDuration < 5s);
+ }));
+
+ SSEClient client(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, "/streams/NETCONF/JSON", std::map<std::string, std::string>{AUTH_ROOT});
+
+ RUN_LOOP_WITH_EXCEPTIONS;
+ }
+
+ SECTION("Keep-alive pings")
+ {
+ constexpr auto pingInterval = 1s;
- auto notifSession = sysrepo::Connection{}.sessionStart();
- auto ctx = notifSession.getContext();
+ RestconfNotificationWatcher netconfWatcher(srConn.sessionStart().getContext());
+ auto server = std::make_unique<rousette::restconf::Server>(srConn, SERVER_ADDRESS, SERVER_PORT, std::chrono::milliseconds{0}, pingInterval);
- PREPARE_LOOP_WITH_EXCEPTIONS;
- auto 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(notification);
- SEND_NOTIFICATION(notification);
- SEND_NOTIFICATION(notification);
- waitForCompletionAndBitMore(seqMod1);
+ EXPECT_NOTIFICATION(notification, seq1);
+ expectations.emplace_back(NAMED_REQUIRE_CALL(netconfWatcher, comment(": ")).IN_SEQUENCE(seq2).TIMES(AT_LEAST(1)));
- auto beforeShutdown = std::chrono::system_clock::now();
- server.reset();
- auto shutdownDuration = std::chrono::system_clock::now() - beforeShutdown;
- REQUIRE(shutdownDuration < 5s);
- }));
+ PREPARE_LOOP_WITH_EXCEPTIONS;
+ auto notificationThread = std::jthread(wrap_exceptions_and_asio(bg, io, [&]() {
+ WAIT_UNTIL_SSE_CLIENT_REQUESTS;
+ SEND_NOTIFICATION(notification);
+ std::this_thread::sleep_for(3s); // Wait for the server to send at least one keep-alive ping
+ server.reset();
+ }));
- SSEClient client(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, "/streams/NETCONF/JSON", std::map<std::string, std::string>{AUTH_ROOT});
+ SSEClient client(
+ io,
+ SERVER_ADDRESS,
+ SERVER_PORT,
+ requestSent,
+ netconfWatcher,
+ "/streams/NETCONF/JSON",
+ std::map<std::string, std::string>{AUTH_ROOT},
+ boost::posix_time::seconds{5},
+ SSEClient::ReportIgnoredLines::Yes);
- RUN_LOOP_WITH_EXCEPTIONS;
+ RUN_LOOP_WITH_EXCEPTIONS;
+ }
}
--
2.43.0
@@ -0,0 +1,300 @@
From fda47b6a6cfdaecc24e96c4d6138c6de3ef116e0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Mon, 7 Oct 2024 21:21:22 +0200
Subject: [PATCH 06/44] tests: test querying lists with union keys
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Test that we correctly work with keys that are unions of something
that can have a module namespace and that must not have it.
By the way, enum values are not supposed to have a namespace prefix,
good to know [1].
[1] https://www.rfc-editor.org/rfc/rfc7951#section-6.4
Change-Id: I5a70f18117bb453330b4bb2ce0d2fb47d35b6ea6
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
tests/restconf-reading.cpp | 53 ++++++++++++++++++++++++-----
tests/restconf-writing.cpp | 68 ++++++++++++++++++++++++++++----------
tests/uri-parser.cpp | 2 +-
tests/yang/example.yang | 26 +++++++++++++--
4 files changed, 120 insertions(+), 29 deletions(-)
diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp
index 96c38ab..2ded3f0 100644
--- a/tests/restconf-reading.cpp
+++ b/tests/restconf-reading.cpp
@@ -289,14 +289,16 @@ TEST_CASE("reading data")
}
)"});
- srSess.setItem("/example:list-with-identity-key[type='example:derived-identity'][name='name']", std::nullopt);
- srSess.setItem("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']", std::nullopt);
+ srSess.setItem("/example:list-with-union-keys[type='example:derived-identity'][name='name']", std::nullopt);
+ srSess.setItem("/example:list-with-union-keys[type='example-types:another-derived-identity'][name='name']", std::nullopt);
+ srSess.setItem("/example:list-with-union-keys[type='fiii'][name='name']", std::nullopt);
+ srSess.setItem("/example:list-with-union-keys[type='zero'][name='name']", std::nullopt); // enum value
srSess.setItem("/example:tlc/decimal-list[.='1.00']", std::nullopt);
srSess.applyChanges();
// dealing with keys which can have prefixes (YANG identities)
- REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=derived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
- "example:list-with-identity-key": [
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=derived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
+ "example:list-with-union-keys": [
{
"type": "derived-identity",
"name": "name"
@@ -304,8 +306,8 @@ TEST_CASE("reading data")
]
}
)"});
- REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example%3Aderived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
- "example:list-with-identity-key": [
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example%3Aderived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
+ "example:list-with-union-keys": [
{
"type": "derived-identity",
"name": "name"
@@ -315,7 +317,7 @@ TEST_CASE("reading data")
)"});
// an identity from another module must be namespace-qualified
- REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=another-derived-identity,name", {AUTH_ROOT}) == Response{404, jsonHeaders, R"({
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=another-derived-identity,name", {AUTH_ROOT}) == Response{404, jsonHeaders, R"({
"ietf-restconf:errors": {
"error": [
{
@@ -328,8 +330,8 @@ TEST_CASE("reading data")
}
)"});
- REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example-types%3Aanother-derived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
- "example:list-with-identity-key": [
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example-types%3Aanother-derived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
+ "example:list-with-union-keys": [
{
"type": "example-types:another-derived-identity",
"name": "name"
@@ -346,6 +348,39 @@ TEST_CASE("reading data")
]
}
}
+)"});
+
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=zero,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
+ "example:list-with-union-keys": [
+ {
+ "type": "zero",
+ "name": "name"
+ }
+ ]
+}
+)"});
+
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example%3Azero,name", {AUTH_ROOT}) == Response{404, jsonHeaders, R"({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "application",
+ "error-tag": "invalid-value",
+ "error-message": "No data from sysrepo."
+ }
+ ]
+ }
+}
+)"});
+
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=fiii,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
+ "example:list-with-union-keys": [
+ {
+ "type": "fiii",
+ "name": "name"
+ }
+ ]
+}
)"});
}
diff --git a/tests/restconf-writing.cpp b/tests/restconf-writing.cpp
index 8418554..c1a9515 100644
--- a/tests/restconf-writing.cpp
+++ b/tests/restconf-writing.cpp
@@ -392,46 +392,69 @@ TEST_CASE("writing data")
SECTION("Test canonicalization of keys")
{
EXPECT_CHANGE(
- CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']", std::nullopt),
- CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/type", "example:derived-identity"),
- CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/name", "name"),
- CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/text", "blabla"));
- REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""});
+ CREATED("/example:list-with-union-keys[type='example:derived-identity'][name='name']", std::nullopt),
+ CREATED("/example:list-with-union-keys[type='example:derived-identity'][name='name']/type", "example:derived-identity"),
+ CREATED("/example:list-with-union-keys[type='example:derived-identity'][name='name']/name", "name"),
+ CREATED("/example:list-with-union-keys[type='example:derived-identity'][name='name']/text", "blabla"));
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""});
// prefixed in the URI, not prefixed in the data
EXPECT_CHANGE(
- MODIFIED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/text", "hehe"));
- REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example%3Aderived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "derived-identity", "text": "hehe"}]}]})") == Response{204, noContentTypeHeaders, ""});
+ MODIFIED("/example:list-with-union-keys[type='example:derived-identity'][name='name']/text", "hehe"));
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example%3Aderived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "derived-identity", "text": "hehe"}]}]})") == Response{204, noContentTypeHeaders, ""});
+ // 'another-derived-identity' comes from a different module than the list itself, so this parses as string
+ EXPECT_CHANGE(
+ CREATED("/example:list-with-union-keys[type='another-derived-identity'][name='name']", std::nullopt),
+ CREATED("/example:list-with-union-keys[type='another-derived-identity'][name='name']/type", "another-derived-identity"),
+ CREATED("/example:list-with-union-keys[type='another-derived-identity'][name='name']/name", "name"),
+ CREATED("/example:list-with-union-keys[type='another-derived-identity'][name='name']/text", "blabla"));
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=another-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "another-derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""});
+
+ EXPECT_CHANGE(
+ CREATED("/example:list-with-union-keys[type='example-types:another-derived-identity'][name='name']", std::nullopt),
+ CREATED("/example:list-with-union-keys[type='example-types:another-derived-identity'][name='name']/type", "example-types:another-derived-identity"),
+ CREATED("/example:list-with-union-keys[type='example-types:another-derived-identity'][name='name']/name", "name"),
+ CREATED("/example:list-with-union-keys[type='example-types:another-derived-identity'][name='name']/text", "blabla"));
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example-types%3Aanother-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "example-types:another-derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""});
- REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=another-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "another-derived-identity", "text": "blabla"}]}]})") == Response{400, jsonHeaders, R"({
+ // missing namespace in the data
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example-types%3Aanother-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "another-derived-identity", "text": "blabla"}]}]})") == Response{400, jsonHeaders, R"({
"ietf-restconf:errors": {
"error": [
{
"error-type": "protocol",
"error-tag": "invalid-value",
- "error-message": "Validation failure: Can't parse data: LY_EVALID"
+ "error-path": "/example:list-with-union-keys[type='another-derived-identity'][name='name']/type",
+ "error-message": "List key mismatch between URI path ('example-types:another-derived-identity') and data ('another-derived-identity')."
}
]
}
}
)"});
+ // zero is enum value
EXPECT_CHANGE(
- CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']", std::nullopt),
- CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']/type", "example-types:another-derived-identity"),
- CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']/name", "name"),
- CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']/text", "blabla"));
- REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example-types%3Aanother-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "example-types:another-derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""});
+ CREATED("/example:list-with-union-keys[type='zero'][name='name']", std::nullopt),
+ CREATED("/example:list-with-union-keys[type='zero'][name='name']/type", "zero"),
+ CREATED("/example:list-with-union-keys[type='zero'][name='name']/name", "name"));
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=zero,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "zero"}]}]})") == Response{201, noContentTypeHeaders, ""});
- // missing namespace in the data
- REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example-types%3Aanother-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "another-derived-identity", "text": "blabla"}]}]})") == Response{400, jsonHeaders, R"({
+ // example:zero is string, enum values are not namespace-prefixed
+ EXPECT_CHANGE(
+ CREATED("/example:list-with-union-keys[type='example:zero'][name='name']", std::nullopt),
+ CREATED("/example:list-with-union-keys[type='example:zero'][name='name']/type", "example:zero"),
+ CREATED("/example:list-with-union-keys[type='example:zero'][name='name']/name", "name"));
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example%3Azero,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "example:zero"}]}]})") == Response{201, noContentTypeHeaders, ""});
+
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=zero,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "example:zero"}]}]})") == Response{400, jsonHeaders, R"({
"ietf-restconf:errors": {
"error": [
{
"error-type": "protocol",
"error-tag": "invalid-value",
- "error-message": "Validation failure: Can't parse data: LY_EVALID"
+ "error-path": "/example:list-with-union-keys[type='example:zero'][name='name']/type",
+ "error-message": "List key mismatch between URI path ('zero') and data ('example:zero')."
}
]
}
@@ -459,6 +482,17 @@ TEST_CASE("writing data")
}
)"});
+ EXPECT_CHANGE(CREATED("/example:fruit-list[.='example:apple']", "example:apple"));
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:fruit-list=example%3Aapple", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:fruit-list": ["apple"]})") == Response{201, noContentTypeHeaders, ""});
+
+ // leafref
+ EXPECT_CHANGE(
+ CREATED("/example:list-with-union-keys[type='example:apple'][name='name']", std::nullopt),
+ CREATED("/example:list-with-union-keys[type='example:apple'][name='name']/type", "example:apple"),
+ CREATED("/example:list-with-union-keys[type='example:apple'][name='name']/name", "name"));
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example%3Aapple,name", {AUTH_ROOT, CONTENT_TYPE_JSON},
+ R"({"example:list-with-union-keys": [{"name": "name", "type": "apple"}]}]})") == Response{201, noContentTypeHeaders, ""});
+
// value in the URI and in the data have the same canonical form
EXPECT_CHANGE(CREATED("/example:tlc/decimal-list[.='1.0']", "1.0"));
REQUIRE(put(RESTCONF_DATA_ROOT "/example:tlc/decimal-list=1.00", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:decimal-list": ["1.0"]})") == Response{201, noContentTypeHeaders, ""});
diff --git a/tests/uri-parser.cpp b/tests/uri-parser.cpp
index 7320c3c..000fe5d 100644
--- a/tests/uri-parser.cpp
+++ b/tests/uri-parser.cpp
@@ -293,7 +293,7 @@ TEST_CASE("URI path parser")
{"/restconf/data/example:tlc/list=eth0/choice1", "/example:tlc/list[name='eth0']/choice1", std::nullopt},
{"/restconf/data/example:tlc/list=eth0/choice2", "/example:tlc/list[name='eth0']/choice2", std::nullopt},
{"/restconf/data/example:tlc/list=eth0/collection=val", "/example:tlc/list[name='eth0']/collection[.='val']", std::nullopt},
- {"/restconf/data/example:list-with-identity-key=example-types%3Aanother-derived-identity,aaa", "/example:list-with-identity-key[type='example-types:another-derived-identity'][name='aaa']", std::nullopt},
+ {"/restconf/data/example:list-with-union-keys=example-types%3Aanother-derived-identity,aaa", "/example:list-with-union-keys[type='example-types:another-derived-identity'][name='aaa']", std::nullopt},
{"/restconf/data/example:tlc/status", "/example:tlc/status", std::nullopt},
// container example:a has a container b inserted locally and also via an augment. Check that we return the correct one
{"/restconf/data/example:a/b", "/example:a/b", std::nullopt},
diff --git a/tests/yang/example.yang b/tests/yang/example.yang
index c46273c..75cd7a6 100644
--- a/tests/yang/example.yang
+++ b/tests/yang/example.yang
@@ -13,6 +13,11 @@ module example {
base base-identity;
}
+ identity fruit { }
+ identity apple {
+ base fruit;
+ }
+
leaf top-level-leaf { type string; }
leaf top-level-leaf2 { type string; default "x"; }
@@ -121,10 +126,23 @@ module example {
}
}
- list list-with-identity-key {
+ list list-with-union-keys {
key "type name";
leaf type {
- type identityref { base base-identity; }
+ type union {
+ type identityref {
+ base base-identity;
+ }
+ type enumeration {
+ enum zero;
+ enum one;
+ }
+ type leafref {
+ require-instance true;
+ path "/fruit-list";
+ }
+ type string;
+ }
}
leaf name { type string; }
leaf text { type string; }
@@ -134,6 +152,10 @@ module example {
type identityref { base base-identity; }
}
+ leaf-list fruit-list {
+ type identityref { base fruit; }
+ }
+
rpc test-rpc {
input {
leaf i {
--
2.43.0
@@ -0,0 +1,32 @@
From 27ef5bc87fdeb70a77609da6ec18ee5c28656bb6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Tue, 29 Oct 2024 18:54:55 +0100
Subject: [PATCH 07/44] error handling in sysrepo has changed
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/+/7969
Change-Id: Id028806ed49114cba4c55e2874bcf3fc98308bdc
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
tests/restconf-rpc.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/restconf-rpc.cpp b/tests/restconf-rpc.cpp
index 4f66f10..9bc1dbc 100644
--- a/tests/restconf-rpc.cpp
+++ b/tests/restconf-rpc.cpp
@@ -301,7 +301,7 @@ TEST_CASE("invoking actions and rpcs")
{
"error-type": "application",
"error-tag": "operation-failed",
- "error-message": "Internal server error due to sysrepo exception: Couldn't send RPC: SR_ERR_CALLBACK_FAILED\u000A Operation failed (SR_ERR_OPERATION_FAILED)\u000A User callback failed. (SR_ERR_CALLBACK_FAILED)"
+ "error-message": "Internal server error due to sysrepo exception: Couldn't send RPC: SR_ERR_OPERATION_FAILED\u000A Operation failed (SR_ERR_OPERATION_FAILED)"
}
]
}
--
2.43.0
@@ -1,202 +0,0 @@
From 06e2751e4da052195db96aaa90549fba398208bd 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
Organization: Wires
This commit refactors the EventStream and NotificationStream to use
named constructors.
Both of these classes are required to be initialized with invoking the
constructor and then the activate method. The NotificationStream class
also required to be passed with a newly constructed Event object.
I find this pattern a bit confusing, so I refactored the code to use
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>
---
src/clock.cpp | 3 +--
src/http/EventStream.cpp | 20 ++++++++++++++++++++
src/http/EventStream.h | 22 +++++++++++++++-------
src/restconf/NotificationStream.cpp | 22 ++++++++++++++++++++++
src/restconf/NotificationStream.h | 12 ++++++++++++
src/restconf/Server.cpp | 10 ++--------
6 files changed, 72 insertions(+), 17 deletions(-)
diff --git a/src/clock.cpp b/src/clock.cpp
index 24e9efb..aa72849 100644
--- a/src/clock.cpp
+++ b/src/clock.cpp
@@ -34,8 +34,7 @@ int main(int argc [[maybe_unused]], char** argv [[maybe_unused]])
server.num_threads(4);
server.handle("/events", [&shutdown, &sig](const auto& req, const auto& res) {
- auto client = std::make_shared<rousette::http::EventStream>(req, res, shutdown, sig, keepAlivePingInterval);
- client->activate();
+ rousette::http::EventStream::create(req, res, shutdown, sig, keepAlivePingInterval);
});
server.handle("/", [](const auto& req, const auto& resp) {
diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp
index 82f8b38..0893653 100644
--- a/src/http/EventStream.cpp
+++ b/src/http/EventStream.cpp
@@ -173,4 +173,24 @@ void EventStream::start_ping()
client->start_ping();
});
}
+
+/** @brief Create a new EventStream instance and activate it immediately.
+ *
+ * The stream is created with the given parameters and activated as if the activate() method was called.
+ * ```
+ * auto a = make_shared<EventStream>(...);
+ * a->activate();
+ * ```
+ */
+std::shared_ptr<EventStream> EventStream::create(const nghttp2::asio_http2::server::request& req,
+ const nghttp2::asio_http2::server::response& res,
+ Termination& terminate,
+ EventSignal& signal,
+ const std::chrono::seconds keepAlivePingInterval,
+ const std::optional<std::string>& initialEvent)
+{
+ auto stream = std::shared_ptr<EventStream>(new EventStream(req, res, terminate, signal, keepAlivePingInterval, initialEvent));
+ stream->activate();
+ return stream;
+}
}
diff --git a/src/http/EventStream.h b/src/http/EventStream.h
index 4b3578a..ef0a002 100644
--- a/src/http/EventStream.h
+++ b/src/http/EventStream.h
@@ -31,13 +31,12 @@ public:
using EventSignal = boost::signals2::signal<void(const std::string& message)>;
using Termination = boost::signals2::signal<void()>;
- EventStream(const nghttp2::asio_http2::server::request& req,
- const nghttp2::asio_http2::server::response& res,
- Termination& terminate,
- EventSignal& signal,
- const std::chrono::seconds keepAlivePingInterval,
- const std::optional<std::string>& initialEvent = std::nullopt);
- void activate();
+ static std::shared_ptr<EventStream> create(const nghttp2::asio_http2::server::request& req,
+ const nghttp2::asio_http2::server::response& res,
+ Termination& terminate,
+ EventSignal& signal,
+ const std::chrono::seconds keepAlivePingInterval,
+ const std::optional<std::string>& initialEvent = std::nullopt);
private:
const nghttp2::asio_http2::server::response& res;
@@ -60,5 +59,14 @@ private:
ssize_t process(uint8_t* destination, std::size_t len, uint32_t* data_flags);
void enqueue(const std::string& fieldName, const std::string& what);
void start_ping();
+
+protected:
+ EventStream(const nghttp2::asio_http2::server::request& req,
+ const nghttp2::asio_http2::server::response& res,
+ Termination& terminate,
+ EventSignal& signal,
+ const std::chrono::seconds keepAlivePingInterval,
+ const std::optional<std::string>& initialEvent = std::nullopt);
+ void activate();
};
}
diff --git a/src/restconf/NotificationStream.cpp b/src/restconf/NotificationStream.cpp
index 5017f3e..fe21809 100644
--- a/src/restconf/NotificationStream.cpp
+++ b/src/restconf/NotificationStream.cpp
@@ -183,4 +183,26 @@ libyang::DataNode replaceStreamLocations(const std::optional<std::string>& schem
return node;
}
+
+/** @brief Create a new NotificationStream instance and activate it immediately.
+ *
+ * The stream is created with the given parameters and activated, which means it starts listening for
+ * NETCONF notifications and sending them to the client.
+ */
+std::shared_ptr<NotificationStream> NotificationStream::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,
+ sysrepo::Session sess,
+ libyang::DataFormat dataFormat,
+ const std::optional<std::string>& filter,
+ const std::optional<sysrepo::NotificationTimeStamp>& startTime,
+ const std::optional<sysrepo::NotificationTimeStamp>& stopTime)
+{
+ auto signal = std::make_shared<rousette::http::EventStream::EventSignal>();
+ auto stream = std::shared_ptr<NotificationStream>(new NotificationStream(req, res, termination, signal, keepAlivePingInterval, std::move(sess), dataFormat, filter, startTime, stopTime));
+ stream->activate();
+ return stream;
+}
}
diff --git a/src/restconf/NotificationStream.h b/src/restconf/NotificationStream.h
index daa79d6..91c5dde 100644
--- a/src/restconf/NotificationStream.h
+++ b/src/restconf/NotificationStream.h
@@ -39,6 +39,18 @@ class NotificationStream : public rousette::http::EventStream {
std::optional<sysrepo::Subscription> m_notifSubs;
public:
+ static std::shared_ptr<NotificationStream> 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,
+ sysrepo::Session sess,
+ libyang::DataFormat dataFormat,
+ const std::optional<std::string>& filter,
+ const std::optional<sysrepo::NotificationTimeStamp>& startTime,
+ const std::optional<sysrepo::NotificationTimeStamp>& stopTime);
+
+protected:
NotificationStream(
const nghttp2::asio_http2::server::request& req,
const nghttp2::asio_http2::server::response& res,
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 31b3d0f..8544210 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -934,8 +934,7 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std::
server->handle("/telemetry/optics", [this, keepAlivePingInterval](const auto& req, const auto& res) {
logRequest(req);
- auto client = std::make_shared<http::EventStream>(req, res, shutdownRequested, opticsChange, keepAlivePingInterval, as_restconf_push_update(dwdmEvents->currentData(), std::chrono::system_clock::now()));
- client->activate();
+ http::EventStream::create(req, res, shutdownRequested, opticsChange, keepAlivePingInterval, as_restconf_push_update(dwdmEvents->currentData(), std::chrono::system_clock::now()));
});
server->handle(netconfStreamRoot, [this, conn, keepAlivePingInterval](const auto& req, const auto& res) mutable {
@@ -968,21 +967,16 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std::
stopTime = libyang::fromYangTimeFormat<std::chrono::system_clock>(std::get<std::string>(it->second));
}
- // The signal is constructed outside NotificationStream class because it is required to be passed to
- // NotificationStream's parent (EventStream) constructor where it already must be constructed
- // Yes, this is a hack.
- auto client = std::make_shared<NotificationStream>(
+ NotificationStream::create(
req,
res,
shutdownRequested,
- std::make_shared<rousette::http::EventStream::EventSignal>(),
keepAlivePingInterval,
sess,
streamRequest.type.encoding,
xpathFilter,
startTime,
stopTime);
- client->activate();
} catch (const auth::Error& e) {
processAuthError(req, res, e, [&res]() {
res.write_head(401, {TEXT_PLAIN, CORS});
--
2.43.0
@@ -1,101 +0,0 @@
From e98e079d0fdedf48facbfcf187950946c1ffed7a 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
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>
---
src/http/EventStream.cpp | 44 ++++++++++++++++++++--------------------
1 file changed, 22 insertions(+), 22 deletions(-)
diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp
index 0893653..3bdd55e 100644
--- a/src/http/EventStream.cpp
+++ b/src/http/EventStream.cpp
@@ -47,11 +47,11 @@ EventStream::EventStream(const server::request& req,
}
state = WantToClose;
- boost::asio::post(this->res.io_service(), [maybeClient = std::weak_ptr<EventStream>{shared_from_this()}]() {
- if (auto client = maybeClient.lock()) {
- std::lock_guard lock{client->mtx};
- if (client->state == WantToClose) { // resume unless somebody closed it before this was picked up by the event loop
- client->res.resume();
+ boost::asio::post(this->res.io_service(), [weak = std::weak_ptr<EventStream>{shared_from_this()}]() {
+ if (auto myself = weak.lock()) {
+ std::lock_guard lock{myself->mtx};
+ if (myself->state == WantToClose) { // resume unless somebody closed it before this was picked up by the event loop
+ myself->res.resume();
}
}
});
@@ -67,23 +67,23 @@ void EventStream::activate()
{
start_ping();
- auto client = shared_from_this();
+ auto myself = shared_from_this();
res.write_head(200, {
{"content-type", {"text/event-stream", false}},
{"access-control-allow-origin", {"*", false}},
});
- res.on_close([client](const auto ec) {
- spdlog::debug("{}: closed ({})", client->peer, nghttp2_http2_strerror(ec));
- std::lock_guard lock{client->mtx};
- client->ping.cancel();
- client->eventSub.disconnect();
- client->terminateSub.disconnect();
- client->state = Closed;
+ 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;
});
- res.end([client](uint8_t* destination, std::size_t len, uint32_t* data_flags) {
- return client->process(destination, len, data_flags);
+ res.end([myself](uint8_t* destination, std::size_t len, uint32_t* data_flags) {
+ return myself->process(destination, len, data_flags);
});
}
@@ -156,21 +156,21 @@ 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.async_wait([maybeClient = weak_from_this()](const boost::system::error_code& ec) {
- auto client = maybeClient.lock();
- if (!client) {
+ ping.async_wait([weak = weak_from_this()](const boost::system::error_code& ec) {
+ auto myself = weak.lock();
+ if (!myself) {
spdlog::trace("ping: client already gone");
return;
}
if (ec == boost::asio::error::operation_aborted) {
- spdlog::trace("{}: ping scheduler cancelled", client->peer);
+ spdlog::trace("{}: ping scheduler cancelled", myself->peer);
return;
}
- client->enqueue("", "\n");
- spdlog::trace("{}: keep-alive ping enqueued", client->peer);
- client->start_ping();
+ myself->enqueue("", "\n");
+ spdlog::trace("{}: keep-alive ping enqueued", myself->peer);
+ myself->start_ping();
});
}
--
2.43.0
@@ -0,0 +1,721 @@
From 6819561d97e38569c319e36ca2e99768036b4032 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Wed, 21 Aug 2024 19:18:08 +0200
Subject: [PATCH 08/44] restconf: support fields query parameter
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
This patch adds support for fields query parameter [1].
I had to modify the original grammar for fields parameter a bit to
allow for the lowest precedence while parsing `;` expression. Also we
allow for more strings than the original grammar specifies to make the
syntax more user friendly.
The fields expression is parsed into an AST which corresponds 1:1
to the parse tree. The tree representing the expression could be
simplified but I chose not to as it would complicate the code even
more (although the translation to XPath would be simpler).
The tree is then transformed into a valid XPath 1.0 expression.
The XPath 1.0 expressions are limited and I could not find a way how to
transform the fields string into a valid XPath. I realized that the
easiest way will be to "unwrap" the expression into individual paths
and join them via union operator.
For example, input `a(b;c/d)` would result into `a/b | a/c/d` XPath.
[1] https://datatracker.ietf.org/doc/html/rfc8040#section-4.8.3
[2] https://www.w3.org/TR/1999/REC-xpath-19991116/#section-Expressions
Change-Id: I3c96bbcf49b38ecf08f56912afd3a8f50c15cd44
Signed-off-by: Jan Kundrát <jan.kundrat@cesnet.cz>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
README.md | 1 -
src/restconf/Server.cpp | 9 ++-
src/restconf/uri.cpp | 95 +++++++++++++++++++++++-
src/restconf/uri.h | 57 ++++++++++++++-
src/restconf/uri_impl.h | 4 ++
tests/pretty_printers.h | 23 ++++++
tests/restconf-reading.cpp | 144 ++++++++++++++++++++++++++++++++++++-
tests/restconf-writing.cpp | 12 +++-
tests/uri-parser.cpp | 93 ++++++++++++++++++++++++
tests/yang/example.yang | 10 +++
10 files changed, 438 insertions(+), 10 deletions(-)
diff --git a/README.md b/README.md
index 1689584..3fbfd21 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,6 @@ This is a [RESTCONF](https://datatracker.ietf.org/doc/html/rfc8040.html) server
- TLS certificate authentication (see [Access control model](#access-control-model) below)
- the [`Last-Modified`](https://datatracker.ietf.org/doc/html/rfc8040.html#section-3.4.1.1) and [`ETag`](https://datatracker.ietf.org/doc/html/rfc8040.html#section-3.4.1.2) headers for [edit collision prevention](https://datatracker.ietf.org/doc/html/rfc8040.html#section-3.4.1) in the datastore resource
- the [`Last-Modified`](https://datatracker.ietf.org/doc/html/rfc8040.html#section-3.5.1) and [`ETag`](https://datatracker.ietf.org/doc/html/rfc8040.html#section-3.5.2) headers in the data resource
- - The [`fields`](https://datatracker.ietf.org/doc/html/rfc8040.html#section-4.8.3) query parameter
- [NMDA](https://datatracker.ietf.org/doc/html/rfc8527.html) datastore access
- no [`with-operational-default`](https://datatracker.ietf.org/doc/html/rfc8527#section-3.2.1) capability
- no [`with-origin`](https://datatracker.ietf.org/doc/html/rfc8527#section-3.2.2) capability
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index b515d66..7c66ea4 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -834,6 +834,7 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std::
m_monitoringSession.setItem("/ietf-restconf-monitoring:restconf-state/capabilities/capability[2]", "urn:ietf:params:restconf:capability:depth:1.0");
m_monitoringSession.setItem("/ietf-restconf-monitoring:restconf-state/capabilities/capability[3]", "urn:ietf:params:restconf:capability:with-defaults:1.0");
m_monitoringSession.setItem("/ietf-restconf-monitoring:restconf-state/capabilities/capability[4]", "urn:ietf:params:restconf:capability:filter:1.0");
+ m_monitoringSession.setItem("/ietf-restconf-monitoring:restconf-state/capabilities/capability[5]", "urn:ietf:params:restconf:capability:fields:1.0");
m_monitoringSession.applyChanges();
m_monitoringOperSub = m_monitoringSession.onOperGet(
@@ -1017,7 +1018,13 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std::
}
}
- if (auto data = sess.getData(restconfRequest.path, maxDepth, getOptions, timeout); data) {
+ auto xpath = restconfRequest.path;
+ if (auto it = restconfRequest.queryParams.find("fields"); it != restconfRequest.queryParams.end()) {
+ auto fields = std::get<queryParams::fields::Expr>(it->second);
+ xpath = fieldsToXPath(sess.getContext(), xpath == "/*" ? "" : xpath, fields);
+ }
+
+ if (auto data = sess.getData(xpath, maxDepth, getOptions, timeout); data) {
res.write_head(
200,
{
diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp
index ac399b7..da1e3a5 100644
--- a/src/restconf/uri.cpp
+++ b/src/restconf/uri.cpp
@@ -4,7 +4,10 @@
* Written by Tomáš Pecka <tomas.pecka@cesnet.cz>
*/
+#include <boost/algorithm/string/join.hpp>
+#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 <experimental/iterator>
#include <libyang-cpp/Enum.hpp>
#include <map>
@@ -112,6 +115,28 @@ struct insertTable: x3::symbols<queryParams::QueryParamValue> {
}
} const insertParam;
+/* This grammar is implemented a little bit differently than the RFC states. The ABNF from RFC is:
+ *
+ * fields-expr = path "(" fields-expr ")" / path ";" fields-expr / path
+ * path = api-identifier [ "/" path ]
+ *
+ * Firstly, the grammar from the RFC doesn't allow for expression like `a(b);c` but allows for `c;a(b)`.
+ * I think both should be valid (as user I would expect that the order of such expression does not matter).
+ * Hence our grammar allows for more strings than the grammar from RFC.
+ * This issue was already raised on IETF mailing list: https://mailarchive.ietf.org/arch/msg/netconf/TYBpTE_ELzzMOe6amrw6fQF07nE/
+ * but neither a formal errata was issued nor there was a resolution on the mailing list.
+ */
+const auto fieldsExpr = x3::rule<class fieldsExpr, queryParams::fields::Expr>{"fieldsExpr"};
+const auto fieldsSemi = x3::rule<class fieldsSemiExpr, queryParams::fields::SemiExpr>{"fieldsSemi"};
+const auto fieldsSlash = x3::rule<class fieldsSlashExpr, queryParams::fields::SlashExpr>{"fieldsSlash"};
+const auto fieldsParen = x3::rule<class fieldsParen, queryParams::fields::ParenExpr>{"fieldsParen"};
+
+const auto fieldsSemi_def = fieldsParen >> -(x3::lit(";") >> fieldsSemi);
+const auto fieldsParen_def = fieldsSlash >> -(x3::lit("(") >> fieldsExpr >> x3::lit(")"));
+const auto fieldsSlash_def = apiIdentifier >> -(x3::lit("/") >> fieldsSlash);
+const auto fieldsExpr_def = fieldsSemi;
+BOOST_SPIRIT_DEFINE(fieldsParen, fieldsExpr, fieldsSlash, fieldsSemi);
+
// early sanity check, this timestamp will be parsed by libyang::fromYangTimeFormat anyways
const auto dateAndTime = x3::rule<class dateAndTime, std::string>{"dateAndTime"} =
x3::repeat(4)[x3::digit] >> x3::char_('-') >> x3::repeat(2)[x3::digit] >> x3::char_('-') >> x3::repeat(2)[x3::digit] >> x3::char_('T') >>
@@ -127,7 +152,8 @@ const auto queryParamPair = x3::rule<class queryParamPair, std::pair<std::string
(x3::string("point") >> "=" >> uriPath) |
(x3::string("filter") >> "=" >> filter) |
(x3::string("start-time") >> "=" >> dateAndTime) |
- (x3::string("stop-time") >> "=" >> dateAndTime);
+ (x3::string("stop-time") >> "=" >> dateAndTime) |
+ (x3::string("fields") >> "=" >> fieldsExpr);
const auto queryParamGrammar = x3::rule<class grammar, queryParams::QueryParams>{"queryParamGrammar"} = queryParamPair % "&" | x3::eps;
@@ -384,7 +410,7 @@ void validateQueryParameters(const std::multimap<std::string, queryParams::Query
}
}
- for (const auto& param : {"depth", "with-defaults", "content"}) {
+ for (const auto& param : {"depth", "with-defaults", "content", "fields"}) {
if (auto it = params.find(param); it != params.end() && httpMethod != "GET" && httpMethod != "HEAD") {
throw ErrorResponse(400, "protocol", "invalid-value", "Query parameter '"s + param + "' can be used only with GET and HEAD methods");
}
@@ -658,4 +684,69 @@ std::set<std::string> allowedHttpMethodsForUri(const libyang::Context& ctx, cons
return allowedHttpMethods;
}
+
+/** @brief Traverses the AST of the fields input expression and collects all the possible paths
+ *
+ * @param expr The fields expressions
+ * @param currentPath The current path in the AST, it serves as a stack for the DFS
+ * @param output The collection of all collected paths
+ * @param end If this is the terminal node, i.e., the last node in the expression. This is needed for the correct handling of the leafs under paren expression, which does not "split" the paths but rather concatenates.
+ * */
+void fieldsToXPath(const queryParams::fields::Expr& expr, std::vector<std::string>& currentPath, std::vector<std::string>& output, bool end = false)
+{
+ boost::apply_visitor([&](auto&& node) {
+ using T = std::decay_t<decltype(node)>;
+
+ if constexpr (std::is_same_v<T, queryParams::fields::ParenExpr>) {
+ // the paths from left and right subtree are concatenated, i.e., the nodes we collect in the left tree
+ // are joined together with the nodes from the right tree
+ fieldsToXPath(node.lhs, currentPath, output, !node.rhs.has_value());
+ if (node.rhs) {
+ fieldsToXPath(*node.rhs, currentPath, output, end);
+ }
+ } else if constexpr (std::is_same_v<T, queryParams::fields::SemiExpr>) {
+ // the two paths are now independent and nodes from left subtree do not affect the right subtree
+ // hence we need to copy the current path
+ auto pathCopy = currentPath;
+ fieldsToXPath(node.lhs, currentPath, output, !node.rhs.has_value());
+ if (node.rhs) {
+ fieldsToXPath(*node.rhs, pathCopy, output, false);
+ }
+ } else if constexpr (std::is_same_v<T, queryParams::fields::SlashExpr>) {
+ // the paths from left and right subtree are concatenated, i.e., the the nodes we collect in the left tree
+ // are joined together with the nodes from the right tree, but if this is the terminal node, we need to
+ // add it to the collection of all the gathered paths
+ currentPath.push_back(node.lhs.name());
+
+ if (node.rhs) {
+ fieldsToXPath(*node.rhs, currentPath, output, end);
+ } else if (end) {
+ output.emplace_back(boost::algorithm::join(currentPath, "/"));
+ }
+ }
+ }, expr);
+}
+
+/** @brief Translates the fields expression into a XPath expression and checks for schema validity of the resulting nodes
+ *
+ * The expressions are "unwrapped" into a linear structure and then a union of such paths is made.
+ * E.g., the expression "a(b;c)" is translated into "a/b | a/c".
+ * */
+std::string fieldsToXPath(const libyang::Context& ctx, const std::string& prefix, const queryParams::fields::Expr& expr)
+{
+ std::vector<std::string> currentPath{prefix};
+ std::vector<std::string> paths;
+
+ fieldsToXPath(expr, currentPath, paths);
+
+ for (auto& xpath : paths) {
+ try {
+ validateMethodForNode("GET", impl::URIPrefix{impl::URIPrefix::Type::BasicRestconfData}, ctx.findPath(xpath));
+ } catch (const libyang::Error& e) {
+ throw ErrorResponse(400, "application", "operation-failed", "Can't find schema node for '" + xpath + "'");
+ }
+ }
+
+ return boost::algorithm::join(paths, " | ");
+}
}
diff --git a/src/restconf/uri.h b/src/restconf/uri.h
index 5e079ef..f6df724 100644
--- a/src/restconf/uri.h
+++ b/src/restconf/uri.h
@@ -6,6 +6,7 @@
#pragma once
#include <boost/optional.hpp>
+#include <boost/variant.hpp>
#include <libyang-cpp/Module.hpp>
#include <libyang-cpp/SchemaNode.hpp>
#include <map>
@@ -101,6 +102,57 @@ struct After {
using PointParsed = std::vector<PathSegment>;
}
+namespace fields {
+struct ParenExpr;
+struct SemiExpr;
+struct SlashExpr;
+
+using Expr = boost::variant<boost::recursive_wrapper<SlashExpr>, boost::recursive_wrapper<ParenExpr>, boost::recursive_wrapper<SemiExpr>>;
+
+struct ParenExpr {
+ Expr lhs;
+ boost::optional<Expr> rhs;
+
+ ParenExpr() = default;
+ ParenExpr(const Expr& lhs, const Expr& rhs) : ParenExpr(lhs, boost::optional<Expr>(rhs)) {}
+ ParenExpr(const Expr& lhs, const boost::optional<Expr>& rhs = boost::none)
+ : lhs(lhs)
+ , rhs(rhs)
+ {
+ }
+
+ bool operator==(const ParenExpr&) const = default;
+};
+struct SemiExpr {
+ Expr lhs;
+ boost::optional<Expr> rhs;
+
+ SemiExpr() = default;
+ SemiExpr(const Expr& lhs, const Expr& rhs) : SemiExpr(lhs, boost::optional<Expr>(rhs)) {}
+ SemiExpr(const Expr& lhs, const boost::optional<Expr>& rhs = boost::none)
+ : lhs(lhs)
+ , rhs(rhs)
+ {
+ }
+
+ bool operator==(const SemiExpr&) const = default;
+};
+struct SlashExpr {
+ ApiIdentifier lhs;
+ boost::optional<Expr> rhs;
+
+ SlashExpr() = default;
+ SlashExpr(const ApiIdentifier& lhs, const Expr& rhs) : SlashExpr(lhs, boost::optional<Expr>(rhs)) {}
+ SlashExpr(const ApiIdentifier& lhs, const boost::optional<Expr>& rhs = boost::none)
+ : lhs(lhs)
+ , rhs(rhs)
+ {
+ }
+
+ bool operator==(const SlashExpr&) const = default;
+};
+}
+
using QueryParamValue = std::variant<
UnboundedDepth,
unsigned int,
@@ -116,7 +168,8 @@ using QueryParamValue = std::variant<
insert::Last,
insert::Before,
insert::After,
- insert::PointParsed>;
+ insert::PointParsed,
+ fields::Expr>;
using QueryParams = std::multimap<std::string, QueryParamValue>;
}
@@ -159,4 +212,6 @@ std::vector<PathSegment> asPathSegments(const std::string& uriPath);
std::optional<std::variant<libyang::Module, libyang::SubmoduleParsed>> asYangModule(const libyang::Context& ctx, const std::string& uriPath);
RestconfStreamRequest asRestconfStreamRequest(const std::string& httpMethod, const std::string& uriPath, const std::string& uriQueryString);
std::set<std::string> allowedHttpMethodsForUri(const libyang::Context& ctx, const std::string& uriPath);
+
+std::string fieldsToXPath(const libyang::Context& ctx, const std::string& prefix, const queryParams::fields::Expr& expr);
}
diff --git a/src/restconf/uri_impl.h b/src/restconf/uri_impl.h
index 8a2e166..2bcdb3f 100644
--- a/src/restconf/uri_impl.h
+++ b/src/restconf/uri_impl.h
@@ -65,3 +65,7 @@ BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::impl::URIPath, prefix, segments);
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::queryParams::fields::ParenExpr, lhs, rhs);
+BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::queryParams::fields::SlashExpr, lhs, rhs);
+BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::queryParams::fields::SemiExpr, lhs, rhs);
diff --git a/tests/pretty_printers.h b/tests/pretty_printers.h
index ec87250..a2befeb 100644
--- a/tests/pretty_printers.h
+++ b/tests/pretty_printers.h
@@ -8,6 +8,7 @@
#pragma once
#include "trompeloeil_doctest.h"
+#include <boost/variant.hpp>
#include <experimental/iterator>
#include <optional>
#include <sstream>
@@ -159,6 +160,28 @@ struct StringMaker<rousette::restconf::queryParams::QueryParamValue> {
[](const rousette::restconf::queryParams::insert::PointParsed& p) -> std::string {
return ("PointParsed{" + StringMaker<decltype(p)>::convert(p) + "}").c_str();
},
+ [](const rousette::restconf::queryParams::fields::Expr& expr) -> std::string {
+ return boost::apply_visitor([&](auto&& next) {
+ using T = std::decay_t<decltype(next)>;
+ std::string res;
+
+ if constexpr (std::is_same_v<T, rousette::restconf::queryParams::fields::ParenExpr> || std::is_same_v<T, rousette::restconf::queryParams::fields::SemiExpr>) {
+ if constexpr (std::is_same_v<T, rousette::restconf::queryParams::fields::ParenExpr>) {
+ res = "ParenExpr{";
+ } else {
+ res = "SemiExpr{";
+ }
+ res += StringMaker<rousette::restconf::queryParams::QueryParamValue>::convert(next.lhs).c_str();
+ } else if constexpr (std::is_same_v<T, rousette::restconf::queryParams::fields::SlashExpr>) {
+ res = "SlashExpr{" + next.lhs.name();
+ }
+
+ if (next.rhs) {
+ res += std::string(", ") + StringMaker<rousette::restconf::queryParams::QueryParamValue>::convert(*next.rhs).c_str();
+ }
+ return res += "}";
+ }, expr);
+ },
}, obj).c_str();
}
};
diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp
index 2ded3f0..d7d507b 100644
--- a/tests/restconf-reading.cpp
+++ b/tests/restconf-reading.cpp
@@ -72,7 +72,8 @@ TEST_CASE("reading data")
"urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit",
"urn:ietf:params:restconf:capability:depth:1.0",
"urn:ietf:params:restconf:capability:with-defaults:1.0",
- "urn:ietf:params:restconf:capability:filter:1.0"
+ "urn:ietf:params:restconf:capability:filter:1.0",
+ "urn:ietf:params:restconf:capability:fields:1.0"
]
},
"streams": {
@@ -116,7 +117,8 @@ TEST_CASE("reading data")
"urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit",
"urn:ietf:params:restconf:capability:depth:1.0",
"urn:ietf:params:restconf:capability:with-defaults:1.0",
- "urn:ietf:params:restconf:capability:filter:1.0"
+ "urn:ietf:params:restconf:capability:filter:1.0",
+ "urn:ietf:params:restconf:capability:fields:1.0"
]
},
"streams": {
@@ -672,7 +674,8 @@ TEST_CASE("reading data")
"urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit",
"urn:ietf:params:restconf:capability:depth:1.0",
"urn:ietf:params:restconf:capability:with-defaults:1.0",
- "urn:ietf:params:restconf:capability:filter:1.0"
+ "urn:ietf:params:restconf:capability:filter:1.0",
+ "urn:ietf:params:restconf:capability:fields:1.0"
]
},
"streams": {
@@ -894,6 +897,141 @@ TEST_CASE("reading data")
)"});
}
+ SECTION("fields filtering")
+ {
+ srSess.switchDatastore(sysrepo::Datastore::Running);
+ srSess.setItem("/example:tlc/list[name='blabla']/choice1", "c1");
+ srSess.setItem("/example:tlc/list[name='blabla']/collection[.='42']", std::nullopt);
+ srSess.setItem("/example:tlc/list[name='blabla']/nested[first='1'][second='2'][third='3']/fourth", "4");
+ srSess.setItem("/example:tlc/list[name='blabla']/nested[first='1'][second='2'][third='3']/data/a", "a");
+ srSess.setItem("/example:tlc/list[name='blabla']/nested[first='1'][second='2'][third='3']/data/other-data/b", "b");
+ srSess.setItem("/example:tlc/list[name='blabla']/nested[first='1'][second='2'][third='3']/data/other-data/c", "c");
+ srSess.applyChanges();
+
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:tlc/list=blabla?fields=choice1;collection", {}) == Response{200, jsonHeaders, R"({
+ "example:tlc": {
+ "list": [
+ {
+ "name": "blabla",
+ "collection": [
+ 42
+ ],
+ "choice1": "c1"
+ }
+ ]
+ }
+}
+)"});
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:tlc/list=blabla?fields=choice1;choice2;nested/data(a;other-data/b)", {}) == Response{200, jsonHeaders, R"({
+ "example:tlc": {
+ "list": [
+ {
+ "name": "blabla",
+ "nested": [
+ {
+ "first": "1",
+ "second": 2,
+ "third": "3",
+ "data": {
+ "a": "a",
+ "other-data": {
+ "b": "b"
+ }
+ }
+ }
+ ],
+ "choice1": "c1"
+ }
+ ]
+ }
+}
+)"});
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:tlc/list=blabla?fields=hehe", {}) == Response{400, jsonHeaders, R"({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "application",
+ "error-tag": "operation-failed",
+ "error-message": "Can't find schema node for '/example:tlc/list[name='blabla']/hehe'"
+ }
+ ]
+ }
+}
+)"});
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:tlc/list=blabla?fields=nested/data&depth=1", {}) == Response{200, jsonHeaders, R"({
+ "example:tlc": {
+ "list": [
+ {
+ "name": "blabla",
+ "nested": [
+ {
+ "first": "1",
+ "second": 2,
+ "third": "3",
+ "data": {
+ "a": "a",
+ "other-data": {}
+ }
+ }
+ ]
+ }
+ ]
+ }
+}
+)"});
+
+ // whole datastore with fields filtering
+ REQUIRE(get(RESTCONF_DATA_ROOT "?fields=example:tlc/list/nested/data&depth=1", {}) == Response{200, jsonHeaders, R"({
+ "example:tlc": {
+ "list": [
+ {
+ "name": "blabla",
+ "nested": [
+ {
+ "first": "1",
+ "second": 2,
+ "third": "3",
+ "data": {
+ "a": "a",
+ "other-data": {}
+ }
+ }
+ ]
+ }
+ ]
+ }
+}
+)"});
+
+ // nonexistent schema node in fields: missing prefix in tlc because we query root node so libyang can't infer the prefix from the path
+ REQUIRE(get(RESTCONF_DATA_ROOT "?fields=tlc", {}) == Response{400, jsonHeaders, R"({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "application",
+ "error-tag": "operation-failed",
+ "error-message": "Can't find schema node for '/tlc'"
+ }
+ ]
+ }
+}
+)"});
+
+ // nonexistent schema node in fields
+ REQUIRE(get(RESTCONF_DATA_ROOT "?fields=example:tlc/ob-la-di-ob-la-da", {}) == Response{400, jsonHeaders, R"({
+ "ietf-restconf:errors": {
+ "error": [
+ {
+ "error-type": "application",
+ "error-tag": "operation-failed",
+ "error-message": "Can't find schema node for '/example:tlc/ob-la-di-ob-la-da'"
+ }
+ ]
+ }
+}
+)"});
+ }
+
SECTION("OPTIONS method")
{
// RPC node
diff --git a/tests/restconf-writing.cpp b/tests/restconf-writing.cpp
index c1a9515..582a262 100644
--- a/tests/restconf-writing.cpp
+++ b/tests/restconf-writing.cpp
@@ -375,6 +375,8 @@ TEST_CASE("writing data")
CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/first", "1"),
CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/second", "2"),
CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/third", "3"),
+ CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/data", std::nullopt),
+ CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/data/other-data", std::nullopt),
CREATED("/example:tlc/list[name='large']/choice2", "large"));
REQUIRE(put(RESTCONF_DATA_ROOT "/example:tlc/list=large", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list":[{"name": "large", "choice2": "large", "example:nested": [{"first": "1", "second": 2, "third": "3"}]}]})") == Response{201, noContentTypeHeaders, ""});
}
@@ -385,7 +387,9 @@ TEST_CASE("writing data")
CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']", std::nullopt),
CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/first", "11"),
CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/second", "12"),
- CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/third", "13"));
+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/third", "13"),
+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/data", std::nullopt),
+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/data/other-data", std::nullopt));
REQUIRE(put(RESTCONF_DATA_ROOT "/example:tlc/list=libyang/nested=11,12,13", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:nested": [{"first": "11", "second": 12, "third": "13"}]}]})") == Response{201, noContentTypeHeaders, ""});
}
@@ -1339,6 +1343,8 @@ TEST_CASE("writing data")
CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/first", "1"),
CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/second", "2"),
CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/third", "3"),
+ CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/data", std::nullopt),
+ CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/data/other-data", std::nullopt),
CREATED("/example:tlc/list[name='large']/choice2", "large"));
REQUIRE(post(RESTCONF_DATA_ROOT "/example:tlc", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list":[{"name": "large", "choice2": "large", "example:nested": [{"first": "1", "second": 2, "third": "3"}]}]})") == Response{201, jsonHeaders, ""});
}
@@ -1349,7 +1355,9 @@ TEST_CASE("writing data")
CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']", std::nullopt),
CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/first", "11"),
CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/second", "12"),
- CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/third", "13"));
+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/third", "13"),
+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/data", std::nullopt),
+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/data/other-data", std::nullopt));
REQUIRE(post(RESTCONF_DATA_ROOT "/example:tlc/list=libyang", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:nested": [{"first": "11", "second": 12, "third": "13"}]}]})") == Response{201, jsonHeaders, ""});
}
diff --git a/tests/uri-parser.cpp b/tests/uri-parser.cpp
index 000fe5d..fd5dd8b 100644
--- a/tests/uri-parser.cpp
+++ b/tests/uri-parser.cpp
@@ -828,6 +828,65 @@ TEST_CASE("URI path parser")
REQUIRE(parseQueryParams("stop-time=2023-05-20T18:30:00") == std::nullopt);
REQUIRE(parseQueryParams("stop-time=20230520T18:30:00Z") == std::nullopt);
REQUIRE(parseQueryParams("stop-time=2023-05-a0T18:30:00+05:30") == std::nullopt);
+ REQUIRE(parseQueryParams("fields=mod:leaf") == QueryParams{{"fields", fields::SemiExpr{fields::ParenExpr{fields::SlashExpr{{"mod", "leaf"}}}}}});
+ REQUIRE(parseQueryParams("fields=b(c;d);e(f)") == QueryParams{{"fields",
+ fields::SemiExpr{
+ fields::ParenExpr{
+ fields::SlashExpr{{"b"}},
+ fields::SemiExpr{
+ fields::ParenExpr{
+ fields::SlashExpr{{"c"}}
+ },
+ fields::SemiExpr{
+ fields::ParenExpr{
+ fields::SlashExpr{{"d"}}
+ }
+ }
+ }
+ },
+ fields::SemiExpr{
+ fields::ParenExpr{
+ fields::SlashExpr{{"e"}},
+ fields::SemiExpr{
+ fields::ParenExpr{
+ fields::SlashExpr{{"f"}}
+ }
+ }
+ }
+ }
+ }
+ }});
+ REQUIRE(parseQueryParams("fields=(xyz)") == std::nullopt);
+ REQUIRE(parseQueryParams("fields=a;(xyz)") == std::nullopt);
+ REQUIRE(parseQueryParams("fields=") == std::nullopt);
+
+ for (const auto& [prefix, fields, xpath] : {
+ std::tuple<std::string, std::string, std::string>{"/example:a", "b", "/example:a/b"},
+ {"/example:a", "b/c", "/example:a/b/c"},
+ {"/example:a/b", "c(enabled;blower)", "/example:a/b/c/enabled | /example:a/b/c/blower"},
+ {"/example:a", "b(c(enabled;blower))", "/example:a/b/c/enabled | /example:a/b/c/blower"},
+ {"/example:a", "b(c)", "/example:a/b/c"},
+ {"/example:a", "example:b;something", "/example:a/example:b | /example:a/something"},
+ {"/example:a", "something;b1;b(c/enabled;c/blower)", "/example:a/something | /example:a/b1 | /example:a/b/c/enabled | /example:a/b/c/blower"},
+ {"/example:a", "b(c/enabled;c/blower);something;b1", "/example:a/b/c/enabled | /example:a/b/c/blower | /example:a/something | /example:a/b1"}, // not allowed by RFC 8040
+ {"", "example:a(b;b1)", "/example:a/b | /example:a/b1"},
+ }) {
+ CAPTURE(fields);
+ CAPTURE(xpath);
+ auto qp = parseQueryParams("fields=" + fields);
+ REQUIRE(qp);
+ REQUIRE(qp->count("fields") == 1);
+ auto fieldExpr = qp->find("fields")->second;
+ REQUIRE(std::holds_alternative<fields::Expr>(fieldExpr));
+ REQUIRE(rousette::restconf::fieldsToXPath(ctx, prefix, std::get<fields::Expr>(fieldExpr)) == xpath);
+ }
+
+ auto qp = parseQueryParams("fields=xxx/xyz(a;b)");
+ REQUIRE(qp);
+ REQUIRE_THROWS_WITH_AS(
+ rousette::restconf::fieldsToXPath(ctx, "/example:a", std::get<fields::Expr>(qp->find("fields")->second)),
+ serializeErrorResponse(400, "application", "operation-failed", "Can't find schema node for '/example:a/xxx/xyz/a'").c_str(),
+ rousette::restconf::ErrorResponse);
}
SECTION("Full requests with validation")
@@ -885,6 +944,40 @@ TEST_CASE("URI path parser")
rousette::restconf::ErrorResponse);
}
+ SECTION("fields")
+ {
+ auto resp = asRestconfRequest(ctx, "GET", "/restconf/data/example:a", "fields=b/c(enabled;blower)");
+ REQUIRE(resp.queryParams == QueryParams({{"fields",
+ fields::SemiExpr{
+ fields::ParenExpr{
+ fields::SlashExpr{
+ {"b"},
+ fields::SlashExpr{{"c"}}
+ },
+ fields::SemiExpr{
+ fields::ParenExpr{
+ fields::SlashExpr{{"enabled"}}
+ },
+ fields::SemiExpr{
+ fields::ParenExpr{
+ fields::SlashExpr{{"blower"}}
+ }
+ }
+ }
+ }
+ }
+ }
+ }));
+
+ REQUIRE_THROWS_WITH_AS(asRestconfRequest(ctx, "POST", "/restconf/data/example:a", "fields=b/c(enabled;blower)"),
+ serializeErrorResponse(400, "protocol", "invalid-value", "Query parameter 'fields' can be used only with GET and HEAD methods").c_str(),
+ rousette::restconf::ErrorResponse);
+
+ REQUIRE_THROWS_WITH_AS(asRestconfStreamRequest("GET", "/streams/NETCONF/XML", "fields=a"),
+ serializeErrorResponse(400, "protocol", "invalid-value", "Query parameter 'fields' can't be used with streams").c_str(),
+ rousette::restconf::ErrorResponse);
+ }
+
SECTION("insert first/last")
{
auto resp = asRestconfRequest(ctx, "PUT", "/restconf/data/example:tlc", "insert=first");
diff --git a/tests/yang/example.yang b/tests/yang/example.yang
index 75cd7a6..5d586a0 100644
--- a/tests/yang/example.yang
+++ b/tests/yang/example.yang
@@ -38,6 +38,14 @@ module example {
leaf first { type string; }
leaf second { type int32; }
leaf third { type string; }
+ leaf fourth { type string; }
+ container data {
+ leaf a { type string; }
+ container other-data {
+ leaf b { type string; }
+ leaf c { type string; }
+ }
+ }
}
choice choose {
mandatory true;
@@ -92,6 +100,8 @@ module example {
}
}
}
+ container b1 { }
+ leaf something { type string; }
}
container two-leafs {
--
2.43.0
@@ -0,0 +1,36 @@
From 48d9b6ba3f3f892b9060b76b505d2f9a3aeb9e02 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Mon, 25 Nov 2024 09:15:55 +0100
Subject: [PATCH 09/44] cmake: adhere to CMP0167
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
After locally updating to cmake 3.30 I have seen a warning that our way
of finding boost library is deprecated [1].
[1] https://cmake.org/cmake/help/latest/policy/CMP0167.html
Change-Id: I0cfc6cd0077fac48723487a280daac5fe8218ebb
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 465bef9..01dd2c2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -72,7 +72,7 @@ find_package(spdlog REQUIRED)
find_package(date REQUIRED) # FIXME: Remove when we have STL with __cpp_lib_chrono >= 201907 (gcc 14)
find_package(PkgConfig)
pkg_check_modules(nghttp2 REQUIRED IMPORTED_TARGET libnghttp2_asio>=0.0.90 libnghttp2)
-find_package(Boost REQUIRED COMPONENTS system thread)
+find_package(Boost REQUIRED CONFIG COMPONENTS system thread)
pkg_check_modules(SYSREPO-CPP REQUIRED IMPORTED_TARGET sysrepo-cpp>=3)
pkg_check_modules(LIBYANG-CPP REQUIRED IMPORTED_TARGET libyang-cpp>=3)
--
2.43.0
@@ -1,29 +0,0 @@
From de7c55a1e8d3dda20ee3dc408612f2ddb309e888 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
Organization: Wires
Change-Id: I8c7f7a943a1d848f15527988cb76c2a0a10089e6
Fixes: 4eae6200 (close long-lived connections on SIGTERM)
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
src/http/EventStream.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp
index 3bdd55e..8703dad 100644
--- a/src/http/EventStream.cpp
+++ b/src/http/EventStream.cpp
@@ -47,7 +47,7 @@ EventStream::EventStream(const server::request& req,
}
state = WantToClose;
- boost::asio::post(this->res.io_service(), [weak = std::weak_ptr<EventStream>{shared_from_this()}]() {
+ boost::asio::post(this->res.io_service(), [weak = weak_from_this()]() {
if (auto myself = weak.lock()) {
std::lock_guard lock{myself->mtx};
if (myself->state == WantToClose) { // resume unless somebody closed it before this was picked up by the event loop
--
2.43.0
@@ -0,0 +1,45 @@
From 64997543d48236cd2aae417568bc54d32c54df21 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Mon, 2 Dec 2024 14:43:36 +0100
Subject: [PATCH 10/44] Fix compatibility with pam_wrapper 1.1.6+
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
An year ago I reported a bug that the pam_wrapper project says that they
use a variable called `PAM_WRAPPER_DISABLE_DEEPBIND`, but in fact they
check `UID_WRAPPER_DISABLE_DEEPBIND`. The upstream listened to me, and
they fixed it [1]. Unfortunately, the old variable name is not read from
as of pam_wrapper 1.1.6, so we require setting *both* variables for
random version compatibility.
[1] https://git.samba.org/?p=pam_wrapper.git;a=commitdiff;h=9f0cccf7432dd9be1de953f9b13a7f9b06c40442
Change-Id: I2959f505f5325950606c68b0b324be7181dd6e4f
Reported-by: Tomáš Pecka <tomas.pecka@cesnet.cz>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 01dd2c2..731d7cb 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -179,10 +179,11 @@ if(BUILD_TESTING)
endif()
if(TEST_WRAP_PAM)
+ # FIXME: remove UID_WRAPPER_... (and keep PAM_WRAPPER_...) once we require pam_wrapper 1.1.6+
set(TEST_COMMAND
${UNSHARE_EXECUTABLE} -r -m sh -c "set -ex $<SEMICOLON>
${MOUNT_EXECUTABLE} -t tmpfs none /tmp $<SEMICOLON>
- export LD_PRELOAD=${pam_wrapper_LDFLAGS} PAM_WRAPPER_SERVICE_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/pam PAM_WRAPPER=1 UID_WRAPPER_DISABLE_DEEPBIND=1 $<SEMICOLON>
+ export LD_PRELOAD=${pam_wrapper_LDFLAGS} PAM_WRAPPER_SERVICE_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/pam PAM_WRAPPER=1 UID_WRAPPER_DISABLE_DEEPBIND=1 PAM_WRAPPER_DISABLE_DEEPBIND=1 $<SEMICOLON>
$<TARGET_FILE:test-${TEST_NAME}>")
else()
set(TEST_COMMAND test-${TEST_NAME})
--
2.43.0
@@ -1,112 +0,0 @@
From 0f034d6da3e6d0a91bbe09717cb33aaf3a3ed747 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
Organization: Wires
In some cases, it might be useful to have a callback that is called when
the client disconnects or when the connection is lost.
For us, this will be useful in the future subscribed notifications
implementation where we would like to clean up stuff after client
disconnects.
Change-Id: Icfc2959e38b812b7c18f45976415209b29151c7b
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
src/http/EventStream.cpp | 18 +++++++++++++++---
src/http/EventStream.h | 10 ++++++++--
2 files changed, 23 insertions(+), 5 deletions(-)
diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp
index 8703dad..0a974d4 100644
--- a/src/http/EventStream.cpp
+++ b/src/http/EventStream.cpp
@@ -25,11 +25,15 @@ EventStream::EventStream(const server::request& req,
Termination& termination,
EventSignal& signal,
const std::chrono::seconds keepAlivePingInterval,
- const std::optional<std::string>& initialEvent)
+ const std::optional<std::string>& initialEvent,
+ const std::function<void()>& onTerminationCb,
+ const std::function<void()>& onClientDisconnectedCb)
: res{res}
, ping{res.io_service()}
, peer{peer_from_request(req)}
, m_keepAlivePingInterval(keepAlivePingInterval)
+ , onTerminationCb(onTerminationCb)
+ , onClientDisconnectedCb(onClientDisconnectedCb)
{
if (initialEvent) {
enqueue(FIELD_DATA, *initialEvent);
@@ -41,6 +45,9 @@ EventStream::EventStream(const server::request& req,
terminateSub = termination.connect([this]() {
spdlog::trace("{}: will terminate", peer);
+ if (this->onTerminationCb) {
+ this->onTerminationCb();
+ }
std::lock_guard lock{mtx};
if (state == Closed) { // we are late to the party, res is already gone
return;
@@ -80,6 +87,9 @@ void EventStream::activate()
myself->eventSub.disconnect();
myself->terminateSub.disconnect();
myself->state = Closed;
+ if (myself->onClientDisconnectedCb) {
+ myself->onClientDisconnectedCb();
+ }
});
res.end([myself](uint8_t* destination, std::size_t len, uint32_t* data_flags) {
@@ -187,9 +197,11 @@ std::shared_ptr<EventStream> EventStream::create(const nghttp2::asio_http2::serv
Termination& terminate,
EventSignal& signal,
const std::chrono::seconds keepAlivePingInterval,
- const std::optional<std::string>& initialEvent)
+ const std::optional<std::string>& initialEvent,
+ const std::function<void()>& onTerminationCb,
+ const std::function<void()>& onClientDisconnectedCb)
{
- auto stream = std::shared_ptr<EventStream>(new EventStream(req, res, terminate, signal, keepAlivePingInterval, initialEvent));
+ auto stream = std::shared_ptr<EventStream>(new EventStream(req, res, terminate, signal, keepAlivePingInterval, initialEvent, onTerminationCb, onClientDisconnectedCb));
stream->activate();
return stream;
}
diff --git a/src/http/EventStream.h b/src/http/EventStream.h
index ef0a002..87d8c82 100644
--- a/src/http/EventStream.h
+++ b/src/http/EventStream.h
@@ -36,7 +36,9 @@ public:
Termination& terminate,
EventSignal& signal,
const std::chrono::seconds keepAlivePingInterval,
- const std::optional<std::string>& initialEvent = std::nullopt);
+ const std::optional<std::string>& initialEvent = std::nullopt,
+ const std::function<void()>& onTerminationCb = std::function<void()>(),
+ const std::function<void()>& onClientDisconnectedCb = std::function<void()>());
private:
const nghttp2::asio_http2::server::response& res;
@@ -54,6 +56,8 @@ private:
boost::signals2::scoped_connection eventSub, terminateSub;
const std::string peer;
const std::chrono::seconds m_keepAlivePingInterval;
+ std::function<void()> onTerminationCb; ///< optional callback when the stream is terminated
+ std::function<void()> onClientDisconnectedCb; ///< optional callback invoked in client.on_close()
size_t send_chunk(uint8_t* destination, std::size_t len, uint32_t* data_flags);
ssize_t process(uint8_t* destination, std::size_t len, uint32_t* data_flags);
@@ -66,7 +70,9 @@ protected:
Termination& terminate,
EventSignal& signal,
const std::chrono::seconds keepAlivePingInterval,
- const std::optional<std::string>& initialEvent = std::nullopt);
+ const std::optional<std::string>& initialEvent = std::nullopt,
+ const std::function<void()>& onTerminationCb = std::function<void()>(),
+ const std::function<void()>& onClientDisconnectedCb = std::function<void()>());
void activate();
};
}
--
2.43.0
@@ -1,92 +0,0 @@
From d72adb3da31cb509e64c8f33174e375fa9468b0b 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
Organization: Wires
This code adds a new internal RPC handler dispatcher that checks if user
is authorized to call the RPC (we are bypassing sysrepo, so we have to
check this manually) and then calls the RPC handler.
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>
---
src/restconf/Server.cpp | 24 ++++++++++++++++++++----
tests/restconf-rpc.cpp | 18 ++++++++++++++++++
2 files changed, 38 insertions(+), 4 deletions(-)
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 8544210..74c06f7 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -438,10 +438,26 @@ libyang::CreatedNodes createEditForPutAndPatch(libyang::Context& ctx, const std:
return {editNode, replacementNode};
}
-std::optional<libyang::DataNode> processInternalRPC(sysrepo::Session&, const libyang::DataNode&)
+std::optional<libyang::DataNode> processInternalRPC(sysrepo::Session& sess, const libyang::DataNode& rpcInput, const libyang::DataFormat requestEncoding)
{
- // TODO: Implement internal RPCs
- throw ErrorResponse(501, "application", "operation-not-supported", "Internal RPCs are not yet supported.");
+ using InternalRPCHandler = std::function<void(sysrepo::Session&, const libyang::DataFormat, const libyang::DataNode&, libyang::DataNode&)>;
+ const std::map<std::string, InternalRPCHandler> handlers;
+
+ 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());
+ }
+
+ 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;
+ }
+
+ throw ErrorResponse(501, "application", "operation-not-supported", "Unsupported RPC call to " + rpcPath, rpcPath);
}
void processActionOrRPC(std::shared_ptr<RequestContext> requestCtx, const std::chrono::milliseconds timeout)
@@ -478,7 +494,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);
+ rpcReply = processInternalRPC(requestCtx->sess, *rpcNode, *requestCtx->dataFormat.request);
}
if (!rpcReply || rpcReply->immediateChildren().empty()) {
diff --git a/tests/restconf-rpc.cpp b/tests/restconf-rpc.cpp
index a3e9309..43aaed4 100644
--- a/tests/restconf-rpc.cpp
+++ b/tests/restconf-rpc.cpp
@@ -420,4 +420,22 @@ TEST_CASE("invoking actions and rpcs")
)"});
}
}
+
+ SECTION("Internal RPC handlers")
+ {
+ // check NACM access
+ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:kill-subscription", {CONTENT_TYPE_JSON}, R"({"ietf-subscribed-notifications:input": {}})") == 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."
+ }
+ ]
+ }
+}
+)"});
+ }
}
--
2.43.0
@@ -0,0 +1,30 @@
From 849aa35274d5e07726cb849fe724962754b3fa29 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Mon, 2 Dec 2024 20:20:48 +0100
Subject: [PATCH 11/44] tests: add missing pragma once
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: I269c20e5a914aa8c7bc9431147dd9785ea1aedda
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
tests/aux-utils.h | 1 +
1 file changed, 1 insertion(+)
diff --git a/tests/aux-utils.h b/tests/aux-utils.h
index a3923bb..7482945 100644
--- a/tests/aux-utils.h
+++ b/tests/aux-utils.h
@@ -6,6 +6,7 @@
*
*/
+#pragma once
#include "trompeloeil_doctest.h"
#include <iostream>
#include <nghttp2/asio_http2_client.h>
--
2.43.0
@@ -1,99 +0,0 @@
From 86ea8a1fdc2cb8baa1edbd32bd4f8b45acdef816 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
Organization: Wires
I have started getting heap-use-after-free errors from sanitizers (see
below). The problem is that EventStream::enqueue posted res.resume()
call to the asio event loop, but sometimes when the function was called,
the response object was already destroyed.
The fix is to check if the response is still valid (and not closed)
before actually resuming the response.
WARNING: ThreadSanitizer: heap-use-after-free (pid=633338)
Read of size 8 at 0x72080000f3d0 by thread T134:
#0 std::__uniq_ptr_impl<nghttp2::asio_http2::server::response_impl, std::default_delete<nghttp2::asio_http2::server::response_impl>>::_M_ptr() const /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/unique_ptr.h:193:51 (libnghttp2_asio.so.0+0x205011) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#1 std::unique_ptr<nghttp2::asio_http2::server::response_impl, std::default_delete<nghttp2::asio_http2::server::response_impl>>::get() const /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/unique_ptr.h:473:21 (libnghttp2_asio.so.0+0x204fc5) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#2 std::unique_ptr<nghttp2::asio_http2::server::response_impl, std::default_delete<nghttp2::asio_http2::server::response_impl>>::operator->() const /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/unique_ptr.h:466:9 (libnghttp2_asio.so.0+0x2046c5) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#3 nghttp2::asio_http2::server::response::resume() const /home/tomas/zdrojaky/cesnet/nghttp2-asio/lib/asio_server_response.cc:63:33 (libnghttp2_asio.so.0+0x204425) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#4 rousette::http::EventStream::enqueue(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&)::$_1::operator()() const /home/tomas/zdrojaky/cesnet/rousette/src/http/EventStream.cpp:167:68 (test-restconf-subscribed-notifications+0x2a084d) (BuildId: 9270c9f5d6da566b5b7f223994c283ecf35f0e43)
#5 boost::asio::detail::binder0<rousette::http::EventStream::enqueue(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&)::$_1>::operator()() /usr/include/boost/asio/detail/bind_handler.hpp:56:5 (test-restconf-subscribed-notifications+0x2a084d)
#6 boost::asio::detail::executor_op<boost::asio::detail::binder0<rousette::http::EventStream::enqueue(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&)::$_1>, std::allocator<void>, boost::asio::detail::scheduler_operation>::do_complete(void*, boost::asio::detail::scheduler_operation*, boost::system::error_code const&, unsigned long) /usr/include/boost/asio/detail/executor_op.hpp:70:7 (test-restconf-subscribed-notifications+0x2a084d)
#7 boost::asio::detail::scheduler_operation::complete(void*, boost::system::error_code const&, unsigned long) /usr/include/boost/asio/detail/scheduler_operation.hpp:40:5 (test-restconf-subscribed-notifications+0x165ce9) (BuildId: 9270c9f5d6da566b5b7f223994c283ecf35f0e43)
#8 boost::asio::detail::scheduler::do_run_one(boost::asio::detail::conditionally_enabled_mutex::scoped_lock&, boost::asio::detail::scheduler_thread_info&, boost::system::error_code const&) /usr/include/boost/asio/detail/impl/scheduler.ipp:492:12 (test-restconf-subscribed-notifications+0x165ce9)
#9 boost::asio::detail::scheduler::run(boost::system::error_code&) /usr/include/boost/asio/detail/impl/scheduler.ipp:208:10 (test-restconf-subscribed-notifications+0x165292) (BuildId: 9270c9f5d6da566b5b7f223994c283ecf35f0e43)
#10 boost::asio::io_context::run() /usr/include/boost/asio/impl/io_context.ipp:71:24 (libnghttp2_asio.so.0+0x1675bf) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
(...)
Previous write of size 8 at 0x72080000f3d0 by thread T134:
#0 operator delete(void*, unsigned long) <null> (test-restconf-subscribed-notifications+0x13f7d3) (BuildId: 9270c9f5d6da566b5b7f223994c283ecf35f0e43)
#1 std::default_delete<nghttp2::asio_http2::server::stream>::operator()(nghttp2::asio_http2::server::stream*) const /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/unique_ptr.h:93:2 (libnghttp2_asio.so.0+0x1fc71d) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#2 std::_Sp_counted_deleter<nghttp2::asio_http2::server::stream*, std::default_delete<nghttp2::asio_http2::server::stream>, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>::_M_dispose() /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/shared_ptr_base.h:526:9 (libnghttp2_asio.so.0+0x1fddaf) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#3 std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release_last_use() /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/shared_ptr_base.h:174:2 (libnghttp2_asio.so.0+0x160e46) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#4 std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release() /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/shared_ptr_base.h:360:4 (libnghttp2_asio.so.0+0x160dfd) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#5 std::__shared_count<(__gnu_cxx::_Lock_policy)2>::~__shared_count() /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/shared_ptr_base.h:1069:11 (libnghttp2_asio.so.0+0x160d08) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#6 std::__shared_ptr<nghttp2::asio_http2::server::stream, (__gnu_cxx::_Lock_policy)2>::~__shared_ptr() /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/shared_ptr_base.h:1531:31 (libnghttp2_asio.so.0+0x1f82e9) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#7 std::shared_ptr<nghttp2::asio_http2::server::stream>::~shared_ptr() /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/shared_ptr.h:175:11 (libnghttp2_asio.so.0+0x1f82a5) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#8 std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>::~pair() /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/stl_pair.h:302:12 (libnghttp2_asio.so.0+0x1f8269) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#9 void std::__new_allocator<std::_Rb_tree_node<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>>::destroy<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>(std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>*) /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/new_allocator.h:198:10 (libnghttp2_asio.so.0+0x1f815d) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#10 void std::allocator_traits<std::allocator<std::_Rb_tree_node<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>>>::destroy<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>(std::allocator<std::_Rb_tree_node<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>>&, std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>*) /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/alloc_traits.h:696:8 (libnghttp2_asio.so.0+0x1f815d)
#11 std::_Rb_tree<int, std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>, std::_Select1st<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>, std::less<int>, std::allocator<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>>::_M_destroy_node(std::_Rb_tree_node<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>*) /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/stl_tree.h:1265:2 (libnghttp2_asio.so.0+0x1f815d)
#12 std::_Rb_tree<int, std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>, std::_Select1st<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>, std::less<int>, std::allocator<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>>::_M_drop_node(std::_Rb_tree_node<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>*) /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/stl_tree.h:1273:2 (libnghttp2_asio.so.0+0x1f80b9) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#13 std::_Rb_tree<int, std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>, std::_Select1st<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>, std::less<int>, std::allocator<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>>::_M_erase(std::_Rb_tree_node<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>*) /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/stl_tree.h:2590:4 (libnghttp2_asio.so.0+0x1f7e98) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#14 std::_Rb_tree<int, std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>, std::_Select1st<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>, std::less<int>, std::allocator<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>>::clear() /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/stl_tree.h:1878:2 (libnghttp2_asio.so.0+0x1ff035) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#15 std::_Rb_tree<int, std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>, std::_Select1st<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>, std::less<int>, std::allocator<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>>::_M_erase_aux(std::_Rb_tree_const_iterator<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>, std::_Rb_tree_const_iterator<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>) /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/stl_tree.h:3127:2 (libnghttp2_asio.so.0+0x1feca6) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#16 std::_Rb_tree<int, std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>, std::_Select1st<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>, std::less<int>, std::allocator<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>>::erase(int const&) /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/stl_tree.h:3141:7 (libnghttp2_asio.so.0+0x1fe8a3) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#17 std::map<int, std::shared_ptr<nghttp2::asio_http2::server::stream>, std::less<int>, std::allocator<std::pair<int const, std::shared_ptr<nghttp2::asio_http2::server::stream>>>>::erase(int const&) /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/stl_map.h:1159:21 (libnghttp2_asio.so.0+0x1f68d5) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#18 nghttp2::asio_http2::server::http2_handler::close_stream(int) /home/tomas/zdrojaky/cesnet/nghttp2-asio/lib/asio_server_http2_handler.cc:315:12 (libnghttp2_asio.so.0+0x1f34b0) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#19 nghttp2::asio_http2::server::(anonymous namespace)::on_stream_close_callback(nghttp2_session*, int, unsigned int, void*) /home/tomas/zdrojaky/cesnet/nghttp2-asio/lib/asio_server_http2_handler.cc:193:12 (libnghttp2_asio.so.0+0x1f3074) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#20 nghttp2_session_close_stream /usr/src/debug/libnghttp2/nghttp2/lib/nghttp2_session.c:1289:9 (libnghttp2.so.14+0x62ac) (BuildId: 8a424dc80fadd83dfdd8c51eebe47046c626d95e)
#21 nghttp2::asio_http2::server::connection<boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::any_io_executor>>::do_write() /home/tomas/zdrojaky/cesnet/nghttp2-asio/lib/asio_server_connection.h:177:20 (libnghttp2_asio.so.0+0x1ebb1c) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#22 nghttp2::asio_http2::server::connection<boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::any_io_executor>>::do_write()::'lambda'(boost::system::error_code const&, unsigned long)::operator()(boost::system::error_code const&, unsigned long) const /home/tomas/zdrojaky/cesnet/nghttp2-asio/lib/asio_server_connection.h:207:11 (libnghttp2_asio.so.0+0x1ec7a8) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#23 boost::asio::detail::write_op<boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::any_io_executor>, boost::asio::mutable_buffer, boost::asio::mutable_buffer const*, boost::asio::detail::transfer_all_t, nghttp2::asio_http2::server::connection<boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::any_io_executor>>::do_write()::'lambda'(boost::system::error_code const&, unsigned long)>::operator()(boost::system::error_code, unsigned long, int) /usr/include/boost/asio/impl/write.hpp:380:9 (libnghttp2_asio.so.0+0x1ec4e5) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#24 boost::asio::detail::binder2<boost::asio::detail::write_op<boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::any_io_executor>, boost::asio::mutable_buffer, boost::asio::mutable_buffer const*, boost::asio::detail::transfer_all_t, nghttp2::asio_http2::server::connection<boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::any_io_executor>>::do_write()::'lambda'(boost::system::error_code const&, unsigned long)>, boost::system::error_code, unsigned long>::operator()() /usr/include/boost/asio/detail/bind_handler.hpp:181:5 (libnghttp2_asio.so.0+0x1edd41) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#25 void boost::asio::detail::handler_work<boost::asio::detail::write_op<boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::any_io_executor>, boost::asio::mutable_buffer, boost::asio::mutable_buffer const*, boost::asio::detail::transfer_all_t, nghttp2::asio_http2::server::connection<boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::any_io_executor>>::do_write()::'lambda'(boost::system::error_code const&, unsigned long)>, boost::asio::any_io_executor, void>::complete<boost::asio::detail::binder2<boost::asio::detail::write_op<boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::any_io_executor>, boost::asio::mutable_buffer, boost::asio::mutable_buffer const*, boost::asio::detail::transfer_all_t, nghttp2::asio_http2::server::connection<boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::any_io_executor>>::do_write()::'lambda'(boost::system::error_code const&, unsigned long)>, boost::system::error_code, unsigned long>>(boost::asio::detail::binder2<boost::asio::detail::write_op<boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::any_io_executor>, boost::asio::mutable_buffer, boost::asio::mutable_buffer const*, boost::asio::detail::transfer_all_t, nghttp2::asio_http2::server::connection<boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::any_io_executor>>::do_write()::'lambda'(boost::system::error_code const&, unsigned long)>, boost::system::error_code, unsigned long>&, boost::asio::detail::write_op<boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::any_io_executor>, boost::asio::mutable_buffer, boost::asio::mutable_buffer const*, boost::asio::detail::transfer_all_t, nghttp2::asio_http2::server::connection<boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::any_io_executor>>::do_write()::'lambda'(boost::system::error_code const&, unsigned long)>&) /usr/include/boost/asio/detail/handler_work.hpp:470:7 (libnghttp2_asio.so.0+0x1edb2e) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#26 boost::asio::detail::reactive_socket_send_op<boost::asio::const_buffer, boost::asio::detail::write_op<boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::any_io_executor>, boost::asio::mutable_buffer, boost::asio::mutable_buffer const*, boost::asio::detail::transfer_all_t, nghttp2::asio_http2::server::connection<boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::any_io_executor>>::do_write()::'lambda'(boost::system::error_code const&, unsigned long)>, boost::asio::any_io_executor>::do_complete(void*, boost::asio::detail::scheduler_operation*, boost::system::error_code const&, unsigned long) /usr/include/boost/asio/detail/reactive_socket_send_op.hpp:154:9 (libnghttp2_asio.so.0+0x1ed66c) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
#27 boost::asio::detail::scheduler_operation::complete(void*, boost::system::error_code const&, unsigned long) /usr/include/boost/asio/detail/scheduler_operation.hpp:40:5 (test-restconf-subscribed-notifications+0x165ce9) (BuildId: 9270c9f5d6da566b5b7f223994c283ecf35f0e43)
#28 boost::asio::detail::scheduler::do_run_one(boost::asio::detail::conditionally_enabled_mutex::scoped_lock&, boost::asio::detail::scheduler_thread_info&, boost::system::error_code const&) /usr/include/boost/asio/detail/impl/scheduler.ipp:492:12 (test-restconf-subscribed-notifications+0x165ce9)
#29 boost::asio::detail::scheduler::run(boost::system::error_code&) /usr/include/boost/asio/detail/impl/scheduler.ipp:208:10 (test-restconf-subscribed-notifications+0x165292) (BuildId: 9270c9f5d6da566b5b7f223994c283ecf35f0e43)
#30 boost::asio::io_context::run() /usr/include/boost/asio/impl/io_context.ipp:71:24 (libnghttp2_asio.so.0+0x1675bf) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4)
(...)
Change-Id: Ifdb1f8610cacffca3bb49da17aa9b1d267cdd472
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
src/http/EventStream.cpp | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp
index 0a974d4..f97d953 100644
--- a/src/http/EventStream.cpp
+++ b/src/http/EventStream.cpp
@@ -160,7 +160,22 @@ void EventStream::enqueue(const std::string& fieldName, const std::string& what)
spdlog::trace("{}: new event, ∑ queue size = {}", peer, len);
queue.push_back(buf);
state = HasEvents;
- boost::asio::post(res.io_service(), [&res = this->res]() { res.resume(); });
+ boost::asio::post(res.io_service(), [weak = weak_from_this()]() {
+ auto myself = weak.lock();
+ if (!myself) {
+ spdlog::trace("enqueue: already disconnected");
+ return;
+ }
+
+ std::lock_guard lock{myself->mtx};
+ // if state is Closed, then res.on_close() already finished, which means we must not use res anymore
+ if (myself->state == Closed) {
+ spdlog::trace("{}: enqueue: not resuming, the response's on_close handler already finished", myself->peer);
+ return;
+ }
+
+ myself->res.resume();
+ });
}
void EventStream::start_ping()
--
2.43.0

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