mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 09:13:01 +02:00
Compare commits
250
Commits
v24.11.1-rc1
...
v25.01.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e89d8ef3e | ||
|
|
028fc578b1 | ||
|
|
52e957d47f | ||
|
|
95cfcaa2fe | ||
|
|
340330b75b | ||
|
|
af8a90d651 | ||
|
|
ee44da6272 | ||
|
|
a5aba02ddd | ||
|
|
cb73ddf6bf | ||
|
|
e45fcfca41 | ||
|
|
27dae1edf0 | ||
|
|
d1e1c9ad33 | ||
|
|
d28fff67f2 | ||
|
|
62d539c424 | ||
|
|
3c1e9f3199 | ||
|
|
b196194e5f | ||
|
|
5ee78e28ad | ||
|
|
bcfd3d4a5b | ||
|
|
f86f184783 | ||
|
|
60f459687c | ||
|
|
455c384164 | ||
|
|
1fef35f77c | ||
|
|
8bc389987c | ||
|
|
ee56ebc205 | ||
|
|
9198383742 | ||
|
|
b010ad04bf | ||
|
|
4458e03cd2 | ||
|
|
4a37369750 | ||
|
|
7af75c0786 | ||
|
|
0fbe225b05 | ||
|
|
2405c002e4 | ||
|
|
1cc9e3c9c8 | ||
|
|
a64c9695cc | ||
|
|
d9be0b3a64 | ||
|
|
8b4682ac8b | ||
|
|
a975da2159 | ||
|
|
e483ddb487 | ||
|
|
267c8e0103 | ||
|
|
25f0d8ca2e | ||
|
|
be0edc00cc | ||
|
|
dc393946ce | ||
|
|
80a522ce08 | ||
|
|
b3ad9e5693 | ||
|
|
cc8c917de1 | ||
|
|
2a3fcbacd0 | ||
|
|
5c9cf276df | ||
|
|
76203b392a | ||
|
|
18913770fe | ||
|
|
2f23baf745 | ||
|
|
a7ce8c2ab4 | ||
|
|
d7ac84cf99 | ||
|
|
04b739b0bd | ||
|
|
131c0b1d4b | ||
|
|
f42c35d8ab | ||
|
|
0187d62246 | ||
|
|
01201708e3 | ||
|
|
986a28c891 | ||
|
|
0bbcd7ec43 | ||
|
|
f9dacaa0ff | ||
|
|
8f6a75806f | ||
|
|
05cb07c0ec | ||
|
|
d6ace7cc73 | ||
|
|
9ef6d2d038 | ||
|
|
c1e6f0c53b | ||
|
|
0e82c108bc | ||
|
|
9427b6a655 | ||
|
|
6a09e2c797 | ||
|
|
3b0b38360c | ||
|
|
3b3fe18f9d | ||
|
|
dedab8c2a2 | ||
|
|
3e0b816351 | ||
|
|
3f8fae2f16 | ||
|
|
70bcebc451 | ||
|
|
6a384bf48b | ||
|
|
10add98d90 | ||
|
|
a2257731ca | ||
|
|
6be990f1bb | ||
|
|
370920e931 | ||
|
|
ee441e172d | ||
|
|
5e90bde8b9 | ||
|
|
0b2f53e3b4 | ||
|
|
1fd55c484a | ||
|
|
1d13c5ea9e | ||
|
|
26303f5168 | ||
|
|
19c1f49fd3 | ||
|
|
5b8b1eec57 | ||
|
|
d83c333b2d | ||
|
|
4598585d4a | ||
|
|
c2959cf41e | ||
|
|
a640460baf | ||
|
|
5d6180a395 | ||
|
|
28c0cee4e3 | ||
|
|
27bba2c47c | ||
|
|
4753e35d7f | ||
|
|
28f67ff608 | ||
|
|
e1a033b97d | ||
|
|
2b7d2b4005 | ||
|
|
39de797b10 | ||
|
|
de9b1cdd2d | ||
|
|
f0c57da4ab | ||
|
|
b5218f7149 | ||
|
|
7cb311b5bc | ||
|
|
254c922254 | ||
|
|
4fb26b1119 | ||
|
|
ad262459b7 | ||
|
|
71b89d06d7 | ||
|
|
72b3058774 | ||
|
|
57c8d0cf1b | ||
|
|
f4c9a57bb5 | ||
|
|
9879e8b685 | ||
|
|
beecff2acf | ||
|
|
6f2face898 | ||
|
|
ddc8282dd7 | ||
|
|
c244042e55 | ||
|
|
78b7b34799 | ||
|
|
e397012394 | ||
|
|
5cc73ab97c | ||
|
|
be1b7cdf45 | ||
|
|
41f3fe15a6 | ||
|
|
df3a55d6a0 | ||
|
|
4e83520b0e | ||
|
|
94cd526772 | ||
|
|
4782cee201 | ||
|
|
907401f6f2 | ||
|
|
9a831217e4 | ||
|
|
1aab75d86a | ||
|
|
147cb713ed | ||
|
|
12b6146551 | ||
|
|
29031be4e9 | ||
|
|
b2db59f3bc | ||
|
|
cf129d1f9c | ||
|
|
41b96e8a01 | ||
|
|
805ddf6c92 | ||
|
|
547a9cd632 | ||
|
|
2ec226640e | ||
|
|
a0e4e813dd | ||
|
|
8812ae7052 | ||
|
|
28bf2d6c3c | ||
|
|
dadf0e2d16 | ||
|
|
c4a79766b7 | ||
|
|
385eab2016 | ||
|
|
bc33d8bc8a | ||
|
|
061fa5fc72 | ||
|
|
ac0acfe7c1 | ||
|
|
f3f313bb98 | ||
|
|
3ef40031dd | ||
|
|
44b60956a8 | ||
|
|
e4535aa856 | ||
|
|
343400c5fe | ||
|
|
bfeeade43b | ||
|
|
b425edffce | ||
|
|
f041d009b5 | ||
|
|
26d06bd13b | ||
|
|
9d51470ee2 | ||
|
|
49a003088b | ||
|
|
c099d887af | ||
|
|
08990556e2 | ||
|
|
cd5b076751 | ||
|
|
081f8e8412 | ||
|
|
465d8cbc82 | ||
|
|
323c77b702 | ||
|
|
780077e729 | ||
|
|
324bcb2533 | ||
|
|
e29a721571 | ||
|
|
389fc96794 | ||
|
|
da260fabf9 | ||
|
|
79bec6877c | ||
|
|
37b97d9e4c | ||
|
|
2e81e99224 | ||
|
|
bea18c2fa9 | ||
|
|
0233fdbf35 | ||
|
|
23ca94ea42 | ||
|
|
a8545e3369 | ||
|
|
1f2973d93f | ||
|
|
e00737ef78 | ||
|
|
a90294fe08 | ||
|
|
77215aeb87 | ||
|
|
7dfb1f131e | ||
|
|
7fecac2973 | ||
|
|
bb939ccdff | ||
|
|
8e38b34c55 | ||
|
|
ff8669e735 | ||
|
|
6a4c8f738e | ||
|
|
03cab6d1e4 | ||
|
|
96779c2620 | ||
|
|
e9a71ec3f7 | ||
|
|
b3da4c50fc | ||
|
|
eb9ae5e4dd | ||
|
|
9591093379 | ||
|
|
68d8c19b53 | ||
|
|
dc2ced8e09 | ||
|
|
02a8c3d78d | ||
|
|
a4ef38fbee | ||
|
|
5608e9457d | ||
|
|
34f0c5a462 | ||
|
|
5675f89d37 | ||
|
|
6d6f4e6dd9 | ||
|
|
f06a42987a | ||
|
|
76ab0fe07a | ||
|
|
734958b89d | ||
|
|
397568f1c8 | ||
|
|
a2c3919c0f | ||
|
|
e08576b008 | ||
|
|
83238aad7e | ||
|
|
1863297b5a | ||
|
|
9cd9440515 | ||
|
|
ec64c1585c | ||
|
|
429c4f1573 | ||
|
|
28ae8fca42 | ||
|
|
c076f0a770 | ||
|
|
219c61081d | ||
|
|
ad32129a13 | ||
|
|
1db039d6b4 | ||
|
|
f023304823 | ||
|
|
85603eb53d | ||
|
|
1f231c2b2a | ||
|
|
967388395f | ||
|
|
94cffd2c48 | ||
|
|
89c5b67a13 | ||
|
|
3436cd68ac | ||
|
|
d16ad7eedc | ||
|
|
e635e3e720 | ||
|
|
42b7cf9f94 | ||
|
|
7d9211a3ba | ||
|
|
1895e161d2 | ||
|
|
7804787201 | ||
|
|
3576450982 | ||
|
|
bb987cfe9e | ||
|
|
997310f9a2 | ||
|
|
bf23f53770 | ||
|
|
e8f9032339 | ||
|
|
80e4018b20 | ||
|
|
27b7d831d0 | ||
|
|
f0ae1aa835 | ||
|
|
88f7c6070a | ||
|
|
a3f85a3872 | ||
|
|
7467b2fcf4 | ||
|
|
3e387d74c2 | ||
|
|
f961bcfe86 | ||
|
|
ce05139452 | ||
|
|
a74eb58000 | ||
|
|
5cafd18b79 | ||
|
|
7334d0e54b | ||
|
|
fffe427497 | ||
|
|
168647ace9 | ||
|
|
0e14e05ae5 | ||
|
|
3ab7a7d531 | ||
|
|
fcef1ead3a | ||
|
|
784b391467 | ||
|
|
6d18ba4c39 |
+3
-3
@@ -6,7 +6,7 @@ project on GitHub, and Discord, see <https://github.com/kernelkit>:
|
||||
|
||||
Support contracts, development of new features, fast-tracking of reviews
|
||||
and contributions, customer branding of Infix, and even customer specific
|
||||
features for dedicated products is provided by Addiva Elektronik.
|
||||
features for dedicated products is provided by _Wires_.
|
||||
|
||||
:globe_with_meridians: <https://www.addiva.se/electronics/>
|
||||
:e-mail: <mailto:ael@addiva.se>
|
||||
:globe_with_meridians: <https://www.wires.se>
|
||||
:e-mail: <mailto:infix@wires.se>
|
||||
|
||||
@@ -22,6 +22,8 @@ jobs:
|
||||
matrix:
|
||||
target: [aarch64, x86_64]
|
||||
fail-fast: false
|
||||
outputs:
|
||||
build_id: ${{ steps.vars.outputs.INFIX_BUILD_ID }}
|
||||
steps:
|
||||
- name: Cleanup Build Folder
|
||||
run: |
|
||||
@@ -38,6 +40,16 @@ jobs:
|
||||
- name: Set Build Variables
|
||||
id: vars
|
||||
run: |
|
||||
if [ -n "${{ github.event.pull_request.head.sha }}" ]; then
|
||||
# Since PRs are built from an internally generated merge
|
||||
# commit, reverse lookups of PRs and/or commits from
|
||||
# image version information are cumbersome. Therefore:
|
||||
# explicitly set a build id that references both the PR
|
||||
# and the commit.
|
||||
printf "INFIX_BUILD_ID=pr%d.%.7s\n" \
|
||||
"${{ github.event.number }}" "${{ github.event.pull_request.head.sha }}" \
|
||||
| tee -a $GITHUB_OUTPUT $GITHUB_ENV
|
||||
fi
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
if [ "${{ github.event.inputs.minimal }}" == "true" ]; then
|
||||
flavor="_minimal"
|
||||
@@ -132,6 +144,11 @@ jobs:
|
||||
- name: Set Build Variables
|
||||
id: vars
|
||||
run: |
|
||||
if [ -n "${{ needs.build.outputs.build_id }}" ]; then
|
||||
echo "INFIX_BUILD_ID=${{ needs.build.outputs.build_id }}" \
|
||||
>>$GITHUB_ENV
|
||||
fi
|
||||
|
||||
if [ "$GITHUB_REF_NAME" != "main" ]; then
|
||||
flavor="_minimal"
|
||||
else
|
||||
@@ -166,6 +183,22 @@ jobs:
|
||||
if: always()
|
||||
run: cat test/.log/last/result-gh.md >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Generate Test Report for x86_64${{ steps.vars.outputs.flv }}
|
||||
# Ensure this runs even if Regression Test fails
|
||||
if: always()
|
||||
run: |
|
||||
asciidoctor-pdf \
|
||||
--theme test/9pm/report/theme.yml \
|
||||
-a pdf-fontsdir=test/9pm/report/fonts \
|
||||
test/.log/last/report.adoc \
|
||||
-o test/.log/last/report.pdf
|
||||
|
||||
- name: Upload Test Report as Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-report
|
||||
path: test/.log/last/report.pdf
|
||||
|
||||
release:
|
||||
if: ${{github.repository_owner == 'kernelkit' && github.ref_name == 'main'}}
|
||||
name: Upload Latest Build
|
||||
|
||||
@@ -128,5 +128,9 @@ SD-card partition.
|
||||
> If possible, serve `infix-aarch64.pkg` over HTTP instead, as
|
||||
> libcurl's TFTP implementation is quite slow.
|
||||
|
||||
## Console Port
|
||||
|
||||
The console port runs at 115200 baud, 8N1.
|
||||
|
||||
[release]: https://github.com/kernelkit/infix/releases
|
||||
[mvebu64boot]: https://github.com/addiva-elektronik/mvebu64boot
|
||||
|
||||
@@ -34,6 +34,10 @@ CONFIG_PROFILING=y
|
||||
CONFIG_ARCH_SPARX5=y
|
||||
CONFIG_ARCH_MVEBU=y
|
||||
CONFIG_ARCH_VEXPRESS=y
|
||||
CONFIG_ARM64_ERRATUM_2441007=y
|
||||
CONFIG_ARM64_ERRATUM_1286807=y
|
||||
CONFIG_ARM64_ERRATUM_1542419=y
|
||||
CONFIG_ARM64_ERRATUM_2441009=y
|
||||
CONFIG_ARM64_VA_BITS_48=y
|
||||
CONFIG_SCHED_MC=y
|
||||
CONFIG_NR_CPUS=64
|
||||
@@ -57,6 +61,7 @@ CONFIG_MODULE_UNLOAD=y
|
||||
CONFIG_KSM=y
|
||||
CONFIG_TRANSPARENT_HUGEPAGE=y
|
||||
CONFIG_CMA=y
|
||||
CONFIG_CMA_AREAS=7
|
||||
CONFIG_NET=y
|
||||
CONFIG_PACKET=y
|
||||
CONFIG_XDP_SOCKETS=y
|
||||
@@ -165,7 +170,6 @@ CONFIG_BRIDGE_EBT_REDIRECT=m
|
||||
CONFIG_BRIDGE_EBT_SNAT=m
|
||||
CONFIG_BRIDGE_EBT_LOG=m
|
||||
CONFIG_BRIDGE_EBT_NFLOG=m
|
||||
CONFIG_BPFILTER=y
|
||||
CONFIG_BRIDGE=y
|
||||
CONFIG_BRIDGE_VLAN_FILTERING=y
|
||||
CONFIG_BRIDGE_MRP=y
|
||||
@@ -227,6 +231,7 @@ CONFIG_SCSI_SAS_ATA=y
|
||||
CONFIG_SCSI_VIRTIO=y
|
||||
CONFIG_ATA=y
|
||||
CONFIG_SATA_AHCI=y
|
||||
CONFIG_SATA_MOBILE_LPM_POLICY=0
|
||||
CONFIG_SATA_AHCI_PLATFORM=y
|
||||
CONFIG_AHCI_MVEBU=y
|
||||
CONFIG_PATA_OF_PLATFORM=y
|
||||
@@ -282,6 +287,7 @@ CONFIG_NET_DSA_MV88E6XXX_PTP=y
|
||||
CONFIG_MVNETA=m
|
||||
CONFIG_MVPP2=m
|
||||
# CONFIG_NET_VENDOR_MELLANOX is not set
|
||||
# CONFIG_NET_VENDOR_META is not set
|
||||
# CONFIG_NET_VENDOR_MICREL is not set
|
||||
CONFIG_SPARX5_SWITCH=y
|
||||
# CONFIG_NET_VENDOR_MICROSEMI is not set
|
||||
@@ -353,13 +359,14 @@ CONFIG_SERIAL_XILINX_PS_UART=y
|
||||
CONFIG_SERIAL_XILINX_PS_UART_CONSOLE=y
|
||||
CONFIG_SERIAL_MVEBU_UART=y
|
||||
CONFIG_VIRTIO_CONSOLE=y
|
||||
CONFIG_HW_RANDOM_CN10K=m
|
||||
CONFIG_I2C=y
|
||||
CONFIG_I2C_CHARDEV=y
|
||||
CONFIG_I2C_MUX=y
|
||||
CONFIG_I2C_MUX_GPIO=y
|
||||
CONFIG_I2C_MUX_PCA954x=y
|
||||
CONFIG_I2C_MUX_PINCTRL=y
|
||||
CONFIG_I2C_DESIGNWARE_PLATFORM=y
|
||||
CONFIG_I2C_DESIGNWARE_CORE=y
|
||||
CONFIG_I2C_MV64XXX=y
|
||||
CONFIG_I2C_SLAVE=y
|
||||
CONFIG_SPI=y
|
||||
@@ -495,6 +502,7 @@ CONFIG_EXTCON_USB_GPIO=y
|
||||
CONFIG_IIO=y
|
||||
CONFIG_TI_ADC081C=y
|
||||
CONFIG_PWM=y
|
||||
CONFIG_RESET_GPIO=y
|
||||
CONFIG_PHY_MVEBU_CP110_COMPHY=y
|
||||
CONFIG_PHY_MVEBU_CP110_UTMI=y
|
||||
CONFIG_PHY_SAMSUNG_USB2=y
|
||||
@@ -523,13 +531,13 @@ CONFIG_9P_FS=y
|
||||
CONFIG_NLS_CODEPAGE_437=y
|
||||
CONFIG_NLS_ISO8859_1=y
|
||||
CONFIG_SECURITY=y
|
||||
CONFIG_LSM="landlock,lockdown,yama,loadpin,safesetid,bpf"
|
||||
CONFIG_CRYPTO_CCM=m
|
||||
CONFIG_CRYPTO_ECHAINIV=y
|
||||
CONFIG_CRYPTO_ANSI_CPRNG=y
|
||||
CONFIG_CRYPTO_GHASH_ARM64_CE=y
|
||||
CONFIG_CRYPTO_SHA1_ARM64_CE=y
|
||||
CONFIG_CRYPTO_SHA2_ARM64_CE=y
|
||||
CONFIG_CRYPTO_AES_ARM64_CE_BLK=y
|
||||
CONFIG_CRYPTO_AES_ARM64_CE_CCM=y
|
||||
CONFIG_DMA_CMA=y
|
||||
CONFIG_CMA_SIZE_MBYTES=0
|
||||
|
||||
@@ -101,6 +101,13 @@ Worth noting, unlike many other boards, the Rockchip family of chipsets
|
||||
runs the UART at 1500000 bps (1.5 Mbps) 8N1.
|
||||
|
||||
|
||||
Console Port
|
||||
------------
|
||||
|
||||
Unlike many other boards, the NanoPi R2S console, and in fact all
|
||||
Rockchip family chipsets, runs at 1500000 bps (1.5 Mbps) 8N1.
|
||||
|
||||
|
||||
Secure Boot
|
||||
-----------
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
set -e
|
||||
|
||||
GIT_VERSION=$(git -C "$BR2_EXTERNAL_INFIX_PATH" describe --always --dirty --tags)
|
||||
|
||||
name=$1
|
||||
compat=$2
|
||||
sign=$3
|
||||
@@ -26,7 +24,7 @@ cp -f "$BINARIES_DIR/rootfs.itbh" "$work/rootfs.itbh"
|
||||
cat >"$work/manifest.raucm" <<EOF
|
||||
[update]
|
||||
compatible=${compat}
|
||||
version=${GIT_VERSION}
|
||||
version=${INFIX_VERSION}
|
||||
|
||||
[bundle]
|
||||
format=verity
|
||||
|
||||
@@ -39,22 +39,6 @@ if [ -n "${ID_LIKE}" ]; then
|
||||
ID="${ID} ${ID_LIKE}"
|
||||
fi
|
||||
|
||||
if [ -z "$GIT_VERSION" ]; then
|
||||
infix_path="$BR2_EXTERNAL_INFIX_PATH"
|
||||
if [ -n "$INFIX_OEM_PATH" ]; then
|
||||
# Use version from br2-external OEM:ing Infix
|
||||
infix_path="$INFIX_OEM_PATH"
|
||||
fi
|
||||
GIT_VERSION=$(git -C "$infix_path" describe --always --dirty --tags)
|
||||
fi
|
||||
|
||||
# Override VERSION in /etc/os-release and filenames for release builds
|
||||
if [ -n "$INFIX_RELEASE" ]; then
|
||||
VERSION="$INFIX_RELEASE"
|
||||
else
|
||||
VERSION=$GIT_VERSION
|
||||
fi
|
||||
|
||||
if [ -n "$INFIX_IMAGE_ID" ]; then
|
||||
NAME="$INFIX_IMAGE_ID"
|
||||
else
|
||||
@@ -71,12 +55,12 @@ rm -f "$TARGET_DIR/etc/os-release"
|
||||
{
|
||||
echo "NAME=\"$INFIX_NAME\""
|
||||
echo "ID=$INFIX_ID"
|
||||
echo "PRETTY_NAME=\"$INFIX_TAGLINE $VERSION\""
|
||||
echo "PRETTY_NAME=\"$INFIX_TAGLINE $INFIX_VERSION\""
|
||||
echo "ID_LIKE=\"${ID}\""
|
||||
echo "DEFAULT_HOSTNAME=$BR2_TARGET_GENERIC_HOSTNAME"
|
||||
echo "VERSION=\"${VERSION}\""
|
||||
echo "VERSION_ID=${VERSION}"
|
||||
echo "BUILD_ID=\"${GIT_VERSION}\""
|
||||
echo "VERSION=\"${INFIX_VERSION}\""
|
||||
echo "VERSION_ID=${INFIX_VERSION}"
|
||||
echo "BUILD_ID=\"${INFIX_BUILD_ID}\""
|
||||
if [ -n "$INFIX_IMAGE_ID" ]; then
|
||||
echo "IMAGE_ID=\"$INFIX_IMAGE_ID\""
|
||||
fi
|
||||
@@ -102,7 +86,7 @@ rm -f "$TARGET_DIR/etc/os-release"
|
||||
fi
|
||||
} > "$TARGET_DIR/etc/os-release"
|
||||
|
||||
echo "$INFIX_TAGLINE $VERSION -- $(date +"%b %e %H:%M %Z %Y")" > "$TARGET_DIR/etc/version"
|
||||
echo "$INFIX_TAGLINE $INFIX_VERSION -- $(date +"%b %e %H:%M %Z %Y")" > "$TARGET_DIR/etc/version"
|
||||
|
||||
# In case of ambguities, this is what the image was built from
|
||||
cp "$BR2_CONFIG" "$TARGET_DIR/usr/share/infix/config"
|
||||
|
||||
@@ -3,16 +3,19 @@
|
||||
# and similar events feed servers and configuration to dnsmasq.
|
||||
domain-needed
|
||||
|
||||
# Only listen to loopback (local system)
|
||||
interface=lo
|
||||
bind-dynamic
|
||||
#listen-address=127.0.0.1,::1
|
||||
|
||||
# Allow configuration and cache clear over D-Bus
|
||||
enable-dbus
|
||||
|
||||
# Disable the following dnsmasq default DHCP options
|
||||
#dhcp-option=option:netmask
|
||||
#dhcp-option=28 # option:broadcast
|
||||
#dhcp-option=option:domain-name
|
||||
dhcp-option=option:router
|
||||
dhcp-option=option:dns-server
|
||||
dhcp-option=12 # option:hostname
|
||||
|
||||
# Generated by openresolv
|
||||
resolv-file=/var/lib/misc/resolv.conf
|
||||
|
||||
# Include all files in a directory which end in .conf
|
||||
conf-dir=/etc/dnsmasq.d/,*.conf
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# A single mstpd instance can manage multiple bridges, which are
|
||||
# dynamically added/removed by the kernel via the /sbin/bridge-stp
|
||||
# usermode helper. We use a manual service so that confd can
|
||||
# enable/disable it without an initctl barrier, since it needs to
|
||||
# already be running when a bridge interface with spanning tree
|
||||
# enabled is created.
|
||||
|
||||
service env:-/etc/default/mstpd manual:yes \
|
||||
[S0123456789] mstpd $MSTPD_ARGS -- Spanning Tree daemon
|
||||
@@ -0,0 +1 @@
|
||||
../available/mstpd.conf
|
||||
@@ -1,3 +0,0 @@
|
||||
HostKey /var/lib/ssh/ssh_host_rsa_key
|
||||
HostKey /var/lib/ssh/ssh_host_ecdsa_key
|
||||
HostKey /var/lib/ssh/ssh_host_ed25519_key
|
||||
@@ -1,3 +0,0 @@
|
||||
net.ipv4.ip_forward=1
|
||||
net.ipv4.ip_forward_update_priority=0
|
||||
net.ipv6.conf.all.forwarding=1
|
||||
@@ -1 +1,32 @@
|
||||
# Router defaults
|
||||
net.ipv4.conf.default.rp_filter=0
|
||||
net.ipv4.conf.all.rp_filter=0
|
||||
|
||||
net.ipv4.conf.lo.rp_filter=0
|
||||
|
||||
net.ipv4.icmp_errors_use_inbound_ifaddr=1
|
||||
net.ipv4.conf.all.ignore_routes_with_linkdown=1
|
||||
|
||||
# Use neigh information on selection of nexthop for multipath hops
|
||||
net.ipv4.fib_multipath_use_neigh=1
|
||||
|
||||
# Sane ARP defaults for a switch/router
|
||||
net.ipv4.conf.default.arp_announce=2
|
||||
net.ipv4.conf.all.arp_announce=2
|
||||
|
||||
net.ipv4.conf.default.arp_notify=1
|
||||
net.ipv4.conf.all.arp_notify=1
|
||||
|
||||
net.ipv4.conf.default.arp_ignore=1
|
||||
net.ipv4.conf.all.arp_ignore=1
|
||||
|
||||
# IP Routing
|
||||
net.ipv4.ip_forward=1
|
||||
net.ipv4.ip_forward_update_priority=0
|
||||
|
||||
net.ipv4.conf.all.forwarding=0
|
||||
net.ipv4.conf.default.forwarding=0
|
||||
|
||||
# Multicast group subscriptions
|
||||
net.ipv4.igmp_max_memberships=1000
|
||||
net.ipv4.neigh.default.mcast_solicit=10
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# Router defaults
|
||||
net.ipv6.route.max_size=131072
|
||||
net.ipv6.conf.all.ignore_routes_with_linkdown=1
|
||||
|
||||
# IP Routing
|
||||
net.ipv6.conf.all.forwarding=1
|
||||
net.ipv6.conf.default.forwarding=0
|
||||
|
||||
# IPv6 SLAAC
|
||||
net.ipv6.conf.all.autoconf=0
|
||||
net.ipv6.conf.default.autoconf=0
|
||||
|
||||
# Keep permanent addresses on an admin down
|
||||
net.ipv6.conf.all.keep_addr_on_down=1
|
||||
net.ipv6.conf.default.keep_addr_on_down=1
|
||||
|
||||
# Multicast group subscriptions
|
||||
net.ipv6.mld_max_msf=512
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
Many of the defaults here are are taken from the Frr recommendations [1].
|
||||
Below are relevant excerpts from the kernel documentation.
|
||||
|
||||
|
||||
accept_ra, accept Router Advertisements; autoconfigure using them, also
|
||||
determines whether or not to transmit Router Solicitations.
|
||||
If and only if the functional setting is to accept Router
|
||||
Advertisements, Router Solicitations will be transmitted.
|
||||
|
||||
0 - Do not accept Router Advertisements.
|
||||
|
||||
1 - Accept Router Advertisements if forwarding is disabled.
|
||||
|
||||
2 - Overrule forwarding behaviour. Accept Router Advertisements even
|
||||
if forwarding is enabled.
|
||||
|
||||
Default:
|
||||
- enabled if local forwarding is disabled
|
||||
- disabled if local forwarding is enabled
|
||||
|
||||
|
||||
accept_ra_pinfo, learn Prefix Information in Router Advertisement.
|
||||
|
||||
Default:
|
||||
- enabled if accept_ra is enabled
|
||||
- disabled if accept_ra is disabled
|
||||
|
||||
|
||||
autoconf, autoconfigure IPv6 addresses using Prefix Information in
|
||||
Router Advertisements.
|
||||
|
||||
Default:
|
||||
- enabled if accept_ra_pinfo is enabled
|
||||
- disabled if accept_ra_pinfo is disabled
|
||||
|
||||
|
||||
arp_announce, define restriction level for announcing the local source
|
||||
address from IP packets in ARP requests sent on interface:
|
||||
|
||||
0 - (default) Use any local address, configured on any interface
|
||||
|
||||
1 - Try to avoid local addresses that are not in the target’s subnet
|
||||
for this interface. Useful when target hosts reachable via this
|
||||
interface require the source IP address in ARP requests to be part
|
||||
of their logical network configured on the receiving interface.
|
||||
When we generate the request we will check all our subnets that
|
||||
include the target IP and will preserve the source address if it
|
||||
is from such subnet. If there is no such subnet we select source
|
||||
address according to the rules for level 2.
|
||||
|
||||
2 - Always use the best local address for this target. In this mode we
|
||||
ignore the source address in the IP packet and try to select local
|
||||
address that we prefer for talks with the target host. Such local
|
||||
address is selected by looking for primary IP addresses on all our
|
||||
subnets on the outgoing interface that include the target address.
|
||||
If no suitable local address is found we select the first local
|
||||
address we have on the outgoing interface or on all other
|
||||
interfaces, with the hope we will receive reply for our request
|
||||
and even sometimes no matter the source IP address we announce.
|
||||
|
||||
|
||||
arp_notify, define mode for notification of address and device changes.
|
||||
|
||||
0 - (default): do nothing
|
||||
1 - generate gratuitous arp requests when device is brought up or
|
||||
hardware address changes.
|
||||
|
||||
|
||||
arp_ignore, define different modes for sending replies in response to
|
||||
received ARP requests that resolve local target addresses:
|
||||
|
||||
0 - (default): reply for any local target IP address, configured on
|
||||
any interface
|
||||
|
||||
1 - reply only if the target IP address is a local address configured
|
||||
on the incoming interface
|
||||
|
||||
2 - reply only if the target IP address is local address configured on
|
||||
the incoming interface and both with the sender’s IP address are part
|
||||
from same subnet on this interface
|
||||
|
||||
3 - do not reply for local addresses configured with scope host, only
|
||||
resolutions for global and link addresses are replied
|
||||
|
||||
4-7 - reserved
|
||||
|
||||
8 - do not reply for all local addresses
|
||||
|
||||
|
||||
arp_accept, define behavior for accepting gratuitous ARP (garp) frames
|
||||
from devices that are not already present in the ARP table:
|
||||
|
||||
0 - don’t create new entries in the ARP table
|
||||
|
||||
1 - create new entries in the ARP table
|
||||
|
||||
2 - create new entries only if the source IP address is in the same
|
||||
subnet as an address configured on the interface that received
|
||||
the garp message.
|
||||
|
||||
Both replies and requests type gratuitous arp will trigger the ARP
|
||||
table to be updated, if this setting is on. If the ARP table already
|
||||
contains the IP address of the gratuitous arp frame, the arp table
|
||||
will be updated regardless if this setting is on or off.
|
||||
|
||||
|
||||
icmp_errors_use_inbound_ifaddr
|
||||
|
||||
0 - (default): icmp error messages are sent with the primary address
|
||||
of the exiting interface.
|
||||
|
||||
1 - the message will be sent with the primary address of the interface
|
||||
that received the packet that caused the icmp error. This is the
|
||||
behaviour many network administrators will expect from a router.
|
||||
And it can make debugging complicated network layouts much easier.
|
||||
|
||||
Note, if no primary address exists for the interface selected, then
|
||||
the primary address of the first non-loopback interface that has one
|
||||
will be used regardless of this setting.
|
||||
|
||||
|
||||
rp_filter, reverse path source filtering:
|
||||
|
||||
0 - (default): no source validation.
|
||||
|
||||
1 - Strict mode as defined in RFC3704, 'Strict Reverse Path'. Each
|
||||
incoming packet is tested against the FIB and if the interface is
|
||||
not the best reverse path the packet check will fail. By default
|
||||
failed packets are discarded.
|
||||
|
||||
2 - Loose mode as defined in RFC3704, 'Loose Reverse Path'. Each
|
||||
incoming packet’s source address is also tested against the FIB
|
||||
and if the source address is not reachable via any interface the
|
||||
packet check will fail.
|
||||
|
||||
Current recommended practice in RFC3704 is to enable strict mode to
|
||||
prevent IP spoofing from DDos attacks. If using asymmetric routing or
|
||||
other complicated routing, then loose mode is recommended.
|
||||
|
||||
The max value from conf/{all,interface}/rp_filter is used when doing
|
||||
source validation on the {interface}.
|
||||
|
||||
|
||||
|
||||
[1]: https://github.com/FRRouting/frr/blob/master/doc/user/Useful_Sysctl_Settings.md
|
||||
@@ -70,12 +70,14 @@ options:
|
||||
-p Show plain output, no bells or whistles
|
||||
|
||||
commands:
|
||||
dhcp Show DHCP server
|
||||
port PORT Show port configuration and link information
|
||||
ports Show ports available for bridging
|
||||
vlans Show port groups in bridge
|
||||
ifaces Show interfaces and their addresses
|
||||
fdb Show forwarding database (unicast)
|
||||
mdb Show multicast forwarding database
|
||||
stp Show spanning tree status
|
||||
ip addr Show IPv4 addresses
|
||||
route Show routing table
|
||||
ipv6 addr Show IPv6 addresses
|
||||
@@ -87,6 +89,41 @@ commands:
|
||||
EOF
|
||||
}
|
||||
|
||||
is_dhcp_running()
|
||||
{
|
||||
sysrepocfg -X -f json -m infix-dhcp-server | jq -r '
|
||||
."infix-dhcp-server:dhcp-server".enabled as $global |
|
||||
if ."infix-dhcp-server:dhcp-server".subnet? then
|
||||
(."infix-dhcp-server:dhcp-server".subnet[] |
|
||||
select(.enabled != false)) |
|
||||
if $global != false and . then "true" else "false" end
|
||||
else "false" end
|
||||
' 2>/dev/null | grep -q true
|
||||
}
|
||||
|
||||
dhcp()
|
||||
{
|
||||
if ! is_dhcp_running; then
|
||||
echo "DHCP server not enabled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case $1 in
|
||||
detail)
|
||||
sysrepocfg -f json -X -d operational -m infix-dhcp-server | \
|
||||
jq -C .
|
||||
;;
|
||||
stat*)
|
||||
sysrepocfg -f json -X -d operational -m infix-dhcp-server | \
|
||||
/usr/libexec/statd/cli-pretty "show-dhcp-server" -s
|
||||
;;
|
||||
*)
|
||||
sysrepocfg -f json -X -d operational -m infix-dhcp-server | \
|
||||
/usr/libexec/statd/cli-pretty "show-dhcp-server"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Usage 1: show port eth0
|
||||
# Usage 2: show port
|
||||
# Usage 3: show ports
|
||||
@@ -188,6 +225,12 @@ rstp()
|
||||
mstpctl showport br0
|
||||
}
|
||||
|
||||
stp()
|
||||
{
|
||||
sysrepocfg -f json -X -d operational -m ietf-interfaces | \
|
||||
/usr/libexec/statd/cli-pretty "show-bridge-stp"
|
||||
}
|
||||
|
||||
fdb()
|
||||
{
|
||||
bridge $bopt fdb show
|
||||
@@ -288,6 +331,9 @@ case $cmd in
|
||||
help)
|
||||
usage
|
||||
;;
|
||||
dhcp | dhcp-server)
|
||||
dhcp $*
|
||||
;;
|
||||
port*)
|
||||
ports $*
|
||||
;;
|
||||
@@ -353,6 +399,9 @@ case $cmd in
|
||||
span*)
|
||||
rstp
|
||||
;;
|
||||
stp*)
|
||||
stp
|
||||
;;
|
||||
sys*)
|
||||
system
|
||||
;;
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
# Store and convert RSA PUBLIC/PRIVATE KEYs to be able to use them in
|
||||
# OpenSSHd.
|
||||
set -e
|
||||
|
||||
NAME="$1"
|
||||
DIR="$2"
|
||||
PUBLIC="$3"
|
||||
PRIVATE="$4"
|
||||
TMP="$(mktemp)"
|
||||
|
||||
echo -e '-----BEGIN RSA PRIVATE KEY-----' > "$DIR/$NAME"
|
||||
echo "$PRIVATE" >> "$DIR/$NAME"
|
||||
echo -e '-----END RSA PRIVATE KEY-----' >> "$DIR/$NAME"
|
||||
|
||||
echo -e "-----BEGIN RSA PUBLIC KEY-----" > "$TMP"
|
||||
echo -e "$PUBLIC" >> "$TMP"
|
||||
echo -e "-----END RSA PUBLIC KEY-----" >> "$TMP"
|
||||
|
||||
ssh-keygen -i -m PKCS8 -f "$TMP" > "$DIR/$NAME.pub"
|
||||
chmod 0600 "$DIR/$NAME.pub"
|
||||
chmod 0600 "$DIR/$NAME"
|
||||
chown sshd:sshd "$DIR/$NAME.pub"
|
||||
chown sshd:sshd "$DIR/$NAME"
|
||||
@@ -226,7 +226,7 @@ create()
|
||||
fi
|
||||
|
||||
if [ -z "$logging" ]; then
|
||||
logging="--log-driver none"
|
||||
logging="--log-driver syslog"
|
||||
fi
|
||||
|
||||
# When we get here we've already fetched, or pulled, the image
|
||||
|
||||
@@ -145,7 +145,6 @@ CONFIG_BRIDGE_EBT_REDIRECT=m
|
||||
CONFIG_BRIDGE_EBT_SNAT=m
|
||||
CONFIG_BRIDGE_EBT_LOG=m
|
||||
CONFIG_BRIDGE_EBT_NFLOG=m
|
||||
CONFIG_BPFILTER=y
|
||||
CONFIG_BRIDGE=y
|
||||
CONFIG_BRIDGE_VLAN_FILTERING=y
|
||||
CONFIG_BRIDGE_MRP=y
|
||||
|
||||
+1
-1
Submodule buildroot updated: 897bb2cf43...92e256798b
@@ -27,13 +27,9 @@ 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.6.52"
|
||||
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.11"
|
||||
BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
|
||||
BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/aarch64/linux_defconfig"
|
||||
BR2_LINUX_KERNEL_DTS_SUPPORT=y
|
||||
BR2_LINUX_KERNEL_INTREE_DTS_NAME="alder/alder styx/dcp-sc-28p-a styx/dcp-sc-28p-b marvell/armada-3720-espressobin marvell/armada-3720-espressobin-emmc marvell/armada-3720-espressobin-v7 marvell/armada-3720-espressobin-v7-emmc marvell/armada-3720-espressobin-ultra marvell/cn9130-crb-A marvell/cn9130-crb-B microchip/sparx5_pcb135_emmc_no_psci"
|
||||
BR2_LINUX_KERNEL_CUSTOM_DTS_OVERLAY="${BR2_EXTERNAL_INFIX_PATH}/board/aarch64/dts"
|
||||
BR2_LINUX_KERNEL_DTB_KEEP_DIRNAME=y
|
||||
BR2_LINUX_KERNEL_INSTALL_TARGET=y
|
||||
BR2_PACKAGE_BUSYBOX_CONFIG="${BR2_EXTERNAL_INFIX_PATH}/board/common/busybox_defconfig"
|
||||
BR2_PACKAGE_STRACE=y
|
||||
@@ -80,6 +76,7 @@ BR2_PACKAGE_IPROUTE2=y
|
||||
BR2_PACKAGE_IPTABLES_NFTABLES=y
|
||||
BR2_PACKAGE_IPUTILS=y
|
||||
BR2_PACKAGE_LLDPD=y
|
||||
BR2_PACKAGE_MSTPD=y
|
||||
BR2_PACKAGE_NETCALC=y
|
||||
BR2_PACKAGE_NETCAT_OPENBSD=y
|
||||
BR2_PACKAGE_NETSNMP=y
|
||||
@@ -109,6 +106,7 @@ BR2_PACKAGE_RAUC=y
|
||||
BR2_PACKAGE_RAUC_DBUS=y
|
||||
BR2_PACKAGE_RAUC_GPT=y
|
||||
BR2_PACKAGE_RAUC_NETWORK=y
|
||||
BR2_PACKAGE_RAUC_JSON=y
|
||||
BR2_PACKAGE_SYSKLOGD=y
|
||||
BR2_PACKAGE_SYSKLOGD_LOGGER=y
|
||||
BR2_PACKAGE_WATCHDOGD=y
|
||||
@@ -130,6 +128,10 @@ INFIX_DESC="Infix is a Network Operating System based on Linux. It can be set u
|
||||
INFIX_HOME="https://github.com/kernelkit/infix/"
|
||||
INFIX_DOC="https://github.com/kernelkit/infix/tree/main/doc"
|
||||
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com"
|
||||
BR2_PACKAGE_ALDER_ALDER=y
|
||||
BR2_PACKAGE_MARVELL_CN9130_CRB=y
|
||||
BR2_PACKAGE_MARVELL_ESPRESSOBIN=y
|
||||
BR2_PACKAGE_STYX_DCP_SC_28P=y
|
||||
BR2_PACKAGE_CONFD=y
|
||||
BR2_PACKAGE_CONFD_TEST_MODE=y
|
||||
BR2_PACKAGE_CURIOS_HTTPD=y
|
||||
@@ -161,6 +163,7 @@ BR2_PACKAGE_TETRIS=y
|
||||
BR2_PACKAGE_ROUSETTE=y
|
||||
BR2_PACKAGE_LIBINPUT=y
|
||||
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
|
||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||
DISK_IMAGE_BOOT_BIN=y
|
||||
TRUSTED_KEYS=y
|
||||
TRUSTED_KEYS_DEVELOPMENT=y
|
||||
|
||||
@@ -27,13 +27,9 @@ 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.6.52"
|
||||
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.11"
|
||||
BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
|
||||
BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/aarch64/linux_defconfig"
|
||||
BR2_LINUX_KERNEL_DTS_SUPPORT=y
|
||||
BR2_LINUX_KERNEL_INTREE_DTS_NAME="alder/alder styx/dcp-sc-28p-a styx/dcp-sc-28p-b marvell/armada-3720-espressobin marvell/armada-3720-espressobin-emmc marvell/armada-3720-espressobin-v7 marvell/armada-3720-espressobin-v7-emmc marvell/armada-3720-espressobin-ultra marvell/cn9130-crb-A marvell/cn9130-crb-B microchip/sparx5_pcb135_emmc_no_psci"
|
||||
BR2_LINUX_KERNEL_CUSTOM_DTS_OVERLAY="${BR2_EXTERNAL_INFIX_PATH}/board/aarch64/dts"
|
||||
BR2_LINUX_KERNEL_DTB_KEEP_DIRNAME=y
|
||||
BR2_LINUX_KERNEL_INSTALL_TARGET=y
|
||||
BR2_PACKAGE_BUSYBOX_CONFIG="${BR2_EXTERNAL_INFIX_PATH}/board/common/busybox_defconfig"
|
||||
BR2_PACKAGE_STRACE=y
|
||||
@@ -75,6 +71,7 @@ BR2_PACKAGE_FRR=y
|
||||
BR2_PACKAGE_IPROUTE2=y
|
||||
BR2_PACKAGE_IPUTILS=y
|
||||
BR2_PACKAGE_LLDPD=y
|
||||
BR2_PACKAGE_MSTPD=y
|
||||
BR2_PACKAGE_NETCALC=y
|
||||
BR2_PACKAGE_NGINX=y
|
||||
BR2_PACKAGE_NGINX_HTTP_SSL_MODULE=y
|
||||
@@ -92,6 +89,7 @@ BR2_PACKAGE_RAUC=y
|
||||
BR2_PACKAGE_RAUC_DBUS=y
|
||||
BR2_PACKAGE_RAUC_GPT=y
|
||||
BR2_PACKAGE_RAUC_NETWORK=y
|
||||
BR2_PACKAGE_RAUC_JSON=y
|
||||
BR2_PACKAGE_SYSKLOGD=y
|
||||
BR2_PACKAGE_SYSKLOGD_LOGGER=y
|
||||
BR2_PACKAGE_WATCHDOGD=y
|
||||
@@ -113,6 +111,10 @@ INFIX_DESC="Infix is a Network Operating System based on Linux. It can be set u
|
||||
INFIX_HOME="https://github.com/kernelkit/infix/"
|
||||
INFIX_DOC="https://github.com/kernelkit/infix/tree/main/doc"
|
||||
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com"
|
||||
BR2_PACKAGE_ALDER_ALDER=y
|
||||
BR2_PACKAGE_MARVELL_CN9130_CRB=y
|
||||
BR2_PACKAGE_MARVELL_ESPRESSOBIN=y
|
||||
BR2_PACKAGE_STYX_DCP_SC_28P=y
|
||||
BR2_PACKAGE_CONFD=y
|
||||
BR2_PACKAGE_CONFD_TEST_MODE=y
|
||||
BR2_PACKAGE_GENCERT=y
|
||||
@@ -134,6 +136,7 @@ BR2_PACKAGE_MCD=y
|
||||
BR2_PACKAGE_MDNS_ALIAS=y
|
||||
BR2_PACKAGE_LIBINPUT=y
|
||||
BR2_PACKAGE_ROUSETTE=y
|
||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||
DISK_IMAGE_BOOT_BIN=y
|
||||
TRUSTED_KEYS=y
|
||||
TRUSTED_KEYS_DEVELOPMENT=y
|
||||
|
||||
@@ -128,6 +128,7 @@ BR2_PACKAGE_RAUC=y
|
||||
BR2_PACKAGE_RAUC_DBUS=y
|
||||
BR2_PACKAGE_RAUC_GPT=y
|
||||
BR2_PACKAGE_RAUC_NETWORK=y
|
||||
BR2_PACKAGE_RAUC_JSON=y
|
||||
BR2_PACKAGE_SYSKLOGD=y
|
||||
BR2_PACKAGE_SYSKLOGD_LOGGER=y
|
||||
BR2_PACKAGE_WATCHDOGD=y
|
||||
@@ -201,6 +202,7 @@ BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
||||
BR2_PACKAGE_TETRIS=y
|
||||
BR2_PACKAGE_ROUSETTE=y
|
||||
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
|
||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||
TRUSTED_KEYS=y
|
||||
TRUSTED_KEYS_DEVELOPMENT=y
|
||||
# GNS3_APPLIANCE is not set
|
||||
|
||||
@@ -88,6 +88,7 @@ BR2_PACKAGE_IPROUTE2=y
|
||||
BR2_PACKAGE_IPTABLES_NFTABLES=y
|
||||
BR2_PACKAGE_IPUTILS=y
|
||||
BR2_PACKAGE_LLDPD=y
|
||||
BR2_PACKAGE_MSTPD=y
|
||||
BR2_PACKAGE_NETCALC=y
|
||||
BR2_PACKAGE_NETCAT_OPENBSD=y
|
||||
BR2_PACKAGE_NETSNMP=y
|
||||
@@ -117,6 +118,7 @@ BR2_PACKAGE_RAUC=y
|
||||
BR2_PACKAGE_RAUC_DBUS=y
|
||||
BR2_PACKAGE_RAUC_GPT=y
|
||||
BR2_PACKAGE_RAUC_NETWORK=y
|
||||
BR2_PACKAGE_RAUC_JSON=y
|
||||
BR2_PACKAGE_SYSKLOGD=y
|
||||
BR2_PACKAGE_SYSKLOGD_LOGGER=y
|
||||
BR2_PACKAGE_WATCHDOGD=y
|
||||
@@ -191,6 +193,7 @@ BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
||||
BR2_PACKAGE_TETRIS=y
|
||||
BR2_PACKAGE_ROUSETTE=y
|
||||
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
|
||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||
TRUSTED_KEYS=y
|
||||
TRUSTED_KEYS_DEVELOPMENT=y
|
||||
# GNS3_APPLIANCE is not set
|
||||
|
||||
@@ -26,7 +26,7 @@ BR2_ROOTFS_POST_BUILD_SCRIPT="board/qemu/x86_64/post-build.sh ${BR2_EXTERNAL_INF
|
||||
BR2_ROOTFS_POST_IMAGE_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-image.sh"
|
||||
BR2_LINUX_KERNEL=y
|
||||
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
|
||||
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.6.52"
|
||||
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.11"
|
||||
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
|
||||
@@ -76,6 +76,7 @@ BR2_PACKAGE_IPROUTE2=y
|
||||
BR2_PACKAGE_IPTABLES_NFTABLES=y
|
||||
BR2_PACKAGE_IPUTILS=y
|
||||
BR2_PACKAGE_LLDPD=y
|
||||
BR2_PACKAGE_MSTPD=y
|
||||
BR2_PACKAGE_NETCALC=y
|
||||
BR2_PACKAGE_NETCAT_OPENBSD=y
|
||||
BR2_PACKAGE_NETSNMP=y
|
||||
@@ -105,6 +106,7 @@ BR2_PACKAGE_RAUC=y
|
||||
BR2_PACKAGE_RAUC_DBUS=y
|
||||
BR2_PACKAGE_RAUC_GPT=y
|
||||
BR2_PACKAGE_RAUC_NETWORK=y
|
||||
BR2_PACKAGE_RAUC_JSON=y
|
||||
BR2_PACKAGE_SYSKLOGD=y
|
||||
BR2_PACKAGE_SYSKLOGD_LOGGER=y
|
||||
BR2_PACKAGE_WATCHDOGD=y
|
||||
@@ -164,6 +166,7 @@ BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
||||
BR2_PACKAGE_TETRIS=y
|
||||
BR2_PACKAGE_ROUSETTE=y
|
||||
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
|
||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||
TRUSTED_KEYS=y
|
||||
TRUSTED_KEYS_DEVELOPMENT=y
|
||||
GNS3_APPLIANCE_RAM=512
|
||||
|
||||
@@ -26,7 +26,7 @@ BR2_ROOTFS_POST_BUILD_SCRIPT="board/qemu/x86_64/post-build.sh ${BR2_EXTERNAL_INF
|
||||
BR2_ROOTFS_POST_IMAGE_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-image.sh"
|
||||
BR2_LINUX_KERNEL=y
|
||||
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
|
||||
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.6.52"
|
||||
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.11"
|
||||
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
|
||||
@@ -71,6 +71,7 @@ BR2_PACKAGE_FRR=y
|
||||
BR2_PACKAGE_IPROUTE2=y
|
||||
BR2_PACKAGE_IPUTILS=y
|
||||
BR2_PACKAGE_LLDPD=y
|
||||
BR2_PACKAGE_MSTPD=y
|
||||
BR2_PACKAGE_NETCALC=y
|
||||
BR2_PACKAGE_NGINX=y
|
||||
BR2_PACKAGE_NGINX_HTTP_SSL_MODULE=y
|
||||
@@ -88,6 +89,7 @@ BR2_PACKAGE_RAUC=y
|
||||
BR2_PACKAGE_RAUC_DBUS=y
|
||||
BR2_PACKAGE_RAUC_GPT=y
|
||||
BR2_PACKAGE_RAUC_NETWORK=y
|
||||
BR2_PACKAGE_RAUC_JSON=y
|
||||
BR2_PACKAGE_SYSKLOGD=y
|
||||
BR2_PACKAGE_SYSKLOGD_LOGGER=y
|
||||
BR2_PACKAGE_WATCHDOGD=y
|
||||
@@ -137,6 +139,7 @@ BR2_PACKAGE_LOWDOWN=y
|
||||
BR2_PACKAGE_MCD=y
|
||||
BR2_PACKAGE_MDNS_ALIAS=y
|
||||
BR2_PACKAGE_ROUSETTE=y
|
||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||
TRUSTED_KEYS=y
|
||||
TRUSTED_KEYS_DEVELOPMENT=y
|
||||
GNS3_APPLIANCE_RAM=512
|
||||
|
||||
+65
-2
@@ -4,6 +4,68 @@ Change Log
|
||||
All notable changes to the project are documented in this file.
|
||||
|
||||
|
||||
[v25.01.0][] - 2025-01-31
|
||||
-------------------------
|
||||
|
||||
> [!NOTE]
|
||||
> This release contains breaking changes in the configuration file
|
||||
> syntax for DHCP clients. Specifically DHCP options *with value*,
|
||||
> i.e., the syntax for sending a hexadecimal value now require `hex`
|
||||
> prefix before a string of colon-separated pairs of hex values.
|
||||
|
||||
### Changes
|
||||
|
||||
- Upgrade Linux kernel to 6.12.11 (LTS)
|
||||
- Upgrade Buildroot to 2024.02.10 (LTS)
|
||||
- Upgrade FRR from 9.1.2 to 9.1.3
|
||||
- Add support for configuring SSH server, issue #441. As a result,
|
||||
both SSH and NETCONF now use the same host key in `factory-config`
|
||||
- Add operational support for reading DNS resolver info, issue #510
|
||||
- Add operational support for NTP client, issue #510
|
||||
- Add support for more mDNS settings: allow/deny interfaces, acting
|
||||
as "reflector" and filtering of reflected services. Issue #678
|
||||
- Add DHCPv4 server support, multiple subnets with static hosts and
|
||||
DHCP options on global, subnet, or host level, issue #703.
|
||||
Contributed by [MINEx Networks](https://minexn.com/)
|
||||
- DHCP client options aligned with DHCP server, `startup-config`
|
||||
files with old syntax are automatically migrated
|
||||
- Breaking change in DHCP client options *with value*. Hexadecimal
|
||||
values must now be formatted as `{ "hex": "c0:ff:ee" }` (JSON)
|
||||
- Add documentation on management via SSH, Web (RESTCONF, Web
|
||||
Console), and Console Port, issue #787
|
||||
- Add documentation of DNS client use and configuration, issue #798
|
||||
- Add support for changing boot order for the system with an RPC,
|
||||
including support for reading boot order from operational datastore
|
||||
- Add support for GRE/GRETAP tunnels
|
||||
- Add support for STP/RSTP on bridges
|
||||
- Add support for VXLAN tunnels
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix #777: Authorized SSH key not applied to `startup-config`
|
||||
- Fix #829: Avahi (mDNS responder) not starting properly on switches
|
||||
with *many* ports (>10). This led to a review of `sysctl`:
|
||||
- New for IPv4:
|
||||
- Adjust IGMP max memberships: 20 -> 1000
|
||||
- Use neighbor information on nexthop selection
|
||||
- Use inbound interface address on ICMP errors
|
||||
- Ignore routes with link down
|
||||
- Disable `rp_filter`
|
||||
- ARP settings have been changed to better fit routers, i.e.,
|
||||
systems with multiple interfaces:
|
||||
- Always use best local address when sending ARP
|
||||
- Only reply to ARP if target IP is on the inbound interface
|
||||
- Generate ARP requests when device is brought up or HW address changes
|
||||
- New for IPv6:
|
||||
- Keep static global addresses on link down
|
||||
- Ignore routes with link down
|
||||
- Fix #861: Fix error when running 251+ reconfigurations in test-mode
|
||||
- Fix #869: Setup of bridges is now more robust
|
||||
- Fix #899: DHCP client with client-id does not work
|
||||
- Minor cleanup of Networking Guide
|
||||
- Fix memory leaks in `confd`
|
||||
|
||||
|
||||
[v24.11.1][] - 2024-11-29
|
||||
-------------------------
|
||||
|
||||
@@ -221,7 +283,7 @@ renamed to ease maintenance, more info below.
|
||||
with `custom-phys-address` to allow for constructing more free-form
|
||||
MAC addresses based on the chassis MAC (a.k.a., base MAC) address.
|
||||
For more information, see the YANG model, a few examples are listed in
|
||||
the updated documentation.
|
||||
the updated documentation.
|
||||
The syntax will be automatically updated in the `startup-config` and
|
||||
`factory-config` -- make sure to verify the changes and update any
|
||||
static `factory-config` used for your products
|
||||
@@ -1409,7 +1471,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/v24.11.0...HEAD
|
||||
[UNRELEASED]: https://github.com/kernelkit/infix/compare/v25.01.0...HEAD
|
||||
[v25.01.0]: https://github.com/kernelkit/infix/compare/v24.11.0...v25.01.0
|
||||
[v24.11.1]: https://github.com/kernelkit/infix/compare/v24.11.0...v24.11.1
|
||||
[v24.11.0]: https://github.com/kernelkit/infix/compare/v24.10.0...v24.11.0
|
||||
[v24.10.2]: https://github.com/kernelkit/infix/compare/v24.10.1...v24.10.2
|
||||
|
||||
+46
-18
@@ -60,14 +60,14 @@ rootfs overlay -- with a [VPD](vpd.md) you can even support several!
|
||||
### Variables & Format Specifiers
|
||||
|
||||
Parts of the configuration you likely always want to generated, like the
|
||||
SSH hostkey used by NETCONF, a unique hostname, or the `admin` user's
|
||||
SSH hostkey used by SSH server and NETCONF, a unique hostname, or the `admin` user's
|
||||
unique (per-device with a VPD) password hash. This section lists the
|
||||
available keywords, see the next section for examples of how to use
|
||||
them:
|
||||
|
||||
- **Default password hash:** `$factory$` (from VPD, .dtb, or built-in)
|
||||
XPath: `/ietf-system:system/authentication/user/password`
|
||||
- **Default NETCONF hostkey:** `genkey` (regenerated at factory reset)
|
||||
- **Default SSH and NETCONF hostkey:** `genkey` (regenerated at factory reset)
|
||||
XPath: `/ietf-keystore:keystore/asymmetric-keys/asymmetric-key[name='genkey']`
|
||||
- **Hostname format specifiers:**
|
||||
XPath: `/ietf-system:system/hostname`
|
||||
@@ -221,8 +221,8 @@ $ echo "Li0tLS0tLS0uCnwgIC4gLiAgfCBJbmZpeCAtLSBhIE5ldHdvcmsgT3BlcmF0aW5nIFN5c3Rl
|
||||
|
||||
**IETF Keystore**
|
||||
|
||||
Notice how both the public and private keys are left empty here. The
|
||||
`genkey` is always automatically regenerated after each factory reset.
|
||||
Notice how both the public and private keys are left empty here, this
|
||||
cause them to be always automatically regenerated after each factory reset.
|
||||
Keeping the `factory-config` snippet like this means we can use the same
|
||||
file on multiple devices, without risking them sharing the same host
|
||||
keys. Sometimes you may want the same host keys, but that is the easy
|
||||
@@ -245,8 +245,6 @@ use-case and not documented here.
|
||||
},
|
||||
```
|
||||
|
||||
The `genkey` is currently only used by the NETCONF SSH backend.
|
||||
|
||||
**IETF NETCONF Server**
|
||||
|
||||
```json
|
||||
@@ -280,6 +278,28 @@ The `genkey` is currently only used by the NETCONF SSH backend.
|
||||
},
|
||||
```
|
||||
|
||||
**Infix Services**
|
||||
```json
|
||||
"infix-services:ssh": {
|
||||
"enabled": true,
|
||||
"hostkey": [
|
||||
"genkey"
|
||||
],
|
||||
"listen": [
|
||||
{
|
||||
"name": "ipv4",
|
||||
"address": "0.0.0.0",
|
||||
"port": 22
|
||||
},
|
||||
{
|
||||
"name": "ipv6",
|
||||
"address": "::1",
|
||||
"port": 22
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
Integration
|
||||
@@ -370,22 +390,30 @@ But you can of course use only two numbers, *major.minor*, as well.
|
||||
> with your own versioning scheme.
|
||||
|
||||
|
||||
### `INFIX_RELEASE`
|
||||
### Specifying Versioning Information
|
||||
|
||||
This global variable **must be** a lower-case string (no spaces or
|
||||
other characters outside of 0–9, a–z, '.', '_' and '-') identifying
|
||||
the operating system version, excluding any OS name information or
|
||||
release code name, and suitable for processing by scripts or usage
|
||||
in generated filenames.
|
||||
Two optional environment variables control the version information
|
||||
recorded in images. Both of these **must be** a lower-case string (no
|
||||
spaces or other characters outside of 0–9, a–z, '.', '_' and '-')
|
||||
identifying the operating system version, excluding any OS name
|
||||
information or release code name, and suitable for processing by
|
||||
scripts or usage in generated filenames.
|
||||
|
||||
#### `INFIX_BUILD_ID`
|
||||
|
||||
Used for `BUILD_ID` in `/etc/os-release`.
|
||||
|
||||
**Default:** `$(git describe --always --dirty --tags)`, from the _top
|
||||
directory_. By default, the top directory refers to the root of the
|
||||
Infix source tree, but this can be changed by setting the branding
|
||||
variable `INFIX_OEM_PATH`, e.g. in a `defconfig` file or via `make
|
||||
menuconfig`, to the path of an enclosing br2-external.
|
||||
|
||||
#### `INFIX_RELEASE`
|
||||
|
||||
Used for `VERSION` and `VERSION_ID` in `/etc/os-release` and
|
||||
generated file names like disk images, etc.
|
||||
|
||||
**Default:** generated using `git describe --always --dirty --tags`,
|
||||
with an additional `-C $infix_path`. This variable defaults to the
|
||||
Infix tree and can be changed by setting the menuconfig branding
|
||||
variable `INFIX_OEM_PATH` to that of the br2-external. It is also
|
||||
possible to set the `GIT_VERSION` variable in your `post-build.sh`
|
||||
script to change how the VCS version is extracted.
|
||||
**Default:** `${INFIX_BUILD_ID}`
|
||||
|
||||
[NanoPi R2S]: https://github.com/kernelkit/infix/blob/main/board/aarch64/r2s/rootfs/etc/factory-config.cfg
|
||||
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
DHCP Server
|
||||
===========
|
||||
|
||||
The DHCPv4 server provides automatic IP address assignment and network
|
||||
configuration for clients. It supports address pools, static host
|
||||
assignments, and customizable DHCP options. It also serves as a DNS
|
||||
proxy for local subnets and can even forward queries to upstream DNS
|
||||
servers[^1].
|
||||
|
||||
> [!NOTE]
|
||||
> When using the CLI, the system automatically enables essential options
|
||||
> like DNS servers and default gateway based on the system's network
|
||||
> configuration. These options can be disabled, changed or overridden,
|
||||
> at any level: global, subnet, or per-host.
|
||||
|
||||
|
||||
## Basic Configuration
|
||||
|
||||
The following example configures a DHCP server for subnet 192.168.2.0/24
|
||||
with an address pool:
|
||||
|
||||
```
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit dhcp-server
|
||||
admin@example:/config/dhcp-server/> edit subnet 192.168.2.0/24
|
||||
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/> set pool start-address 192.168.2.100 end-address 192.168.2.200
|
||||
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/> leave
|
||||
```
|
||||
|
||||
When setting up the server from the CLI, the system automatically adds a
|
||||
few default DHCP options that will be sent to clients: both DNS server
|
||||
and default gateway will use the system address on the matching
|
||||
interface.
|
||||
|
||||
```
|
||||
admin@example:/> show running-config
|
||||
"infix-dhcp-server:dhcp-server": {
|
||||
"subnet": [
|
||||
{
|
||||
"subnet": "192.168.2.0/24",
|
||||
"option": [
|
||||
{
|
||||
"id": "dns-server",
|
||||
"address": "auto"
|
||||
},
|
||||
{
|
||||
"id": "router",
|
||||
"address": "auto"
|
||||
}
|
||||
],
|
||||
"pool": {
|
||||
"start-address": "192.168.2.100",
|
||||
"end-address": "192.168.2.200"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Remember to set up an interface in this subnet, avoid using addresses
|
||||
> in the DHCP pool, or reserved for static hosts. In Class C networks
|
||||
> the router usually has address `.1`. Depending on the use-case, you
|
||||
> may also want to set up routing.
|
||||
|
||||
|
||||
## Static Host Assignment
|
||||
|
||||
To reserve specific IP addresses for clients based on their MAC address,
|
||||
hostname, or client ID:
|
||||
|
||||
```
|
||||
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/> edit host 192.168.2.10
|
||||
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/host/192.168.2.10/> set match mac-address 00:11:22:33:44:55
|
||||
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/host/192.168.2.10/> set hostname printer
|
||||
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/host/192.168.2.10/> leave
|
||||
```
|
||||
|
||||
Match hosts using a client identifier instead of MAC address:
|
||||
|
||||
```
|
||||
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/> edit host 192.168.1.50
|
||||
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/host/192.168.1.50/> edit match
|
||||
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/host/192.168.1.50/match/> set client-id hex c0:ff:ee
|
||||
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/host/192.168.1.50/match/> leave
|
||||
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/host/192.168.1.50/> set lease-time infinite
|
||||
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/host/192.168.1.50/> leave
|
||||
```
|
||||
|
||||
The `hex` prefix here ensures matching of client ID is done using the
|
||||
hexadecimal octets `c0:ff:ee`, three bytes. Without the prefix the
|
||||
ASCII string "c0:ff:ee", eight bytes, is used.
|
||||
|
||||
> [!NOTE]
|
||||
> The DHCP server is fully RFC conformant, in the case of option 61 this
|
||||
> means that using the `hex` prefix will require the client to set the
|
||||
> `htype` field of the option to `00`. See RFC 2132 for details.
|
||||
|
||||
|
||||
## Custom DHCP Options
|
||||
|
||||
Configure additional DHCP options globally, per subnet, or per host:
|
||||
|
||||
```
|
||||
admin@example:/config/dhcp-server/> edit subnet 192.168.2.0/24
|
||||
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/> edit option dns-server
|
||||
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/option/dns-server/> set address 8.8.8.8
|
||||
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/option/dns-server/> leave
|
||||
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/> edit option ntp-server
|
||||
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/option/ntp-server/> set address 192.168.2.1
|
||||
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/option/ntp-server/> leave
|
||||
```
|
||||
|
||||
When configuring, e.g., `dns-server`, or `router` options with the value
|
||||
`auto`, the system uses the IP address from the interface matching the
|
||||
subnet. For example:
|
||||
|
||||
```
|
||||
admin@example:/> show interfaces brief
|
||||
Interface Status Address
|
||||
eth0 UP 192.168.1.1/24
|
||||
eth1 UP 192.168.2.1/24
|
||||
|
||||
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/> edit option dns-server
|
||||
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/option/dns-server/> set address auto
|
||||
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/option/dns-server/> leave
|
||||
```
|
||||
|
||||
In this case, clients in subnet 192.168.1.0/24 will receive 192.168.1.1
|
||||
as their DNS server address.
|
||||
|
||||
|
||||
## Multiple Subnets
|
||||
|
||||
Configure DHCP for multiple networks:
|
||||
|
||||
```
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit dhcp-server
|
||||
admin@example:/config/dhcp-server/> edit subnet 192.168.1.0/24
|
||||
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/> set pool start-address 192.168.1.100 end-address 192.168.1.200
|
||||
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/> leave
|
||||
admin@example:/config/dhcp-server/> edit subnet 192.168.2.0/24
|
||||
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/> set pool start-address 192.168.2.100 end-address 192.168.2.200
|
||||
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/> leave
|
||||
```
|
||||
|
||||
|
||||
## Monitoring
|
||||
|
||||
View active leases and server statistics:
|
||||
|
||||
```
|
||||
admin@example:/> show dhcp-server
|
||||
IP ADDRESS MAC HOSTNAME CLIENT ID EXPIRES
|
||||
192.168.2.22 00:a0:85:00:02:05 00:c0:ff:ee 3591s
|
||||
192.168.1.11 00:a0:85:00:04:06 foo 01:00:a0:85:00:04:06 3591s
|
||||
|
||||
admin@example:/> show dhcp-server statistics
|
||||
DHCP offers sent : 6
|
||||
DHCP ACK messages sent : 5
|
||||
DHCP NAK messages sent : 0
|
||||
DHCP decline messages received : 0
|
||||
DHCP discover messages received : 6
|
||||
DHCP request messages received : 5
|
||||
DHCP release messages received : 6
|
||||
DHCP inform messages received : 6
|
||||
```
|
||||
|
||||
|
||||
[^1]: This requires the system DNS resolver to be configured.
|
||||
+92
-58
@@ -1,21 +1,21 @@
|
||||
Discover Infix Units
|
||||
====================
|
||||
# Discover Devices
|
||||
|
||||
Infix advertises itself via the [mDNS-SD](#mdns-sd) and [LLDP](#lldp)
|
||||
discovery protocols. mDNS-SD has good client support in Windows, macOS
|
||||
and on Linux systems. More on these protocols later.
|
||||
|
||||
An even simpler method is available when directly attached to an Infix
|
||||
device:
|
||||
|
||||
```
|
||||
.----. Ethernet .-------.
|
||||
| PC +---------------------+ Infix |
|
||||
'----' if1 eth0 '-------'
|
||||
.----. Ethernet .-------.
|
||||
| PC +---------------------+ Infix |
|
||||
'----' if1 e1 '-------'
|
||||
```
|
||||
Figure 1: PC directly connected over Ethernet to Infix unit (here eth0).
|
||||
|
||||
|
||||
When you wish to discover the IP address of an Infix switch, the simplest
|
||||
way is probably to *ping the IPv6 all-hosts* address (ff02::1) over a
|
||||
directly connected Ethernet cable. The unit's link-local IPv6 address is
|
||||
seen in the response.
|
||||
|
||||
In the example below, the PC is connected to Infix via interface *tap0*
|
||||
(*tap0* is *if1* in Figure 1) and Infix responds with address
|
||||
With IPv6 you can *ping the all-hosts* address (ff02::1), the device's
|
||||
link-local IPv6 address is then seen in the response. In the following
|
||||
example, the PC here uses *tap0* as *if1*, Infix responds with address
|
||||
*fe80::ff:fec0:ffed*.
|
||||
|
||||
```
|
||||
@@ -31,7 +31,11 @@ rtt min/avg/max/mdev = 0.389/0.455/0.558/0.073 ms
|
||||
linux-pc:#
|
||||
```
|
||||
|
||||
The PC could connect then connect to Infix, e.g., using SSH.
|
||||
> [!TIP]
|
||||
> The `-L` option ignores local responses from the PC.
|
||||
|
||||
This address can then be used to connect to the device, e.g., using SSH.
|
||||
Notice the syntax `username@address%interface`:
|
||||
|
||||
```
|
||||
linux-pc:# ssh admin@fe80::ff:fec0:ffed%tap0
|
||||
@@ -39,17 +43,13 @@ admin@fe80::ff:fec0:ffed%tap0's password: admin
|
||||
admin@infix-c0-ff-ee:~$
|
||||
```
|
||||
|
||||
## Discovery mechanisms available in Infix
|
||||
|
||||
Infix advertises its presence via the [mDNS](#mdns) and [LLDP](#lldp)
|
||||
discovery protocols.
|
||||
## LLDP
|
||||
|
||||
Infix supports LLDP (IEEE 802.1AB). For a device with factory default
|
||||
settings, the link-local IPv6 address can be read from the Management
|
||||
Address TLV using *tcpdump* or other sniffing tools[^1]:
|
||||
|
||||
### LLDP
|
||||
|
||||
Infix supports LLDP (IEEE 802.1AB). For a unit with factory default
|
||||
settings, the PC can readout the link-local IPv6 address from the
|
||||
Management Address TLV using *tcpdump* or other sniffing tools[^1].
|
||||
```
|
||||
linux-pc:# tcpdump -i tap0 -Qin -v ether proto 0x88cc
|
||||
tcpdump: listening on tap0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
|
||||
@@ -83,11 +83,12 @@ tcpdump: listening on tap0, link-type EN10MB (Ethernet), snapshot length 262144
|
||||
linux-pc:#
|
||||
```
|
||||
|
||||
If the unit has an IPv4 address assigned, it is shown in an additional
|
||||
If the device has an IPv4 address assigned, it is shown in an additional
|
||||
Management Address TLV.
|
||||
|
||||
> **Note** The Management Addresses shown by LLDP are not
|
||||
> necessarily associated with the port transmitting the LLDP message.
|
||||
> [!NOTE]
|
||||
> The Management Addresses shown by LLDP are not necessarily associated
|
||||
> with the port transmitting the LLDP message.
|
||||
|
||||
In the example below, the IPv4 address (10.0.1.1) happens to be
|
||||
assigned to *eth0*, while the IPv6 address (2001:db8::1) is not.
|
||||
@@ -130,10 +131,6 @@ tcpdump: listening on tap0, link-type EN10MB (Ethernet), snapshot length 262144
|
||||
linux-pc:#
|
||||
```
|
||||
|
||||
[^1]: [lldpd: implementation of IEEE 802.1ab
|
||||
(LLDP)](https://github.com/lldp/lldpd) includes *lldpcli*, which
|
||||
is handy to sniff and display LLDP packets.
|
||||
|
||||
The LLDP service can be disabled using the following commands.
|
||||
|
||||
```
|
||||
@@ -143,13 +140,38 @@ admin@infix-c0-ff-ee:/config/> leave
|
||||
admin@infix-c0-ff-ee:/>
|
||||
```
|
||||
|
||||
### mDNS
|
||||
|
||||
DNS-SD/mDNS can be used to discover Infix units and services. Infix
|
||||
units present their IP addresses, services and hostname within the
|
||||
.local domain. This method has good client support in Apple and Linux
|
||||
systems. On Linux, tools such as *avahi-browse* or *mdns-scan*[^2] can
|
||||
be used to search for devices advertising their services via mDNS.
|
||||
## mDNS-SD
|
||||
|
||||
DNS-SD/mDNS-SD can be used to discover Infix devices and services. By
|
||||
default, Infix use the `.local` domain for advertising services. Some
|
||||
networks use `.lan` instead, so this configurable:
|
||||
|
||||
```
|
||||
admin@infix-c0-ff-ee:/> configure
|
||||
admin@infix-c0-ff-ee:/config/> edit mdns
|
||||
admin@infix-c0-ff-ee:/config/mdns/> set domain lan
|
||||
```
|
||||
|
||||
Other available settings include limiting the interfaces mDNS responder
|
||||
acts on:
|
||||
|
||||
```
|
||||
admin@infix-c0-ff-ee:/config/> set interfaces allow e1
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
admin@infix-c0-ff-ee:/config/> set interfaces deny wan
|
||||
```
|
||||
|
||||
The `allow` and `deny` settings are complementary, `deny` always wins.
|
||||
|
||||
----
|
||||
|
||||
In Linux, tools such as *avahi-browse* or *mdns-scan*[^2] can be used to
|
||||
search for devices advertising their services via mDNS.
|
||||
|
||||
```
|
||||
linux-pc:# avahi-browse -ar
|
||||
@@ -181,9 +203,15 @@ linux-pc:# avahi-browse -ar
|
||||
linux-pc:#
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> The `-t` option is also very useful, it stops browsing automatically
|
||||
> when a "more or less complete list" has been printed. However, some
|
||||
> devices on the LAN may be in deep sleep so run the command again if
|
||||
> you cannot find the device you are looking for.
|
||||
|
||||
Additionally, *avahi-resolve-host-name* can be used to verify domain
|
||||
name mappings for IP addresses. By default, it translates from IPv4
|
||||
addresses. This function allows users to confirm that addresses are
|
||||
name mappings for IP addresses. By default, it translates from IPv4
|
||||
addresses. This function allows users to confirm that addresses are
|
||||
mapped correctly.
|
||||
|
||||
```
|
||||
@@ -219,20 +247,21 @@ linux-pc:#
|
||||
```
|
||||
|
||||
To disable mDNS/mDNS-SD, type the commands:
|
||||
|
||||
```
|
||||
admin@infix-c0-ff-ee:/> configure
|
||||
admin@infix-c0-ff-ee:/config/> no mdns
|
||||
admin@infix-c0-ff-ee:/config/> leave
|
||||
```
|
||||
|
||||
#### Human-Friendly Hostname Alias
|
||||
### Human-Friendly Hostname Alias
|
||||
|
||||
Each Infix unit will advertise itself as *infix.local*, in addition to
|
||||
its full hostname (e.g., *infix-c0-ff-ee.local* or *foo.local*). This
|
||||
alias works seamlessly on a network with a single Infix device, and
|
||||
makes it easy to connect when the exact hostname is not known in
|
||||
advance. The examples below show how the alias can be used for
|
||||
actions such as pinging or establishing an SSH connection:
|
||||
Each Infix deviuce advertise itself as *infix.local*, in addition to its
|
||||
full hostname (e.g., *infix-c0-ff-ee.local* or *foo.local*). This alias
|
||||
works seamlessly on a network with a single Infix device, and makes it
|
||||
easy to connect when the exact hostname is not known in advance. The
|
||||
examples below show how the alias can be used for actions such as
|
||||
pinging or establishing an SSH connection:
|
||||
|
||||
```
|
||||
linux-pc:# ping infix.local -c 3
|
||||
@@ -254,30 +283,33 @@ linux-pc:# ssh admin@infix.local
|
||||
|
||||
Run the command 'cli' for interactive OAM
|
||||
|
||||
linux-pc:#
|
||||
admin@infix-c0-ff-ee:~$
|
||||
```
|
||||
|
||||
When multiple Infix devices are present on the LAN the alias will not
|
||||
uniquely identify a device; *infix.local* will refer to any of the
|
||||
Infix devices, likely the one that first appeared.
|
||||
|
||||
> When multiple Infix units are present, use the full hostname (e.g.,
|
||||
> *infix-c0-ff-ee.local* or *foo.local*) rather than the alias
|
||||
> infix.local to deterministically connect to a unit.
|
||||
> [!NOTE]
|
||||
> When multiple Infix devices are present on the LAN, use the full name,
|
||||
> e.g., *infix-c0-ff-ee.local* or *foo.local* rather than the alias
|
||||
> *infix.local* to deterministically connect to the device.
|
||||
|
||||
|
||||
#### Netbrowse service to find all your devices
|
||||
### Browse Network Using *network.local*
|
||||
|
||||
Another mDNS alias that all Infix devices can advertise is
|
||||
*network.local*. This is a web service which basically runs avahi-browse
|
||||
and displays a table of other Infix devices and their services.
|
||||
Another mDNS alias that all Infix devices advertise is *network.local*.
|
||||
This is a web service which basically runs `avahi-browse` and displays a
|
||||
table of other Infix devices and their services.
|
||||
|
||||

|
||||
|
||||
With multiple Infix devices on the LAN, one will be your portal to
|
||||
access all others, if it goes down another will take its place.
|
||||
With multiple Infix devices on the LAN, one will take the role of your
|
||||
portal to access all others, if it goes down another takes its place.
|
||||
|
||||
To disable the netbrowse service, and the *network.local* alias, the
|
||||
following commands can be used:
|
||||
|
||||
To disable the netbrowse service, the following commands can be used:
|
||||
```
|
||||
admin@infix-c0-ff-ee:/> configure
|
||||
admin@infix-c0-ff-ee:/config/> edit web
|
||||
@@ -285,6 +317,8 @@ admin@infix-c0-ff-ee:/config/web/> no netbrowse
|
||||
admin@infix-c0-ff-ee:/config/web/> leave
|
||||
```
|
||||
|
||||
|
||||
[^1]: E.g., [lldpd](https://github.com/lldp/lldpd) which includes the
|
||||
*lldpcli* too, handy to sniff and display LLDP packets.
|
||||
[^2]: [mdns-scan](http://0pointer.de/lennart/projects/mdns-scan/): a
|
||||
tool for scanning for mDNS/DNS-SD published services on the local
|
||||
network
|
||||
tool for scanning for mDNS/DNS-SD services on the local network.
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
# Management
|
||||
|
||||
The system utilizes YANG models for keeping configuration and operational
|
||||
data. These databases can be managed through different interfaces such
|
||||
as NETCONF, RESTCONF, and CLI via SSH or Console.
|
||||
|
||||
## SSH Management
|
||||
|
||||
An SSH server (SSHv2) is provided for remote management. It can be
|
||||
enabled/disabled as shown below.
|
||||
|
||||
```
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit ssh
|
||||
admin@example:/config/ssh/> set enabled
|
||||
admin@example:/config/ssh/>
|
||||
```
|
||||
|
||||
By default the SSH server accepts connections to port 22 on all its IP
|
||||
addresses, but this can be adjusted using the `listen` command. To
|
||||
make the server (only) listen for incoming connections to IP address
|
||||
_192.168.1.1_ and port _12345_ the following commands can be used.
|
||||
|
||||
```
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit ssh
|
||||
admin@example:/config/ssh/> show
|
||||
enabled true;
|
||||
hostkey genkey;
|
||||
listen ipv4 {
|
||||
address 0.0.0.0;
|
||||
port 22;
|
||||
}
|
||||
listen ipv6 {
|
||||
address ::;
|
||||
port 22;
|
||||
}
|
||||
admin@example:/config/ssh/> no listen ipv6
|
||||
admin@example:/config/ssh/> edit listen ipv4
|
||||
admin@example:/config/ssh/listen/ipv4/> set address 192.168.1.1
|
||||
admin@example:/config/ssh/listen/ipv4/> set port 12345
|
||||
admin@example:/config/ssh/listen/ipv4/>
|
||||
```
|
||||
|
||||
The default SSH hostkey is generated on first boot and is used in both
|
||||
SSH and NETCONF (SSH transport). Custom keys can be added to the
|
||||
configuration in `ietf-keystore`. The only supported hostkey type is
|
||||
RSA for now, thus the private key must be
|
||||
`ietf-crypto-types:rsa-private-key-format` and the public key
|
||||
`ietf-crypto-types:ssh-public-key-format`
|
||||
|
||||
### Use your own SSH hostkeys
|
||||
|
||||
Hostkeys can be generated with OpenSSL:
|
||||
```bash
|
||||
openssl genpkey -quiet -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -outform PEM > mykey
|
||||
openssl rsa -RSAPublicKey_out < mykey > mykey.pub
|
||||
```
|
||||
Store the keys in `ietf-keystore` _without_ the header and footer information
|
||||
created by OpenSSL.
|
||||
|
||||
After the key has been stored in the keystore and given the name
|
||||
_mykey_ it can be added to SSH configuration:
|
||||
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit ssh
|
||||
admin@example:/config/ssh/> set hostkey mykey
|
||||
|
||||
## Console Port
|
||||
|
||||
For units with a console port, it is possible for users to login to
|
||||
shell/CLI with functionality similar to what is provided via SSH.
|
||||
|
||||
The type and setup for your console port is product specific. For
|
||||
instance, it can be a USB-C port connected to the CPU serial port
|
||||
using a USB-to-serial converter. To connect you would need a USB-C cable
|
||||
connected to the console port of the device. The serial port is
|
||||
typically setup to run at 115200 baud, 8N1.
|
||||
|
||||
|
||||
```
|
||||
Infix -- a Network Operating System v24.11.1 (ttyS0)
|
||||
example login: admin
|
||||
Password:
|
||||
.-------.
|
||||
| . . | Infix -- a Network Operating System
|
||||
|-. v .-| https://kernelkit.org
|
||||
'-'---'-'
|
||||
|
||||
Run the command 'cli' for interactive OAM
|
||||
|
||||
admin@example:~$
|
||||
```
|
||||
|
||||
The `resize` command can be used to update terminal settings to the
|
||||
size of your terminal window.
|
||||
|
||||
```
|
||||
admin@example:~$ resize
|
||||
COLUMNS=115;LINES=59;export COLUMNS LINES;
|
||||
admin@example:~$
|
||||
```
|
||||
|
||||
CLI can be entered from shell in the same way as for SSH.
|
||||
|
||||
```
|
||||
admin@example:~$ cli
|
||||
|
||||
See the 'help' command for an introduction to the system
|
||||
|
||||
admin@example:/> show interfaces
|
||||
INTERFACE PROTOCOL STATE DATA
|
||||
lo ethernet UP 00:00:00:00:00:00
|
||||
ipv4 127.0.0.1/8 (static)
|
||||
ipv6 ::1/128 (static)
|
||||
e1 ethernet LOWER-DOWN 00:53:00:06:03:01
|
||||
e2 ethernet LOWER-DOWN 00:53:00:06:03:02
|
||||
...
|
||||
admin@example:/>
|
||||
```
|
||||
|
||||
## Web, Web-console and RESTCONF
|
||||
|
||||
The system provides a set of Web services:
|
||||
|
||||
- a rudimentary Web server, currently limited to an information page
|
||||
- a RESTCONF server with equivalent management capabilities as NETCONF
|
||||
- a Web console service, where the shell/CLI can be accessed via
|
||||
HTTPS, similar to connecting via a console port or SSH
|
||||
|
||||
There is also a *Netbrowse* Web service presenting information about
|
||||
the unit's neighbors, collected via mDNS (see
|
||||
[Discovery](discovery.md) for more details).
|
||||
|
||||
```
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit web
|
||||
admin@example:/config/web/> help
|
||||
enabled Enable or disable on all web services.
|
||||
console Web console interface.
|
||||
netbrowse mDNS Network Browser.
|
||||
restconf IETF RESTCONF Server.
|
||||
admin@example:/config/web/>
|
||||
```
|
||||
|
||||
### Enable/disable Web Service and Server
|
||||
|
||||
The Web service can be enabled as shown below.
|
||||
|
||||
```
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit web
|
||||
admin@example:/config/web/> set enabled
|
||||
admin@example:/config/web/>
|
||||
```
|
||||
|
||||
Enabling the Web service implies that a Web server is
|
||||
enabled. Currently this Web server provides generic Infix information,
|
||||
as well as a link to a Web console. The Web server uses HTTPS; any
|
||||
HTTP request is redirected to HTTPS.
|
||||
|
||||
The _enabled_ setting for the Web service acts as a global
|
||||
enable/disable setting for the other Web services (Web console,
|
||||
RESTCONF and Netbrowse).
|
||||
|
||||
### Enable/disable Web Console
|
||||
|
||||
The Web console service provides a terminal service similar to Console
|
||||
or SSH. The Web console is secured via HTTPS on port 7861.
|
||||
|
||||
The Web console has its own enable/disable setting, but will only be
|
||||
activated if the Web service is enabled. The example below shows how
|
||||
to disable the Web console.
|
||||
|
||||
```
|
||||
admin@example:/config/web/> edit console
|
||||
admin@example:/config/web/console/> no enabled
|
||||
admin@example:/config/web/console/>
|
||||
```
|
||||
|
||||
### Enable/disable RESTCONF Service
|
||||
|
||||
Alternatively, the system can be managed remotely using
|
||||
RESTCONF. Meaning you can `curl` it instead of using a dedicated
|
||||
NETCONF client.
|
||||
|
||||
The RESTCONF service has its own enable/disable setting, but will
|
||||
only be activated if the Web service is enabled. The example below
|
||||
shows how to disable the RESTCONF service.
|
||||
|
||||
```
|
||||
admin@example:/config/web/> edit restconf
|
||||
admin@example:/config/web/restconf/> no enabled
|
||||
admin@example:/config/web/restconf/>
|
||||
```
|
||||
+77
-76
@@ -43,18 +43,18 @@ it. In this example, traffic assigned to the VLAN in question would be
|
||||
diverted to the VLAN interface before entering the bridge, while all
|
||||
other traffic would be bridged as usual.
|
||||
|
||||
| **Type** | **Yang Model** | **Description** |
|
||||
|----------|----------------------------|---------------------------------------------------------------|
|
||||
| bridge | infix-if-bridge | SW implementation of an IEEE 802.1Q bridge |
|
||||
| ip | ietf-ip, infix-ip | IP address to the subordinate interface |
|
||||
| vlan | infix-if-vlan | Capture all traffic belonging to a specific 802.1Q VID |
|
||||
| lag[^1] | infix-if-lag | Bonds multiple interfaces into one, creating a link aggregate |
|
||||
| lo | ietf-interfaces | Software loopback interface |
|
||||
| eth | ieee802-ethernet-interface | Physical Ethernet device/port. |
|
||||
| | infix-ethernet-interface | |
|
||||
| veth | infix-if-veth | Virtual Ethernet pair, typically one end is in a container |
|
||||
| *common* | ietf-interfaces, | Properties common to all interface types |
|
||||
| | infix-interfaces | |
|
||||
| **Type** | **Yang Model** | **Description** |
|
||||
|----------|----------------------------|--------------------------------------------------------------|
|
||||
| bridge | infix-if-bridge | SW implementation of an IEEE 802.1Q bridge |
|
||||
| ip | ietf-ip, infix-ip | IP address to the subordinate interface |
|
||||
| vlan | infix-if-vlan | Capture all traffic belonging to a specific 802.1Q VID |
|
||||
| lag[^1] | infix-if-lag | Bond multiple interfaces into one, creating a link aggregate |
|
||||
| lo | ietf-interfaces | Software loopback interface |
|
||||
| eth | ieee802-ethernet-interface | Physical Ethernet device/port. |
|
||||
| | infix-ethernet-interface | |
|
||||
| veth | infix-if-veth | Virtual Ethernet pair, typically one end is in a container |
|
||||
| *common* | ietf-interfaces, | Properties common to all interface types |
|
||||
| | infix-interfaces | |
|
||||
|
||||
|
||||
## Data Plane
|
||||
@@ -154,9 +154,9 @@ fabric!
|
||||
#### MAC Bridge
|
||||
|
||||
In Infix ports are by default not switch ports, unless the customer
|
||||
specific factory config sets it up this way. To enable switching
|
||||
between ports you create a bridge and then add ports to that
|
||||
bridge. That's it.
|
||||
specific factory config sets it up this way. To enable switching, with
|
||||
offloading if you have a switch chipset, between ports you create a
|
||||
bridge and then add ports to that bridge. Like this:
|
||||
|
||||
```
|
||||
admin@example:/> configure
|
||||
@@ -170,13 +170,15 @@ admin@example:/config/> leave
|
||||
Here we add two ports to bridge `br0`: `eth0` and `eth1`.
|
||||
|
||||
> [!TIP]
|
||||
> Infix has many built-in helpers controlled by convention. Example,
|
||||
> naming your bridge `brN`, where `N` is a number, hints Infix to set
|
||||
> interface type automatically and unlocks all bridge features. Other
|
||||
> "magic" names are `vethNA`, where `N` is a number and `A` is a letter
|
||||
> ('a' for access port and 'b' for bridge side is common), and `ethN.M`
|
||||
> for VLAN M on top of `ethN`, or `dockerN` to create an IP masquerading
|
||||
> container bridge.
|
||||
> The CLI has several built-in helpers governed by convention. E.g.,
|
||||
> naming bridges `brN`, where `N` is a number, the type is *inferred*
|
||||
> automatically and unlocks all bridge features. Other conventions are
|
||||
> `vethNA`, where `N` is a number and `A` is a letter ('a' for access
|
||||
> port and 'b' for bridge side is common), and `ethN.M` for VLAN M on
|
||||
> top of `ethN`, or `dockerN` for a IP masquerading container bridge.
|
||||
>
|
||||
> Note, this inference only works with the CLI, configuring networking
|
||||
> over NETCONF or RESTCONF requires setting the type explicitly.
|
||||
|
||||

|
||||
|
||||
@@ -187,11 +189,11 @@ bridge should be used instead.
|
||||
|
||||
#### VLAN Filtering Bridge
|
||||
|
||||
By default bridges in Linux do not filter based on VLAN tags. It can be
|
||||
enabled in Infix when creating a bridge by adding a port to a VLAN as a
|
||||
tagged or untagged member. Use the port default VID (PVID) setting to
|
||||
control VLAN association for traffic ingressing a port untagged (default
|
||||
PVID: 1).
|
||||
By default bridges in Linux do not filter based on VLAN tags. This can
|
||||
be enabled when creating a bridge by adding a port to a VLAN as a tagged
|
||||
or untagged member. Use the port default VID (PVID) setting to control
|
||||
VLAN association for traffic ingressing a port untagged (default PVID:
|
||||
1).
|
||||
|
||||
```
|
||||
admin@example:/config/> edit interface br0
|
||||
@@ -283,15 +285,15 @@ br0 224.1.1.1 e3, e2
|
||||
br0 ff02::6a br0
|
||||
```
|
||||
|
||||
It is a small LAN, so our bridge has already become the elected IGMP
|
||||
querier. We see it is ours because the timeout is `None`, and we
|
||||
recognize our IP address. We can also see two ports that have joined
|
||||
the same IPv4 multicast group, 224.1.1.1, and one join from Infix itself
|
||||
for the IPv6 group ff02::6a.
|
||||
This is a rather small LAN, so our bridge has already become the elected
|
||||
IGMP querier. We see it is ours because the timeout is `None`, and we
|
||||
recognize the IP address the system has detected, as ours. We can also
|
||||
see two ports that have joined the same IPv4 multicast group, 224.1.1.1,
|
||||
and one join from the system itself for the IPv6 group ff02::6a.
|
||||
|
||||
Now, let's see what happens when we add another bridge, with VLAN
|
||||
filtering enabled. We skip the boring parts about how to move ports
|
||||
e4-e7 to `br1` and assign them to VLANs, and again, focus on the
|
||||
Now, let us see what happens when we add another bridge, this time with
|
||||
VLAN filtering enabled. We skip the boring parts about how to move
|
||||
ports e4-e7 to `br1` and assign them to VLANs, and again, focus on the
|
||||
multicast bits only:
|
||||
|
||||
```
|
||||
@@ -303,7 +305,7 @@ admin@example:/config/interface/br1/> leave
|
||||
admin@example:/> copy running-config startup-config
|
||||
```
|
||||
|
||||
Let's see what we get:
|
||||
Let us see what we get:
|
||||
|
||||
```
|
||||
admin@example:/> show ip multicast
|
||||
@@ -404,15 +406,15 @@ an IGMP/MLD fast-leave port.
|
||||
|
||||
#### Forwarding of IEEE Reserved Group Addresses
|
||||
|
||||
Addresses in range `01:80:C2:00:00:0X` are used by various bridge
|
||||
signaling protocols, and are not forwarded by default. Still, it is
|
||||
sometimes useful to let the bridge forward such packets, and Infix
|
||||
supports this by specifying protocol names or the last address
|
||||
*nibble* as decimal value `0..15`.
|
||||
Addresses in the range `01:80:C2:00:00:0X` are used by various bridge
|
||||
signaling protocols, and are not forwarded by default. Still, it is
|
||||
sometimes useful to let the bridge forward such packets, this can be
|
||||
done by specifying protocol names or the last address *nibble* as
|
||||
decimal value `0..15`:
|
||||
|
||||
```
|
||||
admin@example:/config/> edit interface br0 bridge
|
||||
admin@example:/config/interface/br0/bridge/> set ieee-group-forward <?>
|
||||
admin@example:/config/interface/br0/bridge/> set ieee-group-forward # Tap the ? ley for alternatives
|
||||
[0..15] List of IEEE link-local protocols to forward, e.g., STP, LLDP
|
||||
dot1x 802.1X Port-Based Network Access Control.
|
||||
lacp 802.3 Slow Protocols, e.g., LACP.
|
||||
@@ -430,7 +432,7 @@ admin@example:/config/interface/br0/bridge/>
|
||||
|
||||
### VLAN Interfaces
|
||||
|
||||
Creating a VLAN can be done in many ways. This section assumes VLAN
|
||||
Creating a VLAN can be done in many ways. This section assumes VLAN
|
||||
interfaces created atop another Linux interface. E.g., the VLAN
|
||||
interfaces created on top of the Ethernet interface or bridge in the
|
||||
picture below.
|
||||
@@ -472,7 +474,7 @@ top of a bridge interface *br0* is named *vlan10*.
|
||||
|
||||
> [!NOTE]
|
||||
> If you name your VLAN interface `foo0.N` or `vlanN`, where `N` is a
|
||||
> number, Infix will set the interface type automatically for you.
|
||||
> number, the CLI infers the interface type automatically.
|
||||
|
||||
|
||||
### Physical Ethernet Interfaces
|
||||
@@ -638,7 +640,8 @@ admin@example:/config/>
|
||||
This section details IP Addresses And Other Per-Interface IP settings.
|
||||
|
||||
Infix support several network interface types, each can be assigned one
|
||||
or more IP addresses, both IPv4 and IPv6 are supported.
|
||||
or more IP addresses, both IPv4 and IPv6 are supported. (There is no
|
||||
concept of a "primary" address.)
|
||||
|
||||

|
||||
|
||||
@@ -661,31 +664,31 @@ are listed below. Configurable options can be disabled on a per client
|
||||
interface basis, some options, like `clientid` and option 81, are
|
||||
possible to set the value of as well.
|
||||
|
||||
| **Opt** | **Name** | **Cfg** | **Description** |
|
||||
|---------|------------------|---------|-----------------------------------------------------|
|
||||
| 1 | `subnet` | No | Request IP address and netmask |
|
||||
| 3 | `router` | Yes | Default route(s), see also option 121 and 249 |
|
||||
| 6 | `dns` | Yes | DNS server(s), static ones take precedence |
|
||||
| 12 | `hostname` | Yes | DHCP cannot set hostname, only for informing server |
|
||||
| 15 | `domain` | Yes | Default domain name, for name resolution |
|
||||
| 28 | `broadcast` | Yes | Broadcast address, calculated if disabled |
|
||||
| 42 | `ntpsrv` | Yes | NTP server(s), static ones take precedence |
|
||||
| 50 | `address` | Yes | Request (previously cached) address |
|
||||
| 61 | `clientid` | Yes | Default MAC address (and option 12) |
|
||||
| 81 | `fqdn` | Yes | Similar to option 12, request FQDN update in DNS |
|
||||
| 119 | `search` | Yes | Request domain search list |
|
||||
| 121 | `staticroutes` | Yes | Classless static routes |
|
||||
| 249 | `msstaticroutes` | Yes | Microsoft static route |
|
||||
| | | | |
|
||||
| **Opt** | **Name** | **Cfg** | **Description** |
|
||||
|---------|-----------------------------|---------|-----------------------------------------------------|
|
||||
| 1 | `netmask` | No | Request IP address and netmask |
|
||||
| 3 | `router` | Yes | Default route(s), see also option 121 and 249 |
|
||||
| 6 | `dns-server` | Yes | DNS server(s), static ones take precedence |
|
||||
| 12 | `hostname` | Yes | DHCP cannot set hostname, only for informing server |
|
||||
| 15 | `domain` | Yes | Default domain name, for name resolution |
|
||||
| 28 | `broadcast` | Yes | Broadcast address, calculated if disabled |
|
||||
| 42 | `ntp-server` | Yes | NTP server(s), static ones take precedence |
|
||||
| 50 | `address` | Yes | Request (previously cached) address |
|
||||
| 61 | `client-id` | Yes | Default MAC address (and option 12) |
|
||||
| 81 | `fqdn` | Yes | Similar to option 12, request FQDN update in DNS |
|
||||
| 119 | `search` | Yes | Request domain search list |
|
||||
| 121 | `classless-static-route` | Yes | Classless static routes |
|
||||
| 249 | `ms-classless-static-route` | Yes | Microsoft static route, same as option 121 |
|
||||
| | | | |
|
||||
|
||||
**Default:** `router`, `dns`, `domain`, `broadcast`, `ntpsrv`, `search`,
|
||||
`address`, `staticroutes`, `msstaticroutes`
|
||||
**Default:** `router`, `dns-server`, `domain`, `broadcast`, `ntp-server`, `search`,
|
||||
`address`, `classless-static-route`, `ms-classless-static-route`
|
||||
|
||||
When configuring a DHCP client, ensure that the NTP client is enabled
|
||||
for the `ntpsrv` DHCP option to be processed correctly. If the NTP
|
||||
for the `ntp-server` DHCP option to be processed correctly. If the NTP
|
||||
client is not enabled, any NTP servers provided by the DHCP server will
|
||||
be ignored. For details on how to enable the NTP client, see the
|
||||
[NTP Client Configuration](system.md#ntp-client-configuration) section.
|
||||
be ignored. For details on how to enable the NTP client, see the [NTP
|
||||
Client Configuration](system.md#ntp-client-configuration) section.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Per [RFC3442][4], if the DHCP server returns both a Classless Static
|
||||
@@ -778,7 +781,6 @@ will be used, otherwise it falls back to the default algorithm.
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit dhcp-client
|
||||
admin@example:/config/dhcp-client/> set client-if eth0
|
||||
admin@example:/config/dhcp-client/> set enabled true
|
||||
admin@example:/config/dhcp-client/> leave
|
||||
admin@example:/> show interfaces
|
||||
INTERFACE PROTOCOL STATE DATA
|
||||
@@ -1226,7 +1228,7 @@ currently supported, namely `ipv4` and `ipv6`.
|
||||
[4]: https://www.rfc-editor.org/rfc/rfc3442
|
||||
[0]: https://frrouting.org/
|
||||
|
||||
[^1]: Please note, link aggregates are not yet supported in Infix.
|
||||
[^1]: Please note, link aggregates are not yet supported.
|
||||
[^2]: Link-local IPv6 addresses are implicitly enabled when enabling
|
||||
IPv6. IPv6 can be enabled/disabled per interface in the
|
||||
[ietf-ip][2] YANG model.
|
||||
@@ -1237,11 +1239,10 @@ currently supported, namely `ipv4` and `ipv6`.
|
||||
[^4]: A YANG deviation was previously used to make it possible to set
|
||||
`phys-address`, but this has been replaced with the more flexible
|
||||
`custom-phys-address`.
|
||||
[^5]: Infix MAC bridges on Marvell Linkstreet devices are currently
|
||||
limited to use a single MAC database, causing issues if the same
|
||||
MAC address appears on different MAC bridges.
|
||||
[^6]: Ethernet counters are described in
|
||||
*ieee802-ethernet-interface.yang* and
|
||||
*infix-ethernet-interface.yang*. [Ethernet
|
||||
Counters](eth-counters.md) page provides additional details on
|
||||
statistics support.
|
||||
[^5]: MAC bridges on Marvell Linkstreet devices are currently limited to
|
||||
a single MAC database, this may be a problem if the same MAC address
|
||||
appears in different MAC bridges.
|
||||
[^6]: Ethernet counters are described in *ieee802-ethernet-interface.yang*
|
||||
and *infix-ethernet-interface.yang*. There is a dedicated document on
|
||||
[Ethernet Counters](eth-counters.md) that provide additional details
|
||||
on the statistics support.
|
||||
|
||||
+106
-59
@@ -11,9 +11,10 @@ specific string followed by the last three octets of the system base MAC
|
||||
address, e.g., `switch-12-34-56`. An example of how to change the
|
||||
hostname is included below.
|
||||
|
||||
> **Note:** when issuing `leave` to activate your changes, remember to
|
||||
> also save your settings, `copy running-config startup-config`. See
|
||||
> the [CLI Introduction](cli/introduction.md) for a background.
|
||||
> [!NOTE]
|
||||
> When issuing `leave` to activate your changes, remember to also save
|
||||
> your settings, `copy running-config startup-config`. See the [CLI
|
||||
> Introduction](cli/introduction.md) for a background.
|
||||
|
||||
|
||||
## Changing Password
|
||||
@@ -24,14 +25,14 @@ available in the system authentication configuration context.
|
||||
```
|
||||
admin@host:/config/> edit system authentication user admin
|
||||
admin@host:/config/system/authentication/user/admin/> change password
|
||||
New password:
|
||||
Retype password:
|
||||
New password:
|
||||
Retype password:
|
||||
admin@host:/config/system/authentication/user/admin/> leave
|
||||
```
|
||||
|
||||
The `change password` command starts an interactive dialogue that asks
|
||||
for the new password, with a confirmation, and then salts and encrypts
|
||||
the password with sha512crypt.
|
||||
the password with sha512crypt.
|
||||
|
||||
It is also possible to use the `set password ...` command. This allows
|
||||
setting an already hashed password. To manually hash a password, use
|
||||
@@ -39,7 +40,8 @@ the `do password encrypt` command. This launches the admin-exec command
|
||||
to hash, and optionally salt, your password. This encrypted string can
|
||||
then be used with `set password ...`.
|
||||
|
||||
> **Tip:** if you are having trouble thinking of a password, Infix has a
|
||||
> [!TIP]
|
||||
> If you are having trouble thinking of a password, there is a nifty
|
||||
> `password generate` command in admin-exec context which generates
|
||||
> random passwords using the UNIX command `pwgen`. Use the `do` prefix
|
||||
> when inside any configuration context to access admin-exec commands.
|
||||
@@ -55,7 +57,7 @@ With SSH keys in place it is possible to disable password login, just
|
||||
remember to verify SSH login and network connectivity before doing so.
|
||||
|
||||
```
|
||||
admin@host:/config/> edit system authentication user admin
|
||||
admin@host:/config/> edit system authentication user admin
|
||||
admin@host:/config/system/authentication/user/admin/> edit authorized-key example@host
|
||||
admin@host:/config/system/authentication/user/admin/authorized-key/example@host/> set algorithm ssh-rsa
|
||||
admin@host:/config/system/authentication/user/admin/authorized-key/example@host/> set key-data AAAAB3NzaC1yc2EAAAADAQABAAABgQC8iBL42yeMBioFay7lty1C4ZDTHcHyo739gc91rTTH8SKvAE4g8Rr97KOz/8PFtOObBrE9G21K7d6UBuPqmd0RUF2CkXXN/eN2PBSHJ50YprRFt/z/304bsBYkDdflKlPDjuSmZ/+OMp4pTsq0R0eNFlX9wcwxEzooIb7VPEdvWE7AYoBRUdf41u3KBHuvjGd1M6QYJtbFLQMMTiVe5IUfyVSZ1RCxEyAB9fR9CBhtVheTVsY3iG0fZc9eCEo89ErDgtGUTJK4Hxt5yCNwI88YaVmkE85cNtw8YwubWQL3/tGZHfbbQ0fynfB4kWNloyRHFr7E1kDxuX5+pbv26EqRdcOVGucNn7hnGU6C1+ejLWdBD7vgsoilFrEaBWF41elJEPKDzpszEijQ9gTrrWeYOQ+x++lvmOdssDu4KvGmj2K/MQTL2jJYrMJ7GDzsUu3XikChRL7zNfS2jYYQLzovboUCgqfPUsVba9hqeX3U67GsJo+hy5MG9RSry4+ucHs=
|
||||
@@ -65,9 +67,10 @@ key-data AAAAB3NzaC1yc2EAAAADAQABAAABgQC8iBL42yeMBioFay7lty1C4ZDTHcHyo739gc91rTT
|
||||
admin@host:/config/system/authentication/user/admin/authorized-key/example@host/> leave
|
||||
```
|
||||
|
||||
> **Note:** the `ssh-keygen` program already base64 encodes the public
|
||||
> key data, so there is no need to use the `text-editor` command, `set`
|
||||
> does the job.
|
||||
> [!NOTE]
|
||||
> The `ssh-keygen` program already base64 encodes the public key data,
|
||||
> so there is no need to use the `text-editor` command, `set` does the
|
||||
> job.
|
||||
|
||||
|
||||
## Multiple Users
|
||||
@@ -145,7 +148,7 @@ is committed by issuing the `leave` command.
|
||||
admin@host:/config/> edit system
|
||||
admin@host:/config/system/> set hostname example
|
||||
admin@host:/config/system/> leave
|
||||
admin@host:/>
|
||||
admin@example:/>
|
||||
```
|
||||
|
||||
The hostname is advertised over mDNS-SD in the `.local` domain. If
|
||||
@@ -154,8 +157,24 @@ case, mDNS will advertise a "uniqified" variant, usually suffixing with
|
||||
an index, e.g., `example-1.local`. Use an mDNS browser to scan for
|
||||
available devices on your LAN.
|
||||
|
||||
> **Note:** critical services like syslog, mDNS, LLDP, and similar that
|
||||
> advertise the hostname, are restarted when the hostname is changed.
|
||||
In some cases you may want to set the device's *domain name* as well.
|
||||
This is handled the same way:
|
||||
|
||||
```
|
||||
admin@host:/config/> edit system
|
||||
admin@host:/config/system/> set hostname foo.example.com
|
||||
admin@host:/config/system/> leave
|
||||
admin@foo:/>
|
||||
```
|
||||
|
||||
Both host and domain name are stored in the system files `/etc/hosts`
|
||||
and `/etc/hostname`. The latter is exclusively for the host name. The
|
||||
domain *may* be used by the system DHCP server when handing out leases
|
||||
to clients, it is up to the clients to request the domain name *option*.
|
||||
|
||||
> [!NOTE]
|
||||
> Critical services like syslog, mDNS, LLDP, and similar that advertise
|
||||
> the hostname, are restarted when the hostname is changed.
|
||||
|
||||
|
||||
## Changing Login Banner
|
||||
@@ -164,8 +183,9 @@ The `motd-banner` setting is an Infix augment and an example of a
|
||||
`binary` type setting that can be changed interactively with the
|
||||
built-in [`text-editor` command](cli/text-editor.md).
|
||||
|
||||
> **Tip:** see the next section for how to change the editor used
|
||||
> to something you may be more familiar with.
|
||||
> [!TIP]
|
||||
> See the next section for how to change the editor used to something
|
||||
> you may be more familiar with.
|
||||
|
||||
```
|
||||
admin@host:/config/> edit system
|
||||
@@ -196,43 +216,90 @@ admin@host:/config/system/> leave
|
||||
admin@host:/>
|
||||
```
|
||||
|
||||
> **Note:** as usual, configuration changes only take effect after
|
||||
> issuing the `leave` command. I.e., you must change the editor first,
|
||||
> and then re-enter configure context to use your editor of choice.
|
||||
> [!IMPORTANT]
|
||||
> Configuration changes only take effect after issuing the `leave`
|
||||
> command. I.e., you must change the editor first, and then re-enter
|
||||
> configure context to use your editor of choice.
|
||||
|
||||
|
||||
## DNS Resolver Configuration
|
||||
|
||||
The system supports both static and dynamic (DHCP) DNS setup. The
|
||||
locally configured (static) server is preferred over any acquired
|
||||
from a DHCP client.
|
||||
|
||||
```
|
||||
admin@host:/> configure
|
||||
admin@host:/config/> edit system dns-resolver
|
||||
admin@host:/config/system/dns-resolver/> set server google udp-and-tcp address 8.8.8.8
|
||||
admin@host:/config/system/dns-resolver/> show
|
||||
server google {
|
||||
udp-and-tcp {
|
||||
address 8.8.8.8;
|
||||
}
|
||||
}
|
||||
admin@host:/config/system/dns-resolver/> leave
|
||||
```
|
||||
|
||||
It is also possible to configure resolver options like timeout and
|
||||
retry attempts. See the YANG model for details, or use the built-in
|
||||
help system in the CLI.
|
||||
|
||||
> [!NOTE]
|
||||
> When acting as a DHCP server and DNS proxy for other devices, any
|
||||
> local DNS server configured here is automatically used as upstream DNS
|
||||
> server.
|
||||
|
||||
|
||||
## NTP Client Configuration
|
||||
|
||||
Below is an example configuration for enabling NTP
|
||||
with a specific server and the `iburst` option for faster initial
|
||||
synchronization.
|
||||
Below is an example configuration for enabling NTP with a specific
|
||||
server and the `iburst` option for faster initial synchronization.
|
||||
|
||||
```
|
||||
admin@host:/> configure
|
||||
admin@host:/config/> set system ntp enabled
|
||||
admin@host:/config/> set system ntp server ntp-pool
|
||||
admin@host:/config/> set system ntp server ntp-pool udp address pool.ntp.org
|
||||
admin@host:/config/> set system ntp server ntp-pool iburst
|
||||
admin@host:/config/> set system ntp server ntp-pool prefer
|
||||
admin@host:/config/> edit system ntp
|
||||
admin@host:/config/system/ntp/> set enabled
|
||||
admin@host:/config/system/ntp/> set server ntp-pool
|
||||
admin@host:/config/system/ntp/> set server ntp-pool udp address pool.ntp.org
|
||||
admin@host:/config/system/ntp/> set server ntp-pool iburst
|
||||
admin@host:/config/system/ntp/> set server ntp-pool prefer
|
||||
admin@host:/config/system/ntp/> leave
|
||||
```
|
||||
|
||||
This configuration enables the NTP client and sets the NTP server to
|
||||
`pool.ntp.org` with the `iburst` and `prefer` options. The `iburst`
|
||||
option ensures faster initial synchronization, and the `prefer` option
|
||||
This configuration enables the NTP client and sets the NTP server to
|
||||
`pool.ntp.org` with the `iburst` and `prefer` options. The `iburst`
|
||||
option ensures faster initial synchronization, and the `prefer` option
|
||||
designates this server as preferred.
|
||||
|
||||
* `prefer false`: The NTP client will choose the best available source
|
||||
based on several factors, such as network delay, stratum, and other
|
||||
based on several factors, such as network delay, stratum, and other
|
||||
metrics (default config).
|
||||
* `prefer true`: The NTP client will try to use the preferred server
|
||||
* `prefer true`: The NTP client will try to use the preferred server
|
||||
as the primary source unless it becomes unreachable or unusable.
|
||||
|
||||
|
||||
### Show NTP Sources
|
||||
|
||||
The status for NTP sources is availble in YANG and accessable with
|
||||
CLI/NETCONF/RESTCONF.
|
||||
|
||||
To view the sources being used by the NTP client, run:
|
||||
```
|
||||
admin@target:/> show ntp
|
||||
ADDRESS MODE STATE STRATUM POLL-INTERVAL
|
||||
192.168.1.1 server candidate 1 6
|
||||
192.168.2.1 server candidate 1 6
|
||||
192.168.3.1 server selected 1 6
|
||||
```
|
||||
|
||||
### Show NTP Status
|
||||
|
||||
To check the status of NTP synchronization, use the following command:
|
||||
To check the status of NTP synchronization (only availble in CLI), use
|
||||
the following command:
|
||||
|
||||
```
|
||||
admin@host:/> show ntp
|
||||
admin@host:/> show ntp tracking
|
||||
Reference ID : C0248F86 (192.36.143.134)
|
||||
Stratum : 2
|
||||
Ref time (UTC) : Mon Oct 21 10:06:45 2024
|
||||
@@ -249,34 +316,14 @@ Leap status : Normal
|
||||
admin@host:/>
|
||||
```
|
||||
|
||||
This output provides detailed information about the NTP status, including
|
||||
This output provides detailed information about the NTP status, including
|
||||
reference ID, stratum, time offsets, frequency, and root delay.
|
||||
|
||||
### Show NTP Sources
|
||||
|
||||
To view the sources being used by the NTP client, run:
|
||||
|
||||
```
|
||||
admin@host:/> show ntp sources
|
||||
|
||||
.-- Source mode '^' = server, '=' = peer, '#' = local clock.
|
||||
/ .- Source state '*' = current best, '+' = combined, '-' = not combined,
|
||||
| / 'x' = may be in error, '~' = too variable, '?' = unusable.
|
||||
|| .- xxxx [ yyyy ] +/- zzzz
|
||||
|| Reachability register (octal) -. | xxxx = adjusted offset,
|
||||
|| Log2(Polling interval) --. | | yyyy = measured offset,
|
||||
|| \ | | zzzz = estimated error.
|
||||
|| | | \
|
||||
MS Name/IP address Stratum Poll Reach LastRx Last sample
|
||||
===============================================================================
|
||||
^* 192.36.143.134 1 6 177 9 +278ms[ -3845s] +/- 514ms
|
||||
admin@host:/>
|
||||
```
|
||||
|
||||
> The system uses `chronyd` for Network Time Protocol (NTP)
|
||||
> synchronization. The output shown here is best explained in the
|
||||
> [Chrony documentation](https://chrony-project.org/doc/4.6.1/chronyc.html).
|
||||
> [!TIP]
|
||||
> The system uses `chronyd` Network Time Protocol (NTP) daemon. The
|
||||
> output shown here is best explained in the [Chrony documentation][4].
|
||||
|
||||
[1]: https://www.rfc-editor.org/rfc/rfc7317
|
||||
[2]: https://github.com/kernelkit/infix/blob/main/src/confd/yang/infix-system%402024-02-29.yang
|
||||
[3]: https://www.rfc-editor.org/rfc/rfc8341
|
||||
[4]: https://chrony-project.org/doc/4.6.1/chronyc.html
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Tunnel configuration
|
||||
|
||||
Tunnel traffic from point A to point B
|
||||
|
||||
|
||||
## Generic Routing Encapsulation (GRE)
|
||||
|
||||
The support for GRE tunnels includes IPv4 and IPv6 tunnels both in GRE
|
||||
(IP) and GRETAP (MAC) modes.
|
||||
```
|
||||
admin@example:/config/> edit interface gre1
|
||||
admin@example:/config/interface/gre1/> set type gretap
|
||||
admin@example:/config/interface/gre1/> set gre local 192.168.3.1 remote 192.168.3.2
|
||||
admin@example:/config/interface/gre1/> leave
|
||||
admin@example:/>
|
||||
```
|
||||
|
||||
## Virtual eXtensible Local Area Network (VXLAN)
|
||||
|
||||
The support for VXLAN tunnels includes IPv4 and IPv6.
|
||||
|
||||
```
|
||||
admin@example:/config/> edit interface vxlan100
|
||||
admin@example:/config/interface/vxlan100/> set vxlan local 192.168.3.1
|
||||
admin@example:/config/interface/vxlan100/> set vxlan remote 192.168.3.2
|
||||
admin@example:/config/interface/vxlan100/> set vxlan vni 100
|
||||
admin@example:/config/interface/vxlan100/> leave
|
||||
```
|
||||
+1
-3
@@ -1,9 +1,7 @@
|
||||
IXMSG = printf "\e[37;44m>>> $(call qstrip,$(1))\e[0m\n"
|
||||
|
||||
include $(BR2_EXTERNAL_INFIX_PATH)/infix.mk
|
||||
include $(sort $(wildcard $(BR2_EXTERNAL_INFIX_PATH)/package/*/*.mk))
|
||||
include $(BR2_EXTERNAL_INFIX_PATH)/board/common/common.mk
|
||||
-include $(BR2_EXTERNAL_INFIX_PATH)/board/$(patsubst "%",%,$(BR2_ARCH))/board.mk
|
||||
include $(BR2_EXTERNAL_INFIX_PATH)/infix.mk
|
||||
include $(BR2_EXTERNAL_INFIX_PATH)/test/test.mk
|
||||
|
||||
.PHONY: local.mk
|
||||
|
||||
@@ -1 +1,12 @@
|
||||
IXMSG = printf "\e[37;44m>>> $(call qstrip,$(1))\e[0m\n"
|
||||
|
||||
oem-dir := $(call qstrip,$(INFIX_OEM_PATH))
|
||||
INFIX_TOPDIR = $(if $(oem-dir),$(oem-dir),$(BR2_EXTERNAL_INFIX_PATH))
|
||||
|
||||
# Unless the user specifies an explicit build id, source it from git.
|
||||
# The build id also becomes the image version, unless an official
|
||||
# release is being built.
|
||||
export INFIX_BUILD_ID ?= $(shell git -C $(INFIX_TOPDIR) describe --dirty --always --tags)
|
||||
export INFIX_VERSION = $(if $(INFIX_RELEASE),$(INFIX_RELEASE),$(INFIX_BUILD_ID))
|
||||
|
||||
INFIX_CFLAGS:=-Wall -Werror -Wextra -Wno-unused-parameter -Wformat=2 -Wformat-overflow=2 -Winit-self -Wstrict-overflow=4 -Wno-format-truncation -Wno-format-nonliteral
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
menu "Packages"
|
||||
|
||||
comment "Hardware Support"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/board/Config.in"
|
||||
|
||||
comment "Software Packages"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/bin/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/confd/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/confd-test-mode/Config.in"
|
||||
@@ -36,5 +40,6 @@ source "$BR2_EXTERNAL_INFIX_PATH/package/sysrepo-cpp/Config.in"
|
||||
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"
|
||||
|
||||
endmenu
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
menu "Boards"
|
||||
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/board/alder-alder/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/board/marvell-cn9130-crb/Config.in"
|
||||
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"
|
||||
|
||||
endmenu
|
||||
@@ -0,0 +1,5 @@
|
||||
config BR2_PACKAGE_ALDER_ALDER
|
||||
bool "Alder"
|
||||
depends on BR2_aarch64
|
||||
help
|
||||
Alder
|
||||
@@ -0,0 +1,2 @@
|
||||
$(eval $(ix-board))
|
||||
$(eval $(generic-package))
|
||||
@@ -0,0 +1,53 @@
|
||||
define inner-ix-board
|
||||
|
||||
$(2)_VERSION = ix-board
|
||||
$(2)_LICENSE = BSD-3-Clause
|
||||
$(2)_LICENSE_FILES = LICENSE
|
||||
$(2)_SITE_METHOD = local
|
||||
$(2)_SITE = $$(BR2_EXTERNAL_INFIX_PATH)/src/board/$(1)
|
||||
$(2)_REDISTRIBUTE = NO
|
||||
|
||||
# The kernel must be built first.
|
||||
$(2)_DEPENDENCIES += \
|
||||
linux \
|
||||
$$(BR2_MAKE_HOST_DEPENDENCY)
|
||||
|
||||
# This is only defined in some infrastructures (e.g. autotools, cmake),
|
||||
# but not in others (e.g. generic). So define it here as well.
|
||||
$(2)_MAKE ?= $$(BR2_MAKE)
|
||||
|
||||
define $(2)_DTBS_BUILD
|
||||
@$$(call MESSAGE,"Building device tree blob(s)")
|
||||
$$(LINUX_MAKE_ENV) $$($$(PKG)_MAKE) \
|
||||
-C $$(LINUX_DIR) \
|
||||
$$(LINUX_MAKE_FLAGS) \
|
||||
$$($(2)_DTB_MAKE_OPTS) \
|
||||
PWD=$$(@D)/dts \
|
||||
M=$$(@D)/dts \
|
||||
modules
|
||||
endef
|
||||
$(2)_POST_BUILD_HOOKS += $(2)_DTBS_BUILD
|
||||
|
||||
define $(2)_DTBS_INSTALL_TARGET
|
||||
@$$(call MESSAGE,"Installing device tree blob(s)")
|
||||
$$(TARGET_MAKE_ENV) $$(TARGET_CONFIGURE_OPTS) $$($$(PKG)_MAKE) \
|
||||
-f $$(BR2_EXTERNAL_INFIX_PATH)/package/board/dtb-inst.makefile \
|
||||
-C $$(@D)/dts \
|
||||
DESTDIR="$$(TARGET_DIR)" \
|
||||
install
|
||||
endef
|
||||
$(2)_POST_INSTALL_TARGET_HOOKS += $(2)_DTBS_INSTALL_TARGET
|
||||
|
||||
define $(2)_OVERLAY_INSTALL_TARGET
|
||||
@test -d $$(@D)/rootfs && \
|
||||
$$(call MESSAGE,"Copying overlay") && \
|
||||
$$(call SYSTEM_RSYNC,$$(@D)/rootfs,$(TARGET_DIR)) || \
|
||||
true
|
||||
endef
|
||||
$(2)_POST_INSTALL_TARGET_HOOKS += $(2)_OVERLAY_INSTALL_TARGET
|
||||
|
||||
endef
|
||||
|
||||
ix-board = $(call inner-ix-board,$(pkgname),$(call UPPERCASE,$(pkgname)))
|
||||
|
||||
include $(sort $(wildcard $(BR2_EXTERNAL_INFIX_PATH)/package/board/*/*.mk))
|
||||
@@ -0,0 +1,9 @@
|
||||
include Makefile
|
||||
|
||||
install: $(addprefix $(DESTDIR)/boot/,$(dtb-y))
|
||||
|
||||
$(DESTDIR)/boot/%.dtb: %.dtb
|
||||
@echo " DTB-INSTALL $<"
|
||||
@install -D $< $@
|
||||
|
||||
.PHONY: install
|
||||
@@ -0,0 +1,5 @@
|
||||
config BR2_PACKAGE_MARVELL_CN9130_CRB
|
||||
bool "Marvell CN9130-CRB"
|
||||
depends on BR2_aarch64
|
||||
help
|
||||
Customer Reference Board for CN9130
|
||||
@@ -0,0 +1,2 @@
|
||||
$(eval $(ix-board))
|
||||
$(eval $(generic-package))
|
||||
@@ -0,0 +1,5 @@
|
||||
config BR2_PACKAGE_MARVELL_ESPRESSOBIN
|
||||
bool "Marvell ESPRESSObin"
|
||||
depends on BR2_aarch64
|
||||
help
|
||||
Marvell ESPRESSObin
|
||||
@@ -0,0 +1,2 @@
|
||||
$(eval $(ix-board))
|
||||
$(eval $(generic-package))
|
||||
@@ -0,0 +1,5 @@
|
||||
config BR2_PACKAGE_MICROCHIP_SPARX5_PCB135
|
||||
bool "Microchip SparX-5i PCB135"
|
||||
depends on BR2_aarch64
|
||||
help
|
||||
Microchip's evaluation board for SparX-5i
|
||||
@@ -0,0 +1,2 @@
|
||||
$(eval $(ix-board))
|
||||
$(eval $(generic-package))
|
||||
@@ -0,0 +1,5 @@
|
||||
config BR2_PACKAGE_STYX_DCP_SC_28P
|
||||
bool "Styx DCP-SC-28P"
|
||||
depends on BR2_aarch64
|
||||
help
|
||||
Styx DCP-SC-28P
|
||||
@@ -0,0 +1,2 @@
|
||||
$(eval $(ix-board))
|
||||
$(eval $(generic-package))
|
||||
@@ -4,7 +4,7 @@
|
||||
#
|
||||
################################################################################
|
||||
|
||||
CONFD_VERSION = 1.2
|
||||
CONFD_VERSION = 1.4
|
||||
CONFD_SITE_METHOD = local
|
||||
CONFD_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/confd
|
||||
CONFD_LICENSE = BSD-3-Clause
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
From d0f6422fee7a46fcb7445c88f499f61b3eb0ead0 Mon Sep 17 00:00:00 2001
|
||||
From: Adam Piecek <Adam.Piecek@cesnet.cz>
|
||||
Date: Wed, 23 Oct 2024 14:37:09 +0200
|
||||
Subject: [PATCH 1/8] added support for RpcYang in Context::parseOp
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
Change-Id: I25182ea2d042be1e6e4246e18aee260cc032e547
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
src/Context.cpp | 6 ++++--
|
||||
tests/context.cpp | 21 +++++++++++++++++++++
|
||||
2 files changed, 25 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/src/Context.cpp b/src/Context.cpp
|
||||
index b844bd9..287f8c8 100644
|
||||
--- a/src/Context.cpp
|
||||
+++ b/src/Context.cpp
|
||||
@@ -221,7 +221,8 @@ std::optional<DataNode> Context::parseExtData(
|
||||
* - a NETCONF RPC,
|
||||
* - a NETCONF notification,
|
||||
* - a RESTCONF notification,
|
||||
- * - a YANG notification.
|
||||
+ * - a YANG notification,
|
||||
+ * - a YANG RPC.
|
||||
*
|
||||
* Parsing any of these requires just the schema (which is available through the Context), and the textual payload.
|
||||
* All the other information are encoded in the textual payload as per the standard.
|
||||
@@ -243,6 +244,7 @@ ParsedOp Context::parseOp(const std::string& input, const DataFormat format, con
|
||||
auto in = wrap_ly_in_new_memory(input);
|
||||
|
||||
switch (opType) {
|
||||
+ case OperationType::RpcYang:
|
||||
case OperationType::RpcNetconf:
|
||||
case OperationType::NotificationNetconf:
|
||||
case OperationType::NotificationRestconf:
|
||||
@@ -254,7 +256,7 @@ ParsedOp Context::parseOp(const std::string& input, const DataFormat format, con
|
||||
ParsedOp res;
|
||||
res.tree = tree ? std::optional{libyang::wrapRawNode(tree)} : std::nullopt;
|
||||
|
||||
- if (opType == OperationType::NotificationYang) {
|
||||
+ if ((opType == OperationType::NotificationYang) || (opType == OperationType::RpcYang)) {
|
||||
res.op = op && tree ? std::optional{DataNode(op, res.tree->m_refs)} : std::nullopt;
|
||||
} else {
|
||||
res.op = op ? std::optional{libyang::wrapRawNode(op)} : std::nullopt;
|
||||
diff --git a/tests/context.cpp b/tests/context.cpp
|
||||
index 71ae873..11019eb 100644
|
||||
--- a/tests/context.cpp
|
||||
+++ b/tests/context.cpp
|
||||
@@ -464,6 +464,27 @@ TEST_CASE("context")
|
||||
REQUIRE(data->findPath("/example-schema:leafInt8")->asTerm().valueStr() == "-43");
|
||||
}
|
||||
|
||||
+ DOCTEST_SUBCASE("Context::parseOp")
|
||||
+ {
|
||||
+ DOCTEST_SUBCASE("RPC")
|
||||
+ {
|
||||
+ ctx->parseModule(example_schema, libyang::SchemaFormat::YANG);
|
||||
+ std::string dataJson = R"({"example-schema:myRpc":{"inputLeaf":"str"}})";
|
||||
+ auto pop = ctx->parseOp(dataJson, libyang::DataFormat::JSON, libyang::OperationType::RpcYang);
|
||||
+ REQUIRE(pop.op->schema().name() == "myRpc");
|
||||
+ REQUIRE(pop.tree->findPath("/example-schema:myRpc/inputLeaf")->asTerm().valueStr() == "str");
|
||||
+ }
|
||||
+ DOCTEST_SUBCASE("action")
|
||||
+ {
|
||||
+ ctx->parseModule(example_schema, libyang::SchemaFormat::YANG);
|
||||
+ std::string datajson = R"({"example-schema:person":[{"name":"john", "poke":{}}]})";
|
||||
+ auto pop = ctx->parseOp(datajson, libyang::DataFormat::JSON, libyang::OperationType::RpcYang);
|
||||
+ REQUIRE(pop.op->schema().name() == "poke");
|
||||
+ REQUIRE(pop.tree->findPath("/example-schema:person[name='john']/poke")->schema().nodeType() == libyang::NodeType::Action);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+
|
||||
DOCTEST_SUBCASE("Context::parseExt")
|
||||
{
|
||||
ctx->setSearchDir(TESTS_DIR / "yang");
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
From 7e015f3486bdbb54f1dcc2e2ce51102b1d623081 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
|
||||
Date: Wed, 23 Oct 2024 12:52:24 +0200
|
||||
Subject: [PATCH 2/8] throw when lyd_validate_all returns error
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
Bug: https://github.com/CESNET/libyang-cpp/issues/20
|
||||
Change-Id: I005a2f1b057978573a4046e7b4cc31d77e36fde3
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
src/DataNode.cpp | 4 +++-
|
||||
tests/data_node.cpp | 7 +++++++
|
||||
2 files changed, 10 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/DataNode.cpp b/src/DataNode.cpp
|
||||
index 51c86c8..2ef17f2 100644
|
||||
--- a/src/DataNode.cpp
|
||||
+++ b/src/DataNode.cpp
|
||||
@@ -1170,7 +1170,9 @@ void validateAll(std::optional<libyang::DataNode>& node, const std::optional<Val
|
||||
}
|
||||
|
||||
// TODO: support the `diff` argument
|
||||
- lyd_validate_all(node ? &node->m_node : nullptr, nullptr, opts ? utils::toValidationOptions(*opts) : 0, nullptr);
|
||||
+ auto ret = lyd_validate_all(node ? &node->m_node : nullptr, nullptr, opts ? utils::toValidationOptions(*opts) : 0, nullptr);
|
||||
+ throwIfError(ret, "libyang:validateAll: lyd_validate_all failed");
|
||||
+
|
||||
if (!node->m_node) {
|
||||
node = std::nullopt;
|
||||
}
|
||||
diff --git a/tests/data_node.cpp b/tests/data_node.cpp
|
||||
index a23e4c2..8a2610e 100644
|
||||
--- a/tests/data_node.cpp
|
||||
+++ b/tests/data_node.cpp
|
||||
@@ -489,6 +489,13 @@ TEST_CASE("Data Node manipulation")
|
||||
REQUIRE_THROWS_WITH_AS(libyang::validateAll(node, libyang::ValidationOptions::NoState), "validateAll: Node is not a unique reference", libyang::Error);
|
||||
}
|
||||
|
||||
+ DOCTEST_SUBCASE("validateAll throws on validation failure")
|
||||
+ {
|
||||
+ ctx.parseModule(type_module, libyang::SchemaFormat::YANG);
|
||||
+ auto node = std::optional{ctx.newPath("/type_module:leafWithConfigFalse", "hi")};
|
||||
+ REQUIRE_THROWS_WITH_AS(libyang::validateAll(node, libyang::ValidationOptions::NoState), "libyang:validateAll: lyd_validate_all failed: LY_EVALID", libyang::ErrorWithCode);
|
||||
+ }
|
||||
+
|
||||
DOCTEST_SUBCASE("unlink")
|
||||
{
|
||||
auto root = ctx.parseData(data2, libyang::DataFormat::JSON);
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
From 490d8bb242d33213b948485f5b94c55e22cf86a6 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
|
||||
Date: Thu, 21 Nov 2024 11:32:44 +0100
|
||||
Subject: [PATCH 3/8] remove a misleading comment
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
The whole intention within action's input/output handling here was to
|
||||
put some emphasis on the fact that we aren't tracking the input/output
|
||||
nodes directly. However, looking at all the other classes this is a bit
|
||||
redundant, we're using a pattern like this all the time. Just drop the
|
||||
comment.
|
||||
|
||||
Change-Id: Ibd9bf9f1e83c650dda3bc43ef48e61dd6d95da5a
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
src/SchemaNode.cpp | 3 ---
|
||||
1 file changed, 3 deletions(-)
|
||||
|
||||
diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp
|
||||
index 81e938f..9934cea 100644
|
||||
--- a/src/SchemaNode.cpp
|
||||
+++ b/src/SchemaNode.cpp
|
||||
@@ -640,9 +640,6 @@ bool List::isUserOrdered() const
|
||||
*/
|
||||
ActionRpcInput ActionRpc::input() const
|
||||
{
|
||||
- // I need a lysc_node* for ActionRpcInput, but m_node->input is a lysp_node_action_inout. lysp_node_action_inout is
|
||||
- // still just a lysc_node, so I'll just convert to lysc_node.
|
||||
- // This is not very pretty, but I don't want to introduce another member for ActionRpcInput and ActionRpcOutput.
|
||||
return ActionRpcInput{reinterpret_cast<const lysc_node*>(&reinterpret_cast<const lysc_node_action*>(m_node)->input), m_ctx};
|
||||
}
|
||||
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
From e1b17386cf61048d2fe27fffb3b763981a225f52 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= <bedrich.schindler@gmail.com>
|
||||
Date: Wed, 27 Nov 2024 09:47:47 +0100
|
||||
Subject: [PATCH 4/8] 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: Addiva Elektronik
|
||||
|
||||
`List::keys()` used `std::move` while iterating over array of leafs.
|
||||
This was solved without using `std::move`.
|
||||
|
||||
Change-Id: I8cbf8780ecd8848e46c1de5d4123a08624536bba
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
src/SchemaNode.cpp | 3 +--
|
||||
1 file changed, 1 insertion(+), 2 deletions(-)
|
||||
|
||||
diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp
|
||||
index 9934cea..20e2aff 100644
|
||||
--- a/src/SchemaNode.cpp
|
||||
+++ b/src/SchemaNode.cpp
|
||||
@@ -593,8 +593,7 @@ std::vector<Leaf> List::keys() const
|
||||
LY_LIST_FOR(list->child, elem)
|
||||
{
|
||||
if (lysc_is_key(elem)) {
|
||||
- Leaf leaf(elem, m_ctx);
|
||||
- res.emplace_back(std::move(leaf));
|
||||
+ res.emplace_back(Leaf(elem, m_ctx));
|
||||
}
|
||||
}
|
||||
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
From 1102ecdcafbc9206f59b383769687e418557838e Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= <bedrich.schindler@gmail.com>
|
||||
Date: Mon, 25 Nov 2024 15:54:02 +0100
|
||||
Subject: [PATCH 5/8] schema: make leaf-list's `default` statement available
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
Make leaf-list's `default` statement available so that it can be
|
||||
accessed if end-user requires reading schema nodes.
|
||||
|
||||
`LeafList::defaultValuesStr()` returns array of canonized string default
|
||||
values.
|
||||
|
||||
Change-Id: Idc42cd877f1fd3d717d491d09c46b59492527bff
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
include/libyang-cpp/SchemaNode.hpp | 1 +
|
||||
src/SchemaNode.cpp | 17 +++++++++++++++++
|
||||
tests/context.cpp | 1 +
|
||||
tests/example_schema.hpp | 8 ++++++++
|
||||
tests/schema_node.cpp | 7 +++++++
|
||||
5 files changed, 34 insertions(+)
|
||||
|
||||
diff --git a/include/libyang-cpp/SchemaNode.hpp b/include/libyang-cpp/SchemaNode.hpp
|
||||
index 83afc06..8ddf9be 100644
|
||||
--- a/include/libyang-cpp/SchemaNode.hpp
|
||||
+++ b/include/libyang-cpp/SchemaNode.hpp
|
||||
@@ -169,6 +169,7 @@ class LIBYANG_CPP_EXPORT LeafList : public SchemaNode {
|
||||
public:
|
||||
bool isMandatory() const;
|
||||
types::Type valueType() const;
|
||||
+ std::vector<std::string> defaultValuesStr() const;
|
||||
libyang::types::constraints::ListSize maxElements() const;
|
||||
libyang::types::constraints::ListSize minElements() const;
|
||||
std::optional<std::string> units() const;
|
||||
diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp
|
||||
index 9934cea..95bc09b 100644
|
||||
--- a/src/SchemaNode.cpp
|
||||
+++ b/src/SchemaNode.cpp
|
||||
@@ -472,6 +472,23 @@ types::Type LeafList::valueType() const
|
||||
return types::Type{reinterpret_cast<const lysc_node_leaflist*>(m_node)->type, typeParsed, m_ctx};
|
||||
}
|
||||
|
||||
+/**
|
||||
+ * @brief Retrieves the default string values for this leaf-list.
|
||||
+ *
|
||||
+ * @return The default values, or an empty vector if the leaf-list does not have default values.
|
||||
+ *
|
||||
+ * Wraps `lysc_node_leaflist::dflts`.
|
||||
+ */
|
||||
+std::vector<std::string> LeafList::defaultValuesStr() const
|
||||
+{
|
||||
+ auto dflts = reinterpret_cast<const lysc_node_leaflist*>(m_node)->dflts;
|
||||
+ std::vector<std::string> res;
|
||||
+ for (const auto& it : std::span(dflts, LY_ARRAY_COUNT(dflts))) {
|
||||
+ res.emplace_back(lyd_value_get_canonical(m_ctx.get(), it));
|
||||
+ }
|
||||
+ return res;
|
||||
+}
|
||||
+
|
||||
/**
|
||||
* @brief Retrieves the units for this leaf.
|
||||
* @return The units, or std::nullopt if no units are available.
|
||||
diff --git a/tests/context.cpp b/tests/context.cpp
|
||||
index 11019eb..902faf6 100644
|
||||
--- a/tests/context.cpp
|
||||
+++ b/tests/context.cpp
|
||||
@@ -733,6 +733,7 @@ TEST_CASE("context")
|
||||
+--rw iid-valid? instance-identifier
|
||||
+--rw iid-relaxed? instance-identifier
|
||||
+--rw leafListBasic* string
|
||||
+ +--rw leafListWithDefault* int32
|
||||
+--rw leafListWithMinMaxElements* int32
|
||||
+--rw leafListWithUnits* int32
|
||||
+--rw listBasic* [primary-key]
|
||||
diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp
|
||||
index c093f50..2861b1a 100644
|
||||
--- a/tests/example_schema.hpp
|
||||
+++ b/tests/example_schema.hpp
|
||||
@@ -525,6 +525,14 @@ module type_module {
|
||||
ordered-by user;
|
||||
}
|
||||
|
||||
+ leaf-list leafListWithDefault {
|
||||
+ type int32;
|
||||
+ default -1;
|
||||
+ default +512;
|
||||
+ default 0x400;
|
||||
+ default 04000;
|
||||
+ }
|
||||
+
|
||||
leaf-list leafListWithMinMaxElements {
|
||||
type int32;
|
||||
min-elements 1;
|
||||
diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp
|
||||
index a86900d..80c7407 100644
|
||||
--- a/tests/schema_node.cpp
|
||||
+++ b/tests/schema_node.cpp
|
||||
@@ -200,6 +200,7 @@ TEST_CASE("SchemaNode")
|
||||
"/type_module:iid-valid",
|
||||
"/type_module:iid-relaxed",
|
||||
"/type_module:leafListBasic",
|
||||
+ "/type_module:leafListWithDefault",
|
||||
"/type_module:leafListWithMinMaxElements",
|
||||
"/type_module:leafListWithUnits",
|
||||
"/type_module:listBasic",
|
||||
@@ -606,6 +607,12 @@ TEST_CASE("SchemaNode")
|
||||
REQUIRE(!ctx->findPath("/type_module:leafListBasic").asLeafList().isMandatory());
|
||||
}
|
||||
|
||||
+ DOCTEST_SUBCASE("LeafList::defaultValuesStr")
|
||||
+ {
|
||||
+ REQUIRE(ctx->findPath("/type_module:leafListWithDefault").asLeafList().defaultValuesStr() == std::vector<std::string>{"-1", "512", "1024", "2048"});
|
||||
+ REQUIRE(ctx->findPath("/type_module:leafListBasic").asLeafList().defaultValuesStr().size() == 0);
|
||||
+ }
|
||||
+
|
||||
DOCTEST_SUBCASE("LeafList::maxElements")
|
||||
{
|
||||
REQUIRE(ctx->findPath("/type_module:leafListWithMinMaxElements").asLeafList().maxElements() == 5);
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
From 01f2633cef60495d5cafc4b4b1f25273b03ab3cd Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= <bedrich.schindler@gmail.com>
|
||||
Date: Tue, 22 Oct 2024 15:11:30 +0200
|
||||
Subject: [PATCH 6/8] schema: Make choice and case statements available
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
Make choice and case statements available so that they can be accessed
|
||||
if end-user requires reading schema nodes.
|
||||
|
||||
By design, choice and case statements do not exist in data tree directly.
|
||||
Only children of one case can be present in the data tree at one time.
|
||||
That means that choice and case children are not instantiable, thus
|
||||
`SchemaNode::immediateChildren` must be used (instead of
|
||||
`SchemaNode::childInstantibles`) if end-user wants to access choice
|
||||
and case substatements.
|
||||
|
||||
Change-Id: Ib089672ad21dda8a0344895835d92d3432fcccb8
|
||||
Co-authored-by: Jan Kundrát <jan.kundrat@cesnet.cz>
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
include/libyang-cpp/SchemaNode.hpp | 34 +++++++++++++
|
||||
src/SchemaNode.cpp | 68 ++++++++++++++++++++++++++
|
||||
tests/context.cpp | 77 +++++++++++++++++++-----------
|
||||
tests/example_schema.hpp | 60 +++++++++++++++++++++++
|
||||
tests/schema_node.cpp | 72 ++++++++++++++++++++++++++++
|
||||
5 files changed, 284 insertions(+), 27 deletions(-)
|
||||
|
||||
diff --git a/include/libyang-cpp/SchemaNode.hpp b/include/libyang-cpp/SchemaNode.hpp
|
||||
index 8ddf9be..0f1a4c4 100644
|
||||
--- a/include/libyang-cpp/SchemaNode.hpp
|
||||
+++ b/include/libyang-cpp/SchemaNode.hpp
|
||||
@@ -22,6 +22,8 @@ class AnyDataAnyXML;
|
||||
class ActionRpc;
|
||||
class ActionRpcInput;
|
||||
class ActionRpcOutput;
|
||||
+class Case;
|
||||
+class Choice;
|
||||
class Container;
|
||||
class Leaf;
|
||||
class LeafList;
|
||||
@@ -62,6 +64,8 @@ public:
|
||||
// drectly by the user.
|
||||
// TODO: turn these into a templated `as<>` method.
|
||||
AnyDataAnyXML asAnyDataAnyXML() const;
|
||||
+ Case asCase() const;
|
||||
+ Choice asChoice() const;
|
||||
Container asContainer() const;
|
||||
Leaf asLeaf() const;
|
||||
LeafList asLeafList() const;
|
||||
@@ -129,6 +133,36 @@ private:
|
||||
using SchemaNode::SchemaNode;
|
||||
};
|
||||
|
||||
+/**
|
||||
+ * @brief Class representing a schema definition of a `case` node.
|
||||
+ *
|
||||
+ * Wraps `lysc_node_case`.
|
||||
+ */
|
||||
+class LIBYANG_CPP_EXPORT Case : public SchemaNode {
|
||||
+public:
|
||||
+ friend SchemaNode;
|
||||
+ friend Choice;
|
||||
+
|
||||
+private:
|
||||
+ using SchemaNode::SchemaNode;
|
||||
+};
|
||||
+
|
||||
+/**
|
||||
+ * @brief Class representing a schema definition of a `choice` node.
|
||||
+ *
|
||||
+ * Wraps `lysc_node_choice`.
|
||||
+ */
|
||||
+class LIBYANG_CPP_EXPORT Choice : public SchemaNode {
|
||||
+public:
|
||||
+ bool isMandatory() const;
|
||||
+ std::vector<Case> cases() const;
|
||||
+ std::optional<Case> defaultCase() const;
|
||||
+ friend SchemaNode;
|
||||
+
|
||||
+private:
|
||||
+ using SchemaNode::SchemaNode;
|
||||
+};
|
||||
+
|
||||
/**
|
||||
* @brief Class representing a schema definition of a `container` node.
|
||||
*/
|
||||
diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp
|
||||
index bd20402..26b5099 100644
|
||||
--- a/src/SchemaNode.cpp
|
||||
+++ b/src/SchemaNode.cpp
|
||||
@@ -191,6 +191,32 @@ NodeType SchemaNode::nodeType() const
|
||||
return utils::toNodeType(m_node->nodetype);
|
||||
}
|
||||
|
||||
+/**
|
||||
+ * @brief Try to cast this SchemaNode to a Case node.
|
||||
+ * @throws Error If this node is not a case.
|
||||
+ */
|
||||
+Case SchemaNode::asCase() const
|
||||
+{
|
||||
+ if (nodeType() != NodeType::Case) {
|
||||
+ throw Error("Schema node is not a case: " + path());
|
||||
+ }
|
||||
+
|
||||
+ return Case{m_node, m_ctx};
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * @brief Try to cast this SchemaNode to a Choice node.
|
||||
+ * @throws Error If this node is not a choice.
|
||||
+ */
|
||||
+Choice SchemaNode::asChoice() const
|
||||
+{
|
||||
+ if (nodeType() != NodeType::Choice) {
|
||||
+ throw Error("Schema node is not a choice: " + path());
|
||||
+ }
|
||||
+
|
||||
+ return Choice{m_node, m_ctx};
|
||||
+}
|
||||
+
|
||||
/**
|
||||
* @brief Try to cast this SchemaNode to a Container node.
|
||||
* @throws Error If this node is not a container.
|
||||
@@ -401,6 +427,48 @@ bool AnyDataAnyXML::isMandatory() const
|
||||
return m_node->flags & LYS_MAND_TRUE;
|
||||
}
|
||||
|
||||
+/**
|
||||
+ * @brief Checks whether this choice is mandatory.
|
||||
+ *
|
||||
+ * Wraps flag `LYS_MAND_TRUE`.
|
||||
+ */
|
||||
+bool Choice::isMandatory() const
|
||||
+{
|
||||
+ return m_node->flags & LYS_MAND_TRUE;
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * @brief Retrieves the list of cases for this choice.
|
||||
+ *
|
||||
+ * Wraps `lysc_node_choice::cases`.
|
||||
+ */
|
||||
+std::vector<Case> Choice::cases() const
|
||||
+{
|
||||
+ auto choice = reinterpret_cast<const lysc_node_choice*>(m_node);
|
||||
+ auto cases = reinterpret_cast<lysc_node*>(choice->cases);
|
||||
+ std::vector<Case> res;
|
||||
+ lysc_node* elem;
|
||||
+ LY_LIST_FOR(cases, elem)
|
||||
+ {
|
||||
+ res.emplace_back(Case(elem, m_ctx));
|
||||
+ }
|
||||
+ return res;
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * @brief Retrieves the default case for this choice.
|
||||
+ *
|
||||
+ * Wraps `lysc_node_choice::dflt`.
|
||||
+ */
|
||||
+std::optional<Case> Choice::defaultCase() const
|
||||
+{
|
||||
+ auto choice = reinterpret_cast<const lysc_node_choice*>(m_node);
|
||||
+ if (!choice->dflt) {
|
||||
+ return std::nullopt;
|
||||
+ }
|
||||
+ return Case{reinterpret_cast<lysc_node*>(choice->dflt), m_ctx};
|
||||
+}
|
||||
+
|
||||
/**
|
||||
* @brief Checks whether this container is mandatory.
|
||||
*
|
||||
diff --git a/tests/context.cpp b/tests/context.cpp
|
||||
index 902faf6..5929b75 100644
|
||||
--- a/tests/context.cpp
|
||||
+++ b/tests/context.cpp
|
||||
@@ -709,33 +709,56 @@ TEST_CASE("context")
|
||||
auto mod = ctx_pp->parseModule(type_module, libyang::SchemaFormat::YANG);
|
||||
|
||||
REQUIRE(mod.printStr(libyang::SchemaOutputFormat::Tree) == R"(module: type_module
|
||||
- +--rw anydataBasic? anydata
|
||||
- +--rw anydataWithMandatoryChild anydata
|
||||
- +--rw anyxmlBasic? anyxml
|
||||
- +--rw anyxmlWithMandatoryChild anyxml
|
||||
- +--rw leafBinary? binary
|
||||
- +--rw leafBits? bits
|
||||
- +--rw leafEnum? enumeration
|
||||
- +--rw leafEnum2? enumeration
|
||||
- +--rw leafNumber? int32
|
||||
- +--rw leafRef? -> /custom-prefix:listAdvancedWithOneKey/lol
|
||||
- +--rw leafRefRelaxed? -> /custom-prefix:listAdvancedWithOneKey/lol
|
||||
- +--rw leafString? string
|
||||
- +--rw leafUnion? union
|
||||
- +--rw meal? identityref
|
||||
- +--ro leafWithConfigFalse? string
|
||||
- +--rw leafWithDefaultValue? string
|
||||
- +--rw leafWithDescription? string
|
||||
- +--rw leafWithMandatoryTrue string
|
||||
- x--rw leafWithStatusDeprecated? string
|
||||
- o--rw leafWithStatusObsolete? string
|
||||
- +--rw leafWithUnits? int32
|
||||
- +--rw iid-valid? instance-identifier
|
||||
- +--rw iid-relaxed? instance-identifier
|
||||
- +--rw leafListBasic* string
|
||||
- +--rw leafListWithDefault* int32
|
||||
- +--rw leafListWithMinMaxElements* int32
|
||||
- +--rw leafListWithUnits* int32
|
||||
+ +--rw anydataBasic? anydata
|
||||
+ +--rw anydataWithMandatoryChild anydata
|
||||
+ +--rw anyxmlBasic? anyxml
|
||||
+ +--rw anyxmlWithMandatoryChild anyxml
|
||||
+ +--rw choiceBasicContainer
|
||||
+ | +--rw (choiceBasic)?
|
||||
+ | +--:(case1)
|
||||
+ | | +--rw l? string
|
||||
+ | | +--rw ll* string
|
||||
+ | +--:(case2)
|
||||
+ | +--rw l2? string
|
||||
+ +--rw choiceWithMandatoryContainer
|
||||
+ | +--rw (choiceWithMandatory)
|
||||
+ | +--:(case3)
|
||||
+ | | +--rw l3? string
|
||||
+ | +--:(case4)
|
||||
+ | +--rw l4? string
|
||||
+ +--rw choiceWithDefaultContainer
|
||||
+ | +--rw (choiceWithDefault)?
|
||||
+ | +--:(case5)
|
||||
+ | | +--rw l5? string
|
||||
+ | +--:(case6)
|
||||
+ | +--rw l6? string
|
||||
+ +--rw implicitCaseContainer
|
||||
+ | +--rw (implicitCase)?
|
||||
+ | +--:(implicitLeaf)
|
||||
+ | +--rw implicitLeaf? string
|
||||
+ +--rw leafBinary? binary
|
||||
+ +--rw leafBits? bits
|
||||
+ +--rw leafEnum? enumeration
|
||||
+ +--rw leafEnum2? enumeration
|
||||
+ +--rw leafNumber? int32
|
||||
+ +--rw leafRef? -> /custom-prefix:listAdvancedWithOneKey/lol
|
||||
+ +--rw leafRefRelaxed? -> /custom-prefix:listAdvancedWithOneKey/lol
|
||||
+ +--rw leafString? string
|
||||
+ +--rw leafUnion? union
|
||||
+ +--rw meal? identityref
|
||||
+ +--ro leafWithConfigFalse? string
|
||||
+ +--rw leafWithDefaultValue? string
|
||||
+ +--rw leafWithDescription? string
|
||||
+ +--rw leafWithMandatoryTrue string
|
||||
+ x--rw leafWithStatusDeprecated? string
|
||||
+ o--rw leafWithStatusObsolete? string
|
||||
+ +--rw leafWithUnits? int32
|
||||
+ +--rw iid-valid? instance-identifier
|
||||
+ +--rw iid-relaxed? instance-identifier
|
||||
+ +--rw leafListBasic* string
|
||||
+ +--rw leafListWithDefault* int32
|
||||
+ +--rw leafListWithMinMaxElements* int32
|
||||
+ +--rw leafListWithUnits* int32
|
||||
+--rw listBasic* [primary-key]
|
||||
| +--rw primary-key string
|
||||
+--rw listAdvancedWithOneKey* [lol]
|
||||
diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp
|
||||
index 2861b1a..ae3b4de 100644
|
||||
--- a/tests/example_schema.hpp
|
||||
+++ b/tests/example_schema.hpp
|
||||
@@ -390,6 +390,66 @@ module type_module {
|
||||
mandatory true;
|
||||
}
|
||||
|
||||
+ container choiceBasicContainer {
|
||||
+ choice choiceBasic {
|
||||
+ case case1 {
|
||||
+ leaf l {
|
||||
+ type string;
|
||||
+ }
|
||||
+ leaf-list ll {
|
||||
+ type string;
|
||||
+ ordered-by user;
|
||||
+ }
|
||||
+ }
|
||||
+ case case2 {
|
||||
+ leaf l2 {
|
||||
+ type string;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ container choiceWithMandatoryContainer {
|
||||
+ choice choiceWithMandatory {
|
||||
+ mandatory true;
|
||||
+ case case3 {
|
||||
+ leaf l3 {
|
||||
+ type string;
|
||||
+ }
|
||||
+ }
|
||||
+ case case4 {
|
||||
+ leaf l4 {
|
||||
+ type string;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ container choiceWithDefaultContainer {
|
||||
+ choice choiceWithDefault {
|
||||
+ default case5;
|
||||
+ case case5 {
|
||||
+ leaf l5 {
|
||||
+ type string;
|
||||
+ }
|
||||
+ }
|
||||
+ case case6 {
|
||||
+ leaf l6 {
|
||||
+ type string;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ container implicitCaseContainer {
|
||||
+ choice implicitCase {
|
||||
+ leaf implicitLeaf {
|
||||
+ type string;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+
|
||||
leaf leafBinary {
|
||||
type binary;
|
||||
}
|
||||
diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp
|
||||
index 80c7407..8d74bd2 100644
|
||||
--- a/tests/schema_node.cpp
|
||||
+++ b/tests/schema_node.cpp
|
||||
@@ -58,6 +58,9 @@ TEST_CASE("SchemaNode")
|
||||
],
|
||||
"type_module:anydataWithMandatoryChild": {"content": "test-string"},
|
||||
"type_module:anyxmlWithMandatoryChild": {"content": "test-string"},
|
||||
+ "type_module:choiceWithMandatoryContainer": {
|
||||
+ "l4": "test-string"
|
||||
+ },
|
||||
"type_module:containerWithMandatoryChild": {
|
||||
"leafWithMandatoryTrue": "test-string"
|
||||
},
|
||||
@@ -180,6 +183,10 @@ TEST_CASE("SchemaNode")
|
||||
"/type_module:anydataWithMandatoryChild",
|
||||
"/type_module:anyxmlBasic",
|
||||
"/type_module:anyxmlWithMandatoryChild",
|
||||
+ "/type_module:choiceBasicContainer",
|
||||
+ "/type_module:choiceWithMandatoryContainer",
|
||||
+ "/type_module:choiceWithDefaultContainer",
|
||||
+ "/type_module:implicitCaseContainer",
|
||||
"/type_module:leafBinary",
|
||||
"/type_module:leafBits",
|
||||
"/type_module:leafEnum",
|
||||
@@ -417,6 +424,71 @@ TEST_CASE("SchemaNode")
|
||||
REQUIRE(!ctx->findPath("/type_module:anyxmlBasic").asAnyDataAnyXML().isMandatory());
|
||||
}
|
||||
|
||||
+ DOCTEST_SUBCASE("Choice and Case")
|
||||
+ {
|
||||
+ std::string xpath;
|
||||
+ bool isMandatory = false;
|
||||
+ std::optional<std::string> defaultCase;
|
||||
+ std::vector<std::string> caseNames;
|
||||
+ std::optional<libyang::SchemaNode> root;
|
||||
+
|
||||
+ DOCTEST_SUBCASE("two cases with nothing fancy")
|
||||
+ {
|
||||
+ root = ctx->findPath("/type_module:choiceBasicContainer");
|
||||
+ caseNames = {"case1", "case2"};
|
||||
+ }
|
||||
+
|
||||
+ DOCTEST_SUBCASE("mandatory choice") {
|
||||
+ root = ctx->findPath("/type_module:choiceWithMandatoryContainer");
|
||||
+ isMandatory = true;
|
||||
+ caseNames = {"case3", "case4"};
|
||||
+ }
|
||||
+
|
||||
+ DOCTEST_SUBCASE("default choice") {
|
||||
+ root = ctx->findPath("/type_module:choiceWithDefaultContainer");
|
||||
+ defaultCase = "case5";
|
||||
+ caseNames = {"case5", "case6"};
|
||||
+ }
|
||||
+
|
||||
+ DOCTEST_SUBCASE("implicit case") {
|
||||
+ root = ctx->findPath("/type_module:implicitCaseContainer");
|
||||
+ caseNames = {"implicitLeaf"};
|
||||
+ }
|
||||
+
|
||||
+ // For testing purposes, we have each choice in its own container. As choice and case are not directly instantiable,
|
||||
+ // we wrap them in a container to simplify the testing process. It allows us to simply address the choice by its
|
||||
+ // container and then get the choice from it. It also prevents polluting the test schema with unnecessary nodes
|
||||
+ // and isolates the choice from other nodes.
|
||||
+ auto container = root->asContainer();
|
||||
+ auto choice = container.immediateChildren().begin()->asChoice();
|
||||
+ REQUIRE(choice.isMandatory() == isMandatory);
|
||||
+ REQUIRE(!!choice.defaultCase() == !!defaultCase);
|
||||
+ if (defaultCase) {
|
||||
+ REQUIRE(choice.defaultCase()->name() == *defaultCase);
|
||||
+ }
|
||||
+ std::vector<std::string> actualCaseNames;
|
||||
+ for (const auto& case_ : choice.cases()) {
|
||||
+ actualCaseNames.push_back(case_.name());
|
||||
+ }
|
||||
+ REQUIRE(actualCaseNames == caseNames);
|
||||
+
|
||||
+ // Also test child node access for one arbitrary choice/case combination
|
||||
+ if (root->path() == "/type_module:choiceBasicContainer") {
|
||||
+ REQUIRE(choice.cases().size() == 2);
|
||||
+ auto case1 = choice.cases()[0];
|
||||
+ auto children = case1.immediateChildren();
|
||||
+ auto it = children.begin();
|
||||
+ REQUIRE(it->asLeaf().name() == "l");
|
||||
+ ++it;
|
||||
+ REQUIRE(it->asLeafList().name() == "ll");
|
||||
+
|
||||
+ auto case2 = choice.cases()[1];
|
||||
+ children = case2.immediateChildren();
|
||||
+ it = children.begin();
|
||||
+ REQUIRE(it->asLeaf().name() == "l2");
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
DOCTEST_SUBCASE("Container::isMandatory")
|
||||
{
|
||||
REQUIRE(ctx->findPath("/type_module:containerWithMandatoryChild").asContainer().isMandatory());
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
From a1acdc794facf8cbf113f73274ecebd5898c81a1 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
|
||||
Date: Tue, 17 Dec 2024 15:08:43 +0100
|
||||
Subject: [PATCH 7/8] 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: Addiva Elektronik
|
||||
|
||||
Previously, the code would require a newPath(...,
|
||||
libyang::CreationOptions::Update), which is quite a mouthful.
|
||||
|
||||
Change-Id: I8a908c0fdd3e48dda830819758522a511adedd3b
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
include/libyang-cpp/DataNode.hpp | 8 ++++++
|
||||
src/DataNode.cpp | 21 ++++++++++++++++
|
||||
tests/data_node.cpp | 42 ++++++++++++++++++++++++++------
|
||||
3 files changed, 63 insertions(+), 8 deletions(-)
|
||||
|
||||
diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp
|
||||
index 2211415..851681b 100644
|
||||
--- a/include/libyang-cpp/DataNode.hpp
|
||||
+++ b/include/libyang-cpp/DataNode.hpp
|
||||
@@ -212,6 +212,14 @@ public:
|
||||
Value value() const;
|
||||
types::Type valueType() const;
|
||||
|
||||
+ /** @brief Was the value changed? */
|
||||
+ enum class ValueChange {
|
||||
+ Changed, /**< Yes, this is an actual change of the stored value */
|
||||
+ ExplicitNonDefault, /**< It still holds the default value, but it's been set explicitly now */
|
||||
+ EqualValueNotChanged, /**< No change, the previous value is the same as the new one, and it isn't an implicit default */
|
||||
+ };
|
||||
+ ValueChange changeValue(const std::string value);
|
||||
+
|
||||
private:
|
||||
using DataNode::DataNode;
|
||||
};
|
||||
diff --git a/src/DataNode.cpp b/src/DataNode.cpp
|
||||
index 2ef17f2..84591e5 100644
|
||||
--- a/src/DataNode.cpp
|
||||
+++ b/src/DataNode.cpp
|
||||
@@ -903,6 +903,27 @@ types::Type DataNodeTerm::valueType() const
|
||||
return impl(reinterpret_cast<const lyd_node_term*>(m_node)->value);
|
||||
}
|
||||
|
||||
+/** @short Change the term's value
|
||||
+ *
|
||||
+ * Wraps `lyd_change_term`.
|
||||
+ * */
|
||||
+DataNodeTerm::ValueChange DataNodeTerm::changeValue(const std::string value)
|
||||
+{
|
||||
+ auto ret = lyd_change_term(m_node, value.c_str());
|
||||
+
|
||||
+ switch (ret) {
|
||||
+ case LY_SUCCESS:
|
||||
+ return ValueChange::Changed;
|
||||
+ case LY_EEXIST:
|
||||
+ return ValueChange::ExplicitNonDefault;
|
||||
+ case LY_ENOT:
|
||||
+ return ValueChange::EqualValueNotChanged;
|
||||
+ default:
|
||||
+ throwIfError(ret, "DataNodeTerm::changeValue failed");
|
||||
+ __builtin_unreachable();
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
/**
|
||||
* @brief Returns a collection for iterating depth-first over the subtree this instance points to.
|
||||
*
|
||||
diff --git a/tests/data_node.cpp b/tests/data_node.cpp
|
||||
index 8a2610e..45fd6c1 100644
|
||||
--- a/tests/data_node.cpp
|
||||
+++ b/tests/data_node.cpp
|
||||
@@ -456,15 +456,41 @@ TEST_CASE("Data Node manipulation")
|
||||
REQUIRE(node.hasDefaultValue());
|
||||
REQUIRE(node.isImplicitDefault());
|
||||
|
||||
- data->newPath("/example-schema3:leafWithDefault", "not-default-value", libyang::CreationOptions::Update);
|
||||
- node = data->findPath("/example-schema3:leafWithDefault")->asTerm();
|
||||
- REQUIRE(!node.hasDefaultValue());
|
||||
- REQUIRE(!node.isImplicitDefault());
|
||||
+ DOCTEST_SUBCASE("newPath")
|
||||
+ {
|
||||
+ data->newPath("/example-schema3:leafWithDefault", "not-default-value", libyang::CreationOptions::Update);
|
||||
+ node = data->findPath("/example-schema3:leafWithDefault")->asTerm();
|
||||
+ REQUIRE(!node.hasDefaultValue());
|
||||
+ REQUIRE(!node.isImplicitDefault());
|
||||
|
||||
- data->newPath("/example-schema3:leafWithDefault", "AHOJ", libyang::CreationOptions::Update);
|
||||
- node = data->findPath("/example-schema3:leafWithDefault")->asTerm();
|
||||
- REQUIRE(node.hasDefaultValue());
|
||||
- REQUIRE(!node.isImplicitDefault());
|
||||
+ data->newPath("/example-schema3:leafWithDefault", "AHOJ", libyang::CreationOptions::Update);
|
||||
+ node = data->findPath("/example-schema3:leafWithDefault")->asTerm();
|
||||
+ REQUIRE(node.hasDefaultValue());
|
||||
+ REQUIRE(!node.isImplicitDefault());
|
||||
+ }
|
||||
+
|
||||
+ DOCTEST_SUBCASE("changing values")
|
||||
+ {
|
||||
+ auto node = data->findPath("/example-schema3:leafWithDefault");
|
||||
+ REQUIRE(!!node);
|
||||
+ auto term = node->asTerm();
|
||||
+
|
||||
+ DOCTEST_SUBCASE("to an arbitrary value") {
|
||||
+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::Changed);
|
||||
+ }
|
||||
+
|
||||
+ DOCTEST_SUBCASE("from an implicit default to an explicit default") {
|
||||
+ REQUIRE(term.changeValue("AHOJ") == libyang::DataNodeTerm::ValueChange::ExplicitNonDefault);
|
||||
+ REQUIRE(term.changeValue("AHOJ") == libyang::DataNodeTerm::ValueChange::EqualValueNotChanged);
|
||||
+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::Changed);
|
||||
+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::EqualValueNotChanged);
|
||||
+ }
|
||||
+
|
||||
+ DOCTEST_SUBCASE("from an implicit default to something else") {
|
||||
+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::Changed);
|
||||
+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::EqualValueNotChanged);
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
|
||||
DOCTEST_SUBCASE("isTerm")
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
From 39c7530caa510144c17521278b721ba1e6d8ff40 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
|
||||
Date: Thu, 9 Jan 2025 15:31:37 +0100
|
||||
Subject: [PATCH 8/8] upstream stopped reporting schema-mounts node
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
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,5 +1,6 @@
|
||||
config BR2_PACKAGE_PYTHON_STATD
|
||||
bool "python-statd"
|
||||
select BR2_PACKAGE_HOST_PYTHON3
|
||||
select BR2_PACKAGE_DBUS_PYTHON
|
||||
help
|
||||
Python helpers for statd.
|
||||
|
||||
@@ -3,7 +3,7 @@ PYTHON_STATD_SITE_METHOD = local
|
||||
PYTHON_STATD_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/statd/python
|
||||
PYTHON_STATD_LICENSE = BSD-3-Clause
|
||||
PYTHON_STATD_LICENSE_FILES = LICENSE
|
||||
PYTHON_STATD_DEPENDENCIES = host-python3 python3 host-python-poetry-core
|
||||
PYTHON_STATD_DEPENDENCIES = host-python3 python3 host-python-poetry-core dbus-python
|
||||
PYTHON_STATD_SETUP_TYPE = pep517 # poetry
|
||||
|
||||
define PYTHON_STATD_MOVE_BINARIES
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
config BR2_PACKAGE_RAUC_INSTALLATION_STATUS
|
||||
bool rauc-installation-status
|
||||
help
|
||||
Simple tool to read the installation status and errors
|
||||
from rauc in a JSON format.
|
||||
|
||||
https://github.com/kernelkit/infix
|
||||
@@ -0,0 +1 @@
|
||||
sha256 fff5e150e496c6bb6035ae388ee13cf2d0df842335e5145a4b902ef6b7892088 LICENSE
|
||||
@@ -0,0 +1,20 @@
|
||||
################################################################################
|
||||
#
|
||||
# rauc installation status
|
||||
#
|
||||
################################################################################
|
||||
|
||||
RAUC_INSTALLATION_STATUS_VERSION = 1.0
|
||||
RAUC_INSTALLATION_STATUS_LICENSE = ISC
|
||||
RAUC_INSTALLATION_STATUS_LICENSE_FILES = LICENSE
|
||||
RAUC_INSTALLATION_STATUS_SITE_METHOD = local
|
||||
RAUC_INSTALLATION_STATUS_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/rauc-installation-status
|
||||
RAUC_INSTALLATION_STATUS_REDISTRIBUTE = NO
|
||||
RAUC_INSTALLATION_STATUS_AUTORECONF = YES
|
||||
RAUC_INSTALLATION_STATUS_DEPENDENCIES = host-pkgconf jansson libglib2
|
||||
RAUC_INSTALLATION_STATUS_CONF_OPTS = --disable-silent-rules
|
||||
define RAUC_INSTALLATION_STATUS_CONF_ENV
|
||||
CFLAGS="$(INFIX_CFLAGS)"
|
||||
endef
|
||||
|
||||
$(eval $(autotools-package))
|
||||
@@ -0,0 +1,199 @@
|
||||
From 9622a68eba4aeaa60619b4c33d050ce91b27653d Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
|
||||
Date: Tue, 8 Oct 2024 12:22:49 +0200
|
||||
Subject: [PATCH 01/20] 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: Addiva Elektronik
|
||||
|
||||
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,50 @@
|
||||
From 070cffb48fda789910581930265d4624a7213e1b Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
|
||||
Date: Wed, 16 Oct 2024 11:02:45 +0200
|
||||
Subject: [PATCH 02/20] uri: rename url-encoding to percent-encoding
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
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,34 @@
|
||||
From 8a233202647f24538f2bbb8fff1e38d52e3599a4 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
|
||||
Date: Wed, 16 Oct 2024 11:05:09 +0200
|
||||
Subject: [PATCH 03/20] uri: correct x3 rule class names
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
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,153 @@
|
||||
From 96cbf730010ee9539d05d0d72697dc960b3a938c Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
|
||||
Date: Mon, 7 Oct 2024 20:46:24 +0200
|
||||
Subject: [PATCH 04/20] 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: Addiva Elektronik
|
||||
|
||||
The difference between nghttp2's uri_ref::path and raw_path is that the
|
||||
raw_path keeps the percent encoding, while the path converts the percent
|
||||
encoded chars to their "normal" form.
|
||||
|
||||
Our parser expects some parts of the URI to be percent encoded so we
|
||||
have to use raw_paths everywhere.
|
||||
|
||||
I thought about rewriting the parser not to expect percent encoded
|
||||
characters but that would bring some complications. For instance when
|
||||
querying lists, the RESTCONF RFC specifies that every key value is
|
||||
percent encoded and individual key values are delimited by commas [1].
|
||||
So, when somebody sends a request like /restconf/data/list=a%2Cb,c the
|
||||
"normal" form is /restconf/data/list=a,b,c and in that case we obtain
|
||||
three keys, but the client sent only two, where the first one contained
|
||||
comma.
|
||||
|
||||
I am adding few tests to check for percent encoded values.
|
||||
|
||||
We realized this while working on one of the reported bugs [1]. The
|
||||
query sent by the client there is wrong, the ':' char should be
|
||||
percent-encoded.
|
||||
|
||||
[1] https://github.com/CESNET/rousette/issues/12
|
||||
|
||||
Bug: https://github.com/CESNET/rousette/issues/12
|
||||
Change-Id: I473501cef3c8eae9af0c5d0751393cdad647e23c
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
src/restconf/Server.cpp | 8 ++++----
|
||||
src/restconf/uri.cpp | 4 ++--
|
||||
tests/restconf-reading.cpp | 15 +++++++++++++++
|
||||
tests/uri-parser.cpp | 5 +++--
|
||||
4 files changed, 24 insertions(+), 8 deletions(-)
|
||||
|
||||
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
|
||||
index 5f560ed..79d8ff6 100644
|
||||
--- a/src/restconf/Server.cpp
|
||||
+++ b/src/restconf/Server.cpp
|
||||
@@ -416,7 +416,7 @@ void processActionOrRPC(std::shared_ptr<RequestContext> requestCtx, const std::c
|
||||
* - The data node exists but might get deleted right after this check: Sysrepo throws an error when this happens.
|
||||
* - The data node does not exist but might get created right after this check: The node was not there when the request was issues so it should not be a problem
|
||||
*/
|
||||
- auto [pathToParent, pathSegment] = asLibyangPathSplit(ctx, requestCtx->req.uri().path);
|
||||
+ auto [pathToParent, pathSegment] = asLibyangPathSplit(ctx, requestCtx->req.uri().raw_path);
|
||||
if (!requestCtx->sess.getData(pathToParent, 0, sysrepo::GetOptions::Default, timeout)) {
|
||||
throw ErrorResponse(400, "application", "operation-failed", "Action data node '" + requestCtx->restconfRequest.path + "' does not exist.");
|
||||
}
|
||||
@@ -539,7 +539,7 @@ void processYangPatchEdit(const std::shared_ptr<RequestContext>& requestCtx, con
|
||||
auto target = childLeafValue(editContainer, "target");
|
||||
auto operation = childLeafValue(editContainer, "operation");
|
||||
|
||||
- auto [singleEdit, replacementNode] = createEditForPutAndPatch(ctx, requestCtx->req.uri().path + target, yangPatchValueAsJSON(editContainer), libyang::DataFormat::JSON);
|
||||
+ auto [singleEdit, replacementNode] = createEditForPutAndPatch(ctx, requestCtx->req.uri().raw_path + target, yangPatchValueAsJSON(editContainer), libyang::DataFormat::JSON);
|
||||
validateInputMetaAttributes(ctx, *singleEdit);
|
||||
|
||||
// insert and move are not defined in RFC6241. sec 7.3 and sysrepo does not support them directly
|
||||
@@ -658,7 +658,7 @@ void processPutOrPlainPatch(std::shared_ptr<RequestContext> requestCtx, const st
|
||||
throw ErrorResponse(400, "protocol", "invalid-value", "Target resource does not exist");
|
||||
}
|
||||
|
||||
- auto [edit, replacementNode] = createEditForPutAndPatch(ctx, requestCtx->req.uri().path, requestCtx->payload, *requestCtx->dataFormat.request /* caller checks if the dataFormat.request is present */);
|
||||
+ auto [edit, replacementNode] = createEditForPutAndPatch(ctx, requestCtx->req.uri().raw_path, requestCtx->payload, *requestCtx->dataFormat.request /* caller checks if the dataFormat.request is present */);
|
||||
validateInputMetaAttributes(ctx, *edit);
|
||||
|
||||
if (requestCtx->req.method() == "PUT") {
|
||||
@@ -954,7 +954,7 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std::
|
||||
dataFormat = chooseDataEncoding(req.header());
|
||||
authorizeRequest(nacm, sess, req);
|
||||
|
||||
- auto restconfRequest = asRestconfRequest(sess.getContext(), req.method(), req.uri().path, req.uri().raw_query);
|
||||
+ auto restconfRequest = asRestconfRequest(sess.getContext(), req.method(), req.uri().raw_path, req.uri().raw_query);
|
||||
|
||||
switch (restconfRequest.type) {
|
||||
case RestconfRequest::Type::RestconfRoot:
|
||||
diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp
|
||||
index 8e8dc23..ac399b7 100644
|
||||
--- a/src/restconf/uri.cpp
|
||||
+++ b/src/restconf/uri.cpp
|
||||
@@ -34,9 +34,9 @@ const auto percentEncodedChar = x3::rule<class percentEncodedChar, unsigned>{"pe
|
||||
/* reserved characters according to RFC 3986, sec. 2.2 with '%' added. The '%' character is not specified as reserved but it effectively is because
|
||||
* "Percent sign serves as the indicator for percent-encoded octets, it must be percent-encoded (...)" [RFC 3986, sec. 2.4]. */
|
||||
const auto reservedChars = x3::lit(':') | '/' | '?' | '#' | '[' | ']' | '@' | '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ',' | ';' | '=' | '%';
|
||||
-const auto keyValue = x3::rule<class keyValue, std::string>{"keyValue"} = *(percentEncodedChar | (x3::char_ - reservedChars));
|
||||
+const auto percentEncodedString = x3::rule<class percentEncodedString, std::string>{"percentEncodedString"} = *(percentEncodedChar | (x3::char_ - reservedChars));
|
||||
|
||||
-const auto keyList = x3::rule<class keyList, std::vector<std::string>>{"keyList"} = keyValue % ',';
|
||||
+const auto keyList = x3::rule<class keyList, std::vector<std::string>>{"keyList"} = percentEncodedString % ',';
|
||||
const auto identifier = x3::rule<class identifier, std::string>{"identifier"} = (x3::alpha | x3::char_('_')) >> *(x3::alnum | x3::char_('_') | x3::char_('-') | x3::char_('.'));
|
||||
const auto apiIdentifier = x3::rule<class apiIdentifier, ApiIdentifier>{"apiIdentifier"} = -(identifier >> ':') >> identifier;
|
||||
const auto listInstance = x3::rule<class keyList, PathSegment>{"listInstance"} = apiIdentifier >> -('=' >> keyList);
|
||||
diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp
|
||||
index fa1cbcc..2898839 100644
|
||||
--- a/tests/restconf-reading.cpp
|
||||
+++ b/tests/restconf-reading.cpp
|
||||
@@ -261,6 +261,21 @@ TEST_CASE("reading data")
|
||||
}
|
||||
)"});
|
||||
|
||||
+ // percent-encoded comma is a part of the key value, it is not a delimiter
|
||||
+ REQUIRE(get(RESTCONF_DATA_ROOT "/ietf-system:system/radius/server=a%2Cb", {AUTH_DWDM}) == Response{404, jsonHeaders, R"({
|
||||
+ "ietf-restconf:errors": {
|
||||
+ "error": [
|
||||
+ {
|
||||
+ "error-type": "application",
|
||||
+ "error-tag": "invalid-value",
|
||||
+ "error-message": "No data from sysrepo."
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+}
|
||||
+)"});
|
||||
+
|
||||
+ // comma is a delimiter of list key values
|
||||
REQUIRE(get(RESTCONF_DATA_ROOT "/ietf-system:system/radius/server=a,b", {AUTH_DWDM}) == Response{400, jsonHeaders, R"({
|
||||
"ietf-restconf:errors": {
|
||||
"error": [
|
||||
diff --git a/tests/uri-parser.cpp b/tests/uri-parser.cpp
|
||||
index a748e09..5977afc 100644
|
||||
--- a/tests/uri-parser.cpp
|
||||
+++ b/tests/uri-parser.cpp
|
||||
@@ -158,9 +158,9 @@ TEST_CASE("URI path parser")
|
||||
{{"prefix", "lst"}, {"key1"}},
|
||||
{{"prefix", "leaf"}},
|
||||
}}},
|
||||
- {"/restconf/data/foo:bar/lst=key1,,key3", {{
|
||||
+ {"/restconf/data/foo:bar/lst=module%3Akey1,,key3", {{
|
||||
{{"foo", "bar"}},
|
||||
- {{"lst"}, {"key1", "", "key3"}},
|
||||
+ {{"lst"}, {"module:key1", "", "key3"}},
|
||||
}}},
|
||||
{"/restconf/data/foo:bar/lst=key%2CWithCommas,,key2C", {{
|
||||
{{"foo", "bar"}},
|
||||
@@ -240,6 +240,7 @@ TEST_CASE("URI path parser")
|
||||
"/restconf/data/foo:list=A%2",
|
||||
"/restconf/data/foo:list=A%2,",
|
||||
"/restconf/data/foo:bar/list1=%%",
|
||||
+ "/restconf/data/foo:bar/list1=module:smth",
|
||||
"/restconf/data/foo:bar/",
|
||||
"/restconf/data/ foo : bar",
|
||||
"/rest conf/data / foo:bar",
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
From 8b13c1e4ccaa61a241674c27063439e257fa88de Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
|
||||
Date: Wed, 2 Oct 2024 20:21:28 +0200
|
||||
Subject: [PATCH 05/20] 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: Addiva Elektronik
|
||||
|
||||
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,300 @@
|
||||
From fda47b6a6cfdaecc24e96c4d6138c6de3ef116e0 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
|
||||
Date: Mon, 7 Oct 2024 21:21:22 +0200
|
||||
Subject: [PATCH 06/20] tests: test querying lists with union keys
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
Test that we correctly work with keys that are unions of something
|
||||
that can have a module namespace and that must not have it.
|
||||
|
||||
By the way, enum values are not supposed to have a namespace prefix,
|
||||
good to know [1].
|
||||
|
||||
[1] https://www.rfc-editor.org/rfc/rfc7951#section-6.4
|
||||
|
||||
Change-Id: I5a70f18117bb453330b4bb2ce0d2fb47d35b6ea6
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
tests/restconf-reading.cpp | 53 ++++++++++++++++++++++++-----
|
||||
tests/restconf-writing.cpp | 68 ++++++++++++++++++++++++++++----------
|
||||
tests/uri-parser.cpp | 2 +-
|
||||
tests/yang/example.yang | 26 +++++++++++++--
|
||||
4 files changed, 120 insertions(+), 29 deletions(-)
|
||||
|
||||
diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp
|
||||
index 96c38ab..2ded3f0 100644
|
||||
--- a/tests/restconf-reading.cpp
|
||||
+++ b/tests/restconf-reading.cpp
|
||||
@@ -289,14 +289,16 @@ TEST_CASE("reading data")
|
||||
}
|
||||
)"});
|
||||
|
||||
- srSess.setItem("/example:list-with-identity-key[type='example:derived-identity'][name='name']", std::nullopt);
|
||||
- srSess.setItem("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']", std::nullopt);
|
||||
+ srSess.setItem("/example:list-with-union-keys[type='example:derived-identity'][name='name']", std::nullopt);
|
||||
+ srSess.setItem("/example:list-with-union-keys[type='example-types:another-derived-identity'][name='name']", std::nullopt);
|
||||
+ srSess.setItem("/example:list-with-union-keys[type='fiii'][name='name']", std::nullopt);
|
||||
+ srSess.setItem("/example:list-with-union-keys[type='zero'][name='name']", std::nullopt); // enum value
|
||||
srSess.setItem("/example:tlc/decimal-list[.='1.00']", std::nullopt);
|
||||
srSess.applyChanges();
|
||||
|
||||
// dealing with keys which can have prefixes (YANG identities)
|
||||
- REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=derived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
|
||||
- "example:list-with-identity-key": [
|
||||
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=derived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
|
||||
+ "example:list-with-union-keys": [
|
||||
{
|
||||
"type": "derived-identity",
|
||||
"name": "name"
|
||||
@@ -304,8 +306,8 @@ TEST_CASE("reading data")
|
||||
]
|
||||
}
|
||||
)"});
|
||||
- REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example%3Aderived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
|
||||
- "example:list-with-identity-key": [
|
||||
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example%3Aderived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
|
||||
+ "example:list-with-union-keys": [
|
||||
{
|
||||
"type": "derived-identity",
|
||||
"name": "name"
|
||||
@@ -315,7 +317,7 @@ TEST_CASE("reading data")
|
||||
)"});
|
||||
|
||||
// an identity from another module must be namespace-qualified
|
||||
- REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=another-derived-identity,name", {AUTH_ROOT}) == Response{404, jsonHeaders, R"({
|
||||
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=another-derived-identity,name", {AUTH_ROOT}) == Response{404, jsonHeaders, R"({
|
||||
"ietf-restconf:errors": {
|
||||
"error": [
|
||||
{
|
||||
@@ -328,8 +330,8 @@ TEST_CASE("reading data")
|
||||
}
|
||||
)"});
|
||||
|
||||
- REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example-types%3Aanother-derived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
|
||||
- "example:list-with-identity-key": [
|
||||
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example-types%3Aanother-derived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
|
||||
+ "example:list-with-union-keys": [
|
||||
{
|
||||
"type": "example-types:another-derived-identity",
|
||||
"name": "name"
|
||||
@@ -346,6 +348,39 @@ TEST_CASE("reading data")
|
||||
]
|
||||
}
|
||||
}
|
||||
+)"});
|
||||
+
|
||||
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=zero,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
|
||||
+ "example:list-with-union-keys": [
|
||||
+ {
|
||||
+ "type": "zero",
|
||||
+ "name": "name"
|
||||
+ }
|
||||
+ ]
|
||||
+}
|
||||
+)"});
|
||||
+
|
||||
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example%3Azero,name", {AUTH_ROOT}) == Response{404, jsonHeaders, R"({
|
||||
+ "ietf-restconf:errors": {
|
||||
+ "error": [
|
||||
+ {
|
||||
+ "error-type": "application",
|
||||
+ "error-tag": "invalid-value",
|
||||
+ "error-message": "No data from sysrepo."
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+}
|
||||
+)"});
|
||||
+
|
||||
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=fiii,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({
|
||||
+ "example:list-with-union-keys": [
|
||||
+ {
|
||||
+ "type": "fiii",
|
||||
+ "name": "name"
|
||||
+ }
|
||||
+ ]
|
||||
+}
|
||||
)"});
|
||||
}
|
||||
|
||||
diff --git a/tests/restconf-writing.cpp b/tests/restconf-writing.cpp
|
||||
index 8418554..c1a9515 100644
|
||||
--- a/tests/restconf-writing.cpp
|
||||
+++ b/tests/restconf-writing.cpp
|
||||
@@ -392,46 +392,69 @@ TEST_CASE("writing data")
|
||||
SECTION("Test canonicalization of keys")
|
||||
{
|
||||
EXPECT_CHANGE(
|
||||
- CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']", std::nullopt),
|
||||
- CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/type", "example:derived-identity"),
|
||||
- CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/name", "name"),
|
||||
- CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/text", "blabla"));
|
||||
- REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""});
|
||||
+ CREATED("/example:list-with-union-keys[type='example:derived-identity'][name='name']", std::nullopt),
|
||||
+ CREATED("/example:list-with-union-keys[type='example:derived-identity'][name='name']/type", "example:derived-identity"),
|
||||
+ CREATED("/example:list-with-union-keys[type='example:derived-identity'][name='name']/name", "name"),
|
||||
+ CREATED("/example:list-with-union-keys[type='example:derived-identity'][name='name']/text", "blabla"));
|
||||
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""});
|
||||
|
||||
// prefixed in the URI, not prefixed in the data
|
||||
EXPECT_CHANGE(
|
||||
- MODIFIED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/text", "hehe"));
|
||||
- REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example%3Aderived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "derived-identity", "text": "hehe"}]}]})") == Response{204, noContentTypeHeaders, ""});
|
||||
+ MODIFIED("/example:list-with-union-keys[type='example:derived-identity'][name='name']/text", "hehe"));
|
||||
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example%3Aderived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "derived-identity", "text": "hehe"}]}]})") == Response{204, noContentTypeHeaders, ""});
|
||||
|
||||
+ // 'another-derived-identity' comes from a different module than the list itself, so this parses as string
|
||||
+ EXPECT_CHANGE(
|
||||
+ CREATED("/example:list-with-union-keys[type='another-derived-identity'][name='name']", std::nullopt),
|
||||
+ CREATED("/example:list-with-union-keys[type='another-derived-identity'][name='name']/type", "another-derived-identity"),
|
||||
+ CREATED("/example:list-with-union-keys[type='another-derived-identity'][name='name']/name", "name"),
|
||||
+ CREATED("/example:list-with-union-keys[type='another-derived-identity'][name='name']/text", "blabla"));
|
||||
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=another-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "another-derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""});
|
||||
+
|
||||
+ EXPECT_CHANGE(
|
||||
+ CREATED("/example:list-with-union-keys[type='example-types:another-derived-identity'][name='name']", std::nullopt),
|
||||
+ CREATED("/example:list-with-union-keys[type='example-types:another-derived-identity'][name='name']/type", "example-types:another-derived-identity"),
|
||||
+ CREATED("/example:list-with-union-keys[type='example-types:another-derived-identity'][name='name']/name", "name"),
|
||||
+ CREATED("/example:list-with-union-keys[type='example-types:another-derived-identity'][name='name']/text", "blabla"));
|
||||
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example-types%3Aanother-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "example-types:another-derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""});
|
||||
|
||||
- REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=another-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "another-derived-identity", "text": "blabla"}]}]})") == Response{400, jsonHeaders, R"({
|
||||
+ // missing namespace in the data
|
||||
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example-types%3Aanother-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "another-derived-identity", "text": "blabla"}]}]})") == Response{400, jsonHeaders, R"({
|
||||
"ietf-restconf:errors": {
|
||||
"error": [
|
||||
{
|
||||
"error-type": "protocol",
|
||||
"error-tag": "invalid-value",
|
||||
- "error-message": "Validation failure: Can't parse data: LY_EVALID"
|
||||
+ "error-path": "/example:list-with-union-keys[type='another-derived-identity'][name='name']/type",
|
||||
+ "error-message": "List key mismatch between URI path ('example-types:another-derived-identity') and data ('another-derived-identity')."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
)"});
|
||||
|
||||
+ // zero is enum value
|
||||
EXPECT_CHANGE(
|
||||
- CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']", std::nullopt),
|
||||
- CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']/type", "example-types:another-derived-identity"),
|
||||
- CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']/name", "name"),
|
||||
- CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']/text", "blabla"));
|
||||
- REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example-types%3Aanother-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "example-types:another-derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""});
|
||||
+ CREATED("/example:list-with-union-keys[type='zero'][name='name']", std::nullopt),
|
||||
+ CREATED("/example:list-with-union-keys[type='zero'][name='name']/type", "zero"),
|
||||
+ CREATED("/example:list-with-union-keys[type='zero'][name='name']/name", "name"));
|
||||
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=zero,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "zero"}]}]})") == Response{201, noContentTypeHeaders, ""});
|
||||
|
||||
- // missing namespace in the data
|
||||
- REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example-types%3Aanother-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "another-derived-identity", "text": "blabla"}]}]})") == Response{400, jsonHeaders, R"({
|
||||
+ // example:zero is string, enum values are not namespace-prefixed
|
||||
+ EXPECT_CHANGE(
|
||||
+ CREATED("/example:list-with-union-keys[type='example:zero'][name='name']", std::nullopt),
|
||||
+ CREATED("/example:list-with-union-keys[type='example:zero'][name='name']/type", "example:zero"),
|
||||
+ CREATED("/example:list-with-union-keys[type='example:zero'][name='name']/name", "name"));
|
||||
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example%3Azero,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "example:zero"}]}]})") == Response{201, noContentTypeHeaders, ""});
|
||||
+
|
||||
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=zero,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "example:zero"}]}]})") == Response{400, jsonHeaders, R"({
|
||||
"ietf-restconf:errors": {
|
||||
"error": [
|
||||
{
|
||||
"error-type": "protocol",
|
||||
"error-tag": "invalid-value",
|
||||
- "error-message": "Validation failure: Can't parse data: LY_EVALID"
|
||||
+ "error-path": "/example:list-with-union-keys[type='example:zero'][name='name']/type",
|
||||
+ "error-message": "List key mismatch between URI path ('zero') and data ('example:zero')."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -459,6 +482,17 @@ TEST_CASE("writing data")
|
||||
}
|
||||
)"});
|
||||
|
||||
+ EXPECT_CHANGE(CREATED("/example:fruit-list[.='example:apple']", "example:apple"));
|
||||
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:fruit-list=example%3Aapple", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:fruit-list": ["apple"]})") == Response{201, noContentTypeHeaders, ""});
|
||||
+
|
||||
+ // leafref
|
||||
+ EXPECT_CHANGE(
|
||||
+ CREATED("/example:list-with-union-keys[type='example:apple'][name='name']", std::nullopt),
|
||||
+ CREATED("/example:list-with-union-keys[type='example:apple'][name='name']/type", "example:apple"),
|
||||
+ CREATED("/example:list-with-union-keys[type='example:apple'][name='name']/name", "name"));
|
||||
+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example%3Aapple,name", {AUTH_ROOT, CONTENT_TYPE_JSON},
|
||||
+ R"({"example:list-with-union-keys": [{"name": "name", "type": "apple"}]}]})") == Response{201, noContentTypeHeaders, ""});
|
||||
+
|
||||
// value in the URI and in the data have the same canonical form
|
||||
EXPECT_CHANGE(CREATED("/example:tlc/decimal-list[.='1.0']", "1.0"));
|
||||
REQUIRE(put(RESTCONF_DATA_ROOT "/example:tlc/decimal-list=1.00", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:decimal-list": ["1.0"]})") == Response{201, noContentTypeHeaders, ""});
|
||||
diff --git a/tests/uri-parser.cpp b/tests/uri-parser.cpp
|
||||
index 7320c3c..000fe5d 100644
|
||||
--- a/tests/uri-parser.cpp
|
||||
+++ b/tests/uri-parser.cpp
|
||||
@@ -293,7 +293,7 @@ TEST_CASE("URI path parser")
|
||||
{"/restconf/data/example:tlc/list=eth0/choice1", "/example:tlc/list[name='eth0']/choice1", std::nullopt},
|
||||
{"/restconf/data/example:tlc/list=eth0/choice2", "/example:tlc/list[name='eth0']/choice2", std::nullopt},
|
||||
{"/restconf/data/example:tlc/list=eth0/collection=val", "/example:tlc/list[name='eth0']/collection[.='val']", std::nullopt},
|
||||
- {"/restconf/data/example:list-with-identity-key=example-types%3Aanother-derived-identity,aaa", "/example:list-with-identity-key[type='example-types:another-derived-identity'][name='aaa']", std::nullopt},
|
||||
+ {"/restconf/data/example:list-with-union-keys=example-types%3Aanother-derived-identity,aaa", "/example:list-with-union-keys[type='example-types:another-derived-identity'][name='aaa']", std::nullopt},
|
||||
{"/restconf/data/example:tlc/status", "/example:tlc/status", std::nullopt},
|
||||
// container example:a has a container b inserted locally and also via an augment. Check that we return the correct one
|
||||
{"/restconf/data/example:a/b", "/example:a/b", std::nullopt},
|
||||
diff --git a/tests/yang/example.yang b/tests/yang/example.yang
|
||||
index c46273c..75cd7a6 100644
|
||||
--- a/tests/yang/example.yang
|
||||
+++ b/tests/yang/example.yang
|
||||
@@ -13,6 +13,11 @@ module example {
|
||||
base base-identity;
|
||||
}
|
||||
|
||||
+ identity fruit { }
|
||||
+ identity apple {
|
||||
+ base fruit;
|
||||
+ }
|
||||
+
|
||||
leaf top-level-leaf { type string; }
|
||||
leaf top-level-leaf2 { type string; default "x"; }
|
||||
|
||||
@@ -121,10 +126,23 @@ module example {
|
||||
}
|
||||
}
|
||||
|
||||
- list list-with-identity-key {
|
||||
+ list list-with-union-keys {
|
||||
key "type name";
|
||||
leaf type {
|
||||
- type identityref { base base-identity; }
|
||||
+ type union {
|
||||
+ type identityref {
|
||||
+ base base-identity;
|
||||
+ }
|
||||
+ type enumeration {
|
||||
+ enum zero;
|
||||
+ enum one;
|
||||
+ }
|
||||
+ type leafref {
|
||||
+ require-instance true;
|
||||
+ path "/fruit-list";
|
||||
+ }
|
||||
+ type string;
|
||||
+ }
|
||||
}
|
||||
leaf name { type string; }
|
||||
leaf text { type string; }
|
||||
@@ -134,6 +152,10 @@ module example {
|
||||
type identityref { base base-identity; }
|
||||
}
|
||||
|
||||
+ leaf-list fruit-list {
|
||||
+ type identityref { base fruit; }
|
||||
+ }
|
||||
+
|
||||
rpc test-rpc {
|
||||
input {
|
||||
leaf i {
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
From 27ef5bc87fdeb70a77609da6ec18ee5c28656bb6 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
|
||||
Date: Tue, 29 Oct 2024 18:54:55 +0100
|
||||
Subject: [PATCH 07/20] error handling in sysrepo has changed
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
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,721 @@
|
||||
From 6819561d97e38569c319e36ca2e99768036b4032 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
|
||||
Date: Wed, 21 Aug 2024 19:18:08 +0200
|
||||
Subject: [PATCH 08/20] restconf: support fields query parameter
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
This patch adds support for fields query parameter [1].
|
||||
|
||||
I had to modify the original grammar for fields parameter a bit to
|
||||
allow for the lowest precedence while parsing `;` expression. Also we
|
||||
allow for more strings than the original grammar specifies to make the
|
||||
syntax more user friendly.
|
||||
|
||||
The fields expression is parsed into an AST which corresponds 1:1
|
||||
to the parse tree. The tree representing the expression could be
|
||||
simplified but I chose not to as it would complicate the code even
|
||||
more (although the translation to XPath would be simpler).
|
||||
The tree is then transformed into a valid XPath 1.0 expression.
|
||||
|
||||
The XPath 1.0 expressions are limited and I could not find a way how to
|
||||
transform the fields string into a valid XPath. I realized that the
|
||||
easiest way will be to "unwrap" the expression into individual paths
|
||||
and join them via union operator.
|
||||
For example, input `a(b;c/d)` would result into `a/b | a/c/d` XPath.
|
||||
|
||||
[1] https://datatracker.ietf.org/doc/html/rfc8040#section-4.8.3
|
||||
[2] https://www.w3.org/TR/1999/REC-xpath-19991116/#section-Expressions
|
||||
|
||||
Change-Id: I3c96bbcf49b38ecf08f56912afd3a8f50c15cd44
|
||||
Signed-off-by: Jan Kundrát <jan.kundrat@cesnet.cz>
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
README.md | 1 -
|
||||
src/restconf/Server.cpp | 9 ++-
|
||||
src/restconf/uri.cpp | 95 +++++++++++++++++++++++-
|
||||
src/restconf/uri.h | 57 ++++++++++++++-
|
||||
src/restconf/uri_impl.h | 4 ++
|
||||
tests/pretty_printers.h | 23 ++++++
|
||||
tests/restconf-reading.cpp | 144 ++++++++++++++++++++++++++++++++++++-
|
||||
tests/restconf-writing.cpp | 12 +++-
|
||||
tests/uri-parser.cpp | 93 ++++++++++++++++++++++++
|
||||
tests/yang/example.yang | 10 +++
|
||||
10 files changed, 438 insertions(+), 10 deletions(-)
|
||||
|
||||
diff --git a/README.md b/README.md
|
||||
index 1689584..3fbfd21 100644
|
||||
--- a/README.md
|
||||
+++ b/README.md
|
||||
@@ -22,7 +22,6 @@ This is a [RESTCONF](https://datatracker.ietf.org/doc/html/rfc8040.html) server
|
||||
- TLS certificate authentication (see [Access control model](#access-control-model) below)
|
||||
- the [`Last-Modified`](https://datatracker.ietf.org/doc/html/rfc8040.html#section-3.4.1.1) and [`ETag`](https://datatracker.ietf.org/doc/html/rfc8040.html#section-3.4.1.2) headers for [edit collision prevention](https://datatracker.ietf.org/doc/html/rfc8040.html#section-3.4.1) in the datastore resource
|
||||
- the [`Last-Modified`](https://datatracker.ietf.org/doc/html/rfc8040.html#section-3.5.1) and [`ETag`](https://datatracker.ietf.org/doc/html/rfc8040.html#section-3.5.2) headers in the data resource
|
||||
- - The [`fields`](https://datatracker.ietf.org/doc/html/rfc8040.html#section-4.8.3) query parameter
|
||||
- [NMDA](https://datatracker.ietf.org/doc/html/rfc8527.html) datastore access
|
||||
- no [`with-operational-default`](https://datatracker.ietf.org/doc/html/rfc8527#section-3.2.1) capability
|
||||
- no [`with-origin`](https://datatracker.ietf.org/doc/html/rfc8527#section-3.2.2) capability
|
||||
diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
|
||||
index b515d66..7c66ea4 100644
|
||||
--- a/src/restconf/Server.cpp
|
||||
+++ b/src/restconf/Server.cpp
|
||||
@@ -834,6 +834,7 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std::
|
||||
m_monitoringSession.setItem("/ietf-restconf-monitoring:restconf-state/capabilities/capability[2]", "urn:ietf:params:restconf:capability:depth:1.0");
|
||||
m_monitoringSession.setItem("/ietf-restconf-monitoring:restconf-state/capabilities/capability[3]", "urn:ietf:params:restconf:capability:with-defaults:1.0");
|
||||
m_monitoringSession.setItem("/ietf-restconf-monitoring:restconf-state/capabilities/capability[4]", "urn:ietf:params:restconf:capability:filter:1.0");
|
||||
+ m_monitoringSession.setItem("/ietf-restconf-monitoring:restconf-state/capabilities/capability[5]", "urn:ietf:params:restconf:capability:fields:1.0");
|
||||
m_monitoringSession.applyChanges();
|
||||
|
||||
m_monitoringOperSub = m_monitoringSession.onOperGet(
|
||||
@@ -1017,7 +1018,13 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std::
|
||||
}
|
||||
}
|
||||
|
||||
- if (auto data = sess.getData(restconfRequest.path, maxDepth, getOptions, timeout); data) {
|
||||
+ auto xpath = restconfRequest.path;
|
||||
+ if (auto it = restconfRequest.queryParams.find("fields"); it != restconfRequest.queryParams.end()) {
|
||||
+ auto fields = std::get<queryParams::fields::Expr>(it->second);
|
||||
+ xpath = fieldsToXPath(sess.getContext(), xpath == "/*" ? "" : xpath, fields);
|
||||
+ }
|
||||
+
|
||||
+ if (auto data = sess.getData(xpath, maxDepth, getOptions, timeout); data) {
|
||||
res.write_head(
|
||||
200,
|
||||
{
|
||||
diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp
|
||||
index ac399b7..da1e3a5 100644
|
||||
--- a/src/restconf/uri.cpp
|
||||
+++ b/src/restconf/uri.cpp
|
||||
@@ -4,7 +4,10 @@
|
||||
* Written by Tomáš Pecka <tomas.pecka@cesnet.cz>
|
||||
*/
|
||||
|
||||
+#include <boost/algorithm/string/join.hpp>
|
||||
+#include <boost/fusion/adapted/struct/adapt_struct.hpp>
|
||||
#include <boost/fusion/include/std_pair.hpp>
|
||||
+#include <boost/spirit/home/x3/support/ast/variant.hpp>
|
||||
#include <experimental/iterator>
|
||||
#include <libyang-cpp/Enum.hpp>
|
||||
#include <map>
|
||||
@@ -112,6 +115,28 @@ struct insertTable: x3::symbols<queryParams::QueryParamValue> {
|
||||
}
|
||||
} const insertParam;
|
||||
|
||||
+/* This grammar is implemented a little bit differently than the RFC states. The ABNF from RFC is:
|
||||
+ *
|
||||
+ * fields-expr = path "(" fields-expr ")" / path ";" fields-expr / path
|
||||
+ * path = api-identifier [ "/" path ]
|
||||
+ *
|
||||
+ * Firstly, the grammar from the RFC doesn't allow for expression like `a(b);c` but allows for `c;a(b)`.
|
||||
+ * I think both should be valid (as user I would expect that the order of such expression does not matter).
|
||||
+ * Hence our grammar allows for more strings than the grammar from RFC.
|
||||
+ * This issue was already raised on IETF mailing list: https://mailarchive.ietf.org/arch/msg/netconf/TYBpTE_ELzzMOe6amrw6fQF07nE/
|
||||
+ * but neither a formal errata was issued nor there was a resolution on the mailing list.
|
||||
+ */
|
||||
+const auto fieldsExpr = x3::rule<class fieldsExpr, queryParams::fields::Expr>{"fieldsExpr"};
|
||||
+const auto fieldsSemi = x3::rule<class fieldsSemiExpr, queryParams::fields::SemiExpr>{"fieldsSemi"};
|
||||
+const auto fieldsSlash = x3::rule<class fieldsSlashExpr, queryParams::fields::SlashExpr>{"fieldsSlash"};
|
||||
+const auto fieldsParen = x3::rule<class fieldsParen, queryParams::fields::ParenExpr>{"fieldsParen"};
|
||||
+
|
||||
+const auto fieldsSemi_def = fieldsParen >> -(x3::lit(";") >> fieldsSemi);
|
||||
+const auto fieldsParen_def = fieldsSlash >> -(x3::lit("(") >> fieldsExpr >> x3::lit(")"));
|
||||
+const auto fieldsSlash_def = apiIdentifier >> -(x3::lit("/") >> fieldsSlash);
|
||||
+const auto fieldsExpr_def = fieldsSemi;
|
||||
+BOOST_SPIRIT_DEFINE(fieldsParen, fieldsExpr, fieldsSlash, fieldsSemi);
|
||||
+
|
||||
// early sanity check, this timestamp will be parsed by libyang::fromYangTimeFormat anyways
|
||||
const auto dateAndTime = x3::rule<class dateAndTime, std::string>{"dateAndTime"} =
|
||||
x3::repeat(4)[x3::digit] >> x3::char_('-') >> x3::repeat(2)[x3::digit] >> x3::char_('-') >> x3::repeat(2)[x3::digit] >> x3::char_('T') >>
|
||||
@@ -127,7 +152,8 @@ const auto queryParamPair = x3::rule<class queryParamPair, std::pair<std::string
|
||||
(x3::string("point") >> "=" >> uriPath) |
|
||||
(x3::string("filter") >> "=" >> filter) |
|
||||
(x3::string("start-time") >> "=" >> dateAndTime) |
|
||||
- (x3::string("stop-time") >> "=" >> dateAndTime);
|
||||
+ (x3::string("stop-time") >> "=" >> dateAndTime) |
|
||||
+ (x3::string("fields") >> "=" >> fieldsExpr);
|
||||
|
||||
const auto queryParamGrammar = x3::rule<class grammar, queryParams::QueryParams>{"queryParamGrammar"} = queryParamPair % "&" | x3::eps;
|
||||
|
||||
@@ -384,7 +410,7 @@ void validateQueryParameters(const std::multimap<std::string, queryParams::Query
|
||||
}
|
||||
}
|
||||
|
||||
- for (const auto& param : {"depth", "with-defaults", "content"}) {
|
||||
+ for (const auto& param : {"depth", "with-defaults", "content", "fields"}) {
|
||||
if (auto it = params.find(param); it != params.end() && httpMethod != "GET" && httpMethod != "HEAD") {
|
||||
throw ErrorResponse(400, "protocol", "invalid-value", "Query parameter '"s + param + "' can be used only with GET and HEAD methods");
|
||||
}
|
||||
@@ -658,4 +684,69 @@ std::set<std::string> allowedHttpMethodsForUri(const libyang::Context& ctx, cons
|
||||
|
||||
return allowedHttpMethods;
|
||||
}
|
||||
+
|
||||
+/** @brief Traverses the AST of the fields input expression and collects all the possible paths
|
||||
+ *
|
||||
+ * @param expr The fields expressions
|
||||
+ * @param currentPath The current path in the AST, it serves as a stack for the DFS
|
||||
+ * @param output The collection of all collected paths
|
||||
+ * @param end If this is the terminal node, i.e., the last node in the expression. This is needed for the correct handling of the leafs under paren expression, which does not "split" the paths but rather concatenates.
|
||||
+ * */
|
||||
+void fieldsToXPath(const queryParams::fields::Expr& expr, std::vector<std::string>& currentPath, std::vector<std::string>& output, bool end = false)
|
||||
+{
|
||||
+ boost::apply_visitor([&](auto&& node) {
|
||||
+ using T = std::decay_t<decltype(node)>;
|
||||
+
|
||||
+ if constexpr (std::is_same_v<T, queryParams::fields::ParenExpr>) {
|
||||
+ // the paths from left and right subtree are concatenated, i.e., the nodes we collect in the left tree
|
||||
+ // are joined together with the nodes from the right tree
|
||||
+ fieldsToXPath(node.lhs, currentPath, output, !node.rhs.has_value());
|
||||
+ if (node.rhs) {
|
||||
+ fieldsToXPath(*node.rhs, currentPath, output, end);
|
||||
+ }
|
||||
+ } else if constexpr (std::is_same_v<T, queryParams::fields::SemiExpr>) {
|
||||
+ // the two paths are now independent and nodes from left subtree do not affect the right subtree
|
||||
+ // hence we need to copy the current path
|
||||
+ auto pathCopy = currentPath;
|
||||
+ fieldsToXPath(node.lhs, currentPath, output, !node.rhs.has_value());
|
||||
+ if (node.rhs) {
|
||||
+ fieldsToXPath(*node.rhs, pathCopy, output, false);
|
||||
+ }
|
||||
+ } else if constexpr (std::is_same_v<T, queryParams::fields::SlashExpr>) {
|
||||
+ // the paths from left and right subtree are concatenated, i.e., the the nodes we collect in the left tree
|
||||
+ // are joined together with the nodes from the right tree, but if this is the terminal node, we need to
|
||||
+ // add it to the collection of all the gathered paths
|
||||
+ currentPath.push_back(node.lhs.name());
|
||||
+
|
||||
+ if (node.rhs) {
|
||||
+ fieldsToXPath(*node.rhs, currentPath, output, end);
|
||||
+ } else if (end) {
|
||||
+ output.emplace_back(boost::algorithm::join(currentPath, "/"));
|
||||
+ }
|
||||
+ }
|
||||
+ }, expr);
|
||||
+}
|
||||
+
|
||||
+/** @brief Translates the fields expression into a XPath expression and checks for schema validity of the resulting nodes
|
||||
+ *
|
||||
+ * The expressions are "unwrapped" into a linear structure and then a union of such paths is made.
|
||||
+ * E.g., the expression "a(b;c)" is translated into "a/b | a/c".
|
||||
+ * */
|
||||
+std::string fieldsToXPath(const libyang::Context& ctx, const std::string& prefix, const queryParams::fields::Expr& expr)
|
||||
+{
|
||||
+ std::vector<std::string> currentPath{prefix};
|
||||
+ std::vector<std::string> paths;
|
||||
+
|
||||
+ fieldsToXPath(expr, currentPath, paths);
|
||||
+
|
||||
+ for (auto& xpath : paths) {
|
||||
+ try {
|
||||
+ validateMethodForNode("GET", impl::URIPrefix{impl::URIPrefix::Type::BasicRestconfData}, ctx.findPath(xpath));
|
||||
+ } catch (const libyang::Error& e) {
|
||||
+ throw ErrorResponse(400, "application", "operation-failed", "Can't find schema node for '" + xpath + "'");
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return boost::algorithm::join(paths, " | ");
|
||||
+}
|
||||
}
|
||||
diff --git a/src/restconf/uri.h b/src/restconf/uri.h
|
||||
index 5e079ef..f6df724 100644
|
||||
--- a/src/restconf/uri.h
|
||||
+++ b/src/restconf/uri.h
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#pragma once
|
||||
#include <boost/optional.hpp>
|
||||
+#include <boost/variant.hpp>
|
||||
#include <libyang-cpp/Module.hpp>
|
||||
#include <libyang-cpp/SchemaNode.hpp>
|
||||
#include <map>
|
||||
@@ -101,6 +102,57 @@ struct After {
|
||||
using PointParsed = std::vector<PathSegment>;
|
||||
}
|
||||
|
||||
+namespace fields {
|
||||
+struct ParenExpr;
|
||||
+struct SemiExpr;
|
||||
+struct SlashExpr;
|
||||
+
|
||||
+using Expr = boost::variant<boost::recursive_wrapper<SlashExpr>, boost::recursive_wrapper<ParenExpr>, boost::recursive_wrapper<SemiExpr>>;
|
||||
+
|
||||
+struct ParenExpr {
|
||||
+ Expr lhs;
|
||||
+ boost::optional<Expr> rhs;
|
||||
+
|
||||
+ ParenExpr() = default;
|
||||
+ ParenExpr(const Expr& lhs, const Expr& rhs) : ParenExpr(lhs, boost::optional<Expr>(rhs)) {}
|
||||
+ ParenExpr(const Expr& lhs, const boost::optional<Expr>& rhs = boost::none)
|
||||
+ : lhs(lhs)
|
||||
+ , rhs(rhs)
|
||||
+ {
|
||||
+ }
|
||||
+
|
||||
+ bool operator==(const ParenExpr&) const = default;
|
||||
+};
|
||||
+struct SemiExpr {
|
||||
+ Expr lhs;
|
||||
+ boost::optional<Expr> rhs;
|
||||
+
|
||||
+ SemiExpr() = default;
|
||||
+ SemiExpr(const Expr& lhs, const Expr& rhs) : SemiExpr(lhs, boost::optional<Expr>(rhs)) {}
|
||||
+ SemiExpr(const Expr& lhs, const boost::optional<Expr>& rhs = boost::none)
|
||||
+ : lhs(lhs)
|
||||
+ , rhs(rhs)
|
||||
+ {
|
||||
+ }
|
||||
+
|
||||
+ bool operator==(const SemiExpr&) const = default;
|
||||
+};
|
||||
+struct SlashExpr {
|
||||
+ ApiIdentifier lhs;
|
||||
+ boost::optional<Expr> rhs;
|
||||
+
|
||||
+ SlashExpr() = default;
|
||||
+ SlashExpr(const ApiIdentifier& lhs, const Expr& rhs) : SlashExpr(lhs, boost::optional<Expr>(rhs)) {}
|
||||
+ SlashExpr(const ApiIdentifier& lhs, const boost::optional<Expr>& rhs = boost::none)
|
||||
+ : lhs(lhs)
|
||||
+ , rhs(rhs)
|
||||
+ {
|
||||
+ }
|
||||
+
|
||||
+ bool operator==(const SlashExpr&) const = default;
|
||||
+};
|
||||
+}
|
||||
+
|
||||
using QueryParamValue = std::variant<
|
||||
UnboundedDepth,
|
||||
unsigned int,
|
||||
@@ -116,7 +168,8 @@ using QueryParamValue = std::variant<
|
||||
insert::Last,
|
||||
insert::Before,
|
||||
insert::After,
|
||||
- insert::PointParsed>;
|
||||
+ insert::PointParsed,
|
||||
+ fields::Expr>;
|
||||
using QueryParams = std::multimap<std::string, QueryParamValue>;
|
||||
}
|
||||
|
||||
@@ -159,4 +212,6 @@ std::vector<PathSegment> asPathSegments(const std::string& uriPath);
|
||||
std::optional<std::variant<libyang::Module, libyang::SubmoduleParsed>> asYangModule(const libyang::Context& ctx, const std::string& uriPath);
|
||||
RestconfStreamRequest asRestconfStreamRequest(const std::string& httpMethod, const std::string& uriPath, const std::string& uriQueryString);
|
||||
std::set<std::string> allowedHttpMethodsForUri(const libyang::Context& ctx, const std::string& uriPath);
|
||||
+
|
||||
+std::string fieldsToXPath(const libyang::Context& ctx, const std::string& prefix, const queryParams::fields::Expr& expr);
|
||||
}
|
||||
diff --git a/src/restconf/uri_impl.h b/src/restconf/uri_impl.h
|
||||
index 8a2e166..2bcdb3f 100644
|
||||
--- a/src/restconf/uri_impl.h
|
||||
+++ b/src/restconf/uri_impl.h
|
||||
@@ -65,3 +65,7 @@ BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::impl::URIPath, prefix, segments);
|
||||
BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::impl::YangModule, name, revision);
|
||||
BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::PathSegment, apiIdent, keys);
|
||||
BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::ApiIdentifier, prefix, identifier);
|
||||
+
|
||||
+BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::queryParams::fields::ParenExpr, lhs, rhs);
|
||||
+BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::queryParams::fields::SlashExpr, lhs, rhs);
|
||||
+BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::queryParams::fields::SemiExpr, lhs, rhs);
|
||||
diff --git a/tests/pretty_printers.h b/tests/pretty_printers.h
|
||||
index ec87250..a2befeb 100644
|
||||
--- a/tests/pretty_printers.h
|
||||
+++ b/tests/pretty_printers.h
|
||||
@@ -8,6 +8,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "trompeloeil_doctest.h"
|
||||
+#include <boost/variant.hpp>
|
||||
#include <experimental/iterator>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
@@ -159,6 +160,28 @@ struct StringMaker<rousette::restconf::queryParams::QueryParamValue> {
|
||||
[](const rousette::restconf::queryParams::insert::PointParsed& p) -> std::string {
|
||||
return ("PointParsed{" + StringMaker<decltype(p)>::convert(p) + "}").c_str();
|
||||
},
|
||||
+ [](const rousette::restconf::queryParams::fields::Expr& expr) -> std::string {
|
||||
+ return boost::apply_visitor([&](auto&& next) {
|
||||
+ using T = std::decay_t<decltype(next)>;
|
||||
+ std::string res;
|
||||
+
|
||||
+ if constexpr (std::is_same_v<T, rousette::restconf::queryParams::fields::ParenExpr> || std::is_same_v<T, rousette::restconf::queryParams::fields::SemiExpr>) {
|
||||
+ if constexpr (std::is_same_v<T, rousette::restconf::queryParams::fields::ParenExpr>) {
|
||||
+ res = "ParenExpr{";
|
||||
+ } else {
|
||||
+ res = "SemiExpr{";
|
||||
+ }
|
||||
+ res += StringMaker<rousette::restconf::queryParams::QueryParamValue>::convert(next.lhs).c_str();
|
||||
+ } else if constexpr (std::is_same_v<T, rousette::restconf::queryParams::fields::SlashExpr>) {
|
||||
+ res = "SlashExpr{" + next.lhs.name();
|
||||
+ }
|
||||
+
|
||||
+ if (next.rhs) {
|
||||
+ res += std::string(", ") + StringMaker<rousette::restconf::queryParams::QueryParamValue>::convert(*next.rhs).c_str();
|
||||
+ }
|
||||
+ return res += "}";
|
||||
+ }, expr);
|
||||
+ },
|
||||
}, obj).c_str();
|
||||
}
|
||||
};
|
||||
diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp
|
||||
index 2ded3f0..d7d507b 100644
|
||||
--- a/tests/restconf-reading.cpp
|
||||
+++ b/tests/restconf-reading.cpp
|
||||
@@ -72,7 +72,8 @@ TEST_CASE("reading data")
|
||||
"urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit",
|
||||
"urn:ietf:params:restconf:capability:depth:1.0",
|
||||
"urn:ietf:params:restconf:capability:with-defaults:1.0",
|
||||
- "urn:ietf:params:restconf:capability:filter:1.0"
|
||||
+ "urn:ietf:params:restconf:capability:filter:1.0",
|
||||
+ "urn:ietf:params:restconf:capability:fields:1.0"
|
||||
]
|
||||
},
|
||||
"streams": {
|
||||
@@ -116,7 +117,8 @@ TEST_CASE("reading data")
|
||||
"urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit",
|
||||
"urn:ietf:params:restconf:capability:depth:1.0",
|
||||
"urn:ietf:params:restconf:capability:with-defaults:1.0",
|
||||
- "urn:ietf:params:restconf:capability:filter:1.0"
|
||||
+ "urn:ietf:params:restconf:capability:filter:1.0",
|
||||
+ "urn:ietf:params:restconf:capability:fields:1.0"
|
||||
]
|
||||
},
|
||||
"streams": {
|
||||
@@ -672,7 +674,8 @@ TEST_CASE("reading data")
|
||||
"urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit",
|
||||
"urn:ietf:params:restconf:capability:depth:1.0",
|
||||
"urn:ietf:params:restconf:capability:with-defaults:1.0",
|
||||
- "urn:ietf:params:restconf:capability:filter:1.0"
|
||||
+ "urn:ietf:params:restconf:capability:filter:1.0",
|
||||
+ "urn:ietf:params:restconf:capability:fields:1.0"
|
||||
]
|
||||
},
|
||||
"streams": {
|
||||
@@ -894,6 +897,141 @@ TEST_CASE("reading data")
|
||||
)"});
|
||||
}
|
||||
|
||||
+ SECTION("fields filtering")
|
||||
+ {
|
||||
+ srSess.switchDatastore(sysrepo::Datastore::Running);
|
||||
+ srSess.setItem("/example:tlc/list[name='blabla']/choice1", "c1");
|
||||
+ srSess.setItem("/example:tlc/list[name='blabla']/collection[.='42']", std::nullopt);
|
||||
+ srSess.setItem("/example:tlc/list[name='blabla']/nested[first='1'][second='2'][third='3']/fourth", "4");
|
||||
+ srSess.setItem("/example:tlc/list[name='blabla']/nested[first='1'][second='2'][third='3']/data/a", "a");
|
||||
+ srSess.setItem("/example:tlc/list[name='blabla']/nested[first='1'][second='2'][third='3']/data/other-data/b", "b");
|
||||
+ srSess.setItem("/example:tlc/list[name='blabla']/nested[first='1'][second='2'][third='3']/data/other-data/c", "c");
|
||||
+ srSess.applyChanges();
|
||||
+
|
||||
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:tlc/list=blabla?fields=choice1;collection", {}) == Response{200, jsonHeaders, R"({
|
||||
+ "example:tlc": {
|
||||
+ "list": [
|
||||
+ {
|
||||
+ "name": "blabla",
|
||||
+ "collection": [
|
||||
+ 42
|
||||
+ ],
|
||||
+ "choice1": "c1"
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+}
|
||||
+)"});
|
||||
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:tlc/list=blabla?fields=choice1;choice2;nested/data(a;other-data/b)", {}) == Response{200, jsonHeaders, R"({
|
||||
+ "example:tlc": {
|
||||
+ "list": [
|
||||
+ {
|
||||
+ "name": "blabla",
|
||||
+ "nested": [
|
||||
+ {
|
||||
+ "first": "1",
|
||||
+ "second": 2,
|
||||
+ "third": "3",
|
||||
+ "data": {
|
||||
+ "a": "a",
|
||||
+ "other-data": {
|
||||
+ "b": "b"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "choice1": "c1"
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+}
|
||||
+)"});
|
||||
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:tlc/list=blabla?fields=hehe", {}) == Response{400, jsonHeaders, R"({
|
||||
+ "ietf-restconf:errors": {
|
||||
+ "error": [
|
||||
+ {
|
||||
+ "error-type": "application",
|
||||
+ "error-tag": "operation-failed",
|
||||
+ "error-message": "Can't find schema node for '/example:tlc/list[name='blabla']/hehe'"
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+}
|
||||
+)"});
|
||||
+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:tlc/list=blabla?fields=nested/data&depth=1", {}) == Response{200, jsonHeaders, R"({
|
||||
+ "example:tlc": {
|
||||
+ "list": [
|
||||
+ {
|
||||
+ "name": "blabla",
|
||||
+ "nested": [
|
||||
+ {
|
||||
+ "first": "1",
|
||||
+ "second": 2,
|
||||
+ "third": "3",
|
||||
+ "data": {
|
||||
+ "a": "a",
|
||||
+ "other-data": {}
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+}
|
||||
+)"});
|
||||
+
|
||||
+ // whole datastore with fields filtering
|
||||
+ REQUIRE(get(RESTCONF_DATA_ROOT "?fields=example:tlc/list/nested/data&depth=1", {}) == Response{200, jsonHeaders, R"({
|
||||
+ "example:tlc": {
|
||||
+ "list": [
|
||||
+ {
|
||||
+ "name": "blabla",
|
||||
+ "nested": [
|
||||
+ {
|
||||
+ "first": "1",
|
||||
+ "second": 2,
|
||||
+ "third": "3",
|
||||
+ "data": {
|
||||
+ "a": "a",
|
||||
+ "other-data": {}
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+}
|
||||
+)"});
|
||||
+
|
||||
+ // nonexistent schema node in fields: missing prefix in tlc because we query root node so libyang can't infer the prefix from the path
|
||||
+ REQUIRE(get(RESTCONF_DATA_ROOT "?fields=tlc", {}) == Response{400, jsonHeaders, R"({
|
||||
+ "ietf-restconf:errors": {
|
||||
+ "error": [
|
||||
+ {
|
||||
+ "error-type": "application",
|
||||
+ "error-tag": "operation-failed",
|
||||
+ "error-message": "Can't find schema node for '/tlc'"
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+}
|
||||
+)"});
|
||||
+
|
||||
+ // nonexistent schema node in fields
|
||||
+ REQUIRE(get(RESTCONF_DATA_ROOT "?fields=example:tlc/ob-la-di-ob-la-da", {}) == Response{400, jsonHeaders, R"({
|
||||
+ "ietf-restconf:errors": {
|
||||
+ "error": [
|
||||
+ {
|
||||
+ "error-type": "application",
|
||||
+ "error-tag": "operation-failed",
|
||||
+ "error-message": "Can't find schema node for '/example:tlc/ob-la-di-ob-la-da'"
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+}
|
||||
+)"});
|
||||
+ }
|
||||
+
|
||||
SECTION("OPTIONS method")
|
||||
{
|
||||
// RPC node
|
||||
diff --git a/tests/restconf-writing.cpp b/tests/restconf-writing.cpp
|
||||
index c1a9515..582a262 100644
|
||||
--- a/tests/restconf-writing.cpp
|
||||
+++ b/tests/restconf-writing.cpp
|
||||
@@ -375,6 +375,8 @@ TEST_CASE("writing data")
|
||||
CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/first", "1"),
|
||||
CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/second", "2"),
|
||||
CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/third", "3"),
|
||||
+ CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/data", std::nullopt),
|
||||
+ CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/data/other-data", std::nullopt),
|
||||
CREATED("/example:tlc/list[name='large']/choice2", "large"));
|
||||
REQUIRE(put(RESTCONF_DATA_ROOT "/example:tlc/list=large", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list":[{"name": "large", "choice2": "large", "example:nested": [{"first": "1", "second": 2, "third": "3"}]}]})") == Response{201, noContentTypeHeaders, ""});
|
||||
}
|
||||
@@ -385,7 +387,9 @@ TEST_CASE("writing data")
|
||||
CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']", std::nullopt),
|
||||
CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/first", "11"),
|
||||
CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/second", "12"),
|
||||
- CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/third", "13"));
|
||||
+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/third", "13"),
|
||||
+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/data", std::nullopt),
|
||||
+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/data/other-data", std::nullopt));
|
||||
REQUIRE(put(RESTCONF_DATA_ROOT "/example:tlc/list=libyang/nested=11,12,13", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:nested": [{"first": "11", "second": 12, "third": "13"}]}]})") == Response{201, noContentTypeHeaders, ""});
|
||||
}
|
||||
|
||||
@@ -1339,6 +1343,8 @@ TEST_CASE("writing data")
|
||||
CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/first", "1"),
|
||||
CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/second", "2"),
|
||||
CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/third", "3"),
|
||||
+ CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/data", std::nullopt),
|
||||
+ CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/data/other-data", std::nullopt),
|
||||
CREATED("/example:tlc/list[name='large']/choice2", "large"));
|
||||
REQUIRE(post(RESTCONF_DATA_ROOT "/example:tlc", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list":[{"name": "large", "choice2": "large", "example:nested": [{"first": "1", "second": 2, "third": "3"}]}]})") == Response{201, jsonHeaders, ""});
|
||||
}
|
||||
@@ -1349,7 +1355,9 @@ TEST_CASE("writing data")
|
||||
CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']", std::nullopt),
|
||||
CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/first", "11"),
|
||||
CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/second", "12"),
|
||||
- CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/third", "13"));
|
||||
+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/third", "13"),
|
||||
+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/data", std::nullopt),
|
||||
+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/data/other-data", std::nullopt));
|
||||
REQUIRE(post(RESTCONF_DATA_ROOT "/example:tlc/list=libyang", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:nested": [{"first": "11", "second": 12, "third": "13"}]}]})") == Response{201, jsonHeaders, ""});
|
||||
}
|
||||
|
||||
diff --git a/tests/uri-parser.cpp b/tests/uri-parser.cpp
|
||||
index 000fe5d..fd5dd8b 100644
|
||||
--- a/tests/uri-parser.cpp
|
||||
+++ b/tests/uri-parser.cpp
|
||||
@@ -828,6 +828,65 @@ TEST_CASE("URI path parser")
|
||||
REQUIRE(parseQueryParams("stop-time=2023-05-20T18:30:00") == std::nullopt);
|
||||
REQUIRE(parseQueryParams("stop-time=20230520T18:30:00Z") == std::nullopt);
|
||||
REQUIRE(parseQueryParams("stop-time=2023-05-a0T18:30:00+05:30") == std::nullopt);
|
||||
+ REQUIRE(parseQueryParams("fields=mod:leaf") == QueryParams{{"fields", fields::SemiExpr{fields::ParenExpr{fields::SlashExpr{{"mod", "leaf"}}}}}});
|
||||
+ REQUIRE(parseQueryParams("fields=b(c;d);e(f)") == QueryParams{{"fields",
|
||||
+ fields::SemiExpr{
|
||||
+ fields::ParenExpr{
|
||||
+ fields::SlashExpr{{"b"}},
|
||||
+ fields::SemiExpr{
|
||||
+ fields::ParenExpr{
|
||||
+ fields::SlashExpr{{"c"}}
|
||||
+ },
|
||||
+ fields::SemiExpr{
|
||||
+ fields::ParenExpr{
|
||||
+ fields::SlashExpr{{"d"}}
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ fields::SemiExpr{
|
||||
+ fields::ParenExpr{
|
||||
+ fields::SlashExpr{{"e"}},
|
||||
+ fields::SemiExpr{
|
||||
+ fields::ParenExpr{
|
||||
+ fields::SlashExpr{{"f"}}
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }});
|
||||
+ REQUIRE(parseQueryParams("fields=(xyz)") == std::nullopt);
|
||||
+ REQUIRE(parseQueryParams("fields=a;(xyz)") == std::nullopt);
|
||||
+ REQUIRE(parseQueryParams("fields=") == std::nullopt);
|
||||
+
|
||||
+ for (const auto& [prefix, fields, xpath] : {
|
||||
+ std::tuple<std::string, std::string, std::string>{"/example:a", "b", "/example:a/b"},
|
||||
+ {"/example:a", "b/c", "/example:a/b/c"},
|
||||
+ {"/example:a/b", "c(enabled;blower)", "/example:a/b/c/enabled | /example:a/b/c/blower"},
|
||||
+ {"/example:a", "b(c(enabled;blower))", "/example:a/b/c/enabled | /example:a/b/c/blower"},
|
||||
+ {"/example:a", "b(c)", "/example:a/b/c"},
|
||||
+ {"/example:a", "example:b;something", "/example:a/example:b | /example:a/something"},
|
||||
+ {"/example:a", "something;b1;b(c/enabled;c/blower)", "/example:a/something | /example:a/b1 | /example:a/b/c/enabled | /example:a/b/c/blower"},
|
||||
+ {"/example:a", "b(c/enabled;c/blower);something;b1", "/example:a/b/c/enabled | /example:a/b/c/blower | /example:a/something | /example:a/b1"}, // not allowed by RFC 8040
|
||||
+ {"", "example:a(b;b1)", "/example:a/b | /example:a/b1"},
|
||||
+ }) {
|
||||
+ CAPTURE(fields);
|
||||
+ CAPTURE(xpath);
|
||||
+ auto qp = parseQueryParams("fields=" + fields);
|
||||
+ REQUIRE(qp);
|
||||
+ REQUIRE(qp->count("fields") == 1);
|
||||
+ auto fieldExpr = qp->find("fields")->second;
|
||||
+ REQUIRE(std::holds_alternative<fields::Expr>(fieldExpr));
|
||||
+ REQUIRE(rousette::restconf::fieldsToXPath(ctx, prefix, std::get<fields::Expr>(fieldExpr)) == xpath);
|
||||
+ }
|
||||
+
|
||||
+ auto qp = parseQueryParams("fields=xxx/xyz(a;b)");
|
||||
+ REQUIRE(qp);
|
||||
+ REQUIRE_THROWS_WITH_AS(
|
||||
+ rousette::restconf::fieldsToXPath(ctx, "/example:a", std::get<fields::Expr>(qp->find("fields")->second)),
|
||||
+ serializeErrorResponse(400, "application", "operation-failed", "Can't find schema node for '/example:a/xxx/xyz/a'").c_str(),
|
||||
+ rousette::restconf::ErrorResponse);
|
||||
}
|
||||
|
||||
SECTION("Full requests with validation")
|
||||
@@ -885,6 +944,40 @@ TEST_CASE("URI path parser")
|
||||
rousette::restconf::ErrorResponse);
|
||||
}
|
||||
|
||||
+ SECTION("fields")
|
||||
+ {
|
||||
+ auto resp = asRestconfRequest(ctx, "GET", "/restconf/data/example:a", "fields=b/c(enabled;blower)");
|
||||
+ REQUIRE(resp.queryParams == QueryParams({{"fields",
|
||||
+ fields::SemiExpr{
|
||||
+ fields::ParenExpr{
|
||||
+ fields::SlashExpr{
|
||||
+ {"b"},
|
||||
+ fields::SlashExpr{{"c"}}
|
||||
+ },
|
||||
+ fields::SemiExpr{
|
||||
+ fields::ParenExpr{
|
||||
+ fields::SlashExpr{{"enabled"}}
|
||||
+ },
|
||||
+ fields::SemiExpr{
|
||||
+ fields::ParenExpr{
|
||||
+ fields::SlashExpr{{"blower"}}
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }));
|
||||
+
|
||||
+ REQUIRE_THROWS_WITH_AS(asRestconfRequest(ctx, "POST", "/restconf/data/example:a", "fields=b/c(enabled;blower)"),
|
||||
+ serializeErrorResponse(400, "protocol", "invalid-value", "Query parameter 'fields' can be used only with GET and HEAD methods").c_str(),
|
||||
+ rousette::restconf::ErrorResponse);
|
||||
+
|
||||
+ REQUIRE_THROWS_WITH_AS(asRestconfStreamRequest("GET", "/streams/NETCONF/XML", "fields=a"),
|
||||
+ serializeErrorResponse(400, "protocol", "invalid-value", "Query parameter 'fields' can't be used with streams").c_str(),
|
||||
+ rousette::restconf::ErrorResponse);
|
||||
+ }
|
||||
+
|
||||
SECTION("insert first/last")
|
||||
{
|
||||
auto resp = asRestconfRequest(ctx, "PUT", "/restconf/data/example:tlc", "insert=first");
|
||||
diff --git a/tests/yang/example.yang b/tests/yang/example.yang
|
||||
index 75cd7a6..5d586a0 100644
|
||||
--- a/tests/yang/example.yang
|
||||
+++ b/tests/yang/example.yang
|
||||
@@ -38,6 +38,14 @@ module example {
|
||||
leaf first { type string; }
|
||||
leaf second { type int32; }
|
||||
leaf third { type string; }
|
||||
+ leaf fourth { type string; }
|
||||
+ container data {
|
||||
+ leaf a { type string; }
|
||||
+ container other-data {
|
||||
+ leaf b { type string; }
|
||||
+ leaf c { type string; }
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
choice choose {
|
||||
mandatory true;
|
||||
@@ -92,6 +100,8 @@ module example {
|
||||
}
|
||||
}
|
||||
}
|
||||
+ container b1 { }
|
||||
+ leaf something { type string; }
|
||||
}
|
||||
|
||||
container two-leafs {
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
From 48d9b6ba3f3f892b9060b76b505d2f9a3aeb9e02 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
|
||||
Date: Mon, 25 Nov 2024 09:15:55 +0100
|
||||
Subject: [PATCH 09/20] cmake: adhere to CMP0167
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
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,45 @@
|
||||
From 64997543d48236cd2aae417568bc54d32c54df21 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
|
||||
Date: Mon, 2 Dec 2024 14:43:36 +0100
|
||||
Subject: [PATCH 10/20] Fix compatibility with pam_wrapper 1.1.6+
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
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,30 @@
|
||||
From 849aa35274d5e07726cb849fe724962754b3fa29 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
|
||||
Date: Mon, 2 Dec 2024 20:20:48 +0100
|
||||
Subject: [PATCH 11/20] tests: add missing pragma once
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
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,205 @@
|
||||
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/20] tests: extend clientRequest wrappers interface and use
|
||||
it
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
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,586 @@
|
||||
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/20] 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: Addiva Elektronik
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
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/20] tests: rename datastoreUtils to event_watchers
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
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/20] tests: rename NotificationWatcher
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
From 30d704588fa7eb9d32f66296ec5f6784f082869e 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:11:32 +0100
|
||||
Subject: [PATCH 16/20] tests: make NotificationWatcher reusable
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
We will need some parts of this in yang push tests
|
||||
|
||||
Change-Id: I72e60d993f75bbc848af096ac325a75fa3dea61a
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
tests/event_watchers.cpp | 28 +++++++++++++++++++++++++
|
||||
tests/event_watchers.h | 13 ++++++++++++
|
||||
tests/restconf-notifications.cpp | 35 --------------------------------
|
||||
3 files changed, 41 insertions(+), 35 deletions(-)
|
||||
|
||||
diff --git a/tests/event_watchers.cpp b/tests/event_watchers.cpp
|
||||
index c696bc5..bde96ec 100644
|
||||
--- a/tests/event_watchers.cpp
|
||||
+++ b/tests/event_watchers.cpp
|
||||
@@ -1,3 +1,4 @@
|
||||
+#include <spdlog/spdlog.h>
|
||||
#include "UniqueResource.h"
|
||||
#include "event_watchers.h"
|
||||
|
||||
@@ -50,3 +51,30 @@ sysrepo::Subscription datastoreNewStateSubscription(sysrepo::Session& session, D
|
||||
0,
|
||||
sysrepo::SubscribeOptions::DoneOnly);
|
||||
}
|
||||
+
|
||||
+RestconfNotificationWatcher::RestconfNotificationWatcher(const libyang::Context& ctx)
|
||||
+ : ctx(ctx)
|
||||
+ , dataFormat(libyang::DataFormat::JSON)
|
||||
+{
|
||||
+}
|
||||
+
|
||||
+void RestconfNotificationWatcher::setDataFormat(const libyang::DataFormat dataFormat)
|
||||
+{
|
||||
+ this->dataFormat = dataFormat;
|
||||
+}
|
||||
+
|
||||
+void RestconfNotificationWatcher::operator()(const std::string& msg) const
|
||||
+{
|
||||
+ spdlog::trace("Client received data: {}", msg);
|
||||
+ auto notifDataNode = ctx.parseOp(msg,
|
||||
+ dataFormat,
|
||||
+ dataFormat == libyang::DataFormat::JSON ? libyang::OperationType::NotificationRestconf : libyang::OperationType::NotificationNetconf);
|
||||
+
|
||||
+ // parsing nested notifications does not return the data tree root node but the notification data node
|
||||
+ auto dataRoot = notifDataNode.op;
|
||||
+ while (dataRoot->parent()) {
|
||||
+ dataRoot = *dataRoot->parent();
|
||||
+ }
|
||||
+
|
||||
+ data(*dataRoot->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink));
|
||||
+}
|
||||
diff --git a/tests/event_watchers.h b/tests/event_watchers.h
|
||||
index 5b2429c..b6f2dd2 100644
|
||||
--- a/tests/event_watchers.h
|
||||
+++ b/tests/event_watchers.h
|
||||
@@ -32,3 +32,16 @@ static DatastoreChangesMock testMockForUntrackedModuleWrites;
|
||||
#define SUBSCRIBE_MODULE(SUBNAME, SESSION, MODULE) \
|
||||
ALLOW_CALL(testMockForUntrackedModuleWrites, change(trompeloeil::_)); \
|
||||
auto SUBNAME = datastoreChangesSubscription(SESSION, testMockForUntrackedModuleWrites, MODULE);
|
||||
+
|
||||
+struct RestconfNotificationWatcher {
|
||||
+ libyang::Context ctx;
|
||||
+ libyang::DataFormat dataFormat;
|
||||
+
|
||||
+ RestconfNotificationWatcher(const libyang::Context& ctx);
|
||||
+ void setDataFormat(const libyang::DataFormat dataFormat);
|
||||
+ void operator()(const std::string& msg) const;
|
||||
+
|
||||
+ MAKE_CONST_MOCK1(data, void(const std::string&));
|
||||
+};
|
||||
+
|
||||
+#define EXPECT_NOTIFICATION(DATA, SEQ) expectations.emplace_back(NAMED_REQUIRE_CALL(netconfWatcher, data(DATA)).IN_SEQUENCE(SEQ));
|
||||
diff --git a/tests/restconf-notifications.cpp b/tests/restconf-notifications.cpp
|
||||
index d873512..6c8c51a 100644
|
||||
--- a/tests/restconf-notifications.cpp
|
||||
+++ b/tests/restconf-notifications.cpp
|
||||
@@ -16,45 +16,10 @@ static const auto SERVER_PORT = "10088";
|
||||
#include "tests/aux-utils.h"
|
||||
#include "tests/pretty_printers.h"
|
||||
|
||||
-#define EXPECT_NOTIFICATION(DATA, SEQ) expectations.emplace_back(NAMED_REQUIRE_CALL(netconfWatcher, data(DATA)).IN_SEQUENCE(SEQ));
|
||||
#define SEND_NOTIFICATION(DATA) notifSession.sendNotification(*ctx.parseOp(DATA, libyang::DataFormat::JSON, libyang::OperationType::NotificationYang).op, sysrepo::Wait::No);
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
-struct RestconfNotificationWatcher {
|
||||
- libyang::Context ctx;
|
||||
- libyang::DataFormat dataFormat;
|
||||
-
|
||||
- RestconfNotificationWatcher(const libyang::Context& ctx)
|
||||
- : ctx(ctx)
|
||||
- , dataFormat(libyang::DataFormat::JSON)
|
||||
- {
|
||||
- }
|
||||
-
|
||||
- void setDataFormat(const libyang::DataFormat dataFormat)
|
||||
- {
|
||||
- this->dataFormat = dataFormat;
|
||||
- }
|
||||
-
|
||||
- void operator()(const std::string& msg) const
|
||||
- {
|
||||
- spdlog::trace("Client received data: {}", msg);
|
||||
- auto notifDataNode = ctx.parseOp(msg,
|
||||
- dataFormat,
|
||||
- dataFormat == libyang::DataFormat::JSON ? libyang::OperationType::NotificationRestconf : libyang::OperationType::NotificationNetconf);
|
||||
-
|
||||
- // parsing nested notifications does not return the data tree root node but the notification data node
|
||||
- auto dataRoot = notifDataNode.op;
|
||||
- while (dataRoot->parent()) {
|
||||
- dataRoot = *dataRoot->parent();
|
||||
- }
|
||||
-
|
||||
- data(*dataRoot->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink));
|
||||
- }
|
||||
-
|
||||
- MAKE_CONST_MOCK1(data, void(const std::string&));
|
||||
-};
|
||||
-
|
||||
struct SSEClient {
|
||||
std::shared_ptr<ng_client::session> client;
|
||||
boost::asio::deadline_timer t;
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
From 36943051d8563bee0c9cf89eec28acf5d3617272 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 12:02:24 +0100
|
||||
Subject: [PATCH 17/20] tests: helper function to construct server URI
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
Change-Id: Ic7c03b32a29b07464291f99027530010a7902f77
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
tests/restconf_utils.cpp | 9 +++++++--
|
||||
1 file changed, 7 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp
|
||||
index ea9a18d..83f568f 100644
|
||||
--- a/tests/restconf_utils.cpp
|
||||
+++ b/tests/restconf_utils.cpp
|
||||
@@ -13,6 +13,12 @@ using namespace std::string_literals;
|
||||
namespace ng = nghttp2::asio_http2;
|
||||
namespace ng_client = ng::client;
|
||||
|
||||
+namespace {
|
||||
+std::string serverAddressAndPort(const std::string& server_address, const std::string& server_port) {
|
||||
+ return "http://["s + server_address + "]" + ":" + server_port;
|
||||
+}
|
||||
+}
|
||||
+
|
||||
Response::Response(int statusCode, const Response::Headers& headers, const std::string& data)
|
||||
: Response(statusCode, transformHeaders(headers), data)
|
||||
{
|
||||
@@ -75,8 +81,7 @@ Response clientRequest(const std::string& server_address,
|
||||
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);
|
||||
+ auto req = client->submit(ec, method, serverAddressAndPort(server_address, server_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);
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
From 7d15f59d20079ba94224e0bc308682aa5a004483 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:15:05 +0100
|
||||
Subject: [PATCH 18/20] tests: make SSEClient reusable
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
We will need it in yang push tests
|
||||
|
||||
Change-Id: I22432553f3abff0de91b3c406abc5567de656065
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
tests/restconf-notifications.cpp | 108 +------------------------------
|
||||
tests/restconf_utils.cpp | 69 ++++++++++++++++++++
|
||||
tests/restconf_utils.h | 47 ++++++++++++++
|
||||
3 files changed, 119 insertions(+), 105 deletions(-)
|
||||
|
||||
diff --git a/tests/restconf-notifications.cpp b/tests/restconf-notifications.cpp
|
||||
index 6c8c51a..4496b63 100644
|
||||
--- a/tests/restconf-notifications.cpp
|
||||
+++ b/tests/restconf-notifications.cpp
|
||||
@@ -7,121 +7,19 @@
|
||||
|
||||
#include "trompeloeil_doctest.h"
|
||||
static const auto SERVER_PORT = "10088";
|
||||
-#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;
|
||||
|
||||
-struct SSEClient {
|
||||
- std::shared_ptr<ng_client::session> client;
|
||||
- boost::asio::deadline_timer t;
|
||||
-
|
||||
- SSEClient(
|
||||
- boost::asio::io_service& io,
|
||||
- std::latch& requestSent,
|
||||
- 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"
|
||||
- : client(std::make_shared<ng_client::session>(io, SERVER_ADDRESS, SERVER_PORT))
|
||||
- , t(io, silenceTimeout)
|
||||
- {
|
||||
- ng::header_map reqHeaders;
|
||||
- for (const auto& [name, value] : headers) {
|
||||
- reqHeaders.insert({name, {value, false}});
|
||||
- }
|
||||
-
|
||||
- // shutdown the client after a period of no traffic
|
||||
- t.async_wait([maybeClient = std::weak_ptr<ng_client::session>{client}](const boost::system::error_code& ec) {
|
||||
- if (ec == boost::asio::error::operation_aborted) {
|
||||
- return;
|
||||
- }
|
||||
- if (auto client = maybeClient.lock()) {
|
||||
- client->shutdown();
|
||||
- }
|
||||
- });
|
||||
-
|
||||
- client->on_connect([&, uri, reqHeaders, silenceTimeout](auto) {
|
||||
- boost::system::error_code ec;
|
||||
-
|
||||
- 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) {
|
||||
- // not a production-ready code. In real-life condition the data received in one callback might probably be incomplete
|
||||
- for (const auto& event : parseEvents(std::string(reinterpret_cast<const char*>(data), len))) {
|
||||
- notification(event);
|
||||
- }
|
||||
- t.expires_from_now(silenceTimeout);
|
||||
- });
|
||||
- });
|
||||
- });
|
||||
-
|
||||
- client->on_error([&](const boost::system::error_code& ec) {
|
||||
- throw std::runtime_error{"HTTP client error: " + ec.message()};
|
||||
- });
|
||||
- }
|
||||
-
|
||||
- static std::vector<std::string> parseEvents(const std::string& msg)
|
||||
- {
|
||||
- static const std::string prefix = "data:";
|
||||
-
|
||||
- std::vector<std::string> res;
|
||||
- std::istringstream iss(msg);
|
||||
- std::string line;
|
||||
- std::string event;
|
||||
-
|
||||
- while (std::getline(iss, line)) {
|
||||
- if (line.compare(0, prefix.size(), prefix) == 0) {
|
||||
- event += line.substr(prefix.size());
|
||||
- } else if (line.empty()) {
|
||||
- res.emplace_back(std::move(event));
|
||||
- event.clear();
|
||||
- } else {
|
||||
- FAIL("Unprefixed response");
|
||||
- }
|
||||
- }
|
||||
- return res;
|
||||
- }
|
||||
-};
|
||||
-
|
||||
-#define PREPARE_LOOP_WITH_EXCEPTIONS \
|
||||
- boost::asio::io_service io; \
|
||||
- std::promise<void> bg; \
|
||||
- std::latch requestSent(1);
|
||||
-
|
||||
-#define RUN_LOOP_WITH_EXCEPTIONS \
|
||||
- do { \
|
||||
- io.run(); \
|
||||
- auto fut = bg.get_future(); \
|
||||
- REQUIRE(fut.wait_for(666ms /* "plenty of time" for the notificationThread to exit after it has called io.stop() */) == std::future_status::ready); \
|
||||
- fut.get(); \
|
||||
- } while (false)
|
||||
-
|
||||
-auto wrap_exceptions_and_asio(std::promise<void>& bg, boost::asio::io_service& io, std::function<void()> func)
|
||||
-{
|
||||
- return [&bg, &io, func]()
|
||||
- {
|
||||
- try {
|
||||
- func();
|
||||
- } catch (...) {
|
||||
- bg.set_exception(std::current_exception());
|
||||
- return;
|
||||
- }
|
||||
- bg.set_value();
|
||||
- io.stop();
|
||||
- };
|
||||
-}
|
||||
-
|
||||
TEST_CASE("NETCONF notification streams")
|
||||
{
|
||||
trompeloeil::sequence seqMod1, seqMod2;
|
||||
@@ -237,7 +135,7 @@ TEST_CASE("NETCONF notification streams")
|
||||
waitForCompletionAndBitMore(seqMod2);
|
||||
}));
|
||||
|
||||
- SSEClient cli(io, requestSent, netconfWatcher, uri, headers);
|
||||
+ SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, headers);
|
||||
RUN_LOOP_WITH_EXCEPTIONS;
|
||||
}
|
||||
|
||||
@@ -399,7 +297,7 @@ TEST_CASE("NETCONF notification streams")
|
||||
}
|
||||
|
||||
oldNotificationsDone.wait();
|
||||
- SSEClient cli(io, requestSent, netconfWatcher, uri, {AUTH_ROOT});
|
||||
+ SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, {AUTH_ROOT});
|
||||
RUN_LOOP_WITH_EXCEPTIONS;
|
||||
}
|
||||
}
|
||||
diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp
|
||||
index 83f568f..8252bba 100644
|
||||
--- a/tests/restconf_utils.cpp
|
||||
+++ b/tests/restconf_utils.cpp
|
||||
@@ -160,3 +160,72 @@ void setupRealNacm(sysrepo::Session session)
|
||||
session.applyChanges();
|
||||
}
|
||||
|
||||
+SSEClient::SSEClient(
|
||||
+ boost::asio::io_service& io,
|
||||
+ const std::string& server_address,
|
||||
+ const std::string& server_port,
|
||||
+ std::latch& requestSent,
|
||||
+ const RestconfNotificationWatcher& notification,
|
||||
+ const std::string& uri,
|
||||
+ const std::map<std::string, std::string>& headers,
|
||||
+ const boost::posix_time::seconds silenceTimeout)
|
||||
+ : client(std::make_shared<ng_client::session>(io, server_address, server_port))
|
||||
+ , t(io, silenceTimeout)
|
||||
+{
|
||||
+ ng::header_map reqHeaders;
|
||||
+ for (const auto& [name, value] : headers) {
|
||||
+ reqHeaders.insert({name, {value, false}});
|
||||
+ }
|
||||
+
|
||||
+ // shutdown the client after a period of no traffic
|
||||
+ t.async_wait([maybeClient = std::weak_ptr<ng_client::session>{client}](const boost::system::error_code& ec) {
|
||||
+ if (ec == boost::asio::error::operation_aborted) {
|
||||
+ return;
|
||||
+ }
|
||||
+ if (auto client = maybeClient.lock()) {
|
||||
+ client->shutdown();
|
||||
+ }
|
||||
+ });
|
||||
+
|
||||
+ client->on_connect([&, uri, reqHeaders, silenceTimeout, server_address, server_port](auto) {
|
||||
+ boost::system::error_code ec;
|
||||
+
|
||||
+ auto req = client->submit(ec, "GET", serverAddressAndPort(server_address, server_port) + uri, "", reqHeaders);
|
||||
+ req->on_response([&, silenceTimeout](const ng_client::response& res) {
|
||||
+ requestSent.count_down();
|
||||
+ res.on_data([&, silenceTimeout](const uint8_t* data, std::size_t len) {
|
||||
+ // not a production-ready code. In real-life condition the data received in one callback might probably be incomplete
|
||||
+ for (const auto& event : parseEvents(std::string(reinterpret_cast<const char*>(data), len))) {
|
||||
+ notification(event);
|
||||
+ }
|
||||
+ t.expires_from_now(silenceTimeout);
|
||||
+ });
|
||||
+ });
|
||||
+ });
|
||||
+
|
||||
+ client->on_error([&](const boost::system::error_code& ec) {
|
||||
+ throw std::runtime_error{"HTTP client error: " + ec.message()};
|
||||
+ });
|
||||
+}
|
||||
+
|
||||
+std::vector<std::string> SSEClient::parseEvents(const std::string& msg)
|
||||
+{
|
||||
+ static const std::string prefix = "data:";
|
||||
+
|
||||
+ std::vector<std::string> res;
|
||||
+ std::istringstream iss(msg);
|
||||
+ std::string line;
|
||||
+ std::string event;
|
||||
+
|
||||
+ while (std::getline(iss, line)) {
|
||||
+ if (line.compare(0, prefix.size(), prefix) == 0) {
|
||||
+ event += line.substr(prefix.size());
|
||||
+ } else if (line.empty()) {
|
||||
+ res.emplace_back(std::move(event));
|
||||
+ event.clear();
|
||||
+ } else {
|
||||
+ FAIL("Unprefixed response");
|
||||
+ }
|
||||
+ }
|
||||
+ return res;
|
||||
+}
|
||||
diff --git a/tests/restconf_utils.h b/tests/restconf_utils.h
|
||||
index 26f0803..8b7386e 100644
|
||||
--- a/tests/restconf_utils.h
|
||||
+++ b/tests/restconf_utils.h
|
||||
@@ -8,7 +8,9 @@
|
||||
|
||||
#pragma once
|
||||
#include "trompeloeil_doctest.h"
|
||||
+#include <latch>
|
||||
#include <nghttp2/asio_http2_client.h>
|
||||
+#include "event_watchers.h"
|
||||
#include "UniqueResource.h"
|
||||
|
||||
namespace sysrepo {
|
||||
@@ -81,3 +83,48 @@ Response clientRequest(
|
||||
|
||||
UniqueResource manageNacm(sysrepo::Session session);
|
||||
void setupRealNacm(sysrepo::Session session);
|
||||
+
|
||||
+struct SSEClient {
|
||||
+ std::shared_ptr<ng_client::session> client;
|
||||
+ boost::asio::deadline_timer t;
|
||||
+
|
||||
+ SSEClient(
|
||||
+ boost::asio::io_service& io,
|
||||
+ const std::string& server_address,
|
||||
+ const std::string& server_port,
|
||||
+ std::latch& requestSent,
|
||||
+ 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"
|
||||
+
|
||||
+ static std::vector<std::string> parseEvents(const std::string& msg);
|
||||
+};
|
||||
+
|
||||
+#define PREPARE_LOOP_WITH_EXCEPTIONS \
|
||||
+ boost::asio::io_service io; \
|
||||
+ std::promise<void> bg; \
|
||||
+ std::latch requestSent(1);
|
||||
+
|
||||
+#define RUN_LOOP_WITH_EXCEPTIONS \
|
||||
+ do { \
|
||||
+ io.run(); \
|
||||
+ auto fut = bg.get_future(); \
|
||||
+ REQUIRE(fut.wait_for(666ms /* "plenty of time" for the notificationThread to exit after it has called io.stop() */) == std::future_status::ready); \
|
||||
+ fut.get(); \
|
||||
+ } while (false)
|
||||
+
|
||||
+inline auto wrap_exceptions_and_asio(std::promise<void>& bg, boost::asio::io_service& io, std::function<void()> func)
|
||||
+{
|
||||
+ return [&bg, &io, func]()
|
||||
+ {
|
||||
+ try {
|
||||
+ func();
|
||||
+ } catch (...) {
|
||||
+ bg.set_exception(std::current_exception());
|
||||
+ return;
|
||||
+ }
|
||||
+ bg.set_value();
|
||||
+ io.stop();
|
||||
+ };
|
||||
+}
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
From 3e3e492f4801df56226a5aff5d82bfa86c9d3812 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 13:59:52 +0100
|
||||
Subject: [PATCH 19/20] tests: fix deadlock in tests
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
In the case the client never requests (because of misconfiguration or
|
||||
something else) the test would still be waiting for somebody to
|
||||
decrement the latch.
|
||||
Unfortunately std::latch does not allow waiting for specified amount of
|
||||
time, so this patch replaces it with std::binary_semaphore in which it
|
||||
is possible to fail the acquiring operation after some time.
|
||||
|
||||
Change-Id: I38c007555bb5ab543329d724a16f024a9d80903e
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
tests/restconf-notifications.cpp | 10 +++++-----
|
||||
tests/restconf_utils.cpp | 4 ++--
|
||||
tests/restconf_utils.h | 8 +++++---
|
||||
3 files changed, 12 insertions(+), 10 deletions(-)
|
||||
|
||||
diff --git a/tests/restconf-notifications.cpp b/tests/restconf-notifications.cpp
|
||||
index 4496b63..04131d7 100644
|
||||
--- a/tests/restconf-notifications.cpp
|
||||
+++ b/tests/restconf-notifications.cpp
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "trompeloeil_doctest.h"
|
||||
static const auto SERVER_PORT = "10088";
|
||||
+#include <latch>
|
||||
#include <libyang-cpp/Time.hpp>
|
||||
#include <nghttp2/asio_http2.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
@@ -119,8 +120,7 @@ TEST_CASE("NETCONF notification streams")
|
||||
auto notifSession = sysrepo::Connection{}.sessionStart();
|
||||
auto ctx = notifSession.getContext();
|
||||
|
||||
- // wait until the client sends its HTTP request
|
||||
- requestSent.wait();
|
||||
+ WAIT_UNTIL_SSE_CLIENT_REQUESTS;
|
||||
|
||||
SEND_NOTIFICATION(notificationsJSON[0]);
|
||||
SEND_NOTIFICATION(notificationsJSON[1]);
|
||||
@@ -239,7 +239,7 @@ TEST_CASE("NETCONF notification streams")
|
||||
SEND_NOTIFICATION(notificationsJSON[3]);
|
||||
SEND_NOTIFICATION(notificationsJSON[4]);
|
||||
oldNotificationsDone.count_down();
|
||||
- requestSent.wait();
|
||||
+ WAIT_UNTIL_SSE_CLIENT_REQUESTS;
|
||||
|
||||
waitForCompletionAndBitMore(seqMod1);
|
||||
waitForCompletionAndBitMore(seqMod2);
|
||||
@@ -256,7 +256,7 @@ TEST_CASE("NETCONF notification streams")
|
||||
SEND_NOTIFICATION(notificationsJSON[1]);
|
||||
|
||||
oldNotificationsDone.count_down();
|
||||
- requestSent.wait();
|
||||
+ WAIT_UNTIL_SSE_CLIENT_REQUESTS;
|
||||
|
||||
SEND_NOTIFICATION(notificationsJSON[2]);
|
||||
SEND_NOTIFICATION(notificationsJSON[3]);
|
||||
@@ -290,7 +290,7 @@ TEST_CASE("NETCONF notification streams")
|
||||
SEND_NOTIFICATION(notificationsJSON[4]);
|
||||
|
||||
oldNotificationsDone.count_down();
|
||||
- requestSent.wait();
|
||||
+ WAIT_UNTIL_SSE_CLIENT_REQUESTS;
|
||||
waitForCompletionAndBitMore(seqMod1);
|
||||
waitForCompletionAndBitMore(seqMod2);
|
||||
}));
|
||||
diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp
|
||||
index 8252bba..c3ff1de 100644
|
||||
--- a/tests/restconf_utils.cpp
|
||||
+++ b/tests/restconf_utils.cpp
|
||||
@@ -164,7 +164,7 @@ SSEClient::SSEClient(
|
||||
boost::asio::io_service& io,
|
||||
const std::string& server_address,
|
||||
const std::string& server_port,
|
||||
- std::latch& requestSent,
|
||||
+ std::binary_semaphore& requestSent,
|
||||
const RestconfNotificationWatcher& notification,
|
||||
const std::string& uri,
|
||||
const std::map<std::string, std::string>& headers,
|
||||
@@ -192,7 +192,7 @@ SSEClient::SSEClient(
|
||||
|
||||
auto req = client->submit(ec, "GET", serverAddressAndPort(server_address, server_port) + uri, "", reqHeaders);
|
||||
req->on_response([&, silenceTimeout](const ng_client::response& res) {
|
||||
- requestSent.count_down();
|
||||
+ requestSent.release();
|
||||
res.on_data([&, silenceTimeout](const uint8_t* data, std::size_t len) {
|
||||
// not a production-ready code. In real-life condition the data received in one callback might probably be incomplete
|
||||
for (const auto& event : parseEvents(std::string(reinterpret_cast<const char*>(data), len))) {
|
||||
diff --git a/tests/restconf_utils.h b/tests/restconf_utils.h
|
||||
index 8b7386e..9efe398 100644
|
||||
--- a/tests/restconf_utils.h
|
||||
+++ b/tests/restconf_utils.h
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
#pragma once
|
||||
#include "trompeloeil_doctest.h"
|
||||
-#include <latch>
|
||||
#include <nghttp2/asio_http2_client.h>
|
||||
+#include <semaphore>
|
||||
#include "event_watchers.h"
|
||||
#include "UniqueResource.h"
|
||||
|
||||
@@ -92,7 +92,7 @@ struct SSEClient {
|
||||
boost::asio::io_service& io,
|
||||
const std::string& server_address,
|
||||
const std::string& server_port,
|
||||
- std::latch& requestSent,
|
||||
+ std::binary_semaphore& requestSent,
|
||||
const RestconfNotificationWatcher& notification,
|
||||
const std::string& uri,
|
||||
const std::map<std::string, std::string>& headers,
|
||||
@@ -104,7 +104,7 @@ struct SSEClient {
|
||||
#define PREPARE_LOOP_WITH_EXCEPTIONS \
|
||||
boost::asio::io_service io; \
|
||||
std::promise<void> bg; \
|
||||
- std::latch requestSent(1);
|
||||
+ std::binary_semaphore requestSent(0);
|
||||
|
||||
#define RUN_LOOP_WITH_EXCEPTIONS \
|
||||
do { \
|
||||
@@ -114,6 +114,8 @@ struct SSEClient {
|
||||
fut.get(); \
|
||||
} while (false)
|
||||
|
||||
+#define WAIT_UNTIL_SSE_CLIENT_REQUESTS requestSent.try_acquire_for(std::chrono::seconds(3))
|
||||
+
|
||||
inline auto wrap_exceptions_and_asio(std::promise<void>& bg, boost::asio::io_service& io, std::function<void()> func)
|
||||
{
|
||||
return [&bg, &io, func]()
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
From ed0ff23f7ad341d663484f0b2a617cd3bc4923c8 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 19:27:49 +0100
|
||||
Subject: [PATCH 20/20] restconf: make as_restconf_notification reusable
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
The YANG-PUSH notifications are supposed to be wrapped in this too, so
|
||||
this needs to be accessible from multiple places.
|
||||
|
||||
Change-Id: Icf25caf5f3be3917c524bf6111b5d92db6e287b0
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
src/restconf/NotificationStream.cpp | 37 +--------------------------
|
||||
src/restconf/utils/yang.cpp | 39 +++++++++++++++++++++++++++++
|
||||
src/restconf/utils/yang.h | 4 +++
|
||||
3 files changed, 44 insertions(+), 36 deletions(-)
|
||||
|
||||
diff --git a/src/restconf/NotificationStream.cpp b/src/restconf/NotificationStream.cpp
|
||||
index 001d19c..eeddc04 100644
|
||||
--- a/src/restconf/NotificationStream.cpp
|
||||
+++ b/src/restconf/NotificationStream.cpp
|
||||
@@ -20,41 +20,6 @@ namespace {
|
||||
|
||||
const auto streamListXPath = "/ietf-restconf-monitoring:restconf-state/streams/stream"s;
|
||||
|
||||
-/** @brief Wraps a notification data tree with RESTCONF notification envelope. */
|
||||
-std::string as_restconf_notification(const libyang::Context& ctx, libyang::DataFormat dataFormat, libyang::DataNode notification, const sysrepo::NotificationTimeStamp& time)
|
||||
-{
|
||||
- static const auto jsonNamespace = "ietf-restconf";
|
||||
- static const auto xmlNamespace = "urn:ietf:params:xml:ns:netconf:notification:1.0";
|
||||
-
|
||||
- std::optional<libyang::DataNode> envelope;
|
||||
- std::optional<libyang::DataNode> eventTime;
|
||||
- std::string timeStr = libyang::yangTimeFormat(time, libyang::TimezoneInterpretation::Local);
|
||||
-
|
||||
- /* The namespaces for XML and JSON envelopes are different. See https://datatracker.ietf.org/doc/html/rfc8040#section-6.4 */
|
||||
- if (dataFormat == libyang::DataFormat::JSON) {
|
||||
- envelope = ctx.newOpaqueJSON(jsonNamespace, "notification", std::nullopt);
|
||||
- eventTime = ctx.newOpaqueJSON(jsonNamespace, "eventTime", libyang::JSON{timeStr});
|
||||
- } else {
|
||||
- envelope = ctx.newOpaqueXML(xmlNamespace, "notification", std::nullopt);
|
||||
- eventTime = ctx.newOpaqueXML(xmlNamespace, "eventTime", libyang::XML{timeStr});
|
||||
- }
|
||||
-
|
||||
- // the notification data node holds only the notification data tree but for nested notification we should print the whole YANG data tree
|
||||
- while (notification.parent()) {
|
||||
- notification = *notification.parent();
|
||||
- }
|
||||
-
|
||||
- envelope->insertChild(*eventTime);
|
||||
- envelope->insertChild(notification);
|
||||
-
|
||||
- auto res = *envelope->printStr(dataFormat, libyang::PrintFlags::WithSiblings);
|
||||
-
|
||||
- // notification node comes from sysrepo and sysrepo will free this; if not unlinked then envelope destructor would try to free this as well
|
||||
- notification.unlink();
|
||||
-
|
||||
- return res;
|
||||
-}
|
||||
-
|
||||
void subscribe(
|
||||
std::optional<sysrepo::Subscription>& sub,
|
||||
sysrepo::Session& session,
|
||||
@@ -70,7 +35,7 @@ void subscribe(
|
||||
return;
|
||||
}
|
||||
|
||||
- signal(as_restconf_notification(session.getContext(), dataFormat, *notificationTree, time));
|
||||
+ signal(rousette::restconf::as_restconf_notification(session.getContext(), dataFormat, *notificationTree, time));
|
||||
};
|
||||
|
||||
if (!sub) {
|
||||
diff --git a/src/restconf/utils/yang.cpp b/src/restconf/utils/yang.cpp
|
||||
index 4c4d619..30661fc 100644
|
||||
--- a/src/restconf/utils/yang.cpp
|
||||
+++ b/src/restconf/utils/yang.cpp
|
||||
@@ -6,8 +6,11 @@
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
+#include <libyang-cpp/Context.hpp>
|
||||
#include <libyang-cpp/DataNode.hpp>
|
||||
#include <libyang-cpp/SchemaNode.hpp>
|
||||
+#include <libyang-cpp/Time.hpp>
|
||||
+#include <sysrepo-cpp/Subscription.hpp>
|
||||
|
||||
namespace rousette::restconf {
|
||||
|
||||
@@ -79,4 +82,40 @@ bool isKeyNode(const libyang::DataNode& maybeList, const libyang::DataNode& node
|
||||
}
|
||||
return false;
|
||||
}
|
||||
+
|
||||
+
|
||||
+/** @brief Wraps a notification data tree with RESTCONF notification envelope. */
|
||||
+std::string as_restconf_notification(const libyang::Context& ctx, libyang::DataFormat dataFormat, libyang::DataNode notification, const sysrepo::NotificationTimeStamp& time)
|
||||
+{
|
||||
+ static const auto jsonNamespace = "ietf-restconf";
|
||||
+ static const auto xmlNamespace = "urn:ietf:params:xml:ns:netconf:notification:1.0";
|
||||
+
|
||||
+ std::optional<libyang::DataNode> envelope;
|
||||
+ std::optional<libyang::DataNode> eventTime;
|
||||
+ std::string timeStr = libyang::yangTimeFormat(time, libyang::TimezoneInterpretation::Local);
|
||||
+
|
||||
+ /* The namespaces for XML and JSON envelopes are different. See https://datatracker.ietf.org/doc/html/rfc8040#section-6.4 */
|
||||
+ if (dataFormat == libyang::DataFormat::JSON) {
|
||||
+ envelope = ctx.newOpaqueJSON(jsonNamespace, "notification", std::nullopt);
|
||||
+ eventTime = ctx.newOpaqueJSON(jsonNamespace, "eventTime", libyang::JSON{timeStr});
|
||||
+ } else {
|
||||
+ envelope = ctx.newOpaqueXML(xmlNamespace, "notification", std::nullopt);
|
||||
+ eventTime = ctx.newOpaqueXML(xmlNamespace, "eventTime", libyang::XML{timeStr});
|
||||
+ }
|
||||
+
|
||||
+ // the notification data node holds only the notification data tree but for nested notification we should print the whole YANG data tree
|
||||
+ while (notification.parent()) {
|
||||
+ notification = *notification.parent();
|
||||
+ }
|
||||
+
|
||||
+ envelope->insertChild(*eventTime);
|
||||
+ envelope->insertChild(notification);
|
||||
+
|
||||
+ auto res = *envelope->printStr(dataFormat, libyang::PrintFlags::WithSiblings);
|
||||
+
|
||||
+ // notification node comes from sysrepo and sysrepo will free this; if not unlinked then envelope destructor would try to free this as well
|
||||
+ notification.unlink();
|
||||
+
|
||||
+ return res;
|
||||
+}
|
||||
}
|
||||
diff --git a/src/restconf/utils/yang.h b/src/restconf/utils/yang.h
|
||||
index e91ba8a..a558eae 100644
|
||||
--- a/src/restconf/utils/yang.h
|
||||
+++ b/src/restconf/utils/yang.h
|
||||
@@ -6,10 +6,13 @@
|
||||
*/
|
||||
|
||||
#include <chrono>
|
||||
+#include <sysrepo-cpp/Subscription.hpp>
|
||||
|
||||
namespace libyang {
|
||||
class Leaf;
|
||||
class DataNode;
|
||||
+class Context;
|
||||
+enum class DataFormat;
|
||||
}
|
||||
|
||||
namespace rousette::restconf {
|
||||
@@ -19,4 +22,5 @@ std::string listKeyPredicate(const std::vector<libyang::Leaf>& listKeyLeafs, con
|
||||
std::string leaflistKeyPredicate(const std::string& keyValue);
|
||||
bool isUserOrderedList(const libyang::DataNode& node);
|
||||
bool isKeyNode(const libyang::DataNode& maybeList, const libyang::DataNode& node);
|
||||
+std::string as_restconf_notification(const libyang::Context& ctx, libyang::DataFormat dataFormat, libyang::DataNode notification, const sysrepo::NotificationTimeStamp& time);
|
||||
}
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
From f7307481fc4ca167592acf925136e5f8a51e2150 Mon Sep 17 00:00:00 2001
|
||||
From: Jan Kundrát <jan.kundrat@cesnet.cz>
|
||||
Date: Wed, 18 Dec 2024 17:26:15 +0100
|
||||
Subject: [PATCH] Update dependencies
|
||||
|
||||
Depends-on: https://gerrit.cesnet.cz/c/CzechLight/dependencies/+/8134
|
||||
Change-Id: Ib170aa61600ffb9089460247f762ff5a947dd6c6
|
||||
---
|
||||
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index 22bce32..5ae8062 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -74,7 +74,7 @@
|
||||
pkg_check_modules(nghttp2 REQUIRED IMPORTED_TARGET libnghttp2_asio>=0.0.90 libnghttp2)
|
||||
find_package(Boost REQUIRED CONFIG COMPONENTS system thread)
|
||||
|
||||
-pkg_check_modules(SYSREPO-CPP REQUIRED IMPORTED_TARGET sysrepo-cpp>=3)
|
||||
+pkg_check_modules(SYSREPO-CPP REQUIRED IMPORTED_TARGET sysrepo-cpp>=4)
|
||||
pkg_check_modules(LIBYANG-CPP REQUIRED IMPORTED_TARGET libyang-cpp>=3)
|
||||
pkg_check_modules(SYSTEMD IMPORTED_TARGET libsystemd)
|
||||
pkg_check_modules(PAM REQUIRED IMPORTED_TARGET pam)
|
||||
diff --git a/tests/restconf-writing.cpp b/tests/restconf-writing.cpp
|
||||
index 0932984..d37ab03 100644
|
||||
--- a/tests/restconf-writing.cpp
|
||||
+++ b/tests/restconf-writing.cpp
|
||||
@@ -772,7 +772,7 @@
|
||||
{
|
||||
"error-type": "protocol",
|
||||
"error-tag": "invalid-value",
|
||||
- "error-message": "Session::applyChanges: Couldn't apply changes: SR_ERR_NOT_FOUND\u000A Node \"lst\" instance to insert next to not found. (SR_ERR_NOT_FOUND)\u000A Applying operation \"replace\" failed. (SR_ERR_NOT_FOUND)"
|
||||
+ "error-message": "Session::applyChanges: Couldn't apply changes: SR_ERR_NOT_FOUND\u000A Node \"lst\" instance to insert next to not found. (SR_ERR_NOT_FOUND)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -855,7 +855,7 @@
|
||||
{
|
||||
"error-type": "protocol",
|
||||
"error-tag": "invalid-value",
|
||||
- "error-message": "Session::applyChanges: Couldn't apply changes: SR_ERR_NOT_FOUND\u000A Node \"ll\" instance to insert next to not found. (SR_ERR_NOT_FOUND)\u000A Applying operation \"replace\" failed. (SR_ERR_NOT_FOUND)"
|
||||
+ "error-message": "Session::applyChanges: Couldn't apply changes: SR_ERR_NOT_FOUND\u000A Node \"ll\" instance to insert next to not found. (SR_ERR_NOT_FOUND)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1560,7 +1560,7 @@
|
||||
{
|
||||
"error-type": "protocol",
|
||||
"error-tag": "invalid-value",
|
||||
- "error-message": "Session::applyChanges: Couldn't apply changes: SR_ERR_NOT_FOUND\u000A Node \"lst\" instance to insert next to not found. (SR_ERR_NOT_FOUND)\u000A Applying operation \"create\" failed. (SR_ERR_NOT_FOUND)"
|
||||
+ "error-message": "Session::applyChanges: Couldn't apply changes: SR_ERR_NOT_FOUND\u000A Node \"lst\" instance to insert next to not found. (SR_ERR_NOT_FOUND)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1626,7 +1626,7 @@
|
||||
{
|
||||
"error-type": "protocol",
|
||||
"error-tag": "invalid-value",
|
||||
- "error-message": "Session::applyChanges: Couldn't apply changes: SR_ERR_NOT_FOUND\u000A Node \"ll\" instance to insert next to not found. (SR_ERR_NOT_FOUND)\u000A Applying operation \"create\" failed. (SR_ERR_NOT_FOUND)"
|
||||
+ "error-message": "Session::applyChanges: Couldn't apply changes: SR_ERR_NOT_FOUND\u000A Node \"ll\" instance to insert next to not found. (SR_ERR_NOT_FOUND)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -146,15 +146,6 @@ endef
|
||||
SKELETON_INIT_FINIT_POST_INSTALL_TARGET_HOOKS += SKELETON_INIT_FINIT_SET_MINI_SNMPD
|
||||
endif
|
||||
|
||||
# OpenSSH
|
||||
ifeq ($(BR2_PACKAGE_OPENSSH),y)
|
||||
define SKELETON_INIT_FINIT_SET_OPENSSH
|
||||
cp $(SKELETON_INIT_FINIT_AVAILABLE)/sshd.conf $(FINIT_D)/available/
|
||||
ln -sf ../available/sshd.conf $(FINIT_D)/enabled/sshd.conf
|
||||
endef
|
||||
SKELETON_INIT_FINIT_POST_INSTALL_TARGET_HOOKS += SKELETON_INIT_FINIT_SET_OPENSSH
|
||||
endif
|
||||
|
||||
ifeq ($(BR2_PACKAGE_QUAGGA),y)
|
||||
define SKELETON_INIT_FINIT_SET_QUAGGA
|
||||
cp $(SKELETON_INIT_FINIT_AVAILABLE)/quagga/zebra.conf $(FINIT_D)/available/
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Daemon is managed by finit(1), /sbin/bridge-stp should assume that
|
||||
# it is already running and simply add/remove bridges as necessary
|
||||
MANAGE_MSTPD='n'
|
||||
@@ -0,0 +1 @@
|
||||
AVAHI_ARGS="-l notice"
|
||||
@@ -0,0 +1,3 @@
|
||||
# -d: Do _not_ [sic] daemonize
|
||||
# -s: Log to syslog
|
||||
MSTPD_ARGS="-d -s"
|
||||
@@ -1 +1,2 @@
|
||||
service [2345] name:mdns avahi-daemon -s -l notice -- Avahi mDNS-SD daemon
|
||||
service <!> name:mdns env:-/etc/default/avahi \
|
||||
[2345] avahi-daemon -s $AVAHI_ARGS -- Avahi mDNS-SD daemon
|
||||
|
||||
@@ -1 +1 @@
|
||||
service [S12345] <pid/syslogd> dnsmasq -k -u root -- DHCP/DNS proxy
|
||||
service [S12345] <!pid/syslogd> dnsmasq -k -u root -- DHCP/DNS proxy
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# <!>: dropbear does not honor SIGHUP.
|
||||
# -R: Create hostkeys if needed.
|
||||
# -F: Run in foreground.
|
||||
service [2345] <!> env:-/etc/default/dropbear dropbear -F -R $DROPBEAR_ARGS -- Dropbear SSH daemon
|
||||
service <!> env:-/etc/default/dropbear \
|
||||
[2345] dropbear -F -R $DROPBEAR_ARGS -- Dropbear SSH daemon
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
service [2345] env:-/etc/default/lldpd lldpd -d $LLDPD_ARGS -- LLDP daemon (IEEE 802.1ab)
|
||||
service <!> env:-/etc/default/lldpd \
|
||||
[2345] lldpd -d $LLDPD_ARGS -- LLDP daemon (IEEE 802.1ab)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Make sure to configure the bridge to run on before starting mstpd
|
||||
# Note: all 'sysv' type services are handed an extra 'start' or 'stop'
|
||||
# argument when starting and stopping.
|
||||
sysv name:mstpd [0123456] pid:!/run/mstpd.pid bridge-stp br0 -- MSTP daemon
|
||||
# A single mstpd instance can manage multiple bridges, which are
|
||||
# dynamically added/removed by the kernel via the /sbin/bridge-stp
|
||||
# usermode helper.
|
||||
service [S0123456789] env:-/etc/default/mstpd mstpd $MSTPD_ARGS -- Spanning Tree daemon
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
service [2345] <usr/mkcert> env:-/etc/default/nginx nginx -g 'daemon off;' $NGINX_ARGS -- Web server
|
||||
service <usr/mkcert> env:-/etc/default/nginx \
|
||||
[2345] nginx -g 'daemon off;' $NGINX_ARGS -- Web server
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
task <pid/syslogd> \
|
||||
[S] /usr/bin/ssh-hostkeys -- Verifying SSH host keys
|
||||
service <task/ssh-hostkeys/success> env:-/etc/default/sshd \
|
||||
service <pid/syslogd> env:-/etc/default/sshd \
|
||||
[2345] /usr/sbin/sshd -D $SSHD_OPTS -- OpenSSH daemon
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
From 23fa270a747c35d65aeeba691599ed6b21b501f0 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
|
||||
Date: Tue, 29 Oct 2024 15:15:52 +0100
|
||||
Subject: [PATCH 1/6] error handling changes in upstream sysrepo
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
Upstream commit 396e331e4634a417420a71ad723567b42d75c443 removed these
|
||||
extra error entries.
|
||||
|
||||
Change-Id: Ifeda21194db7b7f7fdae8b6ae13c1e2d1f6b8d3d
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
CMakeLists.txt | 2 +-
|
||||
tests/subscriptions.cpp | 16 ++++++----------
|
||||
2 files changed, 7 insertions(+), 11 deletions(-)
|
||||
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index eb5f900..1eb625f 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -28,7 +28,7 @@ option(WITH_EXAMPLES "Build examples" ON)
|
||||
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(LIBYANG_CPP REQUIRED libyang-cpp>=3 IMPORTED_TARGET)
|
||||
-pkg_check_modules(SYSREPO REQUIRED sysrepo>=2.11.7 IMPORTED_TARGET)
|
||||
+pkg_check_modules(SYSREPO REQUIRED sysrepo>=2.12.0 IMPORTED_TARGET)
|
||||
set(SYSREPO_CPP_PKG_VERSION "3")
|
||||
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
diff --git a/tests/subscriptions.cpp b/tests/subscriptions.cpp
|
||||
index 0e14fc0..36fd61c 100644
|
||||
--- a/tests/subscriptions.cpp
|
||||
+++ b/tests/subscriptions.cpp
|
||||
@@ -494,11 +494,11 @@ TEST_CASE("subscriptions")
|
||||
sess.applyChanges();
|
||||
} catch (const sysrepo::ErrorWithCode&) {
|
||||
auto errors = sess.getErrors();
|
||||
- REQUIRE(errors.size() == 2);
|
||||
- REQUIRE(errors.at(0).errorMessage == message);
|
||||
- REQUIRE(errors.at(0).code == sysrepo::ErrorCode::OperationFailed);
|
||||
- REQUIRE(errors.at(1).errorMessage == "User callback failed.");
|
||||
- REQUIRE(errors.at(1).code == sysrepo::ErrorCode::CallbackFailed);
|
||||
+ REQUIRE(errors.size() == 1);
|
||||
+ REQUIRE(errors.at(0) == sysrepo::ErrorInfo{
|
||||
+ .code = sysrepo::ErrorCode::OperationFailed,
|
||||
+ .errorMessage = message
|
||||
+ });
|
||||
}
|
||||
|
||||
// The callback does not fail the second time.
|
||||
@@ -548,15 +548,11 @@ TEST_CASE("subscriptions")
|
||||
sess.setItem("/test_module:leafInt32", "123");
|
||||
REQUIRE_THROWS_AS(sess.applyChanges(), sysrepo::ErrorWithCode);
|
||||
auto errors = sess.getErrors();
|
||||
- REQUIRE(errors.size() == 2);
|
||||
+ REQUIRE(errors.size() == 1);
|
||||
REQUIRE(errors.at(0) == sysrepo::ErrorInfo{
|
||||
.code = sysrepo::ErrorCode::OperationFailed,
|
||||
.errorMessage = "Test callback failure.",
|
||||
});
|
||||
- REQUIRE(errors.at(1) == sysrepo::ErrorInfo{
|
||||
- .code = sysrepo::ErrorCode::CallbackFailed,
|
||||
- .errorMessage = "User callback failed."
|
||||
- });
|
||||
auto ncErrors = sess.getNetconfErrors();
|
||||
REQUIRE(ncErrors.size() == 1);
|
||||
REQUIRE(ncErrors.front() == errToSet);
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
From a73ffe1a5bbab51a67cf56dd2864c71a29c6685b 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:18:25 +0100
|
||||
Subject: [PATCH 2/6] Fix error message when sr_session_start fails
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
Thanks to Jason Patterson for sending a bugreport; its investigation led
|
||||
to discovering this bug.
|
||||
|
||||
See-also: https://github.com/CESNET/rousette/issues/15
|
||||
Change-Id: I082987d60380f860e08f26b436d0f9db4a231bec
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
src/Connection.cpp | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/Connection.cpp b/src/Connection.cpp
|
||||
index de65d64..716fd1a 100644
|
||||
--- a/src/Connection.cpp
|
||||
+++ b/src/Connection.cpp
|
||||
@@ -53,7 +53,7 @@ Session Connection::sessionStart(sysrepo::Datastore datastore)
|
||||
sr_session_ctx_t* sess;
|
||||
auto res = sr_session_start(ctx.get(), toDatastore(datastore), &sess);
|
||||
|
||||
- throwIfError(res, "Couldn't connect to sysrepo");
|
||||
+ throwIfError(res, "Couldn't start sysrepo session");
|
||||
return Session{sess, ctx};
|
||||
}
|
||||
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
From 552a366990fe1fe2cdd8d155fa5cecb3ff3fbc13 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
|
||||
Date: Wed, 4 Dec 2024 15:51:41 +0100
|
||||
Subject: [PATCH 3/6] We don't support sysrepo v3 yet
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
That version introduced some backward-incompatible changes to the way
|
||||
how operational data are treated, especially in presence of "discards"
|
||||
of this data. It's a breaking behavior, and before we release a fix,
|
||||
let's pin the version to something which still works. The latest sysrepo
|
||||
semi-release is v 2.12.0, which is unfortunately not tagged.
|
||||
|
||||
Change-Id: I3673de9893d6e806926767a72b1ef83ee62bd610
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
.zuul.yaml | 4 ++--
|
||||
CMakeLists.txt | 2 +-
|
||||
README.md | 1 +
|
||||
3 files changed, 4 insertions(+), 3 deletions(-)
|
||||
|
||||
diff --git a/.zuul.yaml b/.zuul.yaml
|
||||
index dc87c6f..c3aea22 100644
|
||||
--- a/.zuul.yaml
|
||||
+++ b/.zuul.yaml
|
||||
@@ -6,7 +6,7 @@
|
||||
- name: github/CESNET/libyang
|
||||
override-checkout: devel
|
||||
- name: github/sysrepo/sysrepo
|
||||
- override-checkout: devel
|
||||
+ override-checkout: cesnet/2024-10-before-oper-changes
|
||||
- name: github/onqtam/doctest
|
||||
override-checkout: v2.4.8
|
||||
- name: github/rollbear/trompeloeil
|
||||
@@ -17,7 +17,7 @@
|
||||
- name: github/CESNET/libyang
|
||||
override-checkout: devel
|
||||
- name: github/sysrepo/sysrepo
|
||||
- override-checkout: devel
|
||||
+ override-checkout: cesnet/2024-10-before-oper-changes
|
||||
- name: github/onqtam/doctest
|
||||
override-checkout: v2.4.11
|
||||
- name: github/rollbear/trompeloeil
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index 1eb625f..119bdd7 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -28,7 +28,7 @@ option(WITH_EXAMPLES "Build examples" ON)
|
||||
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(LIBYANG_CPP REQUIRED libyang-cpp>=3 IMPORTED_TARGET)
|
||||
-pkg_check_modules(SYSREPO REQUIRED sysrepo>=2.12.0 IMPORTED_TARGET)
|
||||
+pkg_check_modules(SYSREPO REQUIRED sysrepo>=2.12.0 sysrepo<3 IMPORTED_TARGET)
|
||||
set(SYSREPO_CPP_PKG_VERSION "3")
|
||||
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
diff --git a/README.md b/README.md
|
||||
index 27dc748..e14b772 100644
|
||||
--- a/README.md
|
||||
+++ b/README.md
|
||||
@@ -9,6 +9,7 @@ It uses RAII for automatic memory management.
|
||||
|
||||
## Dependencies
|
||||
- [sysrepo](https://github.com/sysrepo/sysrepo) - the `devel` branch (even for the `master` branch of *sysrepo-cpp*)
|
||||
+ - we temporarily require pre-v3 sysrepo which introduced backward-incompatible changes to operational data handling
|
||||
- [libyang-cpp](https://github.com/CESNET/libyang-cpp) - C++ bindings for *libyang*
|
||||
- C++20 compiler (e.g., GCC 10.x+, clang 10+)
|
||||
- CMake 3.19+
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
From f2db30721d909d127645721f4149de0cd36d2b1c Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
|
||||
Date: Tue, 17 Dec 2024 11:41:10 +0100
|
||||
Subject: [PATCH 4/6] refactor: don't use `module` as a variable name
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Addiva Elektronik
|
||||
|
||||
...to unify stuff with all other occurrences, and to not use that magic
|
||||
identifier.
|
||||
|
||||
Change-Id: I8c38777a925271e7659560d0380a7f7968f4cfa7
|
||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
||||
---
|
||||
include/sysrepo-cpp/Session.hpp | 4 ++--
|
||||
src/Session.cpp | 12 ++++++------
|
||||
2 files changed, 8 insertions(+), 8 deletions(-)
|
||||
|
||||
diff --git a/include/sysrepo-cpp/Session.hpp b/include/sysrepo-cpp/Session.hpp
|
||||
index 7da11cb..477e604 100644
|
||||
--- a/include/sysrepo-cpp/Session.hpp
|
||||
+++ b/include/sysrepo-cpp/Session.hpp
|
||||
@@ -95,7 +95,7 @@ public:
|
||||
void copyConfig(const Datastore source, const std::optional<std::string>& moduleName = std::nullopt, std::chrono::milliseconds timeout = std::chrono::milliseconds{0});
|
||||
libyang::DataNode sendRPC(libyang::DataNode input, std::chrono::milliseconds timeout = std::chrono::milliseconds{0});
|
||||
void sendNotification(libyang::DataNode notification, const Wait wait, std::chrono::milliseconds timeout = std::chrono::milliseconds{0});
|
||||
- void replaceConfig(std::optional<libyang::DataNode> config, const std::optional<std::string>& module = std::nullopt, std::chrono::milliseconds timeout = std::chrono::milliseconds{0});
|
||||
+ void replaceConfig(std::optional<libyang::DataNode> config, const std::optional<std::string>& moduleName = std::nullopt, std::chrono::milliseconds timeout = std::chrono::milliseconds{0});
|
||||
|
||||
void setNacmUser(const std::string& user);
|
||||
[[nodiscard]] Subscription initNacm(
|
||||
@@ -172,7 +172,7 @@ public:
|
||||
*
|
||||
* Wraps `sr_lock`.
|
||||
*/
|
||||
- explicit Lock(Session session, std::optional<std::string> module = std::nullopt, std::optional<std::chrono::milliseconds> timeout = std::nullopt);
|
||||
+ explicit Lock(Session session, std::optional<std::string> moduleName = std::nullopt, std::optional<std::chrono::milliseconds> timeout = std::nullopt);
|
||||
/** @brief Release the lock
|
||||
*
|
||||
* Wraps `sr_unlock`.
|
||||
diff --git a/src/Session.cpp b/src/Session.cpp
|
||||
index 2706e1c..94a0fad 100644
|
||||
--- a/src/Session.cpp
|
||||
+++ b/src/Session.cpp
|
||||
@@ -331,17 +331,17 @@ void Session::sendNotification(libyang::DataNode notification, const Wait wait,
|
||||
* Wraps `sr_replace_config`.
|
||||
*
|
||||
* @param config Libyang tree to use as a complete datastore content, or nullopt
|
||||
- * @param module If provided, a module name to limit the operation to
|
||||
+ * @param moduleName If provided, a module name to limit the operation to
|
||||
* @param timeout Optional timeout to wait for
|
||||
*/
|
||||
-void Session::replaceConfig(std::optional<libyang::DataNode> config, const std::optional<std::string>& module, std::chrono::milliseconds timeout)
|
||||
+void Session::replaceConfig(std::optional<libyang::DataNode> config, const std::optional<std::string>& moduleName, std::chrono::milliseconds timeout)
|
||||
{
|
||||
std::optional<libyang::DataNode> thrashable;
|
||||
if (config) {
|
||||
thrashable = config->duplicateWithSiblings(libyang::DuplicationOptions::Recursive | libyang::DuplicationOptions::WithParents);
|
||||
}
|
||||
auto res = sr_replace_config(
|
||||
- m_sess.get(), module ? module->c_str() : nullptr,
|
||||
+ m_sess.get(), moduleName ? moduleName->c_str() : nullptr,
|
||||
config ? libyang::releaseRawNode(*thrashable) : nullptr,
|
||||
timeout.count());
|
||||
throwIfError(res, "sr_replace_config failed", m_sess.get());
|
||||
@@ -723,12 +723,12 @@ uint32_t Session::getId() const
|
||||
return sr_session_get_id(m_sess.get());
|
||||
}
|
||||
|
||||
-Lock::Lock(Session session, std::optional<std::string> module, std::optional<std::chrono::milliseconds> timeout)
|
||||
+Lock::Lock(Session session, std::optional<std::string> moduleName, std::optional<std::chrono::milliseconds> timeout)
|
||||
: m_session(session)
|
||||
, m_lockedDs(m_session.activeDatastore())
|
||||
- , m_module(module)
|
||||
+ , m_module(moduleName)
|
||||
{
|
||||
- auto res = sr_lock(getRawSession(m_session), module ? module->c_str() : nullptr, timeout ? timeout->count() : 0);
|
||||
+ auto res = sr_lock(getRawSession(m_session), moduleName ? moduleName->c_str() : nullptr, timeout ? timeout->count() : 0);
|
||||
throwIfError(res, "Cannot lock session", getRawSession(m_session));
|
||||
}
|
||||
|
||||
--
|
||||
2.43.0
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user