Compare commits

...
94 Commits
Author SHA1 Message Date
Ejub SabicandTobias Waldekranz 4be5217a36 uboot: import first variable from auxiliary environment
The U-Boot env file is generated by
mkenvimage, which prefixes the variable data with a 4-byte CRC32 by default.
ixpreboot.sh previously imported it with `env import -b` as per uboot docs(-b assume binary format (’0’ separated, “00” terminated).),
which treats the buffer as raw binary and does not strip the CRC.
As a result the first variable's name is parsed a part of CRC.
Later variables import correctly because by then the parser is past the CRC.
Switching to `env import -c` as per uboot docs (-c assume checksum protected environment format.)
makes U-Boot validate and strip the CRC before parsing, so the first variable imports under its real name.

Tested on:
- QEMU

Signed-off-by: Ejub Sabic <ejub1946@outlook.com>
2026-06-16 07:29:29 +02:00
Tobias WaldekranzandGitHub ebb37732f4 Merge pull request #1177 from kernelkit/multi-dsa-tree-fixes
Multi DSA tree fixes
2025-10-02 16:09:19 +02:00
Tobias Waldekranz 94f8d1a700 common: nameif: Handle nested dsa ports
In setups like this...

    CPU
    eth0
     |
.----0----.
| dst0sw0 |
'-1-2-3-4-'
        |
   .----0----.
   | dst1sw0 |
   '-1-2-3-4-'

...both eth0 and dst0sw0p4 are DSA ports. But the latter is _also_ a
physical port as far as devlink is concerned. As a result, it would
first be marked as "internal" and is then instantly reclassified as a
"port".

Catch this condition and stick with the initial "internal"
classification.
2025-10-02 14:51:34 +02:00
Tobias Waldekranz 39b4101d19 common: has-quirk: Add support for matching based on "ethtool -i"
In addition to matching on interface names, add support for matching
on ethtool information.

Example:

    {
        "@ethtool:driver=st_gmac": {
	    "broken-mqprio": true
	}
    }

This would mark any interface using the "st_gmac" driver as having a
broken mqprio implementation. Whereas this:

    {
        "@ethtool:driver=st_gmac;bus-info:30bf0000.ethernet": {
	    "broken-mqprio": true
	}
    }

Only matches an st_gmac-backed interface at the specified location.

As matching becomes more complicated, use the shell implementation
from confd as well, to make sure that they are always in agreement.
2025-10-02 14:51:29 +02:00
Joachim WibergandGitHub a096a56428 Merge pull request #1170 from kernelkit/ospf-show-neighbor-fix
OSPF: skip interfaces without OSPF enabled
2025-09-30 09:33:20 +02:00
Richard Alpe 0b81e46306 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-29 14:38:25 +02:00
Joachim WibergandGitHub 3720e09779 Merge pull request #1167 from kernelkit/kernel-update
Upgrade Linux kernel to 6.12.49 (LTS)
2025-09-29 14:34:28 +02:00
Mattias Walström 91fdf6144c Upgrade Linux kernel to 6.12.49 (LTS) 2025-09-29 12:33:26 +02:00
Mattias WalströmandGitHub c23c2245f2 Merge pull request #1168 from kernelkit/doc-updates
Doc updates
2025-09-29 11:28:17 +02:00
Joachim Wiberg cb85b6fc43 doc: update changelog, error in log message, and prep for release
[skip ci]

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-29 11:07:20 +02:00
Joachim Wiberg 9ca0dd6cdc Minor update, mention new Banana Pi-R3 support
Also, split out the i.MX8MP EVK to a separate item.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-29 11:07:07 +02:00
Joachim WibergandGitHub c59f53eeed Merge pull request #1163 from kernelkit/finit-warning
Fix log error at boot about unsupported command
2025-09-29 09:40:11 +02:00
Joachim Wiberg 040bd5b10c doc: update changelog, error in log message, and prep for release
[skip ci]

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-29 09:39:45 +02:00
Joachim Wiberg bb07f87f9c confd: simplify dnsmasq management during DHCP server reconf
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-29 09:37:12 +02:00
Joachim Wiberg 40755424ee Fix error in log during boot about unsupported command
Sep 27 04:10:06 infix finit[1]: api_cb():Unsupported command
	(cmd: 13, data: dnsmasq) in runlevel S and 6/0.

Fixes #1161

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-29 09:37:06 +02:00
Joachim WibergandGitHub fe8610b30d Merge pull request #1166 from kernelkit/infamy
Mix of test framework fixes and improvements
2025-09-29 08:49:31 +02:00
Joachim Wiberg a0a4051c8a test/infamy: fix xpath_to_uri() to handle multiple predicates
The xpath_to_uri() method only processed the first predicate in XPath
expressions with multiple [key='value'] patterns.  Each re.sub() call
was performed on the original xpath instead of the result the previous
substitutions, causing subsequent predicates to be ignored.

Example XPath that would fail:

    /infix-firewall:firewall/zone[name='untrusted']/interface[.='e2']

Would incorrectly convert to:

    /infix-firewall:firewall/zone[name='untrusted']/interface=e2

Instead of the correct RESTCONF URL:

    /infix-firewall:firewall/zone=untrusted/interface=e2

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 22:02:58 +02:00
Joachim WibergandGitHub d38c3f3406 Merge pull request #1164 from kernelkit/rauc-fixes
Rauc integration fixes
2025-09-28 21:44:29 +02:00
Joachim Wiberg 9d9e099cdb test/infamy: slight improvement to tap.py::Test.__exit__()
Before this change:

  ok 2 - Configure basic end-device firewall
  not ok 3 - Verify unused interface assigned to default zone
  # Exiting (2025-09-25 11:33:00)
  # Traceback (most recent call last):
  #   File "/home/jocke/src/x-misc/test/./case/infix_firewall/basic/test.py", line 127, in <module>
  #     assert unused_if not in public_zone["interface"], \
  #            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  # AssertionError: Unused interface e4 should be in default zone 'public', got interfaces: ['e2', 'e3', 'e5', 'e7', 'e8']

After this change:

  ok 2 - Configure basic end-device firewall
  not ok 3 - Verify unused interface assigned to default zone
  # Exiting (2025-09-25 11:35:00)
  #   File "/home/jocke/src/x-misc/test/./case/infix_firewall/basic/test.py", line 127, in <module>
  #     assert unused_if not in public_zone["interface"], \
  #            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  # Unused interface e4 should be in default zone 'public', got interfaces: ['e2', 'e3', 'e5', 'e7', 'e8']

Slightly shorter and arguably easier to read for a non-pythonic human.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 21:35:32 +02:00
Joachim Wiberg a9430070d2 test/infamy: add optional msg support to tap.py::Test.fail()
A very common pattern in our tests is:

  if (condition):
    print(f"the condition failed with {output}")
    test.fail()

This change allows us to write:

  if (condition):
    test.fail(f"the condition failed with {output}")

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 21:35:32 +02:00
Joachim Wiberg a3d30eb32f test/infamy: minor, whitespace only
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 21:35:31 +02:00
Joachim Wiberg 6bd00d8403 doc: update changelog
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 21:34:08 +02:00
Joachim Wiberg d715f02da0 board: enable rauc upgrade history and event logging (json)
The rauc event logger is a separate glib logger that by default logs to
the system console, with annoying ANSI escape sequences.  This commit
drops the default stdout/stderr redirect of these log messages and adds
a more exhaustive bundle install and event log, in json format, instead.

For more information about event logging, see the rauc documentation:
https://rauc.readthedocs.io/en/latest/advanced.html

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 21:32:59 +02:00
Joachim Wiberg 81a56fe304 confd: replace depracated rauc install d-bus method
Fixes #1080

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 21:32:57 +02:00
Joachim WibergandGitHub 99c87d0f1d Merge pull request #1165 from kernelkit/misc
Misc fixes
2025-09-28 21:18:44 +02:00
Joachim Wiberg 3a164e294d doc: update changelog
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 07:26:45 +02:00
Joachim Wiberg c42b0b536d test: minor, cleanup of specifications
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 07:26:45 +02:00
Joachim Wiberg c61b4361ab test: simplify and skip UNIX backup files
- Drop 'local', not available in POSIX shell scripts
 - Check for an assortment of backup file combos
 - Simplify nested if-statements, skip whitelist first

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 07:26:45 +02:00
Joachim Wiberg 065f86a74c confd: minor, replace hard-coded string with define
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 07:26:44 +02:00
Joachim Wiberg 0f6c7608ec package/confd: simplify, relocate netconf service
Follow the good example set by the restconf service.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 07:26:44 +02:00
Joachim Wiberg 93d59b024a package/rousette: add '--log-level LEVEL' command line option
The default 'trace' log level is quite verbose for our production systems.
This commit changes the default to 'info' and adds a command line option
to control the log level if needed.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 07:26:43 +02:00
Joachim Wiberg 096a8ab2e2 package/rousette: use v2-kkit branch for reduced patch set
To be able to maintain our own set of patches on top rousette, a kkit
branch have been set up with a reduced set of backported fixes.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 07:26:43 +02:00
Joachim Wiberg 1a3e1dcabc package/nghttp2-asio: bump to CESNET fork
This is the CESNET fork of nghttp2-asio, introduced here:
https://github.com/CESNET/rousette/commit/fff3429

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 07:26:42 +02:00
Joachim Wiberg 69056bfb2c board/common: fixes for unicode translation in log/pager commands
- cli: add '-r' to log/pager/follow for "raw" control chars, including unicode
 - sysklogd: backport unicode fix for em-dash character (—) in slogan
 - sysklogd: drop old patches
 - sysklogd: run in 8-bit safe mode

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 03:50:23 +02:00
Joachim Wiberg dd4221802f utils: silence srload (debug messages)
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 03:50:22 +02:00
Joachim Wiberg e1783f3c23 cli: minor, xml optimization
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 03:50:22 +02:00
Joachim Wiberg 0957774208 cli: add terminal reset|resize commands
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 03:50:22 +02:00
Joachim Wiberg 77963d5267 patches/libyang: follow-up to e7f68be
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-28 03:50:14 +02:00
Mattias WalströmandGitHub 2f13fc7843 Merge pull request #1160 from kernelkit/upgrade-sysrepo
Bump sysrepo, netopeer,libyang and libnetconf2, libyang-cpp, rousette
2025-09-17 10:41:04 +02:00
Mattias Walström e7f68be143 Bump sysrepo, netopeer,libyang and libnetconf2, libyang-cpp, sysrepo-cpp, rousette 2025-09-17 08:51:31 +02:00
Mattias WalströmandGitHub 197bb1507e Merge pull request #1159 from kernelkit/misc
Misc fixes
2025-09-16 09:27:42 +02:00
Joachim Wiberg b18a719cee Add repobeats analytics
[skip ci]

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-16 07:55:05 +02:00
Joachim Wiberg b330c6c70d .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-15 19:12:51 +02:00
Joachim Wiberg 64a3770a76 doc: update changelog with latest changes
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 18:58:16 +02:00
Joachim Wiberg 6a745f50f8 board/common: adjust log level of udhcpc script, fix missing iface
Issue #1100

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 18:08:40 +02:00
Joachim Wiberg f41ff6d3e7 confd: reduce dhcp client logging overhead by 70%
Adjust DHCP client retry behavior:

 -t 3  (was -t 10) : Maximum discovery attempts per cycle
 -T 5  (was -T 3)  : Seconds to wait between attempts
 -A 30 (was -A 10) : Seconds to wait after all attempts fail

Before: 10 attempts × 3 seconds = 30 seconds of rapid trying, then 10
        second pause = 40 second total cycle

After: 3 attempts × 5 seconds = 15 seconds of trying, then 30 second
       pause = 45 second total cycle

=> 70% fewer log messages during active attempts, with longer quiet
       periods between cycles.

Fixes #1100

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 18:07:50 +02:00
Joachim Wiberg 400691af85 package/klish-plugin-sysrepo: improve CLI UX for no enabled command
Fixes #1119

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 17:25:23 +02:00
Joachim Wiberg 0bada691fc Add neofetch
Fixes #1143
2025-09-15 17:08:08 +02:00
Joachim Wiberg 164a1018a0 board/common: improve default bash settings for better UX
Add HISTCONTROL=ignoreboth to ignore duplicate commands and those
starting with space, enable histappend, and improve tab completion
behavior.  Also add /etc/inputrc for better readline key bindings.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 16:55:55 +02:00
Joachim Wiberg 805acdf782 test: bump 9pm for test ID and meta data section in report
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 16:23:20 +02:00
Joachim Wiberg df3f9f2e40 Add mtr and iperf3
Fixes #1144

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 16:22:13 +02:00
Joachim Wiberg 2761a17abc cli: fix 'show ospf' commands regression, introduced in 827dc098e
Fixes #1155

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 16:22:13 +02:00
Joachim Wiberg 77b364d682 board/common: set show-legacy wrapper as executable
Fix #1150

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 16:22:10 +02:00
Joachim WibergandGitHub 1989effaad Merge pull request #1135 from kernelkit/test-fixes
Test fixes

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 16:21:23 +02:00
Joachim Wiberg 142e4e95b7 test: consistent naming for all yaml files
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 14:26:06 +02:00
Joachim Wiberg 44d37808f1 test/case: use 9pm 'name:' for all test names
To reduce the duplication of effort between 9pm and the Infamy framework
this change consolidates the move from local 'infamy: title:' extension
to 9pm 'name:'.

For the parameterized tunnel tests we leverage the 9pm dynamic test-spec
variable, which looks for a correspodning <case>.adoc instead of static
Readme.adoc, when generating the test report.

Each test documentation should cover all aspects of the test, much like
the usage text of a UNIX program.  To this end, the tunnel test docs are
now more spelled out, including all invariants.

Some refactoring of these tests were also necessary, e.g., replacing any
reserved Python keywords like 'type', and other PEP-8 fixes.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 14:26:06 +02:00
Joachim Wiberg eb3e64d243 test/case: rename foo.adoc -> test.adoc and update all Readme.adoc
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 14:26:05 +02:00
Joachim Wiberg 2171fd12ca test/case/infix_dhcp: allow generating subsystem test resports
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 14:26:04 +02:00
Joachim Wiberg 249af70dcd test: add intro blurb to each subsystem
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 14:26:04 +02:00
Joachim Wiberg b4a5b35c0e test/spec: refactor test spec & report generation
Import new 9pm version for improved test report generation.  With this
in place we can take the opportunity to also refactor and simplify the
test spec. generation.

Fixes #1129

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 14:26:03 +02:00
Joachim Wiberg fe2d0f488d test/case: simplify AsciiDoc image references
This commit greatly simplifies AsciiDoc image references in generated
Readme.adoc files.  The two focused use-cases that remain after this
change are working references in:

 - Generated output/images/test-report.pdf
 - Viewing test's Readme.adoc from GitHub

Previously we aimed to have working images also when the test's Readme
was included in the parent directory's Readme.adoc.  This, however, is
not supported as of this commit.  It seems unlikely also to ever be a
supported feature of AsciiDoc on GitHub, for details, see the following
issue: <https://github.com/github/markup/issues/1095>

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 06:40:38 +02:00
Joachim Wiberg c14140a8fe 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-09-15 06:40:38 +02:00
Joachim Wiberg 9fe661f422 test/infamy: silence schema download in restconf backend
NETCONF backedn already silenced.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 06:40:38 +02:00
Joachim Wiberg 87f9b58957 test/spec: pep-8 fixes, minor cleanup only
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-15 06:40:37 +02:00
Joachim Wiberg b657183209 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-15 06:40:37 +02:00
Joachim Wiberg 1aad255846 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-15 06:40:36 +02:00
Joachim Wiberg 52beeb5814 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-09-15 06:40:35 +02:00
Joachim Wiberg c1ad8075a8 board/common: ensure efi-part.vfat is saved in build tree
Fixes #1132

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-14 22:05:23 +02:00
Mattias WalströmandGitHub 3fcb6a376a Merge pull request #1142 from kernelkit/banana-pi
Add support for Banana Pi (BPi-R3)
2025-09-14 14:56:12 +02:00
Mattias Walström 33e873c2fa workflows: create-image: add banana pi r3 as target board
No eMMC support added yet, only SD card.
2025-09-14 13:37:49 +02:00
Mattias Walström 56fce18d66 workflows/build-boot: Add mt7976 bootloader
It is the chipset used on banana pi r3.
2025-09-14 13:37:49 +02:00
Mattias Walström 399894e9a1 Add new board banana pi r3
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
2025-09-14 13:37:48 +02:00
Mattias Walström 8146e0570e gen-interfaces: Fix bug when generating on boards with mounted Wi-Fi chipsets
These should not appear in a generated factory-config or failure-config.
2025-09-12 23:23:14 +02:00
Mattias WalströmandGitHub 153ceda2f4 Merge pull request #1153 from kernelkit/upgrade-buildroot
Upgrade buildroot and linux kernel
2025-09-11 14:12:49 +02:00
Mattias Walström b134733dd2 Upgrade Buildroot to 2025.02.6 (LTS) 2025-09-11 09:21:39 +02:00
Mattias Walström 8082b271d9 Upgrade Linux kernel to 6.12.46 (LTS) 2025-09-11 09:21:38 +02:00
Mattias WalströmandGitHub 02d8c97e98 Merge pull request #1145 from kernelkit/add-resize
Add resize2fs to be able to resize ext partitions
2025-09-08 10:44:09 +02:00
Mattias Walström 7bb3f64191 Add resize2fs to be able to resize ext partitions
This is useful when using sdcard images, to expand /var to cover rest of disk.
2025-09-08 08:27:16 +02:00
Joachim WibergandGitHub f2af4d6cdf Merge pull request #1141 from kernelkit/defconfig-branding
Update factory-configs and links and description in defconfigs
2025-09-07 23:00:14 +02:00
Joachim Wiberg 4417465135 Update factory-config.cfg for NanoPi and RPi
The motd-banner was behind on the new slogan:

.-------.
|  . .  | Infix OS — Immutable.Friendly.Secure
|-. v .-| https://kernelkit.org
'-'---'-'

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-07 19:30:21 +02:00
Joachim Wiberg cf50c75a3e Update links to vendor + docs, and a more relevant description
Fixes #1134

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-07 19:30:12 +02:00
Joachim WibergandGitHub 7ab265992a Merge pull request #1140 from kernelkit/more-logo-sizes 2025-09-07 18:43:08 +02:00
Joachim Wiberg 35cf9e4cc9 doc: add more logos for convenience
This commit completes the collection.  We now have small, medium, and
large sizes.  Useful for easy conversion to other formats, e.g., bmp.

[skip ci]

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-07 18:21:36 +02:00
Mattias WalströmandGitHub 3fbbd16690 Merge pull request #1118 from kernelkit/create-sdcard
Create sdcard
2025-09-07 18:07:40 +02:00
Mattias Walström 01029321c9 board: rpi4: Add splashscreen on boot
Show splash in uboot.
2025-09-07 18:06:43 +02:00
Mattias Walström 0477446e49 package/bootloader-splashscreen: New packet for adding/modify bootloader splash 2025-09-07 18:06:43 +02:00
Mattias Walström 700ec2f855 Add crete SD Card workflow 2025-09-07 13:43:27 +02:00
Mattias Walström 709f2769ce Build aux when build infix 2025-09-07 13:43:26 +02:00
Mattias Walström 19b965ac41 RPI4: dts: Create a new device tree for the DSI display 2025-09-07 13:43:26 +02:00
Mattias Walström 28d8748bdb uboot: Att uboot patches for 2025.01
- Fix CTRL-C misbehave
- Add new command to detect RPI standard display
2025-09-07 13:43:25 +02:00
Mattias WalströmandGitHub 497990a2d3 Merge pull request #1138 from kernelkit/test-sanity 2025-09-05 18:55:18 +02:00
Mattias Walström dce9a9c53d 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-05 16:34:11 +02:00
Mattias Walström 4f0aded5a0 utils: Add script to add a new version to GNS3 marketplace
You need first to clone gns3-registry from https://github.com/GNS3/gns3-registry

Follow this steps:
1. ./utils/bump-gns3.py 25.08.0 ../../gns3/gns3-registry/appliances/infix.gns3a
2. TEST in gns3, just add the new version, start it.
3. run python3 check.py inside gns3-registry, verify infix is ok.
4. Create a pull request to gns3-registry

This fix #1107
2025-09-04 17:48:43 +02:00
Mattias Walström 73298469d5 Upgrade Linux kernel to 6.12.45 (LTS) 2025-09-04 17:48:42 +02:00
542 changed files with 7175 additions and 27997 deletions
+1
View File
@@ -20,6 +20,7 @@ jobs:
- cn9130_crb_boot
- aarch64_qemu_boot
- rpi4_boot
- mt7986_sd_boot
env:
MAKEFLAGS: -j5
steps:
+225
View File
@@ -0,0 +1,225 @@
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
+4 -9
View File
@@ -39,7 +39,7 @@ env:
jobs:
test:
name: Regression Test ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.name || inputs.name }} ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.target || inputs.target }}
runs-on: [ self-hosted, regression ]
runs-on: [self-hosted, regression]
steps:
- name: Checkout infix repo
uses: actions/checkout@v4
@@ -87,7 +87,7 @@ jobs:
fi
make test
- name: Publish Test Result for ${{ env.TARGET }}
- name: Publish Test Result for ${{ env.TARGET }}
# Ensure this runs even if Regression Test fails
if: always()
run: cat $TEST_PATH/.log/last/result-gh.md >> $GITHUB_STEP_SUMMARY
@@ -96,15 +96,10 @@ jobs:
# Ensure this runs even if Regression Test fails
if: always()
run: |
asciidoctor-pdf \
--theme $TEST_PATH/9pm/report/theme.yml \
-a pdf-fontsdir=$TEST_PATH/9pm/report/fonts \
$TEST_PATH/.log/last/report.adoc \
-o $TEST_PATH/.log/last/report.pdf
make test-dir="$(pwd)/$TEST_PATH" test-report
- name: Upload Test Report as Artifact
uses: actions/upload-artifact@v4
with:
name: test-report
path: ${{ env.TEST_PATH }}/.log/last/report.pdf
path: output/images/test-report.pdf
+4
View File
@@ -9,6 +9,10 @@ on:
- ci-work
workflow_dispatch:
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build-x86_64:
if: startsWith(github.repository, 'kernelkit/')
+7 -3
View File
@@ -112,10 +112,12 @@ containers for any custom functionality you need.
### Supported Platforms
- **Raspberry Pi 4B** - Perfect for home labs, learning, and prototyping
- **NanoPi R2S** - Compact dual-port router in a tiny package
- **Banana Pi-R3** - Your next home router and gateway
- **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 ARM platforms
- **Microchip SparX-5i, NXP i.MX8MP EVK** - Enterprise switching capabilities
- **Marvell CN9130 CRB, EspressoBIN** - High-performance ARM64 platforms
- **Microchip SparX-5i** - Enterprise switching capabilities
- **NXP i.MX8MP EVK** - Highly capable ARM64 SoC
- **StarFive VisionFive2** - RISC-V architecture support
*Why start with Raspberry Pi?* It's affordable, widely available, has
@@ -148,6 +150,8 @@ 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/
+2 -45
View File
@@ -4,51 +4,8 @@ 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-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/
- [Raspberry Pi 4 b](raspberry-pi-4/)
+27
View File
@@ -0,0 +1,27 @@
# 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)
@@ -153,7 +153,7 @@
}
]
},
"infix-system:motd-banner": "Li0tLS0tLS0uCnwgIC4gLiAgfCBJbmZpeCAtLSBhIE5ldHdvcmsgT3BlcmF0aW5nIFN5c3RlbQp8LS4gdiAuLXwgaHR0cHM6Ly9rZXJuZWxraXQuZ2l0aHViLmlvCictJy0tLSctJwo="
"infix-system:motd-banner": "Li0tLS0tLS0uCnwgIC4gLiAgfCBJbmZpeCBPUyDigJQgSW1tdXRhYmxlLkZyaWVuZGx5LlNlY3VyZQp8LS4gdiAuLXwgaHR0cHM6Ly9rZXJuZWxraXQub3JnCictJy0tLSctJwo="
},
"infix-dhcp-client:dhcp-client": {
"client-if": [
+46
View File
@@ -0,0 +1,46 @@
# 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,6 +6,12 @@ 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
+2 -2
View File
@@ -83,7 +83,7 @@ genboot()
{
if [ -d "$bootdata" ]; then
bootimg=$(cat <<EOF
image efi-part.vfat {
image $BINARIES_DIR/efi-part.vfat {
size = $bootsize
vfat {
file EFI {
@@ -98,7 +98,7 @@ EOF
offset = $bootoffs
partition-type-uuid = U
bootable = true
image = efi-part.vfat
image = $BINARIES_DIR/efi-part.vfat
}
EOF
)
+18 -2
View File
@@ -15,18 +15,34 @@ 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 "$fn"
less +G -r "$fn"
}
follow()
{
local fn="/var/log/syslog"
[ -n "$1" ] && fn="/var/log/$1"
tail -F "$fn"
less +F -r "$fn"
}
_logfile_completions()
@@ -0,0 +1,3 @@
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 log:prio:user.notice \
env:-/etc/default/rauc \
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 -F $SYSLOGD_ARGS -- System log daemon
[S0123456789] syslogd -8 -F $SYSLOGD_ARGS -- System log daemon
+1
View File
@@ -0,0 +1 @@
../available/netconf.conf
+66
View File
@@ -0,0 +1,66 @@
# /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
+2 -1
View File
@@ -5,10 +5,11 @@
# -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 "$@"
View File
@@ -1,12 +1,49 @@
#!/bin/sh
#!/bin/bash
IFQUIRKSFILE=${IFQUIRKSFILE:-/etc/product/interface-quirks.json}
if [ $# -lt 2 ]; then
echo "usage: $0 <quirk-name> <ifname>"
exit 1
fi
quirk=$1
ifname=$2
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
[ -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"
@@ -34,6 +34,12 @@ 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,12 +23,22 @@ fi
log()
{
logger -I $$ -t udhcpc -p user.notice "$*"
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}: $*"
}
wait_for_ipv6_default_route()
{
log "waiting for IPv6 default route to be installed."
dbg "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
@@ -37,7 +47,7 @@ wait_for_ipv6_default_route()
printf "."
: $((IF_WAIT_DELAY -= 1))
done
log "Timed out witing for IPv6 default route!"
err "Timed out waiting for IPv6 default route!"
}
# RFC3442: If the DHCP server returns both a Classless
@@ -50,7 +60,7 @@ set_dhcp_routes()
# format: dest1/mask gw1 ... destn/mask gwn
set -- $staticroutes
while [ -n "$1" -a -n "$2" ]; do
log "adding route $1 via $2 metric $metric tag 100"
dbg "adding route $1 via $2 metric $metric tag 100"
echo "ip route $1 $2 $metric tag 100" >> "$NEXT"
shift 2
done
@@ -69,7 +79,7 @@ set_dhcp_routes()
clr_dhcp_routes()
{
log "deleting DHCP routes from $interface"
log "deleting DHCP routes"
[ -f "$NAME" ] || return
rm "$NAME"
@@ -84,7 +94,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 from $interface"
log "removing $ip/$prefix"
ip addr del "$ip/$prefix" dev "$interface"
done
}
@@ -142,12 +152,12 @@ case "$ACTION" in
fi
if [ -n "$search_list" ]; then
log "adding search $search_list"
dbg "adding search $search_list"
echo "search $search_list # $interface" >> $RESOLV_CONF
fi
for i in $dns ; do
log "adding dns $i"
dbg "adding dns $i"
echo "nameserver $i # $interface" >> $RESOLV_CONF
resolvconf -u
done
@@ -155,7 +165,7 @@ case "$ACTION" in
if [ -n "$ntpsrv" ]; then
truncate -s 0 "$NTPFILE"
for srv in $ntpsrv; do
log "got NTP server $srv"
dbg "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 -b ${loadaddr} ${filesize} BOOT_ORDER DEBUG ethact
env import -c ${loadaddr} ${filesize} BOOT_ORDER DEBUG ethact
fi
if test -n "${DEBUG}"; then
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

@@ -6,6 +6,12 @@ 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
+6
View File
@@ -7,6 +7,12 @@ 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
+14 -10
View File
@@ -8,6 +8,7 @@ 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,16 +28,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.44"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.49"
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
@@ -61,6 +62,7 @@ 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
@@ -73,11 +75,13 @@ 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
@@ -97,8 +101,10 @@ 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
@@ -125,23 +131,24 @@ 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://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_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_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://github.com/kernelkit/infix/tree/main/doc"
INFIX_DOC="https://kernelkit.org/infix/"
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
@@ -163,14 +170,11 @@ 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
+4 -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.44"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.49"
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
@@ -107,10 +107,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://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_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_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://github.com/kernelkit/infix/tree/main/doc"
INFIX_DOC="https://kernelkit.org/infix/"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_ALDER_ALDER=y
BR2_PACKAGE_MARVELL_CN9130_CRB=y
+43
View File
@@ -0,0 +1,43 @@
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
+9 -4
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.44"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.49"
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,6 +44,7 @@ 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
@@ -65,6 +66,7 @@ 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
@@ -91,10 +93,12 @@ 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
@@ -117,6 +121,7 @@ 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
@@ -168,11 +173,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://github.com/kernelkit"
INFIX_VENDOR_HOME="https://kernelkit.org"
INFIX_IMAGE_ID="${INFIX_ID}-r2s"
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_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_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://github.com/kernelkit/infix/tree/main/doc"
INFIX_DOC="https://kernelkit.org/infix/"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_CONFD=y
BR2_PACKAGE_GENCERT=y
+15 -11
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_TARGET_ENABLE_ROOT_LOGIN is not set
BR2_ROOTFS_MERGED_USR=y
# BR2_TARGET_ENABLE_ROOT_LOGIN is not set
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,19 +60,22 @@ 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_LIBXCRYPT=y
BR2_PACKAGE_LIBSSH2_OPENSSL=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
@@ -85,11 +88,13 @@ 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
@@ -109,8 +114,10 @@ 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
@@ -161,16 +168,14 @@ 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://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_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_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://github.com/kernelkit/infix/tree/main/doc"
INFIX_DOC="https://kernelkit.org/infix/"
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
@@ -192,12 +197,11 @@ 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_HOST_PYTHON_YANGDOC=y
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
BR2_DOWNLOAD_FORCE_CHECK_HASHES=y
BR2_PACKAGE_GETENT=y
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y
# GNS3_APPLIANCE is not set
+3
View File
@@ -14,6 +14,8 @@ 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
@@ -34,4 +36,5 @@ 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
+14 -10
View File
@@ -7,6 +7,7 @@ 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
@@ -26,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.44"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.49"
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
@@ -34,9 +35,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
@@ -59,6 +60,7 @@ 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
@@ -71,11 +73,13 @@ 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
@@ -95,8 +99,10 @@ 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
@@ -131,18 +137,19 @@ 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://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_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_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://github.com/kernelkit/infix/tree/main/doc"
INFIX_DOC="https://kernelkit.org/infix/"
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
@@ -164,14 +171,11 @@ 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
+4 -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.44"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.49"
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
@@ -114,10 +114,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://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_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_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://github.com/kernelkit/infix/tree/main/doc"
INFIX_DOC="https://kernelkit.org/infix/"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_CONFD=y
BR2_PACKAGE_CONFD_TEST_MODE=y
+33 -1
View File
@@ -3,6 +3,37 @@ Change Log
All notable changes to the project are documented in this file.
[v25.09.0][] - 2025-09-30
-------------------------
### 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
### 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`
[BPI-R3]: https://wiki.banana-pi.org/Banana_Pi_BPI-R3
[v25.08.0][] - 2025-09-01
-------------------------
@@ -1612,7 +1643,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.08.0...HEAD
[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
[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
@@ -0,0 +1,312 @@
<?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>

After

Width:  |  Height:  |  Size: 45 KiB

+312
View File
@@ -0,0 +1,312 @@
<?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>

After

Width:  |  Height:  |  Size: 45 KiB

+1
View File
@@ -42,4 +42,5 @@ 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
+2
View File
@@ -7,5 +7,7 @@ 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
@@ -0,0 +1,10 @@
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
@@ -0,0 +1,40 @@
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,6 +1,7 @@
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
@@ -0,0 +1,12 @@
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/.
@@ -0,0 +1,19 @@
################################################################################
#
# 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,7 +25,3 @@ 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 598089ad964594bbbaf2b7d7214d8761c828174eef53b7cfb1cd98517f407d01 klish-plugin-sysrepo-b693714a1ff5f8021651d7619556afb19945e5e6-git4.tar.gz
sha256 a6fe769a8f24a065b274f4e0f5f1fec634b2aae5bdd586aa6f71fe60ca6b8a64 klish-plugin-sysrepo-30bc6294489807ea026abed20f94d1ad1583cc6b-git4.tar.gz
@@ -4,7 +4,7 @@
#
################################################################################
KLISH_PLUGIN_SYSREPO_VERSION = b693714a1ff5f8021651d7619556afb19945e5e6
KLISH_PLUGIN_SYSREPO_VERSION = 30bc6294489807ea026abed20f94d1ad1583cc6b
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
@@ -0,0 +1,135 @@
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
@@ -1,82 +0,0 @@
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
@@ -1,53 +0,0 @@
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
@@ -1,38 +0,0 @@
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
@@ -1,35 +0,0 @@
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
@@ -1,124 +0,0 @@
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
@@ -1,434 +0,0 @@
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
@@ -1,129 +0,0 @@
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
@@ -1,652 +0,0 @@
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
@@ -1,46 +0,0 @@
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
@@ -1,39 +0,0 @@
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
@@ -1,40 +0,0 @@
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
@@ -1,173 +0,0 @@
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
@@ -1,67 +0,0 @@
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
@@ -1,57 +0,0 @@
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
@@ -1,260 +0,0 @@
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
@@ -1,462 +0,0 @@
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
@@ -1,56 +0,0 @@
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
@@ -1,66 +0,0 @@
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 43c84317ba13470f421a90bca74c062cf63a70e488d2e90c432b662c4638a14e libyang-cpp-v3.tar.gz
sha256 70fd0df940026fb930d5407abe679e064caf4701bff35d79465b9ad2c0915808 libyang-cpp-v4.tar.gz
+1 -1
View File
@@ -3,7 +3,7 @@
# CPP bindings for libyang
#
################################################################################
LIBYANG_CPP_VERSION = v3
LIBYANG_CPP_VERSION = v4
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,3 +5,8 @@ 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
+1 -2
View File
@@ -1,4 +1,3 @@
# Locally calculated
sha256 6b94f3abc1aabd0c72a7c7d92a77f79dda7c8a0cb3df839a97890b4116a2de2a COPYING
sha256 d971c538a31eae5714d2434d90f86794f267615e85e8d2bb0c383e83c300e1ce nghttp2-asio-e877868abe06a83ed0a6ac6e245c07f6f20866b5-git4.tar.gz
sha256 883af672c1d210398170749a8ccda30103b2894bcd4eccd80f36771aad0b15c3 nghttp2-asio-2173b82e6caa85950c769eecc5da6809faadf61a-git4.tar.gz
+2 -2
View File
@@ -4,8 +4,8 @@
#
################################################################################
NGHTTP2_ASIO_VERSION = e877868abe06a83ed0a6ac6e245c07f6f20866b5
NGHTTP2_ASIO_SITE = https://github.com/kernelkit/nghttp2-asio.git
NGHTTP2_ASIO_VERSION = 2173b82e6caa85950c769eecc5da6809faadf61a
NGHTTP2_ASIO_SITE = https://github.com/CESNET/nghttp2-asio
NGHTTP2_ASIO_SITE_METHOD = git
NGHTTP2_ASIO_LICENSE = MIT
NGHTTP2_ASIO_LICENSE_FILES = COPYING
@@ -0,0 +1,64 @@
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
@@ -1,199 +0,0 @@
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
@@ -0,0 +1,137 @@
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
@@ -1,50 +0,0 @@
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
@@ -0,0 +1,172 @@
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
@@ -1,34 +0,0 @@
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
@@ -0,0 +1,374 @@
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
@@ -1,153 +0,0 @@
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
@@ -1,428 +0,0 @@
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
@@ -0,0 +1,51 @@
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
@@ -0,0 +1,425 @@
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
@@ -1,300 +0,0 @@
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
@@ -1,32 +0,0 @@
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
@@ -0,0 +1,202 @@
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
@@ -0,0 +1,101 @@
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
@@ -1,721 +0,0 @@
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
@@ -1,36 +0,0 @@
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
@@ -0,0 +1,29 @@
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
@@ -1,45 +0,0 @@
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
@@ -0,0 +1,112 @@
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
@@ -0,0 +1,92 @@
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
@@ -1,30 +0,0 @@
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
@@ -0,0 +1,99 @@
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
@@ -1,205 +0,0 @@
From 60eac2b2d60f8f1918a0914272975dd53f527c01 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 19:36:10 +0100
Subject: [PATCH 12/44] tests: extend clientRequest wrappers interface and use
it
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
This is a preparation for refactoring in the next few commits.
I will generalize the clientRequest interface to accept server
address and port too.
The head/get/put/post/... helper methods will not require those server
port and address parameters.
Change-Id: Iee54a3b3017ef9875fcd20640b74c7aa42813b9f
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
tests/aux-utils.h | 34 +++++++++++++++++---------------
tests/restconf-nacm.cpp | 14 +++++--------
tests/restconf-notifications.cpp | 12 +++++------
tests/restconf-yang-schema.cpp | 19 ++++++++++--------
4 files changed, 40 insertions(+), 39 deletions(-)
diff --git a/tests/aux-utils.h b/tests/aux-utils.h
index 7482945..9afd7bc 100644
--- a/tests/aux-utils.h
+++ b/tests/aux-utils.h
@@ -153,12 +153,14 @@ const ng::header_map eventStreamHeaders {
#define ACCESS_CONTROL_ALLOW_ORIGIN {"access-control-allow-origin", "*"}
#define ACCEPT_PATCH {"accept-patch", "application/yang-data+json, application/yang-data+xml, application/yang-patch+xml, application/yang-patch+json"}
+// this is a test, and the server is expected to reply "soon"
+static const boost::posix_time::time_duration CLIENT_TIMEOUT = boost::posix_time::seconds(3);
+
Response clientRequest(auto method,
auto uri,
const std::string& data,
const std::map<std::string, std::string>& headers,
- // this is a test, and the server is expected to reply "soon"
- const boost::posix_time::time_duration timeout=boost::posix_time::seconds(3))
+ const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT)
{
boost::asio::io_service io_service;
auto client = std::make_shared<ng_client::session>(io_service, SERVER_ADDRESS, SERVER_PORT);
@@ -199,39 +201,39 @@ Response clientRequest(auto method,
return {statusCode, resHeaders, oss.str()};
}
-Response get(auto uri, const std::map<std::string, std::string>& headers)
+Response get(auto uri, const std::map<std::string, std::string>& headers, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT)
{
- return clientRequest("GET", uri, "", headers);
+ return clientRequest("GET", uri, "", headers, timeout);
}
-Response options(auto uri, const std::map<std::string, std::string>& headers)
+Response options(auto uri, const std::map<std::string, std::string>& headers, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT)
{
- return clientRequest("OPTIONS", uri, "", headers);
+ return clientRequest("OPTIONS", uri, "", headers, timeout);
}
-Response head(auto uri, const std::map<std::string, std::string>& headers)
+Response head(auto uri, const std::map<std::string, std::string>& headers, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT)
{
- return clientRequest("HEAD", uri, "", headers);
+ return clientRequest("HEAD", uri, "", headers, timeout);
}
-Response put(auto xpath, const std::map<std::string, std::string>& headers, const std::string& data)
+Response put(auto xpath, const std::map<std::string, std::string>& headers, const std::string& data, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT)
{
- return clientRequest("PUT", xpath, data, headers);
+ return clientRequest("PUT", xpath, data, headers, timeout);
}
-Response post(auto xpath, const std::map<std::string, std::string>& headers, const std::string& data)
+Response post(auto xpath, const std::map<std::string, std::string>& headers, const std::string& data, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT)
{
- return clientRequest("POST", xpath, data, headers);
+ return clientRequest("POST", xpath, data, headers, timeout);
}
-Response patch(auto uri, const std::map<std::string, std::string>& headers, const std::string& data)
+Response patch(auto uri, const std::map<std::string, std::string>& headers, const std::string& data, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT)
{
- return clientRequest("PATCH", uri, data, headers);
+ return clientRequest("PATCH", uri, data, headers, timeout);
}
-Response httpDelete(auto uri, const std::map<std::string, std::string>& headers)
+Response httpDelete(auto uri, const std::map<std::string, std::string>& headers, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT)
{
- return clientRequest("DELETE", uri, "", headers);
+ return clientRequest("DELETE", uri, "", headers, timeout);
}
auto manageNacm(sysrepo::Session session)
diff --git a/tests/restconf-nacm.cpp b/tests/restconf-nacm.cpp
index 68497c9..29d7723 100644
--- a/tests/restconf-nacm.cpp
+++ b/tests/restconf-nacm.cpp
@@ -225,9 +225,7 @@ TEST_CASE("NACM")
{
// wrong password: the server should delay its response, so let the client wait "long enough"
const auto start = std::chrono::steady_clock::now();
- REQUIRE(clientRequest("GET",
- RESTCONF_DATA_ROOT "/ietf-system:system",
- "",
+ REQUIRE(get(RESTCONF_DATA_ROOT "/ietf-system:system",
{AUTH_WRONG_PASSWORD},
boost::posix_time::seconds(5))
== Response{401, jsonHeaders, R"({
@@ -251,12 +249,10 @@ TEST_CASE("NACM")
// wrong password: the server should delay its response, in this case let the client terminate its
// request and check that the server doesn't crash
const auto start = std::chrono::steady_clock::now();
- REQUIRE_THROWS_WITH(clientRequest("GET",
- RESTCONF_DATA_ROOT "/ietf-system:system",
- "",
- {AUTH_WRONG_PASSWORD},
- boost::posix_time::milliseconds(100)),
- "HTTP client error: Connection timed out");
+ REQUIRE_THROWS_WITH(get(RESTCONF_DATA_ROOT "/ietf-system:system",
+ {AUTH_WRONG_PASSWORD},
+ boost::posix_time::milliseconds(100)),
+ "HTTP client error: Connection timed out");
auto processingMS = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count();
REQUIRE(processingMS <= 500);
}
diff --git a/tests/restconf-notifications.cpp b/tests/restconf-notifications.cpp
index 905ae01..d479f3c 100644
--- a/tests/restconf-notifications.cpp
+++ b/tests/restconf-notifications.cpp
@@ -277,18 +277,18 @@ TEST_CASE("NETCONF notification streams")
SECTION("Other methods")
{
- REQUIRE(clientRequest("HEAD", "/streams/NETCONF/XML", "", {AUTH_ROOT}) == Response{200, eventStreamHeaders, ""});
- REQUIRE(clientRequest("OPTIONS", "/streams/NETCONF/XML", "", {AUTH_ROOT}) == Response{200, Response::Headers{ACCESS_CONTROL_ALLOW_ORIGIN, {"allow", "GET, HEAD, OPTIONS"}}, ""});
+ REQUIRE(head("/streams/NETCONF/XML", {AUTH_ROOT}) == Response{200, eventStreamHeaders, ""});
+ REQUIRE(options("/streams/NETCONF/XML", {AUTH_ROOT}) == Response{200, Response::Headers{ACCESS_CONTROL_ALLOW_ORIGIN, {"allow", "GET, HEAD, OPTIONS"}}, ""});
const std::multimap<std::string, std::string> headers = {
{"access-control-allow-origin", "*"},
{"allow", "GET, HEAD, OPTIONS"},
{"content-type", "text/plain"},
};
- REQUIRE(clientRequest("PUT", "/streams/NETCONF/XML", "", {AUTH_ROOT}) == Response{405, headers, "Method not allowed."});
- REQUIRE(clientRequest("POST", "/streams/NETCONF/XML", "", {AUTH_ROOT}) == Response{405, headers, "Method not allowed."});
- REQUIRE(clientRequest("PATCH", "/streams/NETCONF/XML", "", {AUTH_ROOT}) == Response{405, headers, "Method not allowed."});
- REQUIRE(clientRequest("DELETE", "/streams/NETCONF/XML", "", {AUTH_ROOT}) == Response{405, headers, "Method not allowed."});
+ REQUIRE(put("/streams/NETCONF/XML", {AUTH_ROOT}, "") == Response{405, headers, "Method not allowed."});
+ REQUIRE(post("/streams/NETCONF/XML", {AUTH_ROOT}, "") == Response{405, headers, "Method not allowed."});
+ REQUIRE(patch("/streams/NETCONF/XML", {AUTH_ROOT}, "") == Response{405, headers, "Method not allowed."});
+ REQUIRE(httpDelete("/streams/NETCONF/XML", {AUTH_ROOT}) == Response{405, headers, "Method not allowed."});
}
SECTION("Invalid URLs")
diff --git a/tests/restconf-yang-schema.cpp b/tests/restconf-yang-schema.cpp
index 73821e0..6e374b1 100644
--- a/tests/restconf-yang-schema.cpp
+++ b/tests/restconf-yang-schema.cpp
@@ -156,11 +156,14 @@ TEST_CASE("obtaining YANG schemas")
{
SECTION("unsupported methods")
{
- for (const std::string httpMethod : {"POST", "PUT", "PATCH", "DELETE"}) {
- CAPTURE(httpMethod);
- REQUIRE(clientRequest(httpMethod, YANG_ROOT "/ietf-yang-library@2019-01-04", "", {AUTH_ROOT})
- == Response{405, Response::Headers{ACCESS_CONTROL_ALLOW_ORIGIN, {"allow", "GET, HEAD, OPTIONS"}}, ""});
- }
+ REQUIRE(post(YANG_ROOT "/ietf-yang-library@2019-01-04", {AUTH_ROOT}, "")
+ == Response{405, Response::Headers{ACCESS_CONTROL_ALLOW_ORIGIN, {"allow", "GET, HEAD, OPTIONS"}}, ""});
+ REQUIRE(put(YANG_ROOT "/ietf-yang-library@2019-01-04", {AUTH_ROOT}, "")
+ == Response{405, Response::Headers{ACCESS_CONTROL_ALLOW_ORIGIN, {"allow", "GET, HEAD, OPTIONS"}}, ""});
+ REQUIRE(patch(YANG_ROOT "/ietf-yang-library@2019-01-04", {AUTH_ROOT}, "")
+ == Response{405, Response::Headers{ACCESS_CONTROL_ALLOW_ORIGIN, {"allow", "GET, HEAD, OPTIONS"}}, ""});
+ REQUIRE(httpDelete(YANG_ROOT "/ietf-yang-library@2019-01-04", {AUTH_ROOT})
+ == Response{405, Response::Headers{ACCESS_CONTROL_ALLOW_ORIGIN, {"allow", "GET, HEAD, OPTIONS"}}, ""});
}
REQUIRE(options(YANG_ROOT "/ietf-yang-library@2019-01-04", {}) == Response{200, Response::Headers{ACCESS_CONTROL_ALLOW_ORIGIN, {"allow", "GET, HEAD, OPTIONS"}}, ""});
@@ -190,12 +193,12 @@ TEST_CASE("obtaining YANG schemas")
SECTION("auth failure")
{
// wrong password
- REQUIRE(clientRequest("GET", YANG_ROOT "/ietf-system@2014-08-06", "", {AUTH_WRONG_PASSWORD}, boost::posix_time::seconds{5})
+ REQUIRE(get(YANG_ROOT "/ietf-system@2014-08-06", {AUTH_WRONG_PASSWORD}, boost::posix_time::seconds{5})
== Response{401, plaintextHeaders, "Access denied."});
- REQUIRE(clientRequest("HEAD", YANG_ROOT "/ietf-system@2014-08-06", "", {AUTH_WRONG_PASSWORD}, boost::posix_time::seconds{5})
+ REQUIRE(head(YANG_ROOT "/ietf-system@2014-08-06", {AUTH_WRONG_PASSWORD}, boost::posix_time::seconds{5})
== Response{401, plaintextHeaders, ""});
// anonymous request
- REQUIRE(clientRequest("HEAD", YANG_ROOT "/ietf-system@2014-08-06", "", {FORWARDED}, boost::posix_time::seconds{5})
+ REQUIRE(head(YANG_ROOT "/ietf-system@2014-08-06", {FORWARDED}, boost::posix_time::seconds{5})
== Response{401, plaintextHeaders, ""});
}
}
--
2.43.0
@@ -0,0 +1,78 @@
From ba43271df7cc83806a08d6937719544786fd331d Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Sun, 28 Sep 2025 06:04:12 +0200
Subject: [PATCH 13/13] Add '--log-level LEVEL' command line option
Organization: Wires
The default 'trace' log level is quite verbose for production systems.
This commit changes the default to 'info' and adds a command line option
to control the log level.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
src/restconf/main.cpp | 27 +++++++++++++++++++++++++--
1 file changed, 25 insertions(+), 2 deletions(-)
diff --git a/src/restconf/main.cpp b/src/restconf/main.cpp
index 477e846..f253421 100644
--- a/src/restconf/main.cpp
+++ b/src/restconf/main.cpp
@@ -26,11 +26,12 @@
static const char usage[] =
R"(Rousette - RESTCONF server
Usage:
- rousette [--syslog] [--timeout <SECONDS>] [--help]
+ rousette [--syslog] [--timeout <SECONDS>] [--log-level <LEVEL>] [--help]
Options:
-h --help Show this screen.
-t --timeout <SECONDS> Change default timeout in sysrepo (if not set, use sysrepo internal).
--syslog Log to syslog.
+ --log-level <LEVEL> Set log level (trace, debug, info, warn, error, critical, off) [default: info].
)";
#ifdef HAVE_SYSTEMD
@@ -75,10 +76,32 @@ int main(int argc, char* argv [])
{
auto args = docopt::docopt(usage, {argv + 1, argv + argc}, true,""/* version */, true);
auto timeout = std::chrono::milliseconds{0};
+ auto logLevel = spdlog::level::info;
if (args["--timeout"]) {
timeout = std::chrono::milliseconds{args["--timeout"].asLong() * 1000};
}
+
+ if (args["--log-level"]) {
+ auto levelStr = args["--log-level"].asString();
+ if (levelStr == "trace") {
+ logLevel = spdlog::level::trace;
+ } else if (levelStr == "debug") {
+ logLevel = spdlog::level::debug;
+ } else if (levelStr == "info") {
+ logLevel = spdlog::level::info;
+ } else if (levelStr == "warn" || levelStr == "warning") {
+ logLevel = spdlog::level::warn;
+ } else if (levelStr == "error" || levelStr == "err") {
+ logLevel = spdlog::level::err;
+ } else if (levelStr == "critical") {
+ logLevel = spdlog::level::critical;
+ } else if (levelStr == "off") {
+ logLevel = spdlog::level::off;
+ } else {
+ throw std::runtime_error("Invalid log level: " + levelStr + ". Valid levels: trace, debug, info, warn, error, critical, off");
+ }
+ }
if (args["--syslog"].asBool()) {
auto syslog_sink = std::make_shared<spdlog::sinks::syslog_sink_mt>("rousette", LOG_PID, LOG_USER, true);
auto logger = std::make_shared<spdlog::logger>("rousette", syslog_sink);
@@ -94,7 +117,7 @@ int main(int argc, char* argv [])
auto logger = std::make_shared<spdlog::logger>("rousette", stdout_sink);
spdlog::set_default_logger(logger);
}
- spdlog::set_level(spdlog::level::trace);
+ spdlog::set_level(logLevel);
/* We will parse URIs using boost::spirit's alnum/alpha/... matchers which are locale-dependent.
* Let's use something stable no matter what the system is using
--
2.43.0
@@ -1,586 +0,0 @@
From 0beaee041fd4fcfbebcbb2912eb6b26ec71f50c7 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:01:54 +0100
Subject: [PATCH 13/44] tests: move stuff from the header file into cpp file
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Allright, I have had enough. I am no longer waiting for a minute for a
recompilation of one test.
This patch splits the aux-utils.h file into a cpp file and two headers.
One of the headers is only for declarations of the functions and
datatypes in the cpp file, the second header implements all the helper
functions that require SERVER_PORT and further helper constants for the
restconf tests.
Compile times are a little better. I have measured compilation times of
two arbitrary restconf tests with asan+ubsan (and ccache disabled):
- restconf-reading: 17.05s -> 13.85s
- restconf-delete: 15.18s -> 11.87s
Not ideal, but it is certainly better.
Change-Id: If529cbc8954d50494711a408231ea4c2c4daf072
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 1 +
tests/aux-utils.h | 214 ++-----------------------------
tests/restconf-notifications.cpp | 3 +-
tests/restconf_utils.cpp | 157 +++++++++++++++++++++++
tests/restconf_utils.h | 83 ++++++++++++
5 files changed, 256 insertions(+), 202 deletions(-)
create mode 100644 tests/restconf_utils.cpp
create mode 100644 tests/restconf_utils.h
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 731d7cb..b8a41a7 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -158,6 +158,7 @@ if(BUILD_TESTING)
add_library(DoctestIntegration STATIC
tests/datastoreUtils.cpp
tests/doctest_integration.cpp
+ tests/restconf_utils.cpp
tests/trompeloeil_doctest.h
tests/wait-a-bit-longer.cpp
)
diff --git a/tests/aux-utils.h b/tests/aux-utils.h
index 9afd7bc..d99b3f9 100644
--- a/tests/aux-utils.h
+++ b/tests/aux-utils.h
@@ -8,99 +8,16 @@
#pragma once
#include "trompeloeil_doctest.h"
-#include <iostream>
#include <nghttp2/asio_http2_client.h>
-#include <optional>
-#include <sstream>
-#include <sysrepo-cpp/Session.hpp>
-#include "tests/UniqueResource.h"
+#include "restconf_utils.h"
-using namespace std::string_literals;
-namespace ng = nghttp2::asio_http2;
-namespace ng_client = ng::client;
-
-struct Response {
- int statusCode;
- ng::header_map headers;
- std::string data;
-
- using Headers = std::multimap<std::string, std::string>;
-
- Response(int statusCode, const Headers& headers, const std::string& data)
- : Response(statusCode, transformHeaders(headers), data)
- {
- }
-
- Response(int statusCode, const ng::header_map& headers, const std::string& data)
- : statusCode(statusCode)
- , headers(headers)
- , data(data)
- {
- }
-
- bool equalStatusCodeAndHeaders(const Response& o) const
- {
- // Skipping 'date' header. Its value will not be reproducible in simple tests
- ng::header_map myHeaders(headers);
- ng::header_map otherHeaders(o.headers);
- myHeaders.erase("date");
- otherHeaders.erase("date");
-
- return statusCode == o.statusCode && std::equal(myHeaders.begin(), myHeaders.end(), otherHeaders.begin(), otherHeaders.end(), [](const auto& a, const auto& b) {
- return a.first == b.first && a.second.value == b.second.value; // Skipping 'sensitive' field from ng::header_value which does not seem important for us.
- });
- }
-
- bool operator==(const Response& o) const
- {
- return equalStatusCodeAndHeaders(o) && data == o.data;
- }
-
- static ng::header_map transformHeaders(const Headers& headers)
- {
- ng::header_map res;
- std::transform(headers.begin(), headers.end(), std::inserter(res, res.end()), [](const auto& h) -> std::pair<std::string, ng::header_value> { return {h.first, {h.second, false}}; });
- return res;
- }
-};
-
-namespace doctest {
-
-template <>
-struct StringMaker<ng::header_map> {
- static String convert(const ng::header_map& m)
- {
- std::ostringstream oss;
- oss << "{\n";
- for (const auto& [k, v] : m) {
- oss << "\t"
- << "{\"" << k << "\", "
- << "{\"" << v.value << "\", " << std::boolalpha << v.sensitive << "}},\n";
- }
- oss << "}";
- return oss.str().c_str();
- }
-};
-
-template <>
-struct StringMaker<Response> {
- static String convert(const Response& o)
- {
- std::ostringstream oss;
-
- oss << "{"
- << std::to_string(o.statusCode) << ", "
- << StringMaker<decltype(o.headers)>::convert(o.headers) << ",\n"
- << "\"" << o.data << "\",\n"
- << "}";
-
- return oss.str().c_str();
- }
-};
+namespace sysrepo {
+class Session;
}
+namespace ng = nghttp2::asio_http2;
+
static const auto SERVER_ADDRESS = "::1";
-static const auto SERVER_ADDRESS_AND_PORT = "http://["s + SERVER_ADDRESS + "]" + ":" + SERVER_PORT;
#define AUTH_DWDM {"authorization", "Basic ZHdkbTpEV0RN"}
#define AUTH_NORULES {"authorization", "Basic bm9ydWxlczplbXB0eQ=="}
@@ -145,7 +62,7 @@ const ng::header_map plaintextHeaders{
{"content-type", {"text/plain", false}},
};
-const ng::header_map eventStreamHeaders {
+const ng::header_map eventStreamHeaders{
{"access-control-allow-origin", {"*", false}},
{"content-type", {"text/event-stream", false}},
};
@@ -153,142 +70,37 @@ const ng::header_map eventStreamHeaders {
#define ACCESS_CONTROL_ALLOW_ORIGIN {"access-control-allow-origin", "*"}
#define ACCEPT_PATCH {"accept-patch", "application/yang-data+json, application/yang-data+xml, application/yang-patch+xml, application/yang-patch+json"}
-// this is a test, and the server is expected to reply "soon"
-static const boost::posix_time::time_duration CLIENT_TIMEOUT = boost::posix_time::seconds(3);
-
-Response clientRequest(auto method,
- auto uri,
- const std::string& data,
- const std::map<std::string, std::string>& headers,
- const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT)
-{
- boost::asio::io_service io_service;
- auto client = std::make_shared<ng_client::session>(io_service, SERVER_ADDRESS, SERVER_PORT);
-
- client->read_timeout(timeout);
-
- std::ostringstream oss;
- ng::header_map resHeaders;
- int statusCode;
-
- client->on_connect([&](auto) {
- boost::system::error_code ec;
-
- ng::header_map reqHeaders;
- for (const auto& [name, value] : headers) {
- reqHeaders.insert({name, {value, false}});
- }
-
- auto req = client->submit(ec, method, SERVER_ADDRESS_AND_PORT + uri, data, reqHeaders);
- req->on_response([&](const ng_client::response& res) {
- res.on_data([&oss](const uint8_t* data, std::size_t len) {
- oss.write(reinterpret_cast<const char*>(data), len);
- });
- statusCode = res.status_code();
- resHeaders = res.header();
- });
- 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) {
- throw std::runtime_error{"HTTP client error: " + ec.message()};
- });
- io_service.run();
-
- return {statusCode, resHeaders, oss.str()};
-}
-
Response get(auto uri, const std::map<std::string, std::string>& headers, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT)
{
- return clientRequest("GET", uri, "", headers, timeout);
+ return clientRequest(SERVER_ADDRESS, SERVER_PORT, "GET", uri, "", headers, timeout);
}
Response options(auto uri, const std::map<std::string, std::string>& headers, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT)
{
- return clientRequest("OPTIONS", uri, "", headers, timeout);
+ return clientRequest(SERVER_ADDRESS, SERVER_PORT, "OPTIONS", uri, "", headers, timeout);
}
Response head(auto uri, const std::map<std::string, std::string>& headers, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT)
{
- return clientRequest("HEAD", uri, "", headers, timeout);
+ return clientRequest(SERVER_ADDRESS, SERVER_PORT, "HEAD", uri, "", headers, timeout);
}
Response put(auto xpath, const std::map<std::string, std::string>& headers, const std::string& data, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT)
{
- return clientRequest("PUT", xpath, data, headers, timeout);
+ return clientRequest(SERVER_ADDRESS, SERVER_PORT, "PUT", xpath, data, headers, timeout);
}
Response post(auto xpath, const std::map<std::string, std::string>& headers, const std::string& data, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT)
{
- return clientRequest("POST", xpath, data, headers, timeout);
+ return clientRequest(SERVER_ADDRESS, SERVER_PORT, "POST", xpath, data, headers, timeout);
}
Response patch(auto uri, const std::map<std::string, std::string>& headers, const std::string& data, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT)
{
- return clientRequest("PATCH", uri, data, headers, timeout);
+ return clientRequest(SERVER_ADDRESS, SERVER_PORT, "PATCH", uri, data, headers, timeout);
}
Response httpDelete(auto uri, const std::map<std::string, std::string>& headers, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT)
{
- return clientRequest("DELETE", uri, "", headers, timeout);
-}
-
-auto manageNacm(sysrepo::Session session)
-{
- return make_unique_resource(
- [session]() mutable {
- session.switchDatastore(sysrepo::Datastore::Running);
- session.copyConfig(sysrepo::Datastore::Startup, "ietf-netconf-acm");
- },
- [session]() mutable {
- session.switchDatastore(sysrepo::Datastore::Running);
-
- /* cleanup running DS of ietf-netconf-acm module
- because it contains XPaths to other modules that we
- can't uninstall because the running DS content would be invalid
- */
- session.copyConfig(sysrepo::Datastore::Startup, "ietf-netconf-acm");
- });
-}
-
-void setupRealNacm(sysrepo::Session session)
-{
- session.switchDatastore(sysrepo::Datastore::Running);
- session.setItem("/ietf-netconf-acm:nacm/enable-external-groups", "false");
- session.setItem("/ietf-netconf-acm:nacm/groups/group[name='optics']/user-name[.='dwdm']", "");
- session.setItem("/ietf-netconf-acm:nacm/groups/group[name='yangnobody']/user-name[.='yangnobody']", "");
- session.setItem("/ietf-netconf-acm:nacm/groups/group[name='norules']/user-name[.='norules']", "");
-
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/group[.='yangnobody']", "");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='10']/module-name", "ietf-system");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='10']/action", "permit");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='10']/access-operations", "read");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='10']/path", "/ietf-system:system/contact");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='11']/module-name", "ietf-system");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='11']/action", "permit");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='11']/access-operations", "read");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='11']/path", "/ietf-system:system/hostname");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='12']/module-name", "ietf-system");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='12']/action", "permit");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='12']/access-operations", "read");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='12']/path", "/ietf-system:system/location");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='13']/module-name", "example");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='13']/action", "permit");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='13']/access-operations", "read");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='14']/module-name", "ietf-restconf-monitoring");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='14']/action", "permit");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='14']/access-operations", "read");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/module-name", "example-delete");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/action", "permit");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/access-operations", "read");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/path", "/example-delete:immutable");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='99']/module-name", "*");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='99']/action", "deny");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/group[.='optics']", "");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/rule[name='1']/module-name", "ietf-system");
- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/rule[name='1']/action", "permit"); // overrides nacm:default-deny-* rules in ietf-system model
- session.applyChanges();
+ return clientRequest(SERVER_ADDRESS, SERVER_PORT, "DELETE", uri, "", headers, timeout);
}
diff --git a/tests/restconf-notifications.cpp b/tests/restconf-notifications.cpp
index d479f3c..db9a3f5 100644
--- a/tests/restconf-notifications.cpp
+++ b/tests/restconf-notifications.cpp
@@ -87,7 +87,8 @@ struct SSEClient {
client->on_connect([&, uri, reqHeaders, silenceTimeout](auto) {
boost::system::error_code ec;
- auto req = client->submit(ec, "GET", SERVER_ADDRESS_AND_PORT + uri, "", reqHeaders);
+ static const auto server_address_and_port = std::string("http://[") + SERVER_ADDRESS + "]" + ":" + SERVER_PORT;
+ auto req = client->submit(ec, "GET", server_address_and_port + uri, "", reqHeaders);
req->on_response([&, silenceTimeout](const ng_client::response& res) {
requestSent.count_down();
res.on_data([&, silenceTimeout](const uint8_t* data, std::size_t len) {
diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp
new file mode 100644
index 0000000..ea9a18d
--- /dev/null
+++ b/tests/restconf_utils.cpp
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2023 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@cesnet.cz>
+ * Written by Jan Kundrát <jan.kundrat@cesnet.cz>
+ *
+ */
+
+#include "restconf_utils.h"
+#include "sysrepo-cpp/Session.hpp"
+
+using namespace std::string_literals;
+namespace ng = nghttp2::asio_http2;
+namespace ng_client = ng::client;
+
+Response::Response(int statusCode, const Response::Headers& headers, const std::string& data)
+ : Response(statusCode, transformHeaders(headers), data)
+{
+}
+
+Response::Response(int statusCode, const ng::header_map& headers, const std::string& data)
+ : statusCode(statusCode)
+ , headers(headers)
+ , data(data)
+{
+}
+
+bool Response::equalStatusCodeAndHeaders(const Response& o) const
+{
+ // Skipping 'date' header. Its value will not be reproducible in simple tests
+ ng::header_map myHeaders(headers);
+ ng::header_map otherHeaders(o.headers);
+ myHeaders.erase("date");
+ otherHeaders.erase("date");
+
+ return statusCode == o.statusCode && std::equal(myHeaders.begin(), myHeaders.end(), otherHeaders.begin(), otherHeaders.end(), [](const auto& a, const auto& b) {
+ return a.first == b.first && a.second.value == b.second.value; // Skipping 'sensitive' field from ng::header_value which does not seem important for us.
+ });
+}
+
+bool Response::operator==(const Response& o) const
+{
+ return equalStatusCodeAndHeaders(o) && data == o.data;
+}
+
+ng::header_map Response::transformHeaders(const Response::Headers& headers)
+{
+ ng::header_map res;
+ std::transform(headers.begin(), headers.end(), std::inserter(res, res.end()), [](const auto& h) -> std::pair<std::string, ng::header_value> { return {h.first, {h.second, false}}; });
+ return res;
+}
+
+Response clientRequest(const std::string& server_address,
+ const std::string& server_port,
+ const std::string& method,
+ const std::string& uri,
+ const std::string& data,
+ const std::map<std::string, std::string>& headers,
+ const boost::posix_time::time_duration timeout)
+{
+ boost::asio::io_service io_service;
+ auto client = std::make_shared<ng_client::session>(io_service, server_address, server_port);
+
+ client->read_timeout(timeout);
+
+ std::ostringstream oss;
+ ng::header_map resHeaders;
+ int statusCode;
+
+ client->on_connect([&](auto) {
+ boost::system::error_code ec;
+
+ ng::header_map reqHeaders;
+ for (const auto& [name, value] : headers) {
+ reqHeaders.insert({name, {value, false}});
+ }
+
+ const auto server_address_and_port = std::string("http://[") + server_address + "]" + ":" + server_port;
+ auto req = client->submit(ec, method, server_address_and_port + uri, data, reqHeaders);
+ req->on_response([&](const ng_client::response& res) {
+ res.on_data([&oss](const uint8_t* data, std::size_t len) {
+ oss.write(reinterpret_cast<const char*>(data), len);
+ });
+ statusCode = res.status_code();
+ resHeaders = res.header();
+ });
+ 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) {
+ throw std::runtime_error{"HTTP client error: " + ec.message()};
+ });
+ io_service.run();
+
+ return {statusCode, resHeaders, oss.str()};
+}
+
+UniqueResource manageNacm(sysrepo::Session session)
+{
+ return make_unique_resource(
+ [session]() mutable {
+ session.switchDatastore(sysrepo::Datastore::Running);
+ session.copyConfig(sysrepo::Datastore::Startup, "ietf-netconf-acm");
+ },
+ [session]() mutable {
+ session.switchDatastore(sysrepo::Datastore::Running);
+
+ /* cleanup running DS of ietf-netconf-acm module
+ because it contains XPaths to other modules that we
+ can't uninstall because the running DS content would be invalid
+ */
+ session.copyConfig(sysrepo::Datastore::Startup, "ietf-netconf-acm");
+ });
+}
+
+void setupRealNacm(sysrepo::Session session)
+{
+ session.switchDatastore(sysrepo::Datastore::Running);
+ session.setItem("/ietf-netconf-acm:nacm/enable-external-groups", "false");
+ session.setItem("/ietf-netconf-acm:nacm/groups/group[name='optics']/user-name[.='dwdm']", "");
+ session.setItem("/ietf-netconf-acm:nacm/groups/group[name='yangnobody']/user-name[.='yangnobody']", "");
+ session.setItem("/ietf-netconf-acm:nacm/groups/group[name='norules']/user-name[.='norules']", "");
+
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/group[.='yangnobody']", "");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='10']/module-name", "ietf-system");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='10']/action", "permit");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='10']/access-operations", "read");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='10']/path", "/ietf-system:system/contact");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='11']/module-name", "ietf-system");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='11']/action", "permit");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='11']/access-operations", "read");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='11']/path", "/ietf-system:system/hostname");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='12']/module-name", "ietf-system");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='12']/action", "permit");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='12']/access-operations", "read");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='12']/path", "/ietf-system:system/location");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='13']/module-name", "example");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='13']/action", "permit");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='13']/access-operations", "read");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='14']/module-name", "ietf-restconf-monitoring");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='14']/action", "permit");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='14']/access-operations", "read");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/module-name", "example-delete");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/action", "permit");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/access-operations", "read");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/path", "/example-delete:immutable");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='99']/module-name", "*");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='99']/action", "deny");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/group[.='optics']", "");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/rule[name='1']/module-name", "ietf-system");
+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/rule[name='1']/action", "permit"); // overrides nacm:default-deny-* rules in ietf-system model
+ session.applyChanges();
+}
+
diff --git a/tests/restconf_utils.h b/tests/restconf_utils.h
new file mode 100644
index 0000000..26f0803
--- /dev/null
+++ b/tests/restconf_utils.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@cesnet.cz>
+ * Written by Jan Kundrát <jan.kundrat@cesnet.cz>
+ *
+ */
+
+#pragma once
+#include "trompeloeil_doctest.h"
+#include <nghttp2/asio_http2_client.h>
+#include "UniqueResource.h"
+
+namespace sysrepo {
+class Session;
+}
+
+namespace ng = nghttp2::asio_http2;
+namespace ng_client = ng::client;
+
+struct Response {
+ int statusCode;
+ ng::header_map headers;
+ std::string data;
+
+ using Headers = std::multimap<std::string, std::string>;
+
+ Response(int statusCode, const Headers& headers, const std::string& data);
+ Response(int statusCode, const ng::header_map& headers, const std::string& data);
+ bool equalStatusCodeAndHeaders(const Response& o) const;
+ bool operator==(const Response& o) const;
+ static ng::header_map transformHeaders(const Headers& headers);
+};
+
+namespace doctest {
+
+template <>
+struct StringMaker<ng::header_map> {
+ static String convert(const ng::header_map& m)
+ {
+ std::ostringstream oss;
+ oss << "{\n";
+ for (const auto& [k, v] : m) {
+ oss << "\t"
+ << "{\"" << k << "\", "
+ << "{\"" << v.value << "\", " << std::boolalpha << v.sensitive << "}},\n";
+ }
+ oss << "}";
+ return oss.str().c_str();
+ }
+};
+
+template <>
+struct StringMaker<Response> {
+ static String convert(const Response& o)
+ {
+ std::ostringstream oss;
+
+ oss << "{"
+ << std::to_string(o.statusCode) << ", "
+ << StringMaker<decltype(o.headers)>::convert(o.headers) << ",\n"
+ << "\"" << o.data << "\",\n"
+ << "}";
+
+ return oss.str().c_str();
+ }
+};
+}
+
+// this is a test, and the server is expected to reply "soon"
+static const boost::posix_time::time_duration CLIENT_TIMEOUT = boost::posix_time::seconds(3);
+
+Response clientRequest(
+ const std::string& server_address,
+ const std::string& server_port,
+ const std::string& method,
+ const std::string& uri,
+ const std::string& data,
+ const std::map<std::string, std::string>& headers,
+ const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT);
+
+UniqueResource manageNacm(sysrepo::Session session);
+void setupRealNacm(sysrepo::Session session);
--
2.43.0
@@ -1,169 +0,0 @@
From 53aa2e23ee8acc1881487f3969c2c58fb29437be 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:08:53 +0100
Subject: [PATCH 14/44] tests: rename datastoreUtils to event_watchers
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
The next commit will move RESTCONF notification watchers there, I think
the new name is more appropriate.
Change-Id: Ia8e8cd5fe89bd827fcde4531fe801298bd6f71d2
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 2 +-
tests/{datastoreUtils.cpp => event_watchers.cpp} | 2 +-
tests/{datastoreUtils.h => event_watchers.h} | 0
tests/pretty_printers.h | 2 +-
tests/restconf-defaults.cpp | 2 +-
tests/restconf-delete.cpp | 2 +-
tests/restconf-plain-patch.cpp | 2 +-
tests/restconf-reading.cpp | 2 +-
tests/restconf-rpc.cpp | 2 +-
tests/restconf-writing.cpp | 2 +-
tests/restconf-yang-patch.cpp | 2 +-
11 files changed, 10 insertions(+), 10 deletions(-)
rename tests/{datastoreUtils.cpp => event_watchers.cpp} (98%)
rename tests/{datastoreUtils.h => event_watchers.h} (100%)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b8a41a7..22bce32 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -156,8 +156,8 @@ if(BUILD_TESTING)
include(cmake/SysrepoTest.cmake)
add_library(DoctestIntegration STATIC
- tests/datastoreUtils.cpp
tests/doctest_integration.cpp
+ tests/event_watchers.cpp
tests/restconf_utils.cpp
tests/trompeloeil_doctest.h
tests/wait-a-bit-longer.cpp
diff --git a/tests/datastoreUtils.cpp b/tests/event_watchers.cpp
similarity index 98%
rename from tests/datastoreUtils.cpp
rename to tests/event_watchers.cpp
index 56d8090..c696bc5 100644
--- a/tests/datastoreUtils.cpp
+++ b/tests/event_watchers.cpp
@@ -1,5 +1,5 @@
#include "UniqueResource.h"
-#include "datastoreUtils.h"
+#include "event_watchers.h"
namespace {
void datastoreChanges(auto session, auto& dsChangesMock, auto path)
diff --git a/tests/datastoreUtils.h b/tests/event_watchers.h
similarity index 100%
rename from tests/datastoreUtils.h
rename to tests/event_watchers.h
diff --git a/tests/pretty_printers.h b/tests/pretty_printers.h
index a2befeb..ce64e91 100644
--- a/tests/pretty_printers.h
+++ b/tests/pretty_printers.h
@@ -13,7 +13,7 @@
#include <optional>
#include <sstream>
#include <trompeloeil.hpp>
-#include "datastoreUtils.h"
+#include "event_watchers.h"
#include "restconf/uri.h"
#include "restconf/uri_impl.h"
diff --git a/tests/restconf-defaults.cpp b/tests/restconf-defaults.cpp
index 129dae2..dd8b4da 100644
--- a/tests/restconf-defaults.cpp
+++ b/tests/restconf-defaults.cpp
@@ -11,7 +11,7 @@ static const auto SERVER_PORT = "10087";
#include <spdlog/spdlog.h>
#include "restconf/Server.h"
#include "tests/aux-utils.h"
-#include "tests/datastoreUtils.h"
+#include "tests/event_watchers.h"
TEST_CASE("default handling")
{
diff --git a/tests/restconf-delete.cpp b/tests/restconf-delete.cpp
index 75a6916..4818ff3 100644
--- a/tests/restconf-delete.cpp
+++ b/tests/restconf-delete.cpp
@@ -10,7 +10,7 @@ static const auto SERVER_PORT = "10086";
#include <spdlog/spdlog.h>
#include "restconf/Server.h"
#include "tests/aux-utils.h"
-#include "tests/datastoreUtils.h"
+#include "tests/event_watchers.h"
#include "tests/pretty_printers.h"
TEST_CASE("deleting data")
diff --git a/tests/restconf-plain-patch.cpp b/tests/restconf-plain-patch.cpp
index d4f3952..b550f54 100644
--- a/tests/restconf-plain-patch.cpp
+++ b/tests/restconf-plain-patch.cpp
@@ -10,7 +10,7 @@ static const auto SERVER_PORT = "10089";
#include <spdlog/spdlog.h>
#include "restconf/Server.h"
#include "tests/aux-utils.h"
-#include "tests/datastoreUtils.h"
+#include "tests/event_watchers.h"
#include "tests/pretty_printers.h"
TEST_CASE("Plain patch")
diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp
index d7d507b..e709486 100644
--- a/tests/restconf-reading.cpp
+++ b/tests/restconf-reading.cpp
@@ -11,7 +11,7 @@ static const auto SERVER_PORT = "10081";
#include <nghttp2/asio_http2.h>
#include <spdlog/spdlog.h>
#include "restconf/Server.h"
-#include "tests/datastoreUtils.h"
+#include "tests/event_watchers.h"
TEST_CASE("reading data")
{
diff --git a/tests/restconf-rpc.cpp b/tests/restconf-rpc.cpp
index 9bc1dbc..c4229a0 100644
--- a/tests/restconf-rpc.cpp
+++ b/tests/restconf-rpc.cpp
@@ -10,7 +10,7 @@ static const auto SERVER_PORT = "10084";
#include <spdlog/spdlog.h>
#include "restconf/Server.h"
#include "tests/aux-utils.h"
-#include "tests/datastoreUtils.h"
+#include "tests/event_watchers.h"
#include "tests/pretty_printers.h"
struct RpcCall {
diff --git a/tests/restconf-writing.cpp b/tests/restconf-writing.cpp
index 582a262..0932984 100644
--- a/tests/restconf-writing.cpp
+++ b/tests/restconf-writing.cpp
@@ -10,7 +10,7 @@ static const auto SERVER_PORT = "10083";
#include <spdlog/spdlog.h>
#include "restconf/Server.h"
#include "tests/aux-utils.h"
-#include "tests/datastoreUtils.h"
+#include "tests/event_watchers.h"
#include "tests/pretty_printers.h"
TEST_CASE("writing data")
diff --git a/tests/restconf-yang-patch.cpp b/tests/restconf-yang-patch.cpp
index 7cc8946..2b35c59 100644
--- a/tests/restconf-yang-patch.cpp
+++ b/tests/restconf-yang-patch.cpp
@@ -10,7 +10,7 @@ static const auto SERVER_PORT = "10090";
#include <spdlog/spdlog.h>
#include "restconf/Server.h"
#include "tests/aux-utils.h"
-#include "tests/datastoreUtils.h"
+#include "tests/event_watchers.h"
#include "tests/pretty_printers.h"
TEST_CASE("YANG patch")
--
2.43.0
@@ -1,57 +0,0 @@
From f86df829979a26d5192a86c3b43c1393dcba7140 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Tue, 3 Dec 2024 11:45:41 +0100
Subject: [PATCH 15/44] tests: rename NotificationWatcher
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
This watches notifications going through RESTCONF, so I think
RestconfNotificatonWatcher is better name for this.
Change-Id: Ieb8d7ce489e683348f065e9e84aae06af7c8ccc4
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
tests/restconf-notifications.cpp | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/tests/restconf-notifications.cpp b/tests/restconf-notifications.cpp
index db9a3f5..d873512 100644
--- a/tests/restconf-notifications.cpp
+++ b/tests/restconf-notifications.cpp
@@ -21,11 +21,11 @@ static const auto SERVER_PORT = "10088";
using namespace std::chrono_literals;
-struct NotificationWatcher {
+struct RestconfNotificationWatcher {
libyang::Context ctx;
libyang::DataFormat dataFormat;
- NotificationWatcher(const libyang::Context& ctx)
+ RestconfNotificationWatcher(const libyang::Context& ctx)
: ctx(ctx)
, dataFormat(libyang::DataFormat::JSON)
{
@@ -62,7 +62,7 @@ struct SSEClient {
SSEClient(
boost::asio::io_service& io,
std::latch& requestSent,
- const NotificationWatcher& notification,
+ const RestconfNotificationWatcher& notification,
const std::string& uri,
const std::map<std::string, std::string>& headers,
const boost::posix_time::seconds silenceTimeout = boost::posix_time::seconds(1)) // test code; the server should respond "soon"
@@ -186,7 +186,7 @@ TEST_CASE("NETCONF notification streams")
R"({"example:tlc":{"list":[{"name":"k1","notif":{"message":"nested"}}]}})",
};
- NotificationWatcher netconfWatcher(srConn.sessionStart().getContext());
+ RestconfNotificationWatcher netconfWatcher(srConn.sessionStart().getContext());
SECTION("NETCONF streams")
{
--
2.43.0

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