mirror of
https://github.com/kernelkit/infix.git
synced 2026-08-01 13:23:01 +02:00
Merge pull request #1354 from kernelkit/srperms
This commit is contained in:
@@ -4,14 +4,14 @@ alias ll='ls -alF'
|
|||||||
alias ls='ls --color=auto'
|
alias ls='ls --color=auto'
|
||||||
|
|
||||||
export LANG=C.UTF-8
|
export LANG=C.UTF-8
|
||||||
export EDITOR=/usr/bin/edit
|
export EDITOR=/usr/bin/editor
|
||||||
export VISUAL=/usr/bin/edit
|
export VISUAL=/usr/bin/editor
|
||||||
export LESS="-P %f (press h for help or q to quit)"
|
export LESS="-P %f (press h for help or q to quit)"
|
||||||
export LESSOPEN="|/usr/bin/lesspipe.sh %s"
|
export LESSOPEN="|/usr/bin/lesspipe.sh %s"
|
||||||
alias vim='vi'
|
alias vim='vi'
|
||||||
alias view='vi -R'
|
alias view='vi -R'
|
||||||
alias emacs='mg'
|
alias emacs='mg'
|
||||||
alias sensible-editor=edit
|
alias sensible-editor=editor
|
||||||
alias sensible-pager=pager
|
alias sensible-pager=pager
|
||||||
alias hd="hexdump -C"
|
alias hd="hexdump -C"
|
||||||
|
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# User-friendly wrapper for sysrepocfg
|
|
||||||
# TODO: add import/export, copy, ...
|
|
||||||
|
|
||||||
# Edit YANG binary types using sysrepo, base64, and duct tape.
|
|
||||||
edit()
|
|
||||||
{
|
|
||||||
xpath=$1
|
|
||||||
if [ -z "$xpath" ]; then
|
|
||||||
echo "Usage: cfg edit \"/full/xpath/to/binary/leaf\""
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if tmp=$(sysrepocfg -G "$xpath"); then
|
|
||||||
file=$(mktemp)
|
|
||||||
|
|
||||||
echo "$tmp" | base64 -d > "$file"
|
|
||||||
if /usr/bin/editor "$file"; then
|
|
||||||
tmp=$(base64 -w0 < "$file")
|
|
||||||
sysrepocfg -S "$xpath" -u "$tmp"
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$file"
|
|
||||||
else
|
|
||||||
echo "Failed to retrieve value for $xpath"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
usage()
|
|
||||||
{
|
|
||||||
echo "Usage:"
|
|
||||||
echo " cfg CMD [ARG]"
|
|
||||||
echo
|
|
||||||
echo "Command:"
|
|
||||||
echo " edit XPATH Edit YANG binary type"
|
|
||||||
echo " help This help text"
|
|
||||||
echo
|
|
||||||
echo "As a backwards compatible fallback, this script forwards"
|
|
||||||
echo "all other commands as options to sysrepocfg."
|
|
||||||
echo
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd=$1; shift
|
|
||||||
case $cmd in
|
|
||||||
edit)
|
|
||||||
edit "$1"
|
|
||||||
;;
|
|
||||||
help)
|
|
||||||
usage
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
set -- "$cmd" "$@"
|
|
||||||
exec sysrepocfg -f json "$@"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/etc/alternatives/editor
|
|
||||||
@@ -91,7 +91,7 @@ EOF
|
|||||||
|
|
||||||
is_dhcp_running()
|
is_dhcp_running()
|
||||||
{
|
{
|
||||||
sysrepocfg -X -f json -m infix-dhcp-server | jq -r '
|
copy operational -x /infix-dhcp-server:dhcp-server | jq -r '
|
||||||
."infix-dhcp-server:dhcp-server".enabled as $global |
|
."infix-dhcp-server:dhcp-server".enabled as $global |
|
||||||
if ."infix-dhcp-server:dhcp-server".subnet? then
|
if ."infix-dhcp-server:dhcp-server".subnet? then
|
||||||
(."infix-dhcp-server:dhcp-server".subnet[] |
|
(."infix-dhcp-server:dhcp-server".subnet[] |
|
||||||
@@ -110,15 +110,15 @@ dhcp()
|
|||||||
|
|
||||||
case $1 in
|
case $1 in
|
||||||
detail)
|
detail)
|
||||||
sysrepocfg -f json -X -d operational -m infix-dhcp-server | \
|
copy operational -x /infix-dhcp-server:dhcp-server | \
|
||||||
jq -C .
|
jq -C .
|
||||||
;;
|
;;
|
||||||
stat*)
|
stat*)
|
||||||
sysrepocfg -f json -X -d operational -m infix-dhcp-server | \
|
copy operational -x /infix-dhcp-server:dhcp-server | \
|
||||||
/usr/libexec/statd/cli-pretty "show-dhcp-server" -s
|
/usr/libexec/statd/cli-pretty "show-dhcp-server" -s
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
sysrepocfg -f json -X -d operational -m infix-dhcp-server | \
|
copy operational -x /infix-dhcp-server:dhcp-server | \
|
||||||
/usr/libexec/statd/cli-pretty "show-dhcp-server"
|
/usr/libexec/statd/cli-pretty "show-dhcp-server"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -182,13 +182,13 @@ ifaces()
|
|||||||
else
|
else
|
||||||
if [ $# -gt 0 ]; then
|
if [ $# -gt 0 ]; then
|
||||||
for iface in $*; do
|
for iface in $*; do
|
||||||
sysrepocfg -f json -X -d operational -x \
|
copy operational -x \
|
||||||
"/ietf-interfaces:interfaces/interface[name='$iface']" | \
|
"/ietf-interfaces:interfaces/interface[name='$iface']" | \
|
||||||
/usr/libexec/statd/cli-pretty "show-interfaces" -n "$iface"
|
/usr/libexec/statd/cli-pretty "show-interfaces" -n "$iface"
|
||||||
done
|
done
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
sysrepocfg -f json -X -d operational -m ietf-interfaces | \
|
copy operational -x /ietf-interfaces:interfaces | \
|
||||||
/usr/libexec/statd/cli-pretty "show-interfaces"
|
/usr/libexec/statd/cli-pretty "show-interfaces"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -227,7 +227,7 @@ rstp()
|
|||||||
|
|
||||||
stp()
|
stp()
|
||||||
{
|
{
|
||||||
sysrepocfg -f json -X -d operational -m ietf-interfaces | \
|
copy operational -x /ietf-interfaces:interfaces | \
|
||||||
/usr/libexec/statd/cli-pretty "show-bridge-stp"
|
/usr/libexec/statd/cli-pretty "show-bridge-stp"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ routes()
|
|||||||
else
|
else
|
||||||
arg="-i ipv4"
|
arg="-i ipv4"
|
||||||
fi
|
fi
|
||||||
sysrepocfg -f json -X -d operational -x "/ietf-routing:routing/ribs" | \
|
copy operational -x /ietf-routing:routing/ribs | \
|
||||||
/usr/libexec/statd/cli-pretty "show-routing-table" $arg
|
/usr/libexec/statd/cli-pretty "show-routing-table" $arg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
Submodule buildroot updated: 4166c59afd...4a1f18fb9a
@@ -60,7 +60,7 @@ BR2_PACKAGE_LIBINPUT=y
|
|||||||
BR2_PACKAGE_LIBCURL_CURL=y
|
BR2_PACKAGE_LIBCURL_CURL=y
|
||||||
BR2_PACKAGE_NETOPEER2_CLI=y
|
BR2_PACKAGE_NETOPEER2_CLI=y
|
||||||
BR2_PACKAGE_NSS_MDNS=y
|
BR2_PACKAGE_NSS_MDNS=y
|
||||||
BR2_PACKAGE_SYSREPO_GROUP="sys-cli"
|
BR2_PACKAGE_SYSREPO_GROUP="sysrepo"
|
||||||
BR2_PACKAGE_LINUX_PAM=y
|
BR2_PACKAGE_LINUX_PAM=y
|
||||||
BR2_PACKAGE_ONIGURUMA=y
|
BR2_PACKAGE_ONIGURUMA=y
|
||||||
BR2_PACKAGE_AVAHI_DAEMON=y
|
BR2_PACKAGE_AVAHI_DAEMON=y
|
||||||
@@ -141,7 +141,6 @@ BR2_PACKAGE_LOWDOWN=y
|
|||||||
BR2_PACKAGE_MCD=y
|
BR2_PACKAGE_MCD=y
|
||||||
BR2_PACKAGE_MDNS_ALIAS=y
|
BR2_PACKAGE_MDNS_ALIAS=y
|
||||||
BR2_PACKAGE_ONIEPROM=y
|
BR2_PACKAGE_ONIEPROM=y
|
||||||
BR2_PACKAGE_SHOW=y
|
|
||||||
BR2_PACKAGE_ROUSETTE=y
|
BR2_PACKAGE_ROUSETTE=y
|
||||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||||
IMAGE_ITB_AUX=y
|
IMAGE_ITB_AUX=y
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ BR2_PACKAGE_LIBINPUT=y
|
|||||||
BR2_PACKAGE_LIBCURL_CURL=y
|
BR2_PACKAGE_LIBCURL_CURL=y
|
||||||
BR2_PACKAGE_NETOPEER2_CLI=y
|
BR2_PACKAGE_NETOPEER2_CLI=y
|
||||||
BR2_PACKAGE_NSS_MDNS=y
|
BR2_PACKAGE_NSS_MDNS=y
|
||||||
BR2_PACKAGE_SYSREPO_GROUP="sys-cli"
|
BR2_PACKAGE_SYSREPO_GROUP="sysrepo"
|
||||||
BR2_PACKAGE_LINUX_PAM=y
|
BR2_PACKAGE_LINUX_PAM=y
|
||||||
BR2_PACKAGE_ONIGURUMA=y
|
BR2_PACKAGE_ONIGURUMA=y
|
||||||
BR2_PACKAGE_AVAHI_DAEMON=y
|
BR2_PACKAGE_AVAHI_DAEMON=y
|
||||||
@@ -141,7 +141,6 @@ BR2_PACKAGE_LOWDOWN=y
|
|||||||
BR2_PACKAGE_MCD=y
|
BR2_PACKAGE_MCD=y
|
||||||
BR2_PACKAGE_MDNS_ALIAS=y
|
BR2_PACKAGE_MDNS_ALIAS=y
|
||||||
BR2_PACKAGE_ONIEPROM=y
|
BR2_PACKAGE_ONIEPROM=y
|
||||||
BR2_PACKAGE_SHOW=y
|
|
||||||
BR2_PACKAGE_ROUSETTE=y
|
BR2_PACKAGE_ROUSETTE=y
|
||||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||||
IMAGE_ITB_AUX=y
|
IMAGE_ITB_AUX=y
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ BR2_PACKAGE_LIBINPUT=y
|
|||||||
BR2_PACKAGE_LIBCURL_CURL=y
|
BR2_PACKAGE_LIBCURL_CURL=y
|
||||||
BR2_PACKAGE_NETOPEER2_CLI=y
|
BR2_PACKAGE_NETOPEER2_CLI=y
|
||||||
BR2_PACKAGE_NSS_MDNS=y
|
BR2_PACKAGE_NSS_MDNS=y
|
||||||
BR2_PACKAGE_SYSREPO_GROUP="sys-cli"
|
BR2_PACKAGE_SYSREPO_GROUP="sysrepo"
|
||||||
BR2_PACKAGE_LINUX_PAM=y
|
BR2_PACKAGE_LINUX_PAM=y
|
||||||
BR2_PACKAGE_LIBPAM_RADIUS_AUTH=y
|
BR2_PACKAGE_LIBPAM_RADIUS_AUTH=y
|
||||||
BR2_PACKAGE_ONIGURUMA=y
|
BR2_PACKAGE_ONIGURUMA=y
|
||||||
@@ -178,7 +178,6 @@ BR2_PACKAGE_PODMAN=y
|
|||||||
BR2_PACKAGE_PODMAN_DRIVER_BTRFS=y
|
BR2_PACKAGE_PODMAN_DRIVER_BTRFS=y
|
||||||
BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y
|
BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y
|
||||||
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
||||||
BR2_PACKAGE_SHOW=y
|
|
||||||
BR2_PACKAGE_TETRIS=y
|
BR2_PACKAGE_TETRIS=y
|
||||||
BR2_PACKAGE_ROUSETTE=y
|
BR2_PACKAGE_ROUSETTE=y
|
||||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ BR2_PACKAGE_BUSYBOX_CONFIG="${BR2_EXTERNAL_INFIX_PATH}/board/common/busybox_defc
|
|||||||
BR2_PACKAGE_ODHCP6C=y
|
BR2_PACKAGE_ODHCP6C=y
|
||||||
BR2_PACKAGE_STRACE=y
|
BR2_PACKAGE_STRACE=y
|
||||||
BR2_PACKAGE_STRESS_NG=y
|
BR2_PACKAGE_STRESS_NG=y
|
||||||
BR2_PACKAGE_SYSREPO_GROUP="sys-cli"
|
BR2_PACKAGE_SYSREPO_GROUP="sysrepo"
|
||||||
BR2_PACKAGE_JQ=y
|
BR2_PACKAGE_JQ=y
|
||||||
BR2_PACKAGE_E2FSPROGS=y
|
BR2_PACKAGE_E2FSPROGS=y
|
||||||
BR2_PACKAGE_DBUS_CXX=y
|
BR2_PACKAGE_DBUS_CXX=y
|
||||||
@@ -128,7 +128,6 @@ BR2_PACKAGE_CONFD=y
|
|||||||
BR2_PACKAGE_CONFD_TEST_MODE=y
|
BR2_PACKAGE_CONFD_TEST_MODE=y
|
||||||
BR2_PACKAGE_GENCERT=y
|
BR2_PACKAGE_GENCERT=y
|
||||||
BR2_PACKAGE_STATD=y
|
BR2_PACKAGE_STATD=y
|
||||||
BR2_PACKAGE_SHOW=y
|
|
||||||
BR2_PACKAGE_FACTORY=y
|
BR2_PACKAGE_FACTORY=y
|
||||||
BR2_PACKAGE_FINIT_PLUGIN_HOTPLUG=y
|
BR2_PACKAGE_FINIT_PLUGIN_HOTPLUG=y
|
||||||
BR2_PACKAGE_FINIT_PLUGIN_HOOK_SCRIPTS=y
|
BR2_PACKAGE_FINIT_PLUGIN_HOOK_SCRIPTS=y
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ BR2_PACKAGE_LIBINPUT=y
|
|||||||
BR2_PACKAGE_LIBCURL_CURL=y
|
BR2_PACKAGE_LIBCURL_CURL=y
|
||||||
BR2_PACKAGE_NETOPEER2_CLI=y
|
BR2_PACKAGE_NETOPEER2_CLI=y
|
||||||
BR2_PACKAGE_NSS_MDNS=y
|
BR2_PACKAGE_NSS_MDNS=y
|
||||||
BR2_PACKAGE_SYSREPO_GROUP="sys-cli"
|
BR2_PACKAGE_SYSREPO_GROUP="sysrepo"
|
||||||
BR2_PACKAGE_LINUX_PAM=y
|
BR2_PACKAGE_LINUX_PAM=y
|
||||||
BR2_PACKAGE_LIBPAM_RADIUS_AUTH=y
|
BR2_PACKAGE_LIBPAM_RADIUS_AUTH=y
|
||||||
BR2_PACKAGE_ONIGURUMA=y
|
BR2_PACKAGE_ONIGURUMA=y
|
||||||
@@ -203,7 +203,6 @@ BR2_PACKAGE_PODMAN=y
|
|||||||
BR2_PACKAGE_PODMAN_DRIVER_BTRFS=y
|
BR2_PACKAGE_PODMAN_DRIVER_BTRFS=y
|
||||||
BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y
|
BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y
|
||||||
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
||||||
BR2_PACKAGE_SHOW=y
|
|
||||||
BR2_PACKAGE_TETRIS=y
|
BR2_PACKAGE_TETRIS=y
|
||||||
BR2_PACKAGE_ROUSETTE=y
|
BR2_PACKAGE_ROUSETTE=y
|
||||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ BR2_PACKAGE_LIBOPENSSL_BIN=y
|
|||||||
BR2_PACKAGE_LIBCURL_CURL=y
|
BR2_PACKAGE_LIBCURL_CURL=y
|
||||||
BR2_PACKAGE_NETOPEER2_CLI=y
|
BR2_PACKAGE_NETOPEER2_CLI=y
|
||||||
BR2_PACKAGE_NSS_MDNS=y
|
BR2_PACKAGE_NSS_MDNS=y
|
||||||
BR2_PACKAGE_SYSREPO_GROUP="sys-cli"
|
BR2_PACKAGE_SYSREPO_GROUP="sysrepo"
|
||||||
BR2_PACKAGE_LINUX_PAM=y
|
BR2_PACKAGE_LINUX_PAM=y
|
||||||
BR2_PACKAGE_LIBPAM_RADIUS_AUTH=y
|
BR2_PACKAGE_LIBPAM_RADIUS_AUTH=y
|
||||||
BR2_PACKAGE_ONIGURUMA=y
|
BR2_PACKAGE_ONIGURUMA=y
|
||||||
@@ -174,7 +174,6 @@ BR2_PACKAGE_PODMAN=y
|
|||||||
BR2_PACKAGE_PODMAN_DRIVER_BTRFS=y
|
BR2_PACKAGE_PODMAN_DRIVER_BTRFS=y
|
||||||
BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y
|
BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y
|
||||||
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
||||||
BR2_PACKAGE_SHOW=y
|
|
||||||
BR2_PACKAGE_TETRIS=y
|
BR2_PACKAGE_TETRIS=y
|
||||||
BR2_PACKAGE_ROUSETTE=y
|
BR2_PACKAGE_ROUSETTE=y
|
||||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ BR2_PACKAGE_BUSYBOX_CONFIG="${BR2_EXTERNAL_INFIX_PATH}/board/common/busybox_defc
|
|||||||
BR2_PACKAGE_ODHCP6C=y
|
BR2_PACKAGE_ODHCP6C=y
|
||||||
BR2_PACKAGE_STRACE=y
|
BR2_PACKAGE_STRACE=y
|
||||||
BR2_PACKAGE_STRESS_NG=y
|
BR2_PACKAGE_STRESS_NG=y
|
||||||
BR2_PACKAGE_SYSREPO_GROUP="sys-cli"
|
BR2_PACKAGE_SYSREPO_GROUP="sysrepo"
|
||||||
BR2_PACKAGE_JQ=y
|
BR2_PACKAGE_JQ=y
|
||||||
BR2_PACKAGE_E2FSPROGS=y
|
BR2_PACKAGE_E2FSPROGS=y
|
||||||
BR2_PACKAGE_DBUS_CXX=y
|
BR2_PACKAGE_DBUS_CXX=y
|
||||||
@@ -143,7 +143,6 @@ BR2_PACKAGE_LOWDOWN=y
|
|||||||
BR2_PACKAGE_MCD=y
|
BR2_PACKAGE_MCD=y
|
||||||
BR2_PACKAGE_MDNS_ALIAS=y
|
BR2_PACKAGE_MDNS_ALIAS=y
|
||||||
BR2_PACKAGE_ONIEPROM=y
|
BR2_PACKAGE_ONIEPROM=y
|
||||||
BR2_PACKAGE_SHOW=y
|
|
||||||
BR2_PACKAGE_ROUSETTE=y
|
BR2_PACKAGE_ROUSETTE=y
|
||||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||||
IMAGE_ITB_AUX=y
|
IMAGE_ITB_AUX=y
|
||||||
|
|||||||
+37
-28
@@ -6,35 +6,29 @@ All notable changes to the project are documented in this file.
|
|||||||
[v26.01.0][UNRELEASED]
|
[v26.01.0][UNRELEASED]
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
> [!WARNING]
|
> [!IMPORTANT]
|
||||||
> **BREAKING CHANGES:** This release includes breaking changes to WiFi configuration:
|
> This release includes **breaking changes** to WiFi configuration that will
|
||||||
|
> result in existing configuration being disabled:
|
||||||
>
|
>
|
||||||
> - WiFi station/client configuration has been restructured. The `wifi` container
|
> - WiFi station/client configuration has been restructured. The `wifi` container
|
||||||
> now requires a `radio` reference, and station configuration has moved under a
|
> now requires a `radio` reference, and station configuration has moved under a
|
||||||
> `wifi/station` container. Existing WiFi configurations must be manually updated.
|
> `wifi/station` container. Existing WiFi configurations must be manually updated
|
||||||
> - WiFi radios are now configured via `ietf-hardware` instead of the interfaces module.
|
> - WiFi radios are now configured via `ietf-hardware` instead of the interfaces module
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> **Users of Raspberry Pi must upgrade the bootloader of the system**
|
|
||||||
> the easieast way to do it is to backup your startup-config.cfg and
|
|
||||||
> reflash the SD card with a new [sd card image](https://github.com/kernelkit/infix/releases/download/latest-boot/infix-rpi64-sdcard.img)
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Noteworthy changes and additions in this release:
|
|
||||||
>
|
>
|
||||||
> - WiFi Access Point (AP) mode support with multi-SSID capability
|
> Also, **Raspberry Pi users must upgrade the bootloader** before upgrading to
|
||||||
> - RIPv2 routing support
|
> this release. We recommend backing up your `startup-config.cfg` and reflash
|
||||||
> - WireGuard support
|
> the SD card with a new [sd card image][].
|
||||||
> - New major version of linux kernel 6.18, previous 6.12
|
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Upgrade Linux kernel to 6.18.6 (LTS)
|
Noteworthy changes and additions in this release are marked below in bold text.
|
||||||
|
|
||||||
|
- Upgrade **Linux kernel to 6.18.6** (LTS)
|
||||||
- Upgrade libyang to 4.2.2
|
- Upgrade libyang to 4.2.2
|
||||||
- Upgrade sysrepo to 4.2.10
|
- Upgrade sysrepo to 4.2.10
|
||||||
- Upgrade netopeer2 (NETCONF) to 2.7.0
|
- Upgrade netopeer2 (NETCONF) to 2.7.0
|
||||||
- Add RIPv2 routing support, issue #582
|
- Add **RIPv2 routing support**, issue #582
|
||||||
- Add NTP server support, issue #904
|
- Add **NTP server support**, issue #904
|
||||||
- Migrate DHCPv6 client to odhcp6c for improved Router Advertisement
|
- Migrate DHCPv6 client to odhcp6c for improved Router Advertisement
|
||||||
integration. Adds support for hybrid RA+DHCPv6 deployments where SLAAC
|
integration. Adds support for hybrid RA+DHCPv6 deployments where SLAAC
|
||||||
assigns addresses and DHCPv6 provides DNS (common ISP scenario)
|
assigns addresses and DHCPv6 provides DNS (common ISP scenario)
|
||||||
@@ -51,21 +45,36 @@ All notable changes to the project are documented in this file.
|
|||||||
policy, keeping snapshots from every 5 minutes (recent) to yearly (historical)
|
policy, keeping snapshots from every 5 minutes (recent) to yearly (historical)
|
||||||
- Add support data collection script, useful when troubleshooting issues on
|
- Add support data collection script, useful when troubleshooting issues on
|
||||||
deployed systems. Gathers system information, logs, and more. Issue #1287
|
deployed systems. Gathers system information, logs, and more. Issue #1287
|
||||||
- Add WiFi Access Point (AP) mode with multi-SSID support and WPA2/WPA3 security.
|
- Add **WiFi Access Point (AP) mode with multi-SSID support and WPA2/WPA3**
|
||||||
**BREAKING:** WiFi architecture refactored with radios configured via
|
security. **BREAKING:** WiFi architecture refactored with radios configured
|
||||||
`ietf-hardware` and interfaces requiring `radio` reference. Station config
|
via `ietf-hardware` and interfaces requiring `radio` reference. Station
|
||||||
moved to `wifi/station` container. Existing Wi-Fi interfaces will be
|
config moved to `wifi/station` container. Existing Wi-Fi interfaces will be
|
||||||
removed during upgrade (for the rest of the configuration to apply)
|
removed during upgrade (for the rest of the configuration to apply) and you
|
||||||
and you need to reconfigure them again. See [wifi.md](wifi.md) for details
|
need to reconfigure them again. See the [WiFi][] documentation for details
|
||||||
- Add support for WireGuard VPN tunnels.
|
- Add support for **WireGuard VPN tunnels**.
|
||||||
|
- New default NACM privilege levels (user levels) in `factory-config`:
|
||||||
|
`operator` (network & container manager) and `guest` (read-only). For
|
||||||
|
details, see the updated system configuration documentation, as well as a
|
||||||
|
new dedicated NACM configuration guide
|
||||||
|
- New `show nacm` admin-exec command to inspect access control rules
|
||||||
|
- CLI now uses `copy` and `rpc` tools instead of deprecated `sysrepocfg`. The
|
||||||
|
latter now also require the use of `sudo` for `admin` level users
|
||||||
|
- Enhanced `copy` command with XPath filtering support
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- Fix #1082: Wi-Fi interfaces always scanned, introduce a `scan-mode` to the
|
||||||
|
Wi-Fi concept in Infix
|
||||||
- Fix #1314: Raspberry Pi 4B with 1 or 8 GiB RAM does not boot. This was due
|
- Fix #1314: Raspberry Pi 4B with 1 or 8 GiB RAM does not boot. This was due
|
||||||
newer EEPROM firmware in newer boards require a newer rpi-firmware package
|
newer EEPROM firmware in newer boards require a newer rpi-firmware package
|
||||||
- Fix #1082: Wi-Fi interfaces always scanned, introduce a `scan-mode`
|
- Fix #1345: firewall not updating when interfaces become bridge/lag ports
|
||||||
to the Wi-Fi concept in Infix.
|
- Fix #1346: firewall complains in syslog, missing `/etc/firewalld/firewalld.conf`
|
||||||
|
- Fix default password hash in `do password encrypt` command. New hash is the
|
||||||
|
same as the more commonly used `change password` command, *yescrypt*
|
||||||
|
- Prevent MOTD from showing on non-shell user login attempts
|
||||||
|
|
||||||
|
[wifi]: https://kernelkit.org/infix/latest/wifi/
|
||||||
|
[sd card image]: https://github.com/kernelkit/infix/releases/download/latest-boot/infix-rpi64-sdcard.img
|
||||||
|
|
||||||
[v25.11.0][] - 2025-12-02
|
[v25.11.0][] - 2025-12-02
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|||||||
+1
-1
@@ -157,7 +157,7 @@ built-in help system can also be useful:
|
|||||||
<b>DESCRIPTION</b>
|
<b>DESCRIPTION</b>
|
||||||
Enable masquerading (SNAT) for traffic matching this policy.<br/>
|
Enable masquerading (SNAT) for traffic matching this policy.<br/>
|
||||||
Matching traffic will have their source IP address changed on egress,
|
Matching traffic will have their source IP address changed on egress,
|
||||||
using the IP address of the interface the traffic egresses.";<br/>
|
using the IP address of the interface the traffic egresses.<br/>
|
||||||
admin@example:/config/firewall/policy/lan-to-dmz/>
|
admin@example:/config/firewall/policy/lan-to-dmz/>
|
||||||
</pre></code>
|
</pre></code>
|
||||||
|
|
||||||
|
|||||||
+644
@@ -0,0 +1,644 @@
|
|||||||
|
# Network Access Control Model (NACM)
|
||||||
|
|
||||||
|
NETCONF Access Control Model ([RFC 8341][1]) provides fine-grained access
|
||||||
|
control for YANG data models. NACM controls who can read, write, and
|
||||||
|
execute operations on specific parts of the configuration and operational
|
||||||
|
state.
|
||||||
|
|
||||||
|
This document provides technical details about how NACM works, how rules
|
||||||
|
are evaluated, and best practices for creating custom access control
|
||||||
|
policies.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> For a practical introduction to user management and the built-in user
|
||||||
|
> levels (admin, operator, guest), see the [Multiple Users][2] section
|
||||||
|
> in the System Configuration guide.
|
||||||
|
|
||||||
|
|
||||||
|
## TL;DR - The Factory Defaults Work
|
||||||
|
|
||||||
|
**You don't need to understand NACM to use the system securely.**
|
||||||
|
|
||||||
|
The factory configuration implements a "permit by default, deny sensitive
|
||||||
|
items" policy that provides:
|
||||||
|
|
||||||
|
- **Immediate usability** - Operators can configure the entire system
|
||||||
|
without custom NACM rules
|
||||||
|
- **Automatic security** - Passwords, cryptographic keys, and dangerous
|
||||||
|
operations are protected out-of-the-box
|
||||||
|
- **Future-proof design** - New features work immediately without NACM
|
||||||
|
updates
|
||||||
|
|
||||||
|
The three built-in user levels (admin, operator, guest) cover most use
|
||||||
|
cases. Only read on if you need to create custom access control policies.
|
||||||
|
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
NACM provides three types of access control:
|
||||||
|
|
||||||
|
- **Data node access** - Control read/write access to configuration and state
|
||||||
|
- **Operation access** - Control execution of RPCs (remote procedure calls)
|
||||||
|
- **Notification access** - Control subscription to event notifications (not covered here)
|
||||||
|
|
||||||
|
Access is controlled through:
|
||||||
|
|
||||||
|
1. **Global defaults** - Default permissions for read/write/exec
|
||||||
|
2. **YANG-level annotations** - Security markers in YANG modules (see below)
|
||||||
|
3. **NACM rules** - Explicit permit/deny rules organized in rule-lists
|
||||||
|
|
||||||
|
|
||||||
|
## Rule Evaluation
|
||||||
|
|
||||||
|
NACM rules are evaluated in a specific order:
|
||||||
|
|
||||||
|
1. **Rule-lists are processed sequentially** in the order they appear in the configuration
|
||||||
|
2. **Within each rule-list**, rules are evaluated sequentially
|
||||||
|
3. **First matching rule wins** - no further rules are evaluated
|
||||||
|
4. **If no rule matches**, global defaults apply (read-default, write-default, exec-default)
|
||||||
|
5. **YANG annotations override everything** - `nacm:default-deny-all` in a YANG module requires an explicit permit rule regardless of global defaults
|
||||||
|
|
||||||
|
### Example Rule Evaluation
|
||||||
|
|
||||||
|
Given this configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"read-default": "permit",
|
||||||
|
"write-default": "permit",
|
||||||
|
"rule-list": [
|
||||||
|
{
|
||||||
|
"name": "operator-acl",
|
||||||
|
"group": ["operator"],
|
||||||
|
"rule": [
|
||||||
|
{
|
||||||
|
"name": "permit-system-rpcs",
|
||||||
|
"module-name": "ietf-system",
|
||||||
|
"access-operations": "exec",
|
||||||
|
"action": "permit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "default-deny-all",
|
||||||
|
"group": ["*"],
|
||||||
|
"rule": [
|
||||||
|
{
|
||||||
|
"name": "deny-passwords",
|
||||||
|
"path": "/ietf-system:system/authentication/user/password",
|
||||||
|
"access-operations": "*",
|
||||||
|
"action": "deny"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When operator user "jacky" tries to:
|
||||||
|
|
||||||
|
- **Read password**: Matches "deny-passwords" → **DENIED**
|
||||||
|
- **Write interface config**: No match, uses write-default → **PERMITTED** (write-default=permit)
|
||||||
|
- **Write hostname**: No match, uses write-default → **PERMITTED** (write-default=permit)
|
||||||
|
- **Reboot system**: Matches "permit-system-rpcs" → **PERMITTED** (overrides `nacm:default-deny-all`)
|
||||||
|
|
||||||
|
## Global Defaults
|
||||||
|
|
||||||
|
NACM has three global defaults that apply when no rule matches:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"read-default": "permit",
|
||||||
|
"write-default": "permit",
|
||||||
|
"exec-default": "permit"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Factory configuration defaults:**
|
||||||
|
|
||||||
|
- `read-default: "permit"` - Users can read configuration and state by default
|
||||||
|
- `write-default: "permit"` - Users can modify configuration by default
|
||||||
|
- `exec-default: "permit"` - Users can execute RPCs by default
|
||||||
|
|
||||||
|
This permit-by-default approach, combined with targeted denials for
|
||||||
|
sensitive items (passwords, cryptographic keys), provides a balance
|
||||||
|
between usability and security. It also makes the system "future proof" -
|
||||||
|
when new YANG modules are added, operators can immediately configure them
|
||||||
|
without updating NACM rules.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> YANG modules with `nacm:default-deny-all` or `nacm:default-deny-write`
|
||||||
|
> annotations override these global defaults. You must create explicit
|
||||||
|
> permit rules for those operations.
|
||||||
|
|
||||||
|
## Module-Name vs Path
|
||||||
|
|
||||||
|
NACM rules can match operations using either `module-name` or `path`.
|
||||||
|
The following sub-sections provide detailed information and examples of
|
||||||
|
both.
|
||||||
|
|
||||||
|
### Module-Name Matching
|
||||||
|
|
||||||
|
Matches all nodes **defined** in a specific YANG module:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "permit-keystore",
|
||||||
|
"module-name": "ietf-keystore",
|
||||||
|
"access-operations": "*",
|
||||||
|
"action": "permit"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This permits all operations on data **defined** in ietf-keystore, but does
|
||||||
|
NOT cover augments from other modules.
|
||||||
|
|
||||||
|
**Example:** The `/interfaces/interface/ipv4/address` path:
|
||||||
|
|
||||||
|
- Interface is defined in `ietf-interfaces`
|
||||||
|
- IPv4 config is defined in `ietf-ip` (augments ietf-interfaces)
|
||||||
|
- A rule with `module-name: "ietf-interfaces"` does NOT cover ipv4/address
|
||||||
|
|
||||||
|
### Path Matching
|
||||||
|
|
||||||
|
Matches a specific data tree path and **all nodes under it**, including
|
||||||
|
augments from other modules:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "permit-network-config",
|
||||||
|
"path": "/ietf-interfaces:interfaces/interface",
|
||||||
|
"access-operations": "*",
|
||||||
|
"action": "permit"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This permits operations on `/interfaces/interface` **and all child nodes**,
|
||||||
|
including augments like:
|
||||||
|
|
||||||
|
- `/interfaces/interface/ipv4` (from ietf-ip)
|
||||||
|
- `/interfaces/interface/ipv6` (from ietf-ip)
|
||||||
|
- `/interfaces/interface/bridge-port` (from infix-interfaces)
|
||||||
|
|
||||||
|
**Path syntax:**
|
||||||
|
|
||||||
|
- When used **with** module-name: Path is module-relative (no prefix)
|
||||||
|
|
||||||
|
```json
|
||||||
|
"module-name": "ietf-system",
|
||||||
|
"path": "/system/authentication/user/password"
|
||||||
|
```
|
||||||
|
|
||||||
|
- When used **without** module-name: Path must include module prefix
|
||||||
|
|
||||||
|
```json
|
||||||
|
"path": "/ietf-system:system/authentication/user/password"
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!TIP] Use path-based rules
|
||||||
|
> When you want to permit/deny access to a configuration subtree
|
||||||
|
> including all augments, e.g., all IP settings below
|
||||||
|
> `/ietf-interfaces:interfaces/`. This is more flexible and requires
|
||||||
|
> less maintenance as new features are added.
|
||||||
|
|
||||||
|
## YANG-Level Annotations
|
||||||
|
|
||||||
|
Many YANG modules include NACM annotations that provide baseline security:
|
||||||
|
|
||||||
|
### nacm:default-deny-all
|
||||||
|
|
||||||
|
Requires an explicit permit rule, regardless of global defaults:
|
||||||
|
|
||||||
|
```yang
|
||||||
|
rpc system-restart {
|
||||||
|
nacm:default-deny-all;
|
||||||
|
description "Restart the system";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Even with `exec-default: "permit"`, users need an explicit permit rule to
|
||||||
|
execute system-restart.
|
||||||
|
|
||||||
|
### nacm:default-deny-write
|
||||||
|
|
||||||
|
Write operations require an explicit permit rule:
|
||||||
|
|
||||||
|
```yang
|
||||||
|
container authentication {
|
||||||
|
nacm:default-deny-write;
|
||||||
|
description "User authentication configuration";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Even with `write-default: "permit"`, users need an explicit permit rule to
|
||||||
|
modify authentication settings.
|
||||||
|
|
||||||
|
### Protected Operations
|
||||||
|
|
||||||
|
The following are protected by YANG annotations and require explicit permits:
|
||||||
|
|
||||||
|
**RPC Operations:**
|
||||||
|
|
||||||
|
- `ietf-system:system-restart` ([ietf-system][3])
|
||||||
|
- `ietf-system:system-shutdown` ([ietf-system][3])
|
||||||
|
- `ietf-system:set-current-datetime` ([ietf-system][3])
|
||||||
|
- `infix-factory-default:factory-default`
|
||||||
|
- `ietf-factory-default:factory-reset` ([RFC 8808][4])
|
||||||
|
- `infix-system-software:install-bundle`
|
||||||
|
- `infix-system-software:set-boot-order`
|
||||||
|
|
||||||
|
**Data Containers:**
|
||||||
|
|
||||||
|
- `/system/authentication` (`nacm:default-deny-write`, [ietf-system][3])
|
||||||
|
- `/nacm` (`nacm:default-deny-all`, [RFC 8341][1])
|
||||||
|
- Routing protocol key chains ([ietf-key-chain][5])
|
||||||
|
- RADIUS shared secrets ([ietf-system][3])
|
||||||
|
- TLS client/server credentials ([ietf-tls-client][6])
|
||||||
|
|
||||||
|
This provides defense-in-depth - even if NACM rules are misconfigured, these
|
||||||
|
critical operations remain protected.
|
||||||
|
|
||||||
|
## Access Operations
|
||||||
|
|
||||||
|
NACM supports the following access operations:
|
||||||
|
|
||||||
|
- `create` - Create new data nodes
|
||||||
|
- `read` - Read existing data nodes
|
||||||
|
- `update` - Modify existing data nodes
|
||||||
|
- `delete` - Delete data nodes
|
||||||
|
- `exec` - Execute RPC operations
|
||||||
|
- `*` - All operations (wildcard)
|
||||||
|
|
||||||
|
**Common combinations:**
|
||||||
|
|
||||||
|
- `"create update delete"` - All write operations
|
||||||
|
- `"*"` - Everything (read, write, execute)
|
||||||
|
- `"read"` - Read-only access
|
||||||
|
|
||||||
|
## Rule-List Groups
|
||||||
|
|
||||||
|
Each rule-list applies to one or more user groups:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "operator-acl",
|
||||||
|
"group": ["operator"],
|
||||||
|
"rule": [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Special group names:**
|
||||||
|
|
||||||
|
- `"*"` - Matches all users (including those not in any NACM group)
|
||||||
|
|
||||||
|
**Evaluation:**
|
||||||
|
A user can be in multiple NACM groups. All rule-lists matching the user's
|
||||||
|
groups are evaluated in order until a matching rule is found.
|
||||||
|
|
||||||
|
## Example: Factory Configuration
|
||||||
|
|
||||||
|
The factory configuration uses a "permit by default, deny sensitive items"
|
||||||
|
approach. This design is "future proof" - when new YANG modules are added,
|
||||||
|
operators can immediately configure them without updating NACM rules.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ietf-netconf-acm:nacm": {
|
||||||
|
"enable-nacm": true,
|
||||||
|
"read-default": "permit",
|
||||||
|
"write-default": "permit",
|
||||||
|
"exec-default": "permit",
|
||||||
|
"groups": {
|
||||||
|
"group": [
|
||||||
|
{"name": "admin", "user-name": ["admin"]},
|
||||||
|
{"name": "operator", "user-name": []},
|
||||||
|
{"name": "guest", "user-name": []}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rule-list": [
|
||||||
|
{
|
||||||
|
"name": "admin-acl",
|
||||||
|
"group": ["admin"],
|
||||||
|
"rule": [
|
||||||
|
{
|
||||||
|
"name": "permit-all",
|
||||||
|
"module-name": "*",
|
||||||
|
"access-operations": "*",
|
||||||
|
"action": "permit",
|
||||||
|
"comment": "Admin has full unrestricted access"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "operator-acl",
|
||||||
|
"group": ["operator"],
|
||||||
|
"rule": [
|
||||||
|
{
|
||||||
|
"name": "permit-system-rpcs",
|
||||||
|
"module-name": "ietf-system",
|
||||||
|
"rpc-name": "*",
|
||||||
|
"access-operations": "exec",
|
||||||
|
"action": "permit",
|
||||||
|
"comment": "Operators can reboot, shutdown, and set system time"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "guest-acl",
|
||||||
|
"group": ["guest"],
|
||||||
|
"rule": [
|
||||||
|
{
|
||||||
|
"name": "deny-all-write+exec",
|
||||||
|
"module-name": "*",
|
||||||
|
"access-operations": "create update delete exec",
|
||||||
|
"action": "deny",
|
||||||
|
"comment": "Guests can only read, not modify or execute"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "default-deny-all",
|
||||||
|
"group": ["*"],
|
||||||
|
"rule": [
|
||||||
|
{
|
||||||
|
"name": "deny-password-access",
|
||||||
|
"path": "/ietf-system:system/authentication/user/password",
|
||||||
|
"access-operations": "*",
|
||||||
|
"action": "deny",
|
||||||
|
"comment": "No user except admins can access password hashes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deny-keystore-access",
|
||||||
|
"module-name": "ietf-keystore",
|
||||||
|
"access-operations": "*",
|
||||||
|
"action": "deny",
|
||||||
|
"comment": "No user except admins can access cryptographic keys"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deny-truststore-access",
|
||||||
|
"module-name": "ietf-truststore",
|
||||||
|
"access-operations": "*",
|
||||||
|
"action": "deny",
|
||||||
|
"comment": "No user except admins can access trust store"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key design decisions:**
|
||||||
|
|
||||||
|
1. **Permit by default** - `write-default: permit` allows operators to configure any module
|
||||||
|
2. **Minimal operator rules** - Only one rule to permit system RPCs (reboot, set time)
|
||||||
|
3. **Future proof** - New YANG modules automatically configurable by operators
|
||||||
|
4. **Targeted denials** - Only sensitive items (passwords, keys) are explicitly denied
|
||||||
|
5. **Global denials** - Password/keystore/truststore denied for everyone via group "*"
|
||||||
|
6. **YANG annotations** - Sensitive operations (factory-reset, software upgrades) still protected by `nacm:default-deny-all` in YANG modules
|
||||||
|
|
||||||
|
**Effective permissions by group:**
|
||||||
|
|
||||||
|
| Group | Read | Write | Exec | Exceptions |
|
||||||
|
|-------|------|-------|------|------------|
|
||||||
|
| admin | All | All | All | None |
|
||||||
|
| operator | All | All | All | Cannot access passwords, keystore, truststore |
|
||||||
|
| guest | All | None | None | Read-only access |
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Permit-by-Default
|
||||||
|
|
||||||
|
The factory default approach - allow everything except sensitive items:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"write-default": "permit",
|
||||||
|
"exec-default": "permit",
|
||||||
|
"rule-list": [
|
||||||
|
{
|
||||||
|
"name": "admin-acl",
|
||||||
|
"group": ["admin"],
|
||||||
|
"rule": [
|
||||||
|
{
|
||||||
|
"name": "permit-all",
|
||||||
|
"module-name": "*",
|
||||||
|
"access-operations": "*",
|
||||||
|
"action": "permit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global-denials",
|
||||||
|
"group": ["*"],
|
||||||
|
"rule": [
|
||||||
|
{
|
||||||
|
"name": "deny-passwords",
|
||||||
|
"path": "/ietf-system:system/authentication/user/password",
|
||||||
|
"access-operations": "*",
|
||||||
|
"action": "deny"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach is "future proof" - new YANG modules are automatically
|
||||||
|
accessible without rule updates. Admins bypass the global denials
|
||||||
|
because their `permit-all` rule is evaluated first.
|
||||||
|
|
||||||
|
### Deny-by-Default
|
||||||
|
|
||||||
|
More restrictive approach - deny everything except what is explicitly
|
||||||
|
allowed:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"write-default": "deny",
|
||||||
|
"exec-default": "deny",
|
||||||
|
"rule-list": [
|
||||||
|
{
|
||||||
|
"group": ["limited-user"],
|
||||||
|
"rule": [
|
||||||
|
{
|
||||||
|
"name": "permit-interface-config",
|
||||||
|
"path": "/ietf-interfaces:interfaces/interface",
|
||||||
|
"access-operations": "create update delete",
|
||||||
|
"action": "permit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> This requires updating NACM rules whenever new features are added.
|
||||||
|
|
||||||
|
### Global Restrictions
|
||||||
|
|
||||||
|
Deny access to sensitive data for all users (except admins with permit-all):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"rule-list": [
|
||||||
|
{
|
||||||
|
"group": ["*"],
|
||||||
|
"rule": [
|
||||||
|
{
|
||||||
|
"name": "deny-passwords",
|
||||||
|
"path": "/ietf-system:system/authentication/user/password",
|
||||||
|
"access-operations": "*",
|
||||||
|
"action": "deny"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Debugging NACM
|
||||||
|
|
||||||
|
### Viewing Effective Permissions
|
||||||
|
|
||||||
|
Check what NACM groups a user belongs to:
|
||||||
|
|
||||||
|
```
|
||||||
|
admin@example:/> show nacm
|
||||||
|
enabled : yes
|
||||||
|
default read access : permit
|
||||||
|
default write access : permit
|
||||||
|
default exec access : permit
|
||||||
|
denied operations : 0
|
||||||
|
denied data writes : 0
|
||||||
|
denied notifications : 0
|
||||||
|
|
||||||
|
┌──────────┬─────────┬─────────┬─────────┐
|
||||||
|
│ GROUP │ READ │ WRITE │ EXEC │
|
||||||
|
├──────────┼─────────┼─────────┼─────────┤
|
||||||
|
│ admin │ ✓ │ ✓ │ ✓ │
|
||||||
|
│ operator │ ⚠ │ ⚠ │ ⚠ │
|
||||||
|
│ guest │ ⚠ │ ✗ │ ✗ │
|
||||||
|
└──────────┴─────────┴─────────┴─────────┘
|
||||||
|
✓ Full ⚠ Restricted ✗ Denied
|
||||||
|
|
||||||
|
USER SHELL LOGIN
|
||||||
|
admin bash password+key
|
||||||
|
jacky bash password
|
||||||
|
monitor false key
|
||||||
|
|
||||||
|
GROUP USERS
|
||||||
|
admin admin
|
||||||
|
operator jacky
|
||||||
|
guest monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
For details about a group's restrictions, use `show nacm group <name>`:
|
||||||
|
|
||||||
|
```
|
||||||
|
admin@example:/> show nacm group operator
|
||||||
|
members : jacky
|
||||||
|
read permission : restricted
|
||||||
|
write permission : restricted
|
||||||
|
exec permission : restricted
|
||||||
|
applicable rules : 4
|
||||||
|
──────────────────────────────────────────────────────────────────────
|
||||||
|
permit-system-rpcs
|
||||||
|
action : permit
|
||||||
|
operations : exec
|
||||||
|
target : ietf-system (rpc: *)
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────────────────────────
|
||||||
|
deny-password-access (via '*')
|
||||||
|
action : deny
|
||||||
|
operations : *
|
||||||
|
target : /ietf-system:system/authentication/user/password
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────────────────────────
|
||||||
|
deny-keystore-access (via '*')
|
||||||
|
action : deny
|
||||||
|
operations : *
|
||||||
|
target : ietf-keystore
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────────────────────────
|
||||||
|
deny-truststore-access (via '*')
|
||||||
|
action : deny
|
||||||
|
operations : *
|
||||||
|
target : ietf-truststore
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Access
|
||||||
|
|
||||||
|
The easiest way to test NACM permissions is to log in as the user and try
|
||||||
|
the operation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ssh jacky@host
|
||||||
|
jacky@example:/> configure
|
||||||
|
jacky@example:/config/> edit system authentication user admin
|
||||||
|
jacky@example:/config/system/authentication/user/admin/> set authorized-key foo
|
||||||
|
Error: Access to the data model "ietf-system" is denied because "jacky" NACM authorization failed.
|
||||||
|
Error: Failed applying changes (2).
|
||||||
|
```
|
||||||
|
|
||||||
|
### NACM Statistics
|
||||||
|
|
||||||
|
NACM tracks denied operations. If you suspect permission issues, check
|
||||||
|
the statistics:
|
||||||
|
|
||||||
|
```
|
||||||
|
admin@example:/> show nacm
|
||||||
|
...
|
||||||
|
denied operations : 5
|
||||||
|
denied data writes : 12
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Increasing counters indicate permission denials are occurring.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Leverage permit-by-default** - The factory configuration uses
|
||||||
|
`write-default: "permit"` with targeted denials. This is "future proof" -
|
||||||
|
new features work immediately without NACM updates.
|
||||||
|
|
||||||
|
2. **Protect sensitive items globally** - Use `group: ["*"]` rule-list to
|
||||||
|
deny access to passwords, cryptographic keys, and similar sensitive data.
|
||||||
|
Admin's permit-all rule (evaluated first) bypasses these denials.
|
||||||
|
|
||||||
|
3. **Leverage YANG annotations** - Many sensitive operations are already
|
||||||
|
protected by `nacm:default-deny-all` in YANG modules (factory-reset,
|
||||||
|
software upgrades, etc.). Only add explicit permit rules when needed.
|
||||||
|
|
||||||
|
4. **Order matters** - Rule-lists are evaluated in order. Place admin's
|
||||||
|
permit-all rule first so it bypasses global denials.
|
||||||
|
|
||||||
|
5. **Use path-based denials** - For protecting specific data (like password
|
||||||
|
hashes), use path rules. For protecting entire modules (like keystore),
|
||||||
|
use module-name rules.
|
||||||
|
|
||||||
|
6. **Test thoroughly** - Always test user permissions after changes. NACM
|
||||||
|
errors can be subtle (nodes may be silently omitted from read operations).
|
||||||
|
|
||||||
|
7. **Keep it simple** - The factory configuration uses only 6 rules for
|
||||||
|
three user levels. Fewer rules are easier to understand and maintain.
|
||||||
|
|
||||||
|
8. **Document rules** - Use the "comment" field to explain why specific
|
||||||
|
permissions are granted or denied.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [RFC 8341: Network Configuration Access Control Model (NACM)][1]
|
||||||
|
- [RFC 7317: A YANG Data Model for System Management (ietf-system)][3]
|
||||||
|
- [RFC 8808: A YANG Data Model for Factory Default Settings (ietf-factory-default)][4]
|
||||||
|
- [RFC 8177: YANG Key Chain (ietf-key-chain)][5]
|
||||||
|
- [System Configuration - Multiple Users][2]
|
||||||
|
|
||||||
|
[1]: https://www.rfc-editor.org/rfc/rfc8341
|
||||||
|
[2]: system.md#multiple-users
|
||||||
|
[3]: https://www.rfc-editor.org/rfc/rfc7317
|
||||||
|
[4]: https://www.rfc-editor.org/rfc/rfc8808
|
||||||
|
[5]: https://www.rfc-editor.org/rfc/rfc8177
|
||||||
|
[6]: https://datatracker.ietf.org/doc/html/draft-ietf-netconf-tls-client-server
|
||||||
@@ -112,6 +112,158 @@ command-line options or environment variables:
|
|||||||
The examples below show both raw `curl` commands and the equivalent using
|
The examples below show both raw `curl` commands and the equivalent using
|
||||||
`curl.sh` where applicable.
|
`curl.sh` where applicable.
|
||||||
|
|
||||||
|
## HTTP Methods in RESTCONF
|
||||||
|
|
||||||
|
RESTCONF uses standard HTTP methods to perform different operations on
|
||||||
|
configuration and operational data. Understanding when to use each method is
|
||||||
|
crucial for correct and safe operations.
|
||||||
|
|
||||||
|
### GET - Read Data
|
||||||
|
|
||||||
|
Retrieve configuration or operational data without making changes.
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
|
||||||
|
- Reading current configuration
|
||||||
|
- Querying operational state (interface statistics, routing tables, etc.)
|
||||||
|
- Discovering what exists before making changes
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
|
||||||
|
- Safe operation (no side effects)
|
||||||
|
- Can be repeated without consequences
|
||||||
|
- Works on both configuration and operational datastores
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
~$ ./curl.sh -h example.local GET /ietf-interfaces:interfaces
|
||||||
|
```
|
||||||
|
|
||||||
|
### PUT - Replace Resource
|
||||||
|
|
||||||
|
> [!DANGER] Heads-up!
|
||||||
|
> PUT replaces everything - missing fields will be deleted!
|
||||||
|
|
||||||
|
Replace an entire resource with new content.
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
|
||||||
|
- Replacing entire datastore (backup/restore scenarios)
|
||||||
|
- Complete reconfiguration of a container
|
||||||
|
- When you want to ensure the resource matches exactly what you provide
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
|
||||||
|
- Replaces all content at the target path
|
||||||
|
- Any existing data not in the PUT request is **removed**
|
||||||
|
- Requires complete, valid configuration
|
||||||
|
- Dangerous if you don't include all necessary fields
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
~$ ./curl.sh -h example.local PUT /ietf-system:system \
|
||||||
|
-d '{"ietf-system:system":{"hostname":"newhost","contact":"admin@example.com"}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### PATCH - Merge/Update Resource
|
||||||
|
|
||||||
|
Partially update a resource by merging changes with existing data.
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
|
||||||
|
- Modifying specific fields while preserving others
|
||||||
|
- Working with path-based NACM permissions
|
||||||
|
- Incremental configuration changes
|
||||||
|
- Safer alternative to PUT for most use cases
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
|
||||||
|
- Merges changes with existing configuration
|
||||||
|
- Only specified fields are modified
|
||||||
|
- Unspecified fields remain unchanged
|
||||||
|
- Works with partial data structures
|
||||||
|
- NACM-friendly (respects path-based permissions)
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
~$ ./curl.sh -h example.local PATCH /ietf-interfaces:interfaces \
|
||||||
|
-d '{"ietf-interfaces:interfaces":{"interface":[{"name":"e0","description":"WAN"}]}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!TIP] Best practice
|
||||||
|
> Use PATCH instead of PUT for most configuration updates.
|
||||||
|
|
||||||
|
### POST - Create New Resource
|
||||||
|
|
||||||
|
Create a new resource within a collection or invoke an RPC.
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
|
||||||
|
- Adding new list entries (interfaces, users, routes, etc.)
|
||||||
|
- Creating resources at specific paths
|
||||||
|
- Invoking RPC operations (reboot, factory-reset, etc.)
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
|
||||||
|
- Creates new resources
|
||||||
|
- For lists: adds a new element
|
||||||
|
- For RPCs: executes the operation
|
||||||
|
- Returns error if resource already exists (for configuration)
|
||||||
|
|
||||||
|
**Example - Add IP address:**
|
||||||
|
```bash
|
||||||
|
~$ ./curl.sh -h example.local POST \
|
||||||
|
/ietf-interfaces:interfaces/interface=lo/ietf-ip:ipv4/address=192.168.254.254 \
|
||||||
|
-d '{"prefix-length": 32}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example - Invoke RPC:**
|
||||||
|
```bash
|
||||||
|
~$ curl -kX POST -u admin:admin \
|
||||||
|
-H "Content-Type: application/yang-data+json" \
|
||||||
|
https://example.local/restconf/operations/ietf-system:system-restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### DELETE - Remove Resource
|
||||||
|
|
||||||
|
Delete a resource or configuration element.
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
|
||||||
|
- Removing interfaces, routes, users, etc.
|
||||||
|
- Deleting specific configuration items
|
||||||
|
- Cleaning up unwanted configuration
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
|
||||||
|
- Removes the resource completely
|
||||||
|
- Cannot be undone (except by reconfiguration)
|
||||||
|
- Returns error if resource doesn't exist
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
~$ ./curl.sh -h example.local DELETE \
|
||||||
|
/ietf-interfaces:interfaces/interface=lo/ietf-ip:ipv4/address=192.168.254.254
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quick Reference
|
||||||
|
|
||||||
|
| **Method** | **Use Case** | **Safe?** | **Idempotent?** | **NACM Impact** |
|
||||||
|
|------------|-------------------------|-----------|-----------------|--------------------------|
|
||||||
|
| GET | Read data | ✓ | ✓ | Read permissions |
|
||||||
|
| PUT | Replace entire resource | ✗ | ✓ | Full write access needed |
|
||||||
|
| PATCH | Merge/update fields | ✓ | ✓ | Path-specific write |
|
||||||
|
| POST | Create new/invoke RPC | ✗ | ✗ | Create/exec permissions |
|
||||||
|
| DELETE | Remove resource | ✗ | ✓ | Delete permissions |
|
||||||
|
|
||||||
|
**Key takeaways:**
|
||||||
|
|
||||||
|
- **Use PATCH for updates** - Safer than PUT, works with NACM
|
||||||
|
- **Use PUT sparingly** - Only when you need complete replacement
|
||||||
|
- **GET is always safe** - Read as much as you need
|
||||||
|
- **POST for creation/RPCs** - Creating new items or executing operations
|
||||||
|
- **DELETE with care** - Cannot be undone
|
||||||
|
|
||||||
## Discovery & Common Patterns
|
## Discovery & Common Patterns
|
||||||
|
|
||||||
Before working with specific configuration items, you often need to discover
|
Before working with specific configuration items, you often need to discover
|
||||||
@@ -245,6 +397,68 @@ Example of updating configuration with inline JSON data:
|
|||||||
-d '{"ietf-system:system":{"hostname":"bar"}}'
|
-d '{"ietf-system:system":{"hostname":"bar"}}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Update Interface Description
|
||||||
|
|
||||||
|
PATCH allows you to modify specific parts of the configuration without
|
||||||
|
replacing the entire container. This is particularly useful when working
|
||||||
|
with NACM (Network Access Control Model) permissions that grant access to
|
||||||
|
specific paths.
|
||||||
|
|
||||||
|
**Why use PATCH instead of PUT:**
|
||||||
|
|
||||||
|
- **Partial updates**: Only changes specified fields, preserves others
|
||||||
|
- **NACM-friendly**: Works with path-based access control rules
|
||||||
|
- **Safer**: Reduces risk of accidentally removing unrelated configuration
|
||||||
|
|
||||||
|
**Example - Modify an interface description:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~$ curl -kX PATCH -u admin:admin \
|
||||||
|
-H 'Content-Type: application/yang-data+json' \
|
||||||
|
-d '{"ietf-interfaces:interfaces":{"interface":[{"name":"e0","description":"WAN Port"}]}}' \
|
||||||
|
https://example.local/restconf/data/ietf-interfaces:interfaces
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using curl.sh:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~$ ./curl.sh -h example.local PATCH /ietf-interfaces:interfaces \
|
||||||
|
-d '{"ietf-interfaces:interfaces":{"interface":[{"name":"e0","description":"WAN Port"}]}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Formatted for readability:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~$ curl -kX PATCH -u admin:admin \
|
||||||
|
-H 'Content-Type: application/yang-data+json' \
|
||||||
|
-d '{
|
||||||
|
"ietf-interfaces:interfaces": {
|
||||||
|
"interface": [
|
||||||
|
{
|
||||||
|
"name": "e0",
|
||||||
|
"description": "WAN Port"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}' \
|
||||||
|
https://example.local/restconf/data/ietf-interfaces:interfaces
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key points:**
|
||||||
|
|
||||||
|
- PATCH URL targets the **container** (`/interfaces`), not a specific interface
|
||||||
|
- JSON body includes the full structure with the interface array
|
||||||
|
- Only specified fields are modified; other interface settings remain unchanged
|
||||||
|
- The `name` field identifies which interface to update
|
||||||
|
|
||||||
|
**Verify the change:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~$ ./curl.sh -h example.local GET /ietf-interfaces:interfaces/interface=e0 2>/dev/null \
|
||||||
|
| jq '.["ietf-interfaces:interface"][0].description'
|
||||||
|
"WAN Port"
|
||||||
|
```
|
||||||
|
|
||||||
### Add IP Address to Interface
|
### Add IP Address to Interface
|
||||||
|
|
||||||
Add an IP address to the loopback interface:
|
Add an IP address to the loopback interface:
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
> [!NOTE]
|
> [!WARNING] Deprecated - Use RESTCONF Instead
|
||||||
> This method is a legacy "simple and human-friendly" way to manage the
|
>
|
||||||
> system. These days we strongly recommend using [RESTCONF][1] instead.
|
> This legacy interface requires elevated privileges (`sudo sysrepocfg`) even
|
||||||
|
> for admin users and is no longer intended for production use. All scripting
|
||||||
|
> should use [RESTCONF][1] as the standard management interface. There is
|
||||||
|
> however an NACM compliant replacement `copy` that can be used without sudo.
|
||||||
|
|
||||||
# Legacy Scripting
|
# Legacy Scripting
|
||||||
|
|
||||||
|
|||||||
+302
-83
@@ -6,7 +6,7 @@ like Message of the Day (login message) and user login shell. More
|
|||||||
on this later on in this document.
|
on this later on in this document.
|
||||||
|
|
||||||
For the sake of brevity, the hostname in the following examples has been
|
For the sake of brevity, the hostname in the following examples has been
|
||||||
shortened to `host`. The default hostname is composed from a product
|
shortened to `example`. The default hostname is composed from a product
|
||||||
specific string followed by the last three octets of the system base MAC
|
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
|
address, e.g., `switch-12-34-56`. An example of how to change the
|
||||||
hostname is included below.
|
hostname is included below.
|
||||||
@@ -23,11 +23,11 @@ User management, including passwords, SSH keys, remote authentication is
|
|||||||
available in the system authentication configuration context.
|
available in the system authentication configuration context.
|
||||||
|
|
||||||
```
|
```
|
||||||
admin@host:/config/> edit system authentication user admin
|
admin@example:/config/> edit system authentication user admin
|
||||||
admin@host:/config/system/authentication/user/admin/> change password
|
admin@example:/config/system/authentication/user/admin/> change password
|
||||||
New password:
|
New password:
|
||||||
Retype password:
|
Retype password:
|
||||||
admin@host:/config/system/authentication/user/admin/> leave
|
admin@example:/config/system/authentication/user/admin/> leave
|
||||||
```
|
```
|
||||||
|
|
||||||
The `change password` command starts an interactive dialogue that asks
|
The `change password` command starts an interactive dialogue that asks
|
||||||
@@ -57,14 +57,14 @@ With SSH keys in place it is possible to disable password login, just
|
|||||||
remember to verify SSH login and network connectivity before doing so.
|
remember to verify SSH login and network connectivity before doing so.
|
||||||
|
|
||||||
```
|
```
|
||||||
admin@host:/config/> edit system authentication user admin
|
admin@example:/config/> edit system authentication user admin
|
||||||
admin@host:/config/system/authentication/user/admin/> edit authorized-key example@host
|
admin@example:/config/system/authentication/user/admin/> edit authorized-key admin@example
|
||||||
admin@host:/config/system/authentication/user/admin/authorized-key/example@host/> set algorithm ssh-rsa
|
admin@example:/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=
|
admin@example:/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=
|
||||||
admin@host:/config/system/authentication/user/admin/authorized-key/example@host/> show
|
admin@example:/config/system/authentication/user/admin/authorized-key/example@host/> show
|
||||||
algorithm ssh-rsa;
|
algorithm ssh-rsa;
|
||||||
key-data AAAAB3NzaC1yc2EAAAADAQABAAABgQC8iBL42yeMBioFay7lty1C4ZDTHcHyo739gc91rTTH8SKvAE4g8Rr97KOz/8PFtOObBrE9G21K7d6UBuPqmd0RUF2CkXXN/eN2PBSHJ50YprRFt/z/304bsBYkDdflKlPDjuSmZ/+OMp4pTsq0R0eNFlX9wcwxEzooIb7VPEdvWE7AYoBRUdf41u3KBHuvjGd1M6QYJtbFLQMMTiVe5IUfyVSZ1RCxEyAB9fR9CBhtVheTVsY3iG0fZc9eCEo89ErDgtGUTJK4Hxt5yCNwI88YaVmkE85cNtw8YwubWQL3/tGZHfbbQ0fynfB4kWNloyRHFr7E1kDxuX5+pbv26EqRdcOVGucNn7hnGU6C1+ejLWdBD7vgsoilFrEaBWF41elJEPKDzpszEijQ9gTrrWeYOQ+x++lvmOdssDu4KvGmj2K/MQTL2jJYrMJ7GDzsUu3XikChRL7zNfS2jYYQLzovboUCgqfPUsVba9hqeX3U67GsJo+hy5MG9RSry4+ucHs=;
|
key-data AAAAB3NzaC1yc2EAAAADAQABAAABgQC8iBL42yeMBioFay7lty1C4ZDTHcHyo739gc91rTTH8SKvAE4g8Rr97KOz/8PFtOObBrE9G21K7d6UBuPqmd0RUF2CkXXN/eN2PBSHJ50YprRFt/z/304bsBYkDdflKlPDjuSmZ/+OMp4pTsq0R0eNFlX9wcwxEzooIb7VPEdvWE7AYoBRUdf41u3KBHuvjGd1M6QYJtbFLQMMTiVe5IUfyVSZ1RCxEyAB9fR9CBhtVheTVsY3iG0fZc9eCEo89ErDgtGUTJK4Hxt5yCNwI88YaVmkE85cNtw8YwubWQL3/tGZHfbbQ0fynfB4kWNloyRHFr7E1kDxuX5+pbv26EqRdcOVGucNn7hnGU6C1+ejLWdBD7vgsoilFrEaBWF41elJEPKDzpszEijQ9gTrrWeYOQ+x++lvmOdssDu4KvGmj2K/MQTL2jJYrMJ7GDzsUu3XikChRL7zNfS2jYYQLzovboUCgqfPUsVba9hqeX3U67GsJo+hy5MG9RSry4+ucHs=;
|
||||||
admin@host:/config/system/authentication/user/admin/authorized-key/example@host/> leave
|
admin@example:/config/system/authentication/user/admin/authorized-key/example@host/> leave
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
@@ -72,72 +72,291 @@ admin@host:/config/system/authentication/user/admin/authorized-key/example@host/
|
|||||||
> so there is no need to use the `text-editor` command, `set` does the
|
> so there is no need to use the `text-editor` command, `set` does the
|
||||||
> job.
|
> job.
|
||||||
|
|
||||||
|
|
||||||
## Multiple Users
|
## Multiple Users
|
||||||
|
|
||||||
The system supports multiple users and multiple user levels, or groups,
|
The factory configuration provides three hierarchical user group levels by
|
||||||
that a user can be a member of. Access control is entirely handled by
|
default: **guest ⊂ operator ⊂ admin**. These levels work out-of-the-box
|
||||||
the NETCONF ["NACM"][3] YANG model, which provides granular access to
|
with sensible permissions - operators can configure the system immediately,
|
||||||
configuration, data, and RPC commands over NETCONF.
|
while sensitive items (passwords, cryptographic keys) remain protected.
|
||||||
|
|
||||||
By default the system ships with a single group, `admin`, which the
|
The default levels provide different access to system resources and
|
||||||
default user `admin` is a member of. The broad permissions granted by
|
configuration:
|
||||||
the `admin` group is what gives its users full system administrator
|
|
||||||
privileges. There are no restrictions on the number of users with
|
- **Admin**: Full system access - can manage users, upgrade software,
|
||||||
administrator privileges, nor is the `admin` user reserved or protected
|
restart the system, and modify all configuration including network
|
||||||
in any way -- it is completely possible to remove the default `admin`
|
settings, routing, and firewall rules.
|
||||||
user from the configuration. However, it is recommended to keep at
|
|
||||||
least one user with administrator privileges in the system, otherwise
|
- **Operator**: Configuration access - can modify most system settings
|
||||||
the only way to regain full access is to perform a *factory reset*.
|
including network interfaces, routing, firewall, hostname, and more.
|
||||||
|
*Cannot access* password hashes, cryptographic keys, or perform
|
||||||
|
sensitive operations (factory reset, software upgrade).
|
||||||
|
|
||||||
|
- **Guest**: Read-only access - can view operational state and
|
||||||
|
configuration but cannot modify anything or execute operations.
|
||||||
|
|
||||||
|
System access control is handled by the [ietf-netconf-acm][3] YANG model,
|
||||||
|
usually referred to as [NACM](nacm.md), which provides granular access to
|
||||||
|
configuration, data, and RPC commands. The hierarchical levels in the system
|
||||||
|
are determined by:
|
||||||
|
|
||||||
|
1. **NACM permissions** - what the user can access
|
||||||
|
2. **Shell setting** - which command-line interface the user can use
|
||||||
|
|
||||||
|
By default the system ships with a single user, `admin`, in the `admin`
|
||||||
|
group. There are no restrictions on the number of users with admin
|
||||||
|
privileges, nor is the `admin` user reserved or protected -- it can be
|
||||||
|
removed from the configuration. However, it is strongly recommended to
|
||||||
|
keep at least one user with administrator privileges, otherwise the only
|
||||||
|
way to regain full access is to perform a *factory reset*.
|
||||||
|
|
||||||
|
For an overview of users and groups on the system, there is an admin-exec
|
||||||
|
command:
|
||||||
|
|
||||||
|
```
|
||||||
|
admin@example:/> show nacm
|
||||||
|
enabled : yes
|
||||||
|
default read access : permit
|
||||||
|
default write access : permit
|
||||||
|
default exec access : permit
|
||||||
|
denied operations : 0
|
||||||
|
denied data writes : 0
|
||||||
|
denied notifications : 0
|
||||||
|
|
||||||
|
┌──────────┬─────────┬─────────┬─────────┐
|
||||||
|
│ GROUP │ READ │ WRITE │ EXEC │
|
||||||
|
├──────────┼─────────┼─────────┼─────────┤
|
||||||
|
│ admin │ ✓ │ ✓ │ ✓ │
|
||||||
|
│ operator │ ⚠ │ ⚠ │ ⚠ │
|
||||||
|
│ guest │ ⚠ │ ✗ │ ✗ │
|
||||||
|
└──────────┴─────────┴─────────┴─────────┘
|
||||||
|
✓ Full ⚠ Restricted ✗ Denied
|
||||||
|
|
||||||
|
USER SHELL LOGIN
|
||||||
|
admin bash password+key
|
||||||
|
jacky clish password
|
||||||
|
monitor false key
|
||||||
|
|
||||||
|
GROUP USERS
|
||||||
|
admin admin
|
||||||
|
operator jacky
|
||||||
|
guest monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
The permissions matrix shows effective access for each NACM group:
|
||||||
|
|
||||||
|
- **✓ Full** (green) - unrestricted access
|
||||||
|
- **⚠ Restricted** (yellow) - access with exceptions, use `show nacm group`
|
||||||
|
for details
|
||||||
|
- **✗ Denied** (red) - no access
|
||||||
|
|
||||||
|
For detailed information about a specific group's rules:
|
||||||
|
|
||||||
|
```
|
||||||
|
admin@example:/> show nacm group operator
|
||||||
|
members : jacky
|
||||||
|
read permission : restricted
|
||||||
|
write permission : restricted
|
||||||
|
exec permission : restricted
|
||||||
|
applicable rules : 4
|
||||||
|
──────────────────────────────────────────────────────────────────────
|
||||||
|
permit-system-rpcs
|
||||||
|
action : permit
|
||||||
|
operations : exec
|
||||||
|
target : ietf-system (rpc: *)
|
||||||
|
comment : Operators can reboot, shutdown, and set system time.
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────────────────────────
|
||||||
|
deny-password-access (via '*')
|
||||||
|
action : deny
|
||||||
|
operations : *
|
||||||
|
target : /ietf-system:system/authentication/user/password
|
||||||
|
comment : No user except admins can access password hashes.
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────────────────────────
|
||||||
|
deny-keystore-access (via '*')
|
||||||
|
action : deny
|
||||||
|
operations : *
|
||||||
|
target : ietf-keystore
|
||||||
|
comment : No user except admins can access cryptographic keys.
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────────────────────────
|
||||||
|
deny-truststore-access (via '*')
|
||||||
|
action : deny
|
||||||
|
operations : *
|
||||||
|
target : ietf-truststore
|
||||||
|
comment : No user except admins can access trust store.
|
||||||
|
```
|
||||||
|
|
||||||
|
For user details:
|
||||||
|
|
||||||
|
```
|
||||||
|
admin@example:/> show nacm user jacky
|
||||||
|
shell : clish
|
||||||
|
login : password
|
||||||
|
nacm group : operator
|
||||||
|
read permission : restricted
|
||||||
|
write permission : restricted
|
||||||
|
exec permission : restricted
|
||||||
|
|
||||||
|
For detailed rules, use: show nacm group <name>
|
||||||
|
```
|
||||||
|
|
||||||
### Adding a User
|
### Adding a User
|
||||||
|
|
||||||
Similar to how to change password, adding a new user is done using the
|
Creating a new user starts with defining the user account in the system:
|
||||||
same set of commands:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
admin@host:/config/> edit system authentication user jacky
|
admin@example:/config/> edit system authentication user jacky
|
||||||
admin@host:/config/system/authentication/user/jacky/> change password
|
admin@example:/config/system/authentication/user/jacky/> change password
|
||||||
New password:
|
New password:
|
||||||
Retype password:
|
Retype password:
|
||||||
admin@host:/config/system/authentication/user/jacky/> leave
|
admin@example:/config/system/authentication/user/jacky/> leave
|
||||||
```
|
```
|
||||||
|
|
||||||
An authorized SSH key is added the same way as presented previously.
|
An authorized SSH key can be added the same way as described in the
|
||||||
|
previous sections.
|
||||||
|
|
||||||
### Adding a User to the Admin Group
|
By default, shell access is disabled (`shell false`). To allow CLI/SSH
|
||||||
|
access, set the shell:
|
||||||
The following commands add user `jacky` to the `admin` group.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
admin@host:/config/> edit nacm group admin
|
admin@example:/config/> edit system authentication user jacky
|
||||||
admin@host:/config/nacm/group/admin/> set user-name jacky
|
admin@example:/config/system/authentication/user/jacky/> set shell clish
|
||||||
admin@host:/config/nacm/group/admin/> leave
|
admin@example:/config/system/authentication/user/jacky/> leave
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Available shells:
|
||||||
|
|
||||||
|
- `bash` - Full Bourne-again shell (recommended for admins only)
|
||||||
|
- `sh` - POSIX shell (recommended for admins only)
|
||||||
|
- `clish` - Limited CLI-only shell (recommended for operators and guests)
|
||||||
|
- `false` - No shell access (default)
|
||||||
|
|
||||||
|
> [!WARNING] Security Notice
|
||||||
|
> For security reasons, it is strongly recommended to limit non-admin users
|
||||||
|
> to the `clish` shell, which provides CLI access without exposing the
|
||||||
|
> underlying UNIX system. Reserve `bash` and `sh` for administrators who
|
||||||
|
> need full system access for debugging and maintenance.
|
||||||
|
>
|
||||||
|
> Note that shell and CLI access is not always necessary - the system
|
||||||
|
> supports NETCONF and RESTCONF for remote management and automation.
|
||||||
|
> Setting `shell false` for users who only need programmatic access
|
||||||
|
> minimizes the attack surface and improves overall system security.
|
||||||
|
|
||||||
|
### Adding a User to a Group
|
||||||
|
|
||||||
|
To assign a user to a specific privilege level, add them to the
|
||||||
|
corresponding NACM group:
|
||||||
|
|
||||||
|
**Operator user:**
|
||||||
|
|
||||||
|
```
|
||||||
|
admin@example:/config/> edit nacm group operator
|
||||||
|
admin@example:/config/nacm/group/operator/> set user-name jacky
|
||||||
|
admin@example:/config/nacm/group/operator/> leave
|
||||||
|
```
|
||||||
|
|
||||||
|
**Adding another admin:**
|
||||||
|
|
||||||
|
```
|
||||||
|
admin@example:/config/> edit nacm group admin
|
||||||
|
admin@example:/config/nacm/group/admin/> set user-name alice
|
||||||
|
admin@example:/config/nacm/group/admin/> leave
|
||||||
|
```
|
||||||
|
|
||||||
|
**Guest user:**
|
||||||
|
|
||||||
|
```
|
||||||
|
admin@example:/config/> edit nacm group guest
|
||||||
|
admin@example:/config/nacm/group/guest/> set user-name monitor
|
||||||
|
admin@example:/config/nacm/group/guest/> leave
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> For technical details about NACM rule evaluation, module-name vs path
|
||||||
|
> matching, and creating custom access control policies, see the
|
||||||
|
> [NACM Technical Guide](nacm.md).
|
||||||
|
|
||||||
|
### Access Control Matrix
|
||||||
|
|
||||||
|
The following table shows what each user level can do based on the NACM rules
|
||||||
|
and shell access configured for each user:
|
||||||
|
|
||||||
|
- **Admin**: `bash` — full system access
|
||||||
|
- **Operator**: `clish` — CLI-only access without UNIX system exposure
|
||||||
|
- **Guest**: `false` — no shell access
|
||||||
|
|
||||||
|
| Feature | Admin | Operator | Guest |
|
||||||
|
|------------------------|-------|----------|-----------|
|
||||||
|
| Network interfaces | ✓ | ✓ | Read only |
|
||||||
|
| Routing (FRR) | ✓ | ✓ | Read only |
|
||||||
|
| Firewall rules | ✓ | ✓ | Read only |
|
||||||
|
| VLANs/bridges | ✓ | ✓ | Read only |
|
||||||
|
| Containers | ✓ | ✓ | Read only |
|
||||||
|
| Hostname/system config | ✓ | ✓ | Read only |
|
||||||
|
| CLI/SSH access | ✓ | ✓ | ✗ |
|
||||||
|
| System restart | ✓ | ✓ | ✗ |
|
||||||
|
| Set date/time | ✓ | ✓ | ✗ |
|
||||||
|
| System reboot | ✓ | ✓ | ✗ |
|
||||||
|
| System shutdown | ✓ | ✓ | ✗ |
|
||||||
|
| User management | ✓ | ✗ | Read only |
|
||||||
|
| Keystore (certs/keys) | ✓ | ✗ | ✗ |
|
||||||
|
| Truststore | ✓ | ✗ | ✗ |
|
||||||
|
| Read passwords/secrets | ✓ | ✗ | ✗ |
|
||||||
|
| NACM rules | ✓ | ✗ | ✗ |
|
||||||
|
| Factory reset | ✓ | ✗ | ✗ |
|
||||||
|
| Software upgrade | ✓ | ✗ | ✗ |
|
||||||
|
|
||||||
### Security Aspects
|
### Security Aspects
|
||||||
|
|
||||||
The NACM user levels apply primarily to NETCONF, with exception of the
|
The three default user levels are implemented through a combination of NACM
|
||||||
`admin` group which is granted full system administrator privileges to
|
rules and UNIX group membership. Access control is permission-based, not
|
||||||
the underlying UNIX system with the following ACL rules:
|
name-based - the system detects user levels by examining their NACM
|
||||||
|
permissions and shell settings.
|
||||||
|
|
||||||
|
**Admin users** have unrestricted NACM access with the following rule:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
...
|
|
||||||
"module-name": "*",
|
"module-name": "*",
|
||||||
"access-operations": "*",
|
"access-operations": "*",
|
||||||
"action": "permit",
|
"action": "permit"
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
A user in the `admin` group is allowed to also use a POSIX login shell
|
Admin users are automatically added to the UNIX `wheel` and `frrvty`
|
||||||
and use the `sudo` command to perform system administrative commands.
|
groups, granting them `sudo` privileges and access to FRR routing
|
||||||
This makes it possible to use all the underlying UNIX tooling, which
|
protocols. This makes it possible to use all the underlying UNIX
|
||||||
to many can be very useful, in particular when debugging a system, but
|
tooling, which can be very useful for debugging, but please use with
|
||||||
please remember to use with care -- the system is not built to require
|
care -- the system is designed to be managed through the CLI and
|
||||||
managing from the shell. The tools available in the CLI and automated
|
NETCONF, not directly via shell commands.
|
||||||
services, started from the system's configuration, are the recommended
|
|
||||||
way of using the system, in addition to NETCONF tooling.
|
|
||||||
|
|
||||||
|
**Operator users** use the permit-by-default NACM model (`write-default:
|
||||||
|
"permit"`), which means they can configure most system settings without
|
||||||
|
explicit permit rules. This design is "future proof" - when new features
|
||||||
|
are added, operators can immediately use them.
|
||||||
|
|
||||||
|
The following are explicitly denied to operators through global NACM rules:
|
||||||
|
|
||||||
|
- Password hashes (`/ietf-system:system/authentication/user/password`)
|
||||||
|
- Cryptographic keys (`ietf-keystore` module)
|
||||||
|
- Trust store certificates (`ietf-truststore` module)
|
||||||
|
|
||||||
|
Additionally, sensitive operations like factory reset, software upgrades,
|
||||||
|
and system shutdown are protected by YANG-level `nacm:default-deny-all`
|
||||||
|
annotations and remain restricted to administrators.
|
||||||
|
|
||||||
|
Operators are automatically added to the UNIX `operator` and `frrvty`
|
||||||
|
groups, granting them `sudo` privileges for network operations and FRR
|
||||||
|
access.
|
||||||
|
|
||||||
|
**Guest users** have read-only NACM access through an explicit deny rule
|
||||||
|
that blocks all write and exec operations (`create update delete exec`),
|
||||||
|
while `read-default: "permit"` allows viewing configuration and state.
|
||||||
|
Guests receive no special UNIX group memberships. The shell setting
|
||||||
|
determines whether guests can access the CLI (`clish`) or are restricted
|
||||||
|
from shell access entirely (`false`).
|
||||||
|
|
||||||
|
All users, regardless of level, are denied access to password hashes and
|
||||||
|
cryptographic key material through global NACM rules.
|
||||||
|
|
||||||
## Changing Hostname
|
## Changing Hostname
|
||||||
|
|
||||||
@@ -145,25 +364,25 @@ Notice how the hostname in the prompt does not change until the change
|
|||||||
is committed by issuing the `leave` command.
|
is committed by issuing the `leave` command.
|
||||||
|
|
||||||
```
|
```
|
||||||
admin@host:/config/> edit system
|
admin@example:/config/> edit system
|
||||||
admin@host:/config/system/> set hostname example
|
admin@example:/config/system/> set hostname myrouter
|
||||||
admin@host:/config/system/> leave
|
admin@example:/config/system/> leave
|
||||||
admin@example:/>
|
admin@myrouter:/>
|
||||||
```
|
```
|
||||||
|
|
||||||
The hostname is advertised over mDNS-SD in the `.local` domain. If
|
The hostname is advertised over mDNS-SD in the `.local` domain. If
|
||||||
another device already has claimed the `example.local` CNAME, in our
|
another device already has claimed the `myrouter.local` CNAME, in our
|
||||||
case, mDNS will advertise a "uniqified" variant, usually suffixing with
|
case, mDNS will advertise a "uniqified" variant, usually suffixing with
|
||||||
an index, e.g., `example-1.local`. Use an mDNS browser to scan for
|
an index, e.g., `myrouter-1.local`. Use an mDNS browser to scan for
|
||||||
available devices on your LAN.
|
available devices on your LAN.
|
||||||
|
|
||||||
In some cases you may want to set the device's *domain name* as well.
|
In some cases you may want to set the device's *domain name* as well.
|
||||||
This is handled the same way:
|
This is handled the same way:
|
||||||
|
|
||||||
```
|
```
|
||||||
admin@host:/config/> edit system
|
admin@example:/config/> edit system
|
||||||
admin@host:/config/system/> set hostname foo.example.com
|
admin@example:/config/system/> set hostname foo.example.com
|
||||||
admin@host:/config/system/> leave
|
admin@example:/config/system/> leave
|
||||||
admin@foo:/>
|
admin@foo:/>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -188,10 +407,10 @@ built-in [`text-editor` command](cli/text-editor.md).
|
|||||||
> you may be more familiar with.
|
> you may be more familiar with.
|
||||||
|
|
||||||
```
|
```
|
||||||
admin@host:/config/> edit system
|
admin@example:/config/> edit system
|
||||||
admin@host:/config/system/> text-editor motd-banner
|
admin@example:/config/system/> text-editor motd-banner
|
||||||
admin@host:/config/system/> leave
|
admin@example:/config/system/> leave
|
||||||
admin@host:/>
|
admin@example:/>
|
||||||
```
|
```
|
||||||
|
|
||||||
Log out and log back in again to inspect the changes.
|
Log out and log back in again to inspect the changes.
|
||||||
@@ -209,11 +428,11 @@ as the `text-editor` command:
|
|||||||
To change the editor to GNU Nano:
|
To change the editor to GNU Nano:
|
||||||
|
|
||||||
```
|
```
|
||||||
admin@host:/> configure
|
admin@example:/> configure
|
||||||
admin@host:/config/> edit system
|
admin@example:/config/> edit system
|
||||||
admin@host:/config/system/> set text-editor nano
|
admin@example:/config/system/> set text-editor nano
|
||||||
admin@host:/config/system/> leave
|
admin@example:/config/system/> leave
|
||||||
admin@host:/>
|
admin@example:/>
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
@@ -229,16 +448,16 @@ locally configured (static) server is preferred over any acquired
|
|||||||
from a DHCP client.
|
from a DHCP client.
|
||||||
|
|
||||||
```
|
```
|
||||||
admin@host:/> configure
|
admin@example:/> configure
|
||||||
admin@host:/config/> edit system dns-resolver
|
admin@example:/config/> edit system dns-resolver
|
||||||
admin@host:/config/system/dns-resolver/> set server google udp-and-tcp address 8.8.8.8
|
admin@example:/config/system/dns-resolver/> set server google udp-and-tcp address 8.8.8.8
|
||||||
admin@host:/config/system/dns-resolver/> show
|
admin@example:/config/system/dns-resolver/> show
|
||||||
server google {
|
server google {
|
||||||
udp-and-tcp {
|
udp-and-tcp {
|
||||||
address 8.8.8.8;
|
address 8.8.8.8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
admin@host:/config/system/dns-resolver/> leave
|
admin@example:/config/system/dns-resolver/> leave
|
||||||
```
|
```
|
||||||
|
|
||||||
It is also possible to configure resolver options like timeout and
|
It is also possible to configure resolver options like timeout and
|
||||||
@@ -257,14 +476,14 @@ Below is an example configuration for enabling NTP with a specific
|
|||||||
server and the `iburst` option for faster initial synchronization.
|
server and the `iburst` option for faster initial synchronization.
|
||||||
|
|
||||||
```
|
```
|
||||||
admin@host:/> configure
|
admin@example:/> configure
|
||||||
admin@host:/config/> edit system ntp
|
admin@example:/config/> edit system ntp
|
||||||
admin@host:/config/system/ntp/> set enabled
|
admin@example:/config/system/ntp/> set enabled
|
||||||
admin@host:/config/system/ntp/> set server ntp-pool
|
admin@example:/config/system/ntp/> set server ntp-pool
|
||||||
admin@host:/config/system/ntp/> set server ntp-pool udp address pool.ntp.org
|
admin@example:/config/system/ntp/> set server ntp-pool udp address pool.ntp.org
|
||||||
admin@host:/config/system/ntp/> set server ntp-pool iburst
|
admin@example:/config/system/ntp/> set server ntp-pool iburst
|
||||||
admin@host:/config/system/ntp/> set server ntp-pool prefer
|
admin@example:/config/system/ntp/> set server ntp-pool prefer
|
||||||
admin@host:/config/system/ntp/> leave
|
admin@example:/config/system/ntp/> leave
|
||||||
```
|
```
|
||||||
|
|
||||||
This configuration enables the NTP client and sets the NTP server to
|
This configuration enables the NTP client and sets the NTP server to
|
||||||
@@ -299,7 +518,7 @@ To check the status of NTP synchronization (only availble in CLI), use
|
|||||||
the following command:
|
the following command:
|
||||||
|
|
||||||
```
|
```
|
||||||
admin@host:/> show ntp tracking
|
admin@example:/> show ntp tracking
|
||||||
Reference ID : C0248F86 (192.36.143.134)
|
Reference ID : C0248F86 (192.36.143.134)
|
||||||
Stratum : 2
|
Stratum : 2
|
||||||
Ref time (UTC) : Mon Oct 21 10:06:45 2024
|
Ref time (UTC) : Mon Oct 21 10:06:45 2024
|
||||||
@@ -313,7 +532,7 @@ Root delay : 1.024467230 seconds
|
|||||||
Root dispersion : 0.273462683 seconds
|
Root dispersion : 0.273462683 seconds
|
||||||
Update interval : 0.0 seconds
|
Update interval : 0.0 seconds
|
||||||
Leap status : Normal
|
Leap status : Normal
|
||||||
admin@host:/>
|
admin@example:/>
|
||||||
```
|
```
|
||||||
|
|
||||||
This output provides detailed information about the NTP status, including
|
This output provides detailed information about the NTP status, including
|
||||||
|
|||||||
+3
-2
@@ -42,6 +42,7 @@ nav:
|
|||||||
- System:
|
- System:
|
||||||
- Boot Procedure: boot.md
|
- Boot Procedure: boot.md
|
||||||
- Configuration: system.md
|
- Configuration: system.md
|
||||||
|
- Access Control (NACM): nacm.md
|
||||||
- Hardware Info & Status: hardware.md
|
- Hardware Info & Status: hardware.md
|
||||||
- Management: management.md
|
- Management: management.md
|
||||||
- Syslog Support: syslog.md
|
- Syslog Support: syslog.md
|
||||||
@@ -50,9 +51,9 @@ nav:
|
|||||||
- Scripting:
|
- Scripting:
|
||||||
- Introduction: scripting.md
|
- Introduction: scripting.md
|
||||||
- Legacy Scripting: scripting-sysrepocfg.md
|
- Legacy Scripting: scripting-sysrepocfg.md
|
||||||
|
- NETCONF Scripting: scripting-netconf.md
|
||||||
|
- RESTCONF Scripting: scripting-restconf.md
|
||||||
- Production Testing: scripting-prod.md
|
- Production Testing: scripting-prod.md
|
||||||
- With NETCONF: scripting-netconf.md
|
|
||||||
- With RESTCONF: scripting-restconf.md
|
|
||||||
- Developer's Corner:
|
- Developer's Corner:
|
||||||
- Branding & Releases: branding.md
|
- Branding & Releases: branding.md
|
||||||
- Developer's Guide: developers-guide.md
|
- Developer's Guide: developers-guide.md
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ source "$BR2_EXTERNAL_INFIX_PATH/package/podman/Config.in"
|
|||||||
source "$BR2_EXTERNAL_INFIX_PATH/package/python-libyang/Config.in"
|
source "$BR2_EXTERNAL_INFIX_PATH/package/python-libyang/Config.in"
|
||||||
source "$BR2_EXTERNAL_INFIX_PATH/package/python-yangdoc/Config.in"
|
source "$BR2_EXTERNAL_INFIX_PATH/package/python-yangdoc/Config.in"
|
||||||
source "$BR2_EXTERNAL_INFIX_PATH/package/skeleton-init-finit/Config.in"
|
source "$BR2_EXTERNAL_INFIX_PATH/package/skeleton-init-finit/Config.in"
|
||||||
source "$BR2_EXTERNAL_INFIX_PATH/package/show/Config.in"
|
|
||||||
source "$BR2_EXTERNAL_INFIX_PATH/package/tetris/Config.in"
|
source "$BR2_EXTERNAL_INFIX_PATH/package/tetris/Config.in"
|
||||||
source "$BR2_EXTERNAL_INFIX_PATH/package/libyang-cpp/Config.in"
|
source "$BR2_EXTERNAL_INFIX_PATH/package/libyang-cpp/Config.in"
|
||||||
source "$BR2_EXTERNAL_INFIX_PATH/package/sysrepo-cpp/Config.in"
|
source "$BR2_EXTERNAL_INFIX_PATH/package/sysrepo-cpp/Config.in"
|
||||||
|
|||||||
@@ -5,4 +5,8 @@ config BR2_PACKAGE_BIN
|
|||||||
help
|
help
|
||||||
Misc. tools for CLI and shell users.
|
Misc. tools for CLI and shell users.
|
||||||
|
|
||||||
|
Includes show (show.py) copy (NACM-aware datastore operations),
|
||||||
|
rpc (YANG RPC execution with NACM enforcement), erase, and files
|
||||||
|
commands.
|
||||||
|
|
||||||
https://github.com/kernelkit/infix
|
https://github.com/kernelkit/infix
|
||||||
|
|||||||
+30
-1
@@ -10,7 +10,9 @@ BIN_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/bin
|
|||||||
BIN_LICENSE = BSD-3-Clause
|
BIN_LICENSE = BSD-3-Clause
|
||||||
BIN_LICENSE_FILES = LICENSE
|
BIN_LICENSE_FILES = LICENSE
|
||||||
BIN_REDISTRIBUTE = NO
|
BIN_REDISTRIBUTE = NO
|
||||||
BIN_DEPENDENCIES = sysrepo libite
|
BIN_DEPENDENCIES = sysrepo libite \
|
||||||
|
host-python3 python3 host-python-pypa-build host-python-installer \
|
||||||
|
host-python-poetry-core
|
||||||
BIN_CONF_OPTS = --disable-silent-rules
|
BIN_CONF_OPTS = --disable-silent-rules
|
||||||
BIN_AUTORECONF = YES
|
BIN_AUTORECONF = YES
|
||||||
|
|
||||||
@@ -18,4 +20,31 @@ define BIN_CONF_ENV
|
|||||||
CFLAGS="$(INFIX_CFLAGS)"
|
CFLAGS="$(INFIX_CFLAGS)"
|
||||||
endef
|
endef
|
||||||
|
|
||||||
|
define BIN_PERMISSIONS
|
||||||
|
/usr/bin/copy d 04750 root klish - - - - -
|
||||||
|
endef
|
||||||
|
|
||||||
|
define BIN_BUILD_PYTHON
|
||||||
|
cd $(BIN_SITE) && \
|
||||||
|
$(PKG_PYTHON_PEP517_ENV) $(HOST_DIR)/bin/python3 $(PKG_PYTHON_PEP517_BUILD_CMD) -o $(@D)/dist
|
||||||
|
mkdir -p $(TARGET_DIR)/usr/bin
|
||||||
|
rm -f $(TARGET_DIR)/usr/bin/show
|
||||||
|
cd $(@D) && \
|
||||||
|
$(HOST_DIR)/bin/python3 $(TOPDIR)/support/scripts/pyinstaller.py \
|
||||||
|
dist/*.whl \
|
||||||
|
--interpreter=/usr/bin/python3 \
|
||||||
|
--script-kind=posix \
|
||||||
|
--purelib=$(TARGET_DIR)/usr/lib/python$(PYTHON3_VERSION_MAJOR)/site-packages \
|
||||||
|
--headers=$(TARGET_DIR)/usr/include/python$(PYTHON3_VERSION_MAJOR) \
|
||||||
|
--scripts=$(TARGET_DIR)/usr/bin \
|
||||||
|
--data=$(TARGET_DIR)
|
||||||
|
endef
|
||||||
|
BIN_POST_INSTALL_TARGET_HOOKS += BIN_BUILD_PYTHON
|
||||||
|
|
||||||
|
define BIN_INSTALL_BASH_COMPLETION
|
||||||
|
install -D $(@D)/bash_completion.d/show \
|
||||||
|
$(TARGET_DIR)/etc/bash_completion.d/show
|
||||||
|
endef
|
||||||
|
BIN_POST_INSTALL_TARGET_HOOKS += BIN_INSTALL_BASH_COMPLETION
|
||||||
|
|
||||||
$(eval $(autotools-package))
|
$(eval $(autotools-package))
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ define CONFD_TEST_MODE_INSTALL_YANG_MODULES
|
|||||||
$(BR2_EXTERNAL_INFIX_PATH)/utils/srload $(@D)/yang/test-mode.inc
|
$(BR2_EXTERNAL_INFIX_PATH)/utils/srload $(@D)/yang/test-mode.inc
|
||||||
endef
|
endef
|
||||||
define CONFD_TEST_MODE_PERMISSIONS
|
define CONFD_TEST_MODE_PERMISSIONS
|
||||||
/etc/sysrepo/data/ r 660 root wheel - - - - -
|
/etc/sysrepo/data/ r 660 root sysrepo - - - - -
|
||||||
/etc/sysrepo/data d 770 root wheel - - - - -
|
/etc/sysrepo/data d 770 root sysrepo - - - - -
|
||||||
endef
|
endef
|
||||||
define CONFD_TEST_MODE_CLEANUP
|
define CONFD_TEST_MODE_CLEANUP
|
||||||
rm -f /dev/shm/$(CONFD_TEST_MODE_SYSREPO_SHM_PREFIX)*
|
rm -f /dev/shm/$(CONFD_TEST_MODE_SYSREPO_SHM_PREFIX)*
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
From abaad560f0bf1f7de832a55222e20e2a9a61986f Mon Sep 17 00:00:00 2001
|
||||||
|
From: Aaron Andersen <aaron@fosslib.net>
|
||||||
|
Date: Sat, 10 Jan 2026 18:57:52 -0500
|
||||||
|
Subject: [PATCH 1/2] Set USER and LOGNAME environment variables when dropping
|
||||||
|
privileges
|
||||||
|
Organization: Wires
|
||||||
|
|
||||||
|
When a service is configured to run as a non-root user (@user), finit
|
||||||
|
correctly drops privileges via setuid() and sets HOME and PATH, but
|
||||||
|
does not set the USER and LOGNAME environment variables. They remain
|
||||||
|
set to "root" from boot time.
|
||||||
|
|
||||||
|
This causes problems for software that determines its identity from
|
||||||
|
the environment rather than getuid(). For example, rootless Podman
|
||||||
|
checks os.Getenv("USER") first when looking up subordinate UID/GID
|
||||||
|
ranges in /etc/subuid and /etc/subgid.
|
||||||
|
|
||||||
|
With USER=root but UID=1000, Podman looks up root's subuid entry
|
||||||
|
instead of the actual user's, causing applications like newuidmap
|
||||||
|
to fail. Setting USER and LOGNAME to match the actual user identity
|
||||||
|
follows POSIX conventions and matches the behavior of su, sudo, and
|
||||||
|
login.
|
||||||
|
|
||||||
|
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
||||||
|
---
|
||||||
|
src/service.c | 5 ++++-
|
||||||
|
1 file changed, 4 insertions(+), 1 deletion(-)
|
||||||
|
|
||||||
|
diff --git a/src/service.c b/src/service.c
|
||||||
|
index 2c8fb6e..d45db40 100644
|
||||||
|
--- a/src/service.c
|
||||||
|
+++ b/src/service.c
|
||||||
|
@@ -627,8 +627,11 @@ static pid_t service_fork(svc_t *svc)
|
||||||
|
set_uid(uid, svc);
|
||||||
|
|
||||||
|
/* Set default path for regular users */
|
||||||
|
- if (uid > 0)
|
||||||
|
+ if (uid > 0) {
|
||||||
|
setenv("PATH", _PATH_DEFPATH, 1);
|
||||||
|
+ setenv("USER", svc->username, 1);
|
||||||
|
+ setenv("LOGNAME", svc->username, 1);
|
||||||
|
+ }
|
||||||
|
if (home) {
|
||||||
|
setenv("HOME", home, 1);
|
||||||
|
if (chdir(home)) {
|
||||||
|
--
|
||||||
|
2.43.0
|
||||||
|
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
From 34bf9a77765db4d963fc66dde29b415ecc8ab611 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Joachim Wiberg <troglobit@gmail.com>
|
||||||
|
Date: Mon, 12 Jan 2026 19:31:39 +0100
|
||||||
|
Subject: [PATCH 2/2] Fix #467: TTY services stuck in restart state after
|
||||||
|
non-zero exit
|
||||||
|
Organization: Wires
|
||||||
|
|
||||||
|
When a TTY exited with non-zero code (e.g., user with shell=/sbin/false),
|
||||||
|
it would enter restart state but never recover, requiring manual restart.
|
||||||
|
|
||||||
|
The throttling logic from commit f0032ab had two issues:
|
||||||
|
|
||||||
|
1. Duplicate exit code check in service_retry() created infinite timer loop
|
||||||
|
2. TTYs lacked default restart_tmo, causing timer to never start
|
||||||
|
|
||||||
|
Fix by removing duplicate check and ensuring TTYs get a 2-second default
|
||||||
|
restart_tmo for proper throttling.
|
||||||
|
|
||||||
|
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
||||||
|
---
|
||||||
|
src/service.c | 31 ++++++++++++++-----------------
|
||||||
|
1 file changed, 14 insertions(+), 17 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/src/service.c b/src/service.c
|
||||||
|
index d45db40..ff8e180 100644
|
||||||
|
--- a/src/service.c
|
||||||
|
+++ b/src/service.c
|
||||||
|
@@ -2194,15 +2194,20 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
|
||||||
|
/* only set forking based on pidfile if user supplied pid: option */
|
||||||
|
if (pid && svc->pidfile[0] == '!')
|
||||||
|
svc->forking = 1;
|
||||||
|
+ }
|
||||||
|
|
||||||
|
- if (svc->restart_tmo == 0) {
|
||||||
|
- if (svc_is_forking(svc))
|
||||||
|
- svc->restart_tmo = 2000;
|
||||||
|
- else
|
||||||
|
- svc->restart_tmo = 1;
|
||||||
|
- }
|
||||||
|
+ /* Set default restart_tmo for services and TTYs that can restart */
|
||||||
|
+ if (svc_is_daemon(svc) && svc->restart_tmo == 0) {
|
||||||
|
+ if (svc_is_forking(svc))
|
||||||
|
+ svc->restart_tmo = 2000;
|
||||||
|
+ else
|
||||||
|
+ svc->restart_tmo = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ /* TTYs need a longer default to throttle errors (e.g., missing device) */
|
||||||
|
+ if (svc_is_tty(svc) && svc->restart_tmo == 0)
|
||||||
|
+ svc->restart_tmo = 2000;
|
||||||
|
+
|
||||||
|
/* Set configured limits */
|
||||||
|
memcpy(svc->rlimit, rlimit, sizeof(svc->rlimit));
|
||||||
|
|
||||||
|
@@ -2631,7 +2636,6 @@ static void service_cleanup_script(svc_t *svc)
|
||||||
|
static void service_retry(svc_t *svc)
|
||||||
|
{
|
||||||
|
char *restart_cnt = (char *)&svc->restart_cnt;
|
||||||
|
- int rc = WEXITSTATUS(svc->status);
|
||||||
|
int timeout;
|
||||||
|
|
||||||
|
service_timeout_cancel(svc);
|
||||||
|
@@ -2641,17 +2645,10 @@ static void service_retry(svc_t *svc)
|
||||||
|
|
||||||
|
if (svc->respawn) {
|
||||||
|
/*
|
||||||
|
- * Non-zero exit indicates an error that may not be resolved
|
||||||
|
- * by immediate retry. Add delay to prevent busy-loop and to
|
||||||
|
- * rate-limit retries, e.g. when TTY device doesn't exist.
|
||||||
|
+ * Respawn services (TTYs) that exited with non-zero status
|
||||||
|
+ * have already been delayed in the SVC_RUNNING_STATE handler.
|
||||||
|
+ * Just restart now.
|
||||||
|
*/
|
||||||
|
- if (WIFEXITED(svc->status) && rc != 0) {
|
||||||
|
- dbg("%s exited with error %d, delaying respawn ...",
|
||||||
|
- svc_ident(svc, NULL, 0), rc);
|
||||||
|
- service_timeout_after(svc, svc->restart_tmo, service_retry);
|
||||||
|
- return;
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
dbg("%s crashed/exited, respawning ...", svc_ident(svc, NULL, 0));
|
||||||
|
svc_unblock(svc);
|
||||||
|
service_step(svc);
|
||||||
|
--
|
||||||
|
2.43.0
|
||||||
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
# Locally calculated
|
# Locally calculated
|
||||||
sha256 9d9d33b873917ca5d0bdcc47a36d2fd385971ab0c045d1472fcadf95ee5bcf5b LICENCE
|
sha256 9d9d33b873917ca5d0bdcc47a36d2fd385971ab0c045d1472fcadf95ee5bcf5b LICENCE
|
||||||
sha256 d3148fd6d3fc106384c88efe5a4ce3c7b1d512bd919b864d84c13b7b9ad523f0 klish-4fd82287cfe74c6bbafea8d6c0ba8d8b4c65cdd0-git4.tar.gz
|
sha256 4c1b6b461a805f0cf80f797f85415e0663ee371df6495641bb494bfb18829fe8 klish-7c5f1c4045d9a868dea547bd7ef0143bff5a84d7-git4.tar.gz
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
KLISH_VERSION = 4fd82287cfe74c6bbafea8d6c0ba8d8b4c65cdd0
|
KLISH_VERSION = 7c5f1c4045d9a868dea547bd7ef0143bff5a84d7
|
||||||
KLISH_SITE = https://github.com/kernelkit/klish.git
|
KLISH_SITE = https://github.com/kernelkit/klish.git
|
||||||
#KLISH_VERSION = tags/3.0.0
|
#KLISH_VERSION = tags/3.0.0
|
||||||
#KLISH_SITE = https://src.libcode.org/pkun/klish.git
|
#KLISH_SITE = https://src.libcode.org/pkun/klish.git
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# Overrides for config file /etc/klish/klishd.conf
|
# Overrides for config file /etc/klish/klishd.conf
|
||||||
|
|
||||||
|
# The klishd uses UNIX domain socket to receive connections. It will create an
|
||||||
|
# filesystem entry to allow clients to find connection point. By default klishd
|
||||||
|
# uses /tmp/klish-unix-socket path.
|
||||||
UnixSocketPath=/run/klishd.sock
|
UnixSocketPath=/run/klishd.sock
|
||||||
|
|
||||||
DBs=libxml2
|
DBs=libxml2
|
||||||
|
|
||||||
|
# The group to set on the UNIX socket. By default, the socket retains the group
|
||||||
|
# of the user starting the daemon.
|
||||||
|
SocketGroup=klish
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
config BR2_PACKAGE_SHOW
|
|
||||||
bool "show"
|
|
||||||
help
|
|
||||||
Tool to retrieve and present operational data from sysrepo.
|
|
||||||
|
|
||||||
https://github.com/kernelkit/infix
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
################################################################################
|
|
||||||
#
|
|
||||||
# show
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
SHOW_VERSION = 1.0
|
|
||||||
SHOW_LICENSE = MIT
|
|
||||||
SHOW_LICENSE_FILES = LICENSE
|
|
||||||
SHOW_SITE_METHOD = local
|
|
||||||
SHOW_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/show
|
|
||||||
SHOW_REDISTRIBUTE = NO
|
|
||||||
|
|
||||||
define SHOW_INSTALL_TARGET_CMDS
|
|
||||||
$(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) $(MAKE) -C $(@D) \
|
|
||||||
DESTDIR="$(TARGET_DIR)" install
|
|
||||||
endef
|
|
||||||
|
|
||||||
$(eval $(generic-package))
|
|
||||||
@@ -20,7 +20,8 @@ backup:x:34:
|
|||||||
utmp:x:43:
|
utmp:x:43:
|
||||||
plugdev:x:46:
|
plugdev:x:46:
|
||||||
lock:x:54:
|
lock:x:54:
|
||||||
sys-cli:x:60:
|
sysrepo:x:60:
|
||||||
|
klish:x:70:
|
||||||
netdev:x:82:
|
netdev:x:82:
|
||||||
users:x:100:
|
users:x:100:
|
||||||
nobody:x:65534:
|
nobody:x:65534:
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ sync:x:4:100:sync:/bin:/bin/sync
|
|||||||
mail:x:8:8:mail:/var/spool/mail:/bin/false
|
mail:x:8:8:mail:/var/spool/mail:/bin/false
|
||||||
www-data:x:33:33:www-data:/var/www:/bin/false
|
www-data:x:33:33:www-data:/var/www:/bin/false
|
||||||
backup:x:34:34:backup:/var/backups:/bin/false
|
backup:x:34:34:backup:/var/backups:/bin/false
|
||||||
sys-cli:x:60:60:CLI capability:/var:/bin/false
|
sysrepo:x:60:60:sysrepo:/var:/bin/false
|
||||||
|
klish:x:70:70:CLI capability:/var:/bin/false
|
||||||
nobody:x:65534:65534:nobody:/home:/bin/false
|
nobody:x:65534:65534:nobody:/home:/bin/false
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ sys:*:::::::
|
|||||||
sync:*:::::::
|
sync:*:::::::
|
||||||
mail:*:::::::
|
mail:*:::::::
|
||||||
www-data:*:::::::
|
www-data:*:::::::
|
||||||
sys-cli:*:::::::
|
sysrepo:*:::::::
|
||||||
|
klish:*:::::::
|
||||||
backup:*:::::::
|
backup:*:::::::
|
||||||
nobody:*:::::::
|
nobody:*:::::::
|
||||||
|
|||||||
+1
-5
@@ -1,15 +1,11 @@
|
|||||||
From efe7706fd7397c2feb384afea00ee97e74287df0 Mon Sep 17 00:00:00 2001
|
From efe7706fd7397c2feb384afea00ee97e74287df0 Mon Sep 17 00:00:00 2001
|
||||||
From: Joachim Wiberg <troglobit@gmail.com>
|
From: Joachim Wiberg <troglobit@gmail.com>
|
||||||
Date: Tue, 28 Mar 2023 10:37:53 +0200
|
Date: Tue, 28 Mar 2023 10:37:53 +0200
|
||||||
Subject: [PATCH 1/8] sysrepo-plugind: add support for running in foreground
|
Subject: [PATCH 1/9] sysrepo-plugind: add support for running in foreground
|
||||||
with syslog
|
with syslog
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: text/plain; charset=UTF-8
|
|
||||||
Content-Transfer-Encoding: 8bit
|
|
||||||
Organization: Wires
|
Organization: Wires
|
||||||
|
|
||||||
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
||||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
|
||||||
---
|
---
|
||||||
src/executables/sysrepo-plugind.c | 10 +++++++++-
|
src/executables/sysrepo-plugind.c | 10 +++++++++-
|
||||||
1 file changed, 9 insertions(+), 1 deletion(-)
|
1 file changed, 9 insertions(+), 1 deletion(-)
|
||||||
|
|||||||
+1
-5
@@ -1,10 +1,7 @@
|
|||||||
From 11b9938206cf2bafc456bb22e14c7f85a604760c Mon Sep 17 00:00:00 2001
|
From 11b9938206cf2bafc456bb22e14c7f85a604760c Mon Sep 17 00:00:00 2001
|
||||||
From: Joachim Wiberg <troglobit@gmail.com>
|
From: Joachim Wiberg <troglobit@gmail.com>
|
||||||
Date: Tue, 7 May 2024 15:41:53 +0200
|
Date: Tue, 7 May 2024 15:41:53 +0200
|
||||||
Subject: [PATCH 2/8] Allow SR_EV_DONE to return any error to sysrepocfg
|
Subject: [PATCH 2/9] Allow SR_EV_DONE to return any error to sysrepocfg
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: text/plain; charset=UTF-8
|
|
||||||
Content-Transfer-Encoding: 8bit
|
|
||||||
Organization: Wires
|
Organization: Wires
|
||||||
|
|
||||||
Importing a system configuration with sysrepocfg the model callbacks do
|
Importing a system configuration with sysrepocfg the model callbacks do
|
||||||
@@ -20,7 +17,6 @@ This patch is a clumsy way of forcing the (first) error to bubble up to
|
|||||||
the surface and cause a non-zero exit code from sysrepocfg.
|
the surface and cause a non-zero exit code from sysrepocfg.
|
||||||
|
|
||||||
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
||||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
|
||||||
---
|
---
|
||||||
src/shm_sub.c | 40 +++++++++++++++++++++++++++++++---------
|
src/shm_sub.c | 40 +++++++++++++++++++++++++++++++---------
|
||||||
src/shm_sub.h | 2 +-
|
src/shm_sub.h | 2 +-
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
From 9e0267d4f20733b2a26df6d0ee0bc4019db8b13f Mon Sep 17 00:00:00 2001
|
From 9e0267d4f20733b2a26df6d0ee0bc4019db8b13f Mon Sep 17 00:00:00 2001
|
||||||
From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= <lazzer@gmail.com>
|
From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= <lazzer@gmail.com>
|
||||||
Date: Wed, 8 May 2024 17:00:50 +0200
|
Date: Wed, 8 May 2024 17:00:50 +0200
|
||||||
Subject: [PATCH 3/8] Allow to copy from factory default
|
Subject: [PATCH 3/9] Allow to copy from factory default
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: text/plain; charset=UTF-8
|
|
||||||
Content-Transfer-Encoding: 8bit
|
|
||||||
Organization: Wires
|
Organization: Wires
|
||||||
|
|
||||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
||||||
---
|
---
|
||||||
src/sysrepo.c | 2 +-
|
src/sysrepo.c | 2 +-
|
||||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||||
|
|||||||
+2
-5
@@ -1,16 +1,13 @@
|
|||||||
From e0c899ba266b959544d7cc08c917cebba7ac91c7 Mon Sep 17 00:00:00 2001
|
From e0c899ba266b959544d7cc08c917cebba7ac91c7 Mon Sep 17 00:00:00 2001
|
||||||
From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= <lazzer@gmail.com>
|
From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= <lazzer@gmail.com>
|
||||||
Date: Mon, 6 May 2024 14:49:32 +0200
|
Date: Mon, 6 May 2024 14:49:32 +0200
|
||||||
Subject: [PATCH 4/8] Add -z switch to sysrepoctl to install factory config
|
Subject: [PATCH 4/9] Add -z switch to sysrepoctl to install factory config
|
||||||
from a json file
|
from a json file
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: text/plain; charset=UTF-8
|
|
||||||
Content-Transfer-Encoding: 8bit
|
|
||||||
Organization: Wires
|
Organization: Wires
|
||||||
|
|
||||||
This to be able to load the yang modules during build time instead on boot.
|
This to be able to load the yang modules during build time instead on boot.
|
||||||
|
|
||||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
||||||
---
|
---
|
||||||
src/executables/sysrepoctl.c | 20 +++++++++++++--
|
src/executables/sysrepoctl.c | 20 +++++++++++++--
|
||||||
src/lyd_mods.h | 7 ++++++
|
src/lyd_mods.h | 7 ++++++
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
From c7602dc8eabb941e0a163208aaf4de92dd5ef526 Mon Sep 17 00:00:00 2001
|
From c7602dc8eabb941e0a163208aaf4de92dd5ef526 Mon Sep 17 00:00:00 2001
|
||||||
From: Joachim Wiberg <troglobit@gmail.com>
|
From: Joachim Wiberg <troglobit@gmail.com>
|
||||||
Date: Wed, 21 Aug 2024 16:00:35 +0200
|
Date: Wed, 21 Aug 2024 16:00:35 +0200
|
||||||
Subject: [PATCH 5/8] Introduce new log level [SEC] for audit trails
|
Subject: [PATCH 5/9] Introduce new log level [SEC] for audit trails
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: text/plain; charset=UTF-8
|
|
||||||
Content-Transfer-Encoding: 8bit
|
|
||||||
Organization: Wires
|
Organization: Wires
|
||||||
|
|
||||||
This adds a new log level for security and audit trail related log
|
This adds a new log level for security and audit trail related log
|
||||||
@@ -22,7 +19,6 @@ system log daemon, dropping any [SEVERITY] prefix. Also, \n is most
|
|||||||
often dropped by log daemons.
|
often dropped by log daemons.
|
||||||
|
|
||||||
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
||||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
|
||||||
---
|
---
|
||||||
src/log.c | 18 +++++++++++++++++-
|
src/log.c | 18 +++++++++++++++++-
|
||||||
src/log.h | 1 +
|
src/log.h | 1 +
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
From a86dfdd4a5cb74c1f8c90c8d5aea6f5505c1b88c Mon Sep 17 00:00:00 2001
|
From a86dfdd4a5cb74c1f8c90c8d5aea6f5505c1b88c Mon Sep 17 00:00:00 2001
|
||||||
From: Joachim Wiberg <troglobit@gmail.com>
|
From: Joachim Wiberg <troglobit@gmail.com>
|
||||||
Date: Wed, 21 Aug 2024 16:04:43 +0200
|
Date: Wed, 21 Aug 2024 16:04:43 +0200
|
||||||
Subject: [PATCH 6/8] Add audit trail for high priority system changes
|
Subject: [PATCH 6/9] Add audit trail for high priority system changes
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: text/plain; charset=UTF-8
|
|
||||||
Content-Transfer-Encoding: 8bit
|
|
||||||
Organization: Wires
|
Organization: Wires
|
||||||
|
|
||||||
Committing a change to running, copying to a datastore, or calling an
|
Committing a change to running, copying to a datastore, or calling an
|
||||||
@@ -16,7 +13,6 @@ is when the system actually activates the changes. Copying to startup
|
|||||||
or other datastores is handled separately.
|
or other datastores is handled separately.
|
||||||
|
|
||||||
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
||||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
|
||||||
---
|
---
|
||||||
src/sysrepo.c | 12 ++++++++++++
|
src/sysrepo.c | 12 ++++++++++++
|
||||||
1 file changed, 12 insertions(+)
|
1 file changed, 12 insertions(+)
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
From dbf08c67d8f17bdf98466b18fd72a230269e5d46 Mon Sep 17 00:00:00 2001
|
From dbf08c67d8f17bdf98466b18fd72a230269e5d46 Mon Sep 17 00:00:00 2001
|
||||||
From: Joachim Wiberg <troglobit@gmail.com>
|
From: Joachim Wiberg <troglobit@gmail.com>
|
||||||
Date: Fri, 23 Aug 2024 12:22:06 +0200
|
Date: Fri, 23 Aug 2024 12:22:06 +0200
|
||||||
Subject: [PATCH 7/8] On error in sr_shmsub_listen_thread(), exit process
|
Subject: [PATCH 7/9] On error in sr_shmsub_listen_thread(), exit process
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: text/plain; charset=UTF-8
|
|
||||||
Content-Transfer-Encoding: 8bit
|
|
||||||
Organization: Wires
|
Organization: Wires
|
||||||
|
|
||||||
If processing callback events in, e.g., sysrepo-plugind, make sure to
|
If processing callback events in, e.g., sysrepo-plugind, make sure to
|
||||||
@@ -12,7 +9,6 @@ log the error and exit(1) the entire process so the system can decide
|
|||||||
to handle the problem. For example, restart all dependent services.
|
to handle the problem. For example, restart all dependent services.
|
||||||
|
|
||||||
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
||||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
|
||||||
---
|
---
|
||||||
src/shm_sub.c | 2 ++
|
src/shm_sub.c | 2 ++
|
||||||
1 file changed, 2 insertions(+)
|
1 file changed, 2 insertions(+)
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
From 2549c966c090dd38a7a09907d27d13107d15aedd Mon Sep 17 00:00:00 2001
|
From 2549c966c090dd38a7a09907d27d13107d15aedd Mon Sep 17 00:00:00 2001
|
||||||
From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= <lazzer@gmail.com>
|
From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= <lazzer@gmail.com>
|
||||||
Date: Tue, 16 Dec 2025 08:18:32 +0100
|
Date: Tue, 16 Dec 2025 08:18:32 +0100
|
||||||
Subject: [PATCH 8/8] Cross compile fixes
|
Subject: [PATCH 8/9] Cross compile fixes
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: text/plain; charset=UTF-8
|
|
||||||
Content-Transfer-Encoding: 8bit
|
|
||||||
Organization: Wires
|
Organization: Wires
|
||||||
|
|
||||||
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
||||||
---
|
---
|
||||||
CMakeModules/SetupPrintedContext.cmake | 9 +++++++++
|
CMakeModules/SetupPrintedContext.cmake | 9 +++++++++
|
||||||
1 file changed, 9 insertions(+)
|
1 file changed, 9 insertions(+)
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
From 78d62382bf9d665764844a0f686b27e42d73bea9 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Joachim Wiberg <troglobit@gmail.com>
|
||||||
|
Date: Wed, 7 Jan 2026 18:09:32 +0100
|
||||||
|
Subject: [PATCH 9/9] sr_mkfifo(): set sysrepo group if available
|
||||||
|
Organization: Wires
|
||||||
|
|
||||||
|
We already set the umask, set the group to allow users of the sysrepo
|
||||||
|
group to initiate events.
|
||||||
|
|
||||||
|
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
||||||
|
---
|
||||||
|
src/common.c | 12 ++++++++++++
|
||||||
|
1 file changed, 12 insertions(+)
|
||||||
|
|
||||||
|
diff --git a/src/common.c b/src/common.c
|
||||||
|
index 447fbd28..ad39f0ef 100644
|
||||||
|
--- a/src/common.c
|
||||||
|
+++ b/src/common.c
|
||||||
|
@@ -3883,6 +3883,7 @@ sr_error_info_t *
|
||||||
|
sr_mkfifo(const char *path, mode_t mode)
|
||||||
|
{
|
||||||
|
sr_error_info_t *err_info = NULL;
|
||||||
|
+ gid_t gid;
|
||||||
|
|
||||||
|
/* apply umask on mode */
|
||||||
|
mode &= ~SR_UMASK;
|
||||||
|
@@ -3900,6 +3901,17 @@ sr_mkfifo(const char *path, mode_t mode)
|
||||||
|
return err_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ /* and group, if any */
|
||||||
|
+ if (sr_is_prod_env() && strlen(SR_GROUP)) {
|
||||||
|
+ if ((err_info = sr_get_gid(SR_GROUP, &gid)))
|
||||||
|
+ return err_info;
|
||||||
|
+
|
||||||
|
+ if (chown(path, -1, gid) == -1) {
|
||||||
|
+ SR_ERRINFO_SYSERRNO(&err_info, "chown");
|
||||||
|
+ return err_info;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
--
|
||||||
|
2.43.0
|
||||||
|
|
||||||
@@ -25,3 +25,7 @@ files_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE
|
|||||||
files_CFLAGS = -W -Wall -Wextra
|
files_CFLAGS = -W -Wall -Wextra
|
||||||
files_CFLAGS += $(libite_CFLAGS) $(sysrepo_CFLAGS)
|
files_CFLAGS += $(libite_CFLAGS) $(sysrepo_CFLAGS)
|
||||||
files_LDADD = $(libite_LIBS) $(sysrepo_LIBS)
|
files_LDADD = $(libite_LIBS) $(sysrepo_LIBS)
|
||||||
|
|
||||||
|
# Create rpc symlink for multicall binary
|
||||||
|
install-exec-hook:
|
||||||
|
cd $(DESTDIR)$(bindir) && $(LN_S) -f copy$(EXEEXT) rpc$(EXEEXT)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ AC_CONFIG_FILES([
|
|||||||
|
|
||||||
AC_PROG_CC
|
AC_PROG_CC
|
||||||
AC_PROG_INSTALL
|
AC_PROG_INSTALL
|
||||||
|
AC_PROG_LN_S
|
||||||
|
|
||||||
# Check for pkg-config first, warn if it's not installed
|
# Check for pkg-config first, warn if it's not installed
|
||||||
PKG_PROG_PKG_CONFIG
|
PKG_PROG_PKG_CONFIG
|
||||||
|
|||||||
+343
-100
@@ -14,9 +14,16 @@
|
|||||||
|
|
||||||
#include <sysrepo.h>
|
#include <sysrepo.h>
|
||||||
#include <sysrepo/netconf_acm.h>
|
#include <sysrepo/netconf_acm.h>
|
||||||
|
#include <sysrepo/values.h>
|
||||||
|
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
|
||||||
|
#define err(rc, fmt, args...) { fprintf(stderr, ERRMSG fmt ":%s\n", ##args, strerror(errno)); exit(rc); }
|
||||||
|
#define errx(rc, fmt, args...) { fprintf(stderr, ERRMSG fmt "\n", ##args); exit(rc); }
|
||||||
|
#define warnx(fmt, args...) fprintf(stderr, ERRMSG fmt "\n", ##args)
|
||||||
|
#define warn(fmt, args...) fprintf(stderr, ERRMSG fmt ":%s\n", ##args, strerror(errno))
|
||||||
|
#define dbg(fmt, args...) if (debug) fprintf(stderr, DBGMSG fmt "\n", ##args)
|
||||||
|
|
||||||
struct infix_ds {
|
struct infix_ds {
|
||||||
char *name; /* startup-config, etc. */
|
char *name; /* startup-config, etc. */
|
||||||
int datastore; /* sr_datastore_t and -1 */
|
int datastore; /* sr_datastore_t and -1 */
|
||||||
@@ -32,26 +39,28 @@ const struct infix_ds infix_config[] = {
|
|||||||
{ "factory-config", SR_DS_FACTORY_DEFAULT, false, NULL }
|
{ "factory-config", SR_DS_FACTORY_DEFAULT, false, NULL }
|
||||||
};
|
};
|
||||||
|
|
||||||
static const char *prognm = "copy";
|
static const char *prognm;
|
||||||
static const char *remote_user;
|
static const char *remote_user;
|
||||||
|
static char *xpath = "/*";
|
||||||
|
static int debug;
|
||||||
|
static int force;
|
||||||
static int timeout;
|
static int timeout;
|
||||||
static int dry_run;
|
static int dry_run;
|
||||||
static int sanitize;
|
static int sanitize;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Current system user, same as sysrepo user
|
* Current system user, same as sysrepo user. We use getuid() here
|
||||||
|
* because `copy` is SUID root to work around sysrepo issues with a
|
||||||
|
* /dev/shm that's moounted 01777.
|
||||||
*/
|
*/
|
||||||
static const char *getuser(void)
|
static const char *getuser(void)
|
||||||
{
|
{
|
||||||
const struct passwd *pw;
|
const struct passwd *pw;
|
||||||
uid_t uid;
|
uid_t uid = getuid();
|
||||||
|
|
||||||
uid = getuid();
|
|
||||||
pw = getpwuid(uid);
|
pw = getpwuid(uid);
|
||||||
if (!pw) {
|
if (!pw)
|
||||||
perror("getpwuid");
|
err(1, "failed querying user info for uid %d", uid);
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return pw->pw_name;
|
return pw->pw_name;
|
||||||
}
|
}
|
||||||
@@ -86,7 +95,7 @@ static int in_group(const char *user, const char *fn, gid_t *gid)
|
|||||||
num = NGROUPS_MAX;
|
num = NGROUPS_MAX;
|
||||||
groups = malloc(num * sizeof(gid_t));
|
groups = malloc(num * sizeof(gid_t));
|
||||||
if (!groups) {
|
if (!groups) {
|
||||||
perror("in_group() malloc");
|
warn("failed in_group()");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,11 +136,10 @@ static void set_owner(const char *fn, const char *user)
|
|||||||
return; /* user not in parent directory's group */
|
return; /* user not in parent directory's group */
|
||||||
|
|
||||||
if (chown(fn, -1, gid) && errno != EPERM) {
|
if (chown(fn, -1, gid) && errno != EPERM) {
|
||||||
int _errno = errno;
|
|
||||||
const struct group *gr = getgrgid(gid);
|
const struct group *gr = getgrgid(gid);
|
||||||
|
|
||||||
fprintf(stderr, ERRMSG "setting group owner %s (%d) on %s: %s\n",
|
warn("setting group owner %s (%d) on %s",
|
||||||
gr ? gr->gr_name : "<unknown>", gid, fn, strerror(_errno));
|
gr ? gr->gr_name : "<unknown>", gid, fn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +163,16 @@ static bool is_uri(const char *str)
|
|||||||
return strstr(str, "://") != NULL;
|
return strstr(str, "://") != NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool is_stdout(const char *path)
|
||||||
|
{
|
||||||
|
if (!path)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
return !strcmp(path, "-") ||
|
||||||
|
!strcmp(path, "/dev/stdout") ||
|
||||||
|
!strcmp(path, "/dev/fd/1");
|
||||||
|
}
|
||||||
|
|
||||||
static char *mktmp(void)
|
static char *mktmp(void)
|
||||||
{
|
{
|
||||||
mode_t oldmask;
|
mode_t oldmask;
|
||||||
@@ -172,6 +190,9 @@ static char *mktmp(void)
|
|||||||
if (fd < 0)
|
if (fd < 0)
|
||||||
goto err;
|
goto err;
|
||||||
|
|
||||||
|
if (chown(path, getuid(), -1))
|
||||||
|
dbg("Failed to chown %s: %s", path, strerror(errno));
|
||||||
|
|
||||||
close(fd);
|
close(fd);
|
||||||
return path;
|
return path;
|
||||||
err:
|
err:
|
||||||
@@ -185,29 +206,73 @@ static void rmtmp(const char *path)
|
|||||||
if (errno == ENOENT)
|
if (errno == ENOENT)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
fprintf(stderr, ERRMSG "removal of temporary file %s failed\n", path);
|
warn("failed removing temporary file %s", path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void sysrepo_print_error(sr_session_ctx_t *sess)
|
static void sysrepo_print_error(sr_session_ctx_t *sess)
|
||||||
{
|
{
|
||||||
const sr_error_info_t *erri = NULL;
|
const sr_error_info_t *erri = NULL;
|
||||||
int err;
|
int err;
|
||||||
|
|
||||||
|
if (!sess)
|
||||||
|
return;
|
||||||
|
|
||||||
err = sr_session_get_error(sess, &erri);
|
err = sr_session_get_error(sess, &erri);
|
||||||
if (err || !erri || !erri->err_count)
|
if (err || !erri || !erri->err_count)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
fprintf(stderr, ERRMSG "%s (%d)\n", erri->err->message, erri->err->err_code);
|
warnx("%s (%d)", erri->err->message, erri->err->err_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Connect to sysrepo and create NACM-aware session on running datastore */
|
||||||
|
static int sysrepo_init(sr_conn_ctx_t **conn, sr_session_ctx_t **sess,
|
||||||
|
sr_subscription_ctx_t **sub)
|
||||||
|
{
|
||||||
|
const char *user = getuser();
|
||||||
|
int err;
|
||||||
|
|
||||||
|
err = sr_connect(SR_CONN_DEFAULT, conn);
|
||||||
|
if (err != SR_ERR_OK) {
|
||||||
|
warnx("failed connecting to sysrepo: %s", sr_strerror(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Always open running, because sr_nacm_init() does not work
|
||||||
|
* against the factory DS.
|
||||||
|
*/
|
||||||
|
err = sr_session_start(*conn, SR_DS_RUNNING, sess);
|
||||||
|
if (err != SR_ERR_OK) {
|
||||||
|
warnx("failed starting session: %s", sr_strerror(err));
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sr_nacm_init(*sess, 0, sub);
|
||||||
|
if (err != SR_ERR_OK) {
|
||||||
|
warnx("NACM init failed: %s", sr_strerror(err));
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbg("Setting NACM user %s for session", user);
|
||||||
|
err = sr_nacm_set_user(*sess, user);
|
||||||
|
if (err != SR_ERR_OK) {
|
||||||
|
warnx("NACM setup failed for user %s: %s", user, sr_strerror(err));
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SR_ERR_OK;
|
||||||
|
fail:
|
||||||
|
sysrepo_print_error(*sess);
|
||||||
|
sr_session_stop(*sess);
|
||||||
|
sr_disconnect(*conn);
|
||||||
|
|
||||||
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
static sr_session_ctx_t *sysrepo_session(const struct infix_ds *ds)
|
static sr_session_ctx_t *sysrepo_session(const struct infix_ds *ds)
|
||||||
{
|
{
|
||||||
|
static sr_subscription_ctx_t *sub = NULL;
|
||||||
static sr_session_ctx_t *sess;
|
static sr_session_ctx_t *sess;
|
||||||
|
|
||||||
sr_subscription_ctx_t *sub = NULL;
|
|
||||||
const char *user = getuser();
|
|
||||||
sr_conn_ctx_t *conn = NULL;
|
sr_conn_ctx_t *conn = NULL;
|
||||||
int err;
|
int err;
|
||||||
|
|
||||||
@@ -218,88 +283,58 @@ static sr_session_ctx_t *sysrepo_session(const struct infix_ds *ds)
|
|||||||
conn = sr_session_get_connection(sess);
|
conn = sr_session_get_connection(sess);
|
||||||
sr_session_stop(sess);
|
sr_session_stop(sess);
|
||||||
sr_disconnect(conn);
|
sr_disconnect(conn);
|
||||||
|
sess = NULL;
|
||||||
|
sub = NULL;
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sess) {
|
if (!sess) {
|
||||||
err = sr_connect(0, &conn);
|
err = sysrepo_init(&conn, &sess, &sub);
|
||||||
if (err != SR_ERR_OK) {
|
if (err != SR_ERR_OK) {
|
||||||
sysrepo_print_error(sess);
|
warnx("Failed to initialize session for %s", ds->name);
|
||||||
fprintf(stderr, ERRMSG "could not connect to %s\n", ds->name);
|
return NULL;
|
||||||
goto err;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Always open running, because sr_nacm_init() does not work
|
|
||||||
* against the factory DS.
|
|
||||||
*/
|
|
||||||
err = sr_session_start(conn, SR_DS_RUNNING, &sess);
|
|
||||||
if (err != SR_ERR_OK) {
|
|
||||||
sysrepo_print_error(sess);
|
|
||||||
fprintf(stderr, ERRMSG "%s session setup failed\n", ds->name);
|
|
||||||
goto err_disconnect;
|
|
||||||
}
|
|
||||||
|
|
||||||
err = sr_nacm_init(sess, 0, &sub);
|
|
||||||
if (err != SR_ERR_OK) {
|
|
||||||
sysrepo_print_error(sess);
|
|
||||||
fprintf(stderr, ERRMSG "%s NACM setup failed\n", ds->name);
|
|
||||||
goto err_stop;
|
|
||||||
}
|
|
||||||
|
|
||||||
err = sr_nacm_set_user(sess, user);
|
|
||||||
if (err != SR_ERR_OK) {
|
|
||||||
sysrepo_print_error(sess);
|
|
||||||
fprintf(stderr, ERRMSG "%s NACM setup for %s failed\n", ds->name, user);
|
|
||||||
goto err_nacm_destroy;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = sr_session_switch_ds(sess, ds->datastore);
|
err = sr_session_switch_ds(sess, ds->datastore);
|
||||||
if (err) {
|
if (err) {
|
||||||
sysrepo_print_error(sess);
|
sysrepo_print_error(sess);
|
||||||
fprintf(stderr, ERRMSG "%s activation failed\n", ds->name);
|
warnx("%s activation failed", ds->name);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sess;
|
return sess;
|
||||||
|
|
||||||
err_nacm_destroy:
|
|
||||||
sr_nacm_destroy();
|
|
||||||
err_stop:
|
|
||||||
sr_session_stop(sess);
|
|
||||||
err_disconnect:
|
|
||||||
sr_disconnect(conn);
|
|
||||||
err:
|
|
||||||
sess = NULL;
|
|
||||||
return NULL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int sysrepo_export(const struct infix_ds *ds, const char *path)
|
static int sysrepo_export(const struct infix_ds *ds, const char *path)
|
||||||
{
|
{
|
||||||
sr_session_ctx_t *sess;
|
sr_session_ctx_t *sess;
|
||||||
sr_data_t *data;
|
sr_data_t *data = NULL;
|
||||||
int err;
|
int err;
|
||||||
|
|
||||||
sess = sysrepo_session(ds);
|
sess = sysrepo_session(ds);
|
||||||
if (!sess)
|
if (!sess)
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
err = sr_get_data(sess, "/*", 0, timeout * 1000, SR_OPER_DEFAULT, &data);
|
err = sr_get_data(sess, xpath, 0, timeout * 1000, SR_OPER_DEFAULT, &data);
|
||||||
if (err) {
|
if (err) {
|
||||||
sysrepo_print_error(sess);
|
sysrepo_print_error(sess);
|
||||||
fprintf(stderr, ERRMSG "retrieval of %s data failed\n", ds->name);
|
warnx("failed retrieving %s data", ds->name);
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!data)
|
||||||
|
return 0;
|
||||||
|
|
||||||
err = lyd_print_path(path, data->tree, LYD_JSON, LYD_PRINT_SIBLINGS);
|
err = lyd_print_path(path, data->tree, LYD_JSON, LYD_PRINT_SIBLINGS);
|
||||||
sr_release_data(data);
|
sr_release_data(data);
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
sysrepo_print_error(sess);
|
sysrepo_print_error(sess);
|
||||||
fprintf(stderr, ERRMSG "failed to store %s data\n", ds->name);
|
warnx("failed storing %s data", ds->name);
|
||||||
return err;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int sysrepo_import(const struct infix_ds *ds, const char *path)
|
static int sysrepo_import(const struct infix_ds *ds, const char *path)
|
||||||
@@ -319,14 +354,14 @@ static int sysrepo_import(const struct infix_ds *ds, const char *path)
|
|||||||
LYD_PARSE_NO_STATE | LYD_PARSE_ONLY |
|
LYD_PARSE_NO_STATE | LYD_PARSE_ONLY |
|
||||||
LYD_PARSE_STORE_ONLY | LYD_PARSE_STRICT, 0, &data);
|
LYD_PARSE_STORE_ONLY | LYD_PARSE_STRICT, 0, &data);
|
||||||
if (err) {
|
if (err) {
|
||||||
fprintf(stderr, ERRMSG "failed to parse %s data\n", ds->name);
|
warnx("failed parsing %s data", ds->name);
|
||||||
goto out;
|
goto out;
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dry_run ? 0 : sr_replace_config(sess, NULL, data, timeout * 1000);
|
err = dry_run ? 0 : sr_replace_config(sess, NULL, data, timeout * 1000);
|
||||||
if (err) {
|
if (err) {
|
||||||
sysrepo_print_error(sess);
|
sysrepo_print_error(sess);
|
||||||
fprintf(stderr, ERRMSG "failed import %s data\n", ds->name);
|
warnx("failed importing %s data, error %d", ds->name, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
out:
|
out:
|
||||||
@@ -390,7 +425,7 @@ static int curl_upload(const char *srcpath, const char *uri)
|
|||||||
char upload[] = "-T";
|
char upload[] = "-T";
|
||||||
|
|
||||||
if (curl(upload, srcpath, uri)) {
|
if (curl(upload, srcpath, uri)) {
|
||||||
fprintf(stderr, ERRMSG "upload to %s failed\n", uri);
|
warnx("upload to %s failed", uri);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,15 +435,33 @@ static int curl_upload(const char *srcpath, const char *uri)
|
|||||||
static int curl_download(const char *uri, const char *dstpath)
|
static int curl_download(const char *uri, const char *dstpath)
|
||||||
{
|
{
|
||||||
char download[] = "-o";
|
char download[] = "-o";
|
||||||
|
int err;
|
||||||
|
|
||||||
if (curl(download, dstpath, uri)) {
|
if ((err = curl(download, dstpath, uri))) {
|
||||||
fprintf(stderr, ERRMSG "download of %s failed\n", uri);
|
warnx("download of %s failed, exit code %d", uri, err);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int cat(const char *srcpath)
|
||||||
|
{
|
||||||
|
char *argv[] = { "cat", NULL, NULL };
|
||||||
|
int err;
|
||||||
|
|
||||||
|
argv[1] = strdup(srcpath);
|
||||||
|
if (!argv[1])
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
err = subprocess(argv);
|
||||||
|
if (err)
|
||||||
|
warnx("failed writing to stdout, exit code %d", err);
|
||||||
|
|
||||||
|
free(argv[1]);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
static int cp(const char *srcpath, const char *dstpath)
|
static int cp(const char *srcpath, const char *dstpath)
|
||||||
{
|
{
|
||||||
char *argv[] = {
|
char *argv[] = {
|
||||||
@@ -423,7 +476,7 @@ static int cp(const char *srcpath, const char *dstpath)
|
|||||||
|
|
||||||
err = subprocess(argv);
|
err = subprocess(argv);
|
||||||
if (err)
|
if (err)
|
||||||
fprintf(stderr, ERRMSG "failed to save %s\n", dstpath);
|
warnx("failed to save %s, exit code %d", dstpath, err);
|
||||||
out:
|
out:
|
||||||
free(argv[2]);
|
free(argv[2]);
|
||||||
free(argv[1]);
|
free(argv[1]);
|
||||||
@@ -437,6 +490,8 @@ static int put(const char *srcpath, const char *dst,
|
|||||||
|
|
||||||
if (ds)
|
if (ds)
|
||||||
err = sysrepo_import(ds, srcpath);
|
err = sysrepo_import(ds, srcpath);
|
||||||
|
else if (is_stdout(dst))
|
||||||
|
err = cat(srcpath);
|
||||||
else if (is_uri(dst))
|
else if (is_uri(dst))
|
||||||
err = curl_upload(srcpath, dst);
|
err = curl_upload(srcpath, dst);
|
||||||
|
|
||||||
@@ -480,7 +535,7 @@ static int resolve_src(const char **src, const struct infix_ds **ds, char **path
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!*path) {
|
if (!*path) {
|
||||||
fprintf(stderr, ERRMSG "no such file %s.", *src);
|
warn("no such file %s", *src);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,11 +545,14 @@ static int resolve_src(const char **src, const struct infix_ds **ds, char **path
|
|||||||
|
|
||||||
static int resolve_dst(const char **dst, const struct infix_ds **ds, char **path)
|
static int resolve_dst(const char **dst, const struct infix_ds **ds, char **path)
|
||||||
{
|
{
|
||||||
|
if (is_stdout(*dst) || is_uri(*dst))
|
||||||
|
return 0;
|
||||||
|
|
||||||
*dst = infix_ds(*dst, ds);
|
*dst = infix_ds(*dst, ds);
|
||||||
|
|
||||||
if (*ds) {
|
if (*ds) {
|
||||||
if (!(*ds)->rw) {
|
if (!(*ds)->rw) {
|
||||||
fprintf(stderr, ERRMSG "%s is not writable", (*ds)->name);
|
warn("%s is not writable", (*ds)->name);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,19 +560,17 @@ static int resolve_dst(const char **dst, const struct infix_ds **ds, char **path
|
|||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
*path = strdup((*ds)->path);
|
*path = strdup((*ds)->path);
|
||||||
} else if (is_uri(*dst)) {
|
|
||||||
return 0;
|
|
||||||
} else {
|
} else {
|
||||||
*path = cfg_adjust(*dst, NULL, sanitize);
|
*path = cfg_adjust(*dst, NULL, sanitize);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!*path) {
|
if (!*path) {
|
||||||
fprintf(stderr, ERRMSG "no such file: %s", *dst);
|
warn("no such file: %s", *dst);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!*ds && !access(*path, F_OK) && !yorn("Overwrite existing file %s", *path)) {
|
if (!force && !*ds && !access(*path, F_OK) && !yorn("Overwrite existing file %s", *path)) {
|
||||||
fprintf(stderr, "OK, aborting.\n");
|
warnx("OK, aborting.");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,8 +588,8 @@ static int copy(const char *src, const char *dst)
|
|||||||
/* rw for user and group only */
|
/* rw for user and group only */
|
||||||
oldmask = umask(0006);
|
oldmask = umask(0006);
|
||||||
|
|
||||||
if (!strcmp(src, dst)) {
|
if (dst && !strcmp(src, dst)) {
|
||||||
fprintf(stderr, ERRMSG "source and destination are the same, aborting.\n");
|
warn("source and destination are the same, aborting.");
|
||||||
goto err;
|
goto err;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,7 +614,8 @@ err:
|
|||||||
if (rmsrc)
|
if (rmsrc)
|
||||||
rmtmp(srcpath);
|
rmtmp(srcpath);
|
||||||
|
|
||||||
free(dstpath);
|
if (dstpath)
|
||||||
|
free(dstpath);
|
||||||
free(srcpath);
|
free(srcpath);
|
||||||
|
|
||||||
sync();
|
sync();
|
||||||
@@ -568,38 +625,158 @@ err:
|
|||||||
|
|
||||||
static int usage(int rc)
|
static int usage(int rc)
|
||||||
{
|
{
|
||||||
printf("Usage: %s [OPTIONS] SRC DST\n"
|
printf("Usage: %s [OPTIONS] SRC [DST]\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Options:\n"
|
"Options:\n"
|
||||||
" -h This help text\n"
|
" -d Enable debug mode, verbose output on stderr\n"
|
||||||
" -n Dry-run, validate configuration without applying\n"
|
" -f Force yes when copying to a file that exists already\n"
|
||||||
" -s Sanitize paths for CLI use (restrict path traversal)\n"
|
" -h This help text\n"
|
||||||
" -t SEC Timeout for the operation, or default %d sec\n"
|
" -n Dry-run, validate configuration without applying\n"
|
||||||
" -u USER Username for remote commands, like scp\n"
|
" -s Sanitize paths for CLI use (restrict path traversal)\n"
|
||||||
" -v Show version\n"
|
" -t SEC Timeout for the operation, or default %d sec\n"
|
||||||
|
" -u USER Username for remote commands, like scp\n"
|
||||||
|
" -v Show version\n"
|
||||||
|
" -x PATH XPath to copy, default: all\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Files:\n"
|
"Files:\n"
|
||||||
" SRC JSON configuration file, or a datastore\n"
|
" SRC JSON configuration file, or a datastore\n"
|
||||||
" DST A file or datastore, except factory-config\n"
|
" DST Optiional file or datastore, except factory-config,\n"
|
||||||
|
" when omitted output goes to stdout\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Datastores:\n"
|
"Datastores (short forms possible):\n"
|
||||||
" running-config The running datastore, current active config\n"
|
" running-config The running datastore, current active config\n"
|
||||||
" startup-config The non-volatile config used at startup\n"
|
" startup-config The non-volatile config used at startup\n"
|
||||||
" factory-config The device's factory default configuration\n"
|
" factory-config The device's factory default configuration\n"
|
||||||
"\n", prognm, timeout);
|
" operational-state Operational status and state data"
|
||||||
|
"\n"
|
||||||
|
"Examples:\n"
|
||||||
|
" %s operational -x /system-state/software/boot-order\n"
|
||||||
|
"\n", prognm, timeout, prognm);
|
||||||
|
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
static int usage_rpc(int rc)
|
||||||
{
|
{
|
||||||
const char *src = NULL, *dst = NULL;
|
printf("Usage: %s [OPTIONS] <rpc-xpath> [key value ...]\n"
|
||||||
|
"\n"
|
||||||
|
"Execute a YANG RPC/action with NACM enforcement.\n"
|
||||||
|
"\n"
|
||||||
|
"Options:\n"
|
||||||
|
" -d Enable debug mode, verbose output on stderr\n"
|
||||||
|
" -h This help text\n"
|
||||||
|
" -t SEC Timeout for the operation, or default %d sec\n"
|
||||||
|
" -v Show version\n"
|
||||||
|
"\n"
|
||||||
|
"Arguments:\n"
|
||||||
|
" rpc-xpath RPC XPath (e.g., /ietf-system:set-current-datetime)\n"
|
||||||
|
" key value Pairs of RPC argument names and values\n"
|
||||||
|
" Values can be comma-separated for lists/leaf-lists\n"
|
||||||
|
"\n"
|
||||||
|
"Examples:\n"
|
||||||
|
" %s /ietf-system:set-current-datetime current-datetime \"2025-01-01T00:00:00Z\"\n"
|
||||||
|
" %s /infix-system:set-boot-order boot-order primary boot-order secondary\n"
|
||||||
|
" %s /infix-system:set-boot-order boot-order primary,secondary,net\n"
|
||||||
|
"\n", prognm, timeout, prognm, prognm, prognm);
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Execute RPC from CLI arguments: xpath and key-value pairs */
|
||||||
|
static int rpc_exec(const char *rpc_xpath, int argc, char *argv[])
|
||||||
|
{
|
||||||
|
sr_subscription_ctx_t *sub = NULL;
|
||||||
|
sr_conn_ctx_t *conn = NULL;
|
||||||
|
sr_session_ctx_t *sess = NULL;
|
||||||
|
sr_val_t *input = NULL;
|
||||||
|
sr_val_t *output = NULL;
|
||||||
|
size_t icnt = 0, ocnt = 0;
|
||||||
|
int rc = 1, err, i;
|
||||||
|
|
||||||
|
dbg("Executing RPC %s with %d arguments", rpc_xpath, argc / 2);
|
||||||
|
|
||||||
|
err = sysrepo_init(&conn, &sess, &sub);
|
||||||
|
if (err != SR_ERR_OK)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
for (i = 0; i < argc - 1; i += 2) {
|
||||||
|
const char *key = argv[i];
|
||||||
|
const char *val = argv[i + 1];
|
||||||
|
char *val_copy, *token, *saveptr;
|
||||||
|
|
||||||
|
/* Check if value contains commas - split into multiple values */
|
||||||
|
if (strchr(val, ',')) {
|
||||||
|
val_copy = strdup(val);
|
||||||
|
if (!val_copy) {
|
||||||
|
warnx("Memory allocation failed");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
token = strtok_r(val_copy, ",", &saveptr);
|
||||||
|
while (token) {
|
||||||
|
sr_realloc_values(icnt, icnt + 1, &input);
|
||||||
|
sr_val_build_xpath(&input[icnt], "%s/%s", rpc_xpath, key);
|
||||||
|
sr_val_set_str_data(&input[icnt], SR_STRING_T, token);
|
||||||
|
dbg("Adding RPC argument %zu: %s = %s", icnt, input[icnt].xpath, token);
|
||||||
|
icnt++;
|
||||||
|
token = strtok_r(NULL, ",", &saveptr);
|
||||||
|
}
|
||||||
|
free(val_copy);
|
||||||
|
} else {
|
||||||
|
/* Single value */
|
||||||
|
sr_realloc_values(icnt, icnt + 1, &input);
|
||||||
|
sr_val_build_xpath(&input[icnt], "%s/%s", rpc_xpath, key);
|
||||||
|
sr_val_set_str_data(&input[icnt], SR_STRING_T, val);
|
||||||
|
dbg("Adding RPC argument %zu: %s = %s", icnt, input[icnt].xpath, val);
|
||||||
|
icnt++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dbg("Sending RPC %s (timeout: %d ms)", rpc_xpath, timeout * 1000);
|
||||||
|
err = sr_rpc_send(sess, rpc_xpath, input, icnt, timeout * 1000, &output, &ocnt);
|
||||||
|
if (err != SR_ERR_OK) {
|
||||||
|
sysrepo_print_error(sess);
|
||||||
|
warnx("RPC execution failed: %s", sr_strerror(err));
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print output if any */
|
||||||
|
for (i = 0; i < (int)ocnt; i++) {
|
||||||
|
sr_print_val(&output[i]);
|
||||||
|
puts("");
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = 0;
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
sr_free_values(input, icnt);
|
||||||
|
sr_free_values(output, ocnt);
|
||||||
|
if (sub)
|
||||||
|
sr_nacm_destroy();
|
||||||
|
if (sess)
|
||||||
|
sr_session_stop(sess);
|
||||||
|
if (conn)
|
||||||
|
sr_disconnect(conn);
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int copy_main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
const char *dst = "/dev/stdout";
|
||||||
|
const char *src = NULL;
|
||||||
int c;
|
int c;
|
||||||
|
|
||||||
timeout = fgetint("/etc/default/confd", "=", "CONFD_TIMEOUT");
|
timeout = fgetint("/etc/default/confd", "=", "CONFD_TIMEOUT");
|
||||||
|
|
||||||
while ((c = getopt(argc, argv, "hnst:u:v")) != EOF) {
|
while ((c = getopt(argc, argv, "dfhnst:u:vx:")) != EOF) {
|
||||||
switch(c) {
|
switch(c) {
|
||||||
|
case 'd':
|
||||||
|
debug = 1;
|
||||||
|
break;
|
||||||
|
case 'f':
|
||||||
|
force = 1;
|
||||||
|
break;
|
||||||
case 'h':
|
case 'h':
|
||||||
return usage(0);
|
return usage(0);
|
||||||
case 'n':
|
case 'n':
|
||||||
@@ -617,17 +794,83 @@ int main(int argc, char *argv[])
|
|||||||
case 'v':
|
case 'v':
|
||||||
puts(PACKAGE_VERSION);
|
puts(PACKAGE_VERSION);
|
||||||
return 0;
|
return 0;
|
||||||
|
case 'x':
|
||||||
|
xpath = optarg;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeout < 0)
|
if (timeout < 0)
|
||||||
timeout = 120;
|
timeout = 120;
|
||||||
|
|
||||||
if (argc - optind != 2)
|
switch (argc - optind) {
|
||||||
|
case 2:
|
||||||
|
src = argv[optind++];
|
||||||
|
dst = argv[optind++];
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
src = argv[optind++];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
return usage(1);
|
return usage(1);
|
||||||
|
}
|
||||||
src = argv[optind++];
|
|
||||||
dst = argv[optind++];
|
|
||||||
|
|
||||||
return copy(src, dst);
|
return copy(src, dst);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int rpc_main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
int c;
|
||||||
|
|
||||||
|
timeout = fgetint("/etc/default/confd", "=", "CONFD_TIMEOUT");
|
||||||
|
|
||||||
|
while ((c = getopt(argc, argv, "dht:v")) != EOF) {
|
||||||
|
switch(c) {
|
||||||
|
case 'd':
|
||||||
|
debug = 1;
|
||||||
|
break;
|
||||||
|
case 'h':
|
||||||
|
return usage_rpc(0);
|
||||||
|
case 't':
|
||||||
|
timeout = atoi(optarg);
|
||||||
|
break;
|
||||||
|
case 'v':
|
||||||
|
puts(PACKAGE_VERSION);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeout < 0)
|
||||||
|
timeout = 120;
|
||||||
|
|
||||||
|
/* Require at least RPC xpath */
|
||||||
|
if (optind >= argc) {
|
||||||
|
warnx("Missing RPC xpath");
|
||||||
|
return usage_rpc(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Validate RPC xpath starts with '/' */
|
||||||
|
if (argv[optind][0] != '/') {
|
||||||
|
warnx("RPC xpath must start with '/'");
|
||||||
|
return usage_rpc(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Validate argument count (must be key-value pairs) */
|
||||||
|
argc -= optind + 1;
|
||||||
|
if (argc % 2 != 0) {
|
||||||
|
warnx("Arguments must be key-value pairs after RPC xpath");
|
||||||
|
return usage_rpc(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rpc_exec(argv[optind], argc, &argv[optind + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
prognm = basename(argv[0]);
|
||||||
|
|
||||||
|
if (!strcmp(prognm, "rpc"))
|
||||||
|
return rpc_main(argc, argv);
|
||||||
|
|
||||||
|
return copy_main(argc, argv);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "infix-show"
|
||||||
|
version = "1.0"
|
||||||
|
description = "Infix show commands"
|
||||||
|
license = "MIT"
|
||||||
|
packages = [
|
||||||
|
{ include = "show" }
|
||||||
|
]
|
||||||
|
authors = [
|
||||||
|
"KernelKit developers"
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.poetry.scripts]
|
||||||
|
show = "show:main"
|
||||||
@@ -10,26 +10,30 @@ import argparse
|
|||||||
|
|
||||||
RAW_OUTPUT = False
|
RAW_OUTPUT = False
|
||||||
|
|
||||||
def run_sysrepocfg(xpath: str) -> dict:
|
|
||||||
|
def get_json(xpath: str, datastore: str = "operational", quiet: bool = False) -> dict:
|
||||||
if not isinstance(xpath, str) or not xpath.startswith("/"):
|
if not isinstance(xpath, str) or not xpath.startswith("/"):
|
||||||
print("Invalid XPATH. It must be a valid string starting with '/'.")
|
if not quiet:
|
||||||
|
print("Invalid XPATH. It must be a valid string starting with '/'.")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
safe_xpath = shlex.quote(xpath)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run([
|
result = subprocess.run(["copy", datastore, "-x", shlex.quote(xpath)],
|
||||||
"sysrepocfg", "-f", "json", "-X", "-d", "operational", "-x", safe_xpath
|
capture_output=True, text=True, check=True)
|
||||||
], capture_output=True, text=True, check=True)
|
if not result.stdout.strip():
|
||||||
|
return {}
|
||||||
json_data = json.loads(result.stdout)
|
json_data = json.loads(result.stdout)
|
||||||
return json_data
|
return json_data
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print(f"Error running sysrepocfg: {e}")
|
if not quiet:
|
||||||
|
print(f"Error running copy: {e}")
|
||||||
return {}
|
return {}
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
print(f"Error parsing JSON output: {e}")
|
if not quiet:
|
||||||
|
print(f"Error parsing JSON output: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def cli_pretty(json_data: dict, command: str, *args: str):
|
def cli_pretty(json_data: dict, command: str, *args: str):
|
||||||
if not command or not all(isinstance(arg, str) for arg in args):
|
if not command or not all(isinstance(arg, str) for arg in args):
|
||||||
print("Invalid command or arguments. All arguments must be strings.")
|
print("Invalid command or arguments. All arguments must be strings.")
|
||||||
@@ -46,8 +50,9 @@ def cli_pretty(json_data: dict, command: str, *args: str):
|
|||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print(f"Error running cli-pretty: {e}")
|
print(f"Error running cli-pretty: {e}")
|
||||||
|
|
||||||
|
|
||||||
def dhcp(args: List[str]) -> None:
|
def dhcp(args: List[str]) -> None:
|
||||||
data = run_sysrepocfg("/infix-dhcp-server:dhcp-server")
|
data = get_json("/infix-dhcp-server:dhcp-server")
|
||||||
if not data:
|
if not data:
|
||||||
print("No interface data retrieved.")
|
print("No interface data retrieved.")
|
||||||
return
|
return
|
||||||
@@ -57,8 +62,9 @@ def dhcp(args: List[str]) -> None:
|
|||||||
return
|
return
|
||||||
cli_pretty(data, "show-dhcp-server")
|
cli_pretty(data, "show-dhcp-server")
|
||||||
|
|
||||||
|
|
||||||
def hardware(args: List[str]) -> None:
|
def hardware(args: List[str]) -> None:
|
||||||
data = run_sysrepocfg("/ietf-hardware:hardware")
|
data = get_json("/ietf-hardware:hardware")
|
||||||
if not data:
|
if not data:
|
||||||
print("No hardware data retrieved.")
|
print("No hardware data retrieved.")
|
||||||
return
|
return
|
||||||
@@ -68,6 +74,7 @@ def hardware(args: List[str]) -> None:
|
|||||||
return
|
return
|
||||||
cli_pretty(data, "show-hardware")
|
cli_pretty(data, "show-hardware")
|
||||||
|
|
||||||
|
|
||||||
def ntp(args: List[str]) -> None:
|
def ntp(args: List[str]) -> None:
|
||||||
# Create argument parser for ntp subcommands
|
# Create argument parser for ntp subcommands
|
||||||
parser = argparse.ArgumentParser(prog='show ntp', add_help=False)
|
parser = argparse.ArgumentParser(prog='show ntp', add_help=False)
|
||||||
@@ -97,8 +104,8 @@ def ntp(args: List[str]) -> None:
|
|||||||
|
|
||||||
# Default: show ntp (no subcommand or address)
|
# Default: show ntp (no subcommand or address)
|
||||||
# Fetch both client and server operational data
|
# Fetch both client and server operational data
|
||||||
client_data = run_sysrepocfg("/system-state/ntp")
|
client_data = get_json("/system-state/ntp")
|
||||||
server_data = run_sysrepocfg("/ietf-ntp:ntp")
|
server_data = get_json("/ietf-ntp:ntp")
|
||||||
|
|
||||||
# Merge into single data structure
|
# Merge into single data structure
|
||||||
data = {}
|
data = {}
|
||||||
@@ -121,8 +128,9 @@ def ntp(args: List[str]) -> None:
|
|||||||
# Default show ntp - summary view (no address support at top level)
|
# Default show ntp - summary view (no address support at top level)
|
||||||
cli_pretty(data, "show-ntp")
|
cli_pretty(data, "show-ntp")
|
||||||
|
|
||||||
|
|
||||||
def ntp_tracking(args: List[str]) -> None:
|
def ntp_tracking(args: List[str]) -> None:
|
||||||
data = run_sysrepocfg("/ietf-ntp:ntp")
|
data = get_json("/ietf-ntp:ntp")
|
||||||
if not data:
|
if not data:
|
||||||
print("No ntp server data retrieved.")
|
print("No ntp server data retrieved.")
|
||||||
return
|
return
|
||||||
@@ -132,8 +140,9 @@ def ntp_tracking(args: List[str]) -> None:
|
|||||||
return
|
return
|
||||||
cli_pretty(data, "show-ntp-tracking")
|
cli_pretty(data, "show-ntp-tracking")
|
||||||
|
|
||||||
|
|
||||||
def ntp_source(args: List[str]) -> None:
|
def ntp_source(args: List[str]) -> None:
|
||||||
data = run_sysrepocfg("/ietf-ntp:ntp")
|
data = get_json("/ietf-ntp:ntp")
|
||||||
if not data:
|
if not data:
|
||||||
print("No ntp server data retrieved.")
|
print("No ntp server data retrieved.")
|
||||||
return
|
return
|
||||||
@@ -148,6 +157,7 @@ def ntp_source(args: List[str]) -> None:
|
|||||||
else:
|
else:
|
||||||
cli_pretty(data, "show-ntp-source")
|
cli_pretty(data, "show-ntp-source")
|
||||||
|
|
||||||
|
|
||||||
def is_valid_interface_name(interface_name: str) -> bool:
|
def is_valid_interface_name(interface_name: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Validates a Linux network interface name.
|
Validates a Linux network interface name.
|
||||||
@@ -158,14 +168,15 @@ def is_valid_interface_name(interface_name: str) -> bool:
|
|||||||
pattern = r'^[a-zA-Z0-9._-]+$'
|
pattern = r'^[a-zA-Z0-9._-]+$'
|
||||||
return bool(re.match(pattern, interface_name))
|
return bool(re.match(pattern, interface_name))
|
||||||
|
|
||||||
|
|
||||||
def interface(args: List[str]) -> None:
|
def interface(args: List[str]) -> None:
|
||||||
data = run_sysrepocfg("/ietf-interfaces:interfaces")
|
data = get_json("/ietf-interfaces:interfaces")
|
||||||
if not data:
|
if not data:
|
||||||
print("No interface data retrieved.")
|
print("No interface data retrieved.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Also fetch routing interface list for forwarding indication
|
# Also fetch routing interface list for forwarding indication
|
||||||
routing_data = run_sysrepocfg("/ietf-routing:routing")
|
routing_data = get_json("/ietf-routing:routing")
|
||||||
if routing_data:
|
if routing_data:
|
||||||
# Merge routing data into the main data structure
|
# Merge routing data into the main data structure
|
||||||
data.update(routing_data)
|
data.update(routing_data)
|
||||||
@@ -185,8 +196,9 @@ def interface(args: List[str]) -> None:
|
|||||||
else:
|
else:
|
||||||
print("Too many arguments provided. Only one interface name is expected.")
|
print("Too many arguments provided. Only one interface name is expected.")
|
||||||
|
|
||||||
|
|
||||||
def stp(args: List[str]) -> None:
|
def stp(args: List[str]) -> None:
|
||||||
data = run_sysrepocfg("/ietf-interfaces:interfaces")
|
data = get_json("/ietf-interfaces:interfaces")
|
||||||
if not data:
|
if not data:
|
||||||
print("No interface data retrieved.")
|
print("No interface data retrieved.")
|
||||||
return
|
return
|
||||||
@@ -196,8 +208,9 @@ def stp(args: List[str]) -> None:
|
|||||||
return
|
return
|
||||||
cli_pretty(data, "show-bridge-stp")
|
cli_pretty(data, "show-bridge-stp")
|
||||||
|
|
||||||
|
|
||||||
def software(args: List[str]) -> None:
|
def software(args: List[str]) -> None:
|
||||||
data = run_sysrepocfg("/ietf-system:system-state/infix-system:software")
|
data = get_json("/ietf-system:system-state/infix-system:software")
|
||||||
if not data:
|
if not data:
|
||||||
print("No software data retrieved.")
|
print("No software data retrieved.")
|
||||||
return
|
return
|
||||||
@@ -217,8 +230,9 @@ def software(args: List[str]) -> None:
|
|||||||
else:
|
else:
|
||||||
print("Too many arguments provided. Only one name is expected.")
|
print("Too many arguments provided. Only one name is expected.")
|
||||||
|
|
||||||
|
|
||||||
def boot_order(args: List[str]) -> None:
|
def boot_order(args: List[str]) -> None:
|
||||||
data = run_sysrepocfg("/ietf-system:system-state/infix-system:software")
|
data = get_json("/ietf-system:system-state/infix-system:software")
|
||||||
if not data:
|
if not data:
|
||||||
print("No software data retrieved.")
|
print("No software data retrieved.")
|
||||||
return
|
return
|
||||||
@@ -230,8 +244,9 @@ def boot_order(args: List[str]) -> None:
|
|||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError):
|
||||||
print("No boot order data available.")
|
print("No boot order data available.")
|
||||||
|
|
||||||
|
|
||||||
def services(args: List[str]) -> None:
|
def services(args: List[str]) -> None:
|
||||||
data = run_sysrepocfg("/ietf-system:system-state/infix-system:services")
|
data = get_json("/ietf-system:system-state/infix-system:services")
|
||||||
if not data:
|
if not data:
|
||||||
print("No service data retrieved.")
|
print("No service data retrieved.")
|
||||||
return
|
return
|
||||||
@@ -240,7 +255,8 @@ def services(args: List[str]) -> None:
|
|||||||
print(json.dumps(data, indent=2))
|
print(json.dumps(data, indent=2))
|
||||||
return
|
return
|
||||||
|
|
||||||
cli_pretty(data, f"show-services")
|
cli_pretty(data, "show-services")
|
||||||
|
|
||||||
|
|
||||||
def container(args: List[str]) -> None:
|
def container(args: List[str]) -> None:
|
||||||
"""Handle show container [name]
|
"""Handle show container [name]
|
||||||
@@ -249,44 +265,39 @@ def container(args: List[str]) -> None:
|
|||||||
(none) - Show all containers in table format
|
(none) - Show all containers in table format
|
||||||
name - Show detailed view of specific container
|
name - Show detailed view of specific container
|
||||||
"""
|
"""
|
||||||
data = run_sysrepocfg("/infix-containers:containers")
|
data = get_json("/infix-containers:containers")
|
||||||
if not data:
|
if not data:
|
||||||
print("No container data retrieved.")
|
print("No container data retrieved.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Fetch interface data for bridge resolution (both table and detailed views)
|
# Fetch interface data for bridge resolution (table and detailed views)
|
||||||
# Fetch operational interface data
|
# Fetch operational interface data
|
||||||
iface_oper = run_sysrepocfg("/ietf-interfaces:interfaces")
|
iface_oper = get_json("/ietf-interfaces:interfaces")
|
||||||
|
|
||||||
# Also fetch config data for veth peer information (not in operational)
|
# Also fetch config data for veth peer information (not in operational)
|
||||||
try:
|
iface_config = get_json("/ietf-interfaces:interfaces", "running")
|
||||||
result = subprocess.run([
|
|
||||||
"sysrepocfg", "-f", "json", "-X", "-d", "running", "-x", "/ietf-interfaces:interfaces"
|
|
||||||
], capture_output=True, text=True, check=True)
|
|
||||||
iface_config = json.loads(result.stdout)
|
|
||||||
|
|
||||||
# Merge config veth peer info into operational data
|
# Merge config veth peer info into operational data
|
||||||
if iface_oper and iface_config:
|
if iface_oper and iface_config:
|
||||||
oper_ifaces = iface_oper.get('ietf-interfaces:interfaces', {}).get('interface', [])
|
oper_ifaces = iface_oper.get('ietf-interfaces:interfaces', {}).get('interface', [])
|
||||||
config_ifaces = iface_config.get('ietf-interfaces:interfaces', {}).get('interface', [])
|
config_ifaces = iface_config.get('ietf-interfaces:interfaces', {}).get('interface', [])
|
||||||
|
|
||||||
# Create a map of config interfaces
|
# Create a map of config interfaces
|
||||||
config_map = {iface['name']: iface for iface in config_ifaces}
|
config_map = {iface['name']: iface for iface in config_ifaces}
|
||||||
|
|
||||||
# Merge veth peer info from config into operational
|
# Merge veth peer info from config into operational
|
||||||
for oper_iface in oper_ifaces:
|
for oper_iface in oper_ifaces:
|
||||||
name = oper_iface.get('name')
|
name = oper_iface.get('name')
|
||||||
if name in config_map:
|
if name in config_map:
|
||||||
config_iface = config_map[name]
|
config_iface = config_map[name]
|
||||||
# Add veth peer if it exists in config but not in operational
|
# Add veth peer if it exists in config but not in operational
|
||||||
if 'infix-interfaces:veth' in config_iface and 'infix-interfaces:veth' not in oper_iface:
|
if 'infix-interfaces:veth' in config_iface and 'infix-interfaces:veth' not in oper_iface:
|
||||||
oper_iface['infix-interfaces:veth'] = config_iface['infix-interfaces:veth']
|
oper_iface['infix-interfaces:veth'] = config_iface['infix-interfaces:veth']
|
||||||
|
|
||||||
data.update(iface_oper)
|
data.update(iface_oper)
|
||||||
except (subprocess.CalledProcessError, json.JSONDecodeError):
|
elif iface_oper:
|
||||||
# If config fetch fails, just use operational data
|
# If config fetch fails, just use operational data
|
||||||
if iface_oper:
|
data.update(iface_oper)
|
||||||
data.update(iface_oper)
|
|
||||||
|
|
||||||
if RAW_OUTPUT:
|
if RAW_OUTPUT:
|
||||||
print(json.dumps(data, indent=2))
|
print(json.dumps(data, indent=2))
|
||||||
@@ -300,6 +311,7 @@ def container(args: List[str]) -> None:
|
|||||||
else:
|
else:
|
||||||
print("Too many arguments provided. Expected: show container [name]")
|
print("Too many arguments provided. Expected: show container [name]")
|
||||||
|
|
||||||
|
|
||||||
def bfd(args: List[str]) -> None:
|
def bfd(args: List[str]) -> None:
|
||||||
"""Handle show bfd [subcommand] [peer] [brief]
|
"""Handle show bfd [subcommand] [peer] [brief]
|
||||||
|
|
||||||
@@ -309,8 +321,7 @@ def bfd(args: List[str]) -> None:
|
|||||||
peers brief - Show BFD peers in brief format
|
peers brief - Show BFD peers in brief format
|
||||||
peer <addr> - Show specific BFD peer details
|
peer <addr> - Show specific BFD peer details
|
||||||
"""
|
"""
|
||||||
# Fetch operational data from sysrepocfg
|
data = get_json("/ietf-routing:routing/control-plane-protocols/control-plane-protocol")
|
||||||
data = run_sysrepocfg("/ietf-routing:routing/control-plane-protocols/control-plane-protocol")
|
|
||||||
if not data:
|
if not data:
|
||||||
print("No BFD data retrieved.")
|
print("No BFD data retrieved.")
|
||||||
return
|
return
|
||||||
@@ -344,7 +355,7 @@ def bfd(args: List[str]) -> None:
|
|||||||
|
|
||||||
# For brief view, fetch interface data to show interface:ip format
|
# For brief view, fetch interface data to show interface:ip format
|
||||||
if brief_flag:
|
if brief_flag:
|
||||||
ifaces_data = run_sysrepocfg("/ietf-interfaces:interfaces")
|
ifaces_data = get_json("/ietf-interfaces:interfaces")
|
||||||
if ifaces_data:
|
if ifaces_data:
|
||||||
data['_interfaces'] = ifaces_data
|
data['_interfaces'] = ifaces_data
|
||||||
|
|
||||||
@@ -361,6 +372,7 @@ def bfd(args: List[str]) -> None:
|
|||||||
else:
|
else:
|
||||||
print(f"Unknown BFD subcommand: {subcommand}")
|
print(f"Unknown BFD subcommand: {subcommand}")
|
||||||
|
|
||||||
|
|
||||||
def ospf(args: List[str]) -> None:
|
def ospf(args: List[str]) -> None:
|
||||||
"""Handle show ospf [subcommand] [ifname] [detail]
|
"""Handle show ospf [subcommand] [ifname] [detail]
|
||||||
|
|
||||||
@@ -374,8 +386,7 @@ def ospf(args: List[str]) -> None:
|
|||||||
ifname - Interface name (for interfaces subcommand)
|
ifname - Interface name (for interfaces subcommand)
|
||||||
detail - Show detailed information (Cisco-style)
|
detail - Show detailed information (Cisco-style)
|
||||||
"""
|
"""
|
||||||
# Fetch operational data from sysrepocfg
|
data = get_json("/ietf-routing:routing/control-plane-protocols/control-plane-protocol")
|
||||||
data = run_sysrepocfg("/ietf-routing:routing/control-plane-protocols/control-plane-protocol")
|
|
||||||
if not data:
|
if not data:
|
||||||
print("No OSPF data retrieved.")
|
print("No OSPF data retrieved.")
|
||||||
return
|
return
|
||||||
@@ -407,12 +418,12 @@ def ospf(args: List[str]) -> None:
|
|||||||
data['_ifname'] = ifname
|
data['_ifname'] = ifname
|
||||||
# For detailed interface view, fetch additional data (interfaces and BFD)
|
# For detailed interface view, fetch additional data (interfaces and BFD)
|
||||||
if subcommand == "interface" or subcommand == "interfaces":
|
if subcommand == "interface" or subcommand == "interfaces":
|
||||||
ifaces_data = run_sysrepocfg("/ietf-interfaces:interfaces")
|
ifaces_data = get_json("/ietf-interfaces:interfaces")
|
||||||
if ifaces_data:
|
if ifaces_data:
|
||||||
data['_interfaces'] = ifaces_data
|
data['_interfaces'] = ifaces_data
|
||||||
|
|
||||||
# Fetch BFD data for per-interface status
|
# Fetch BFD data for per-interface status
|
||||||
routing_data = run_sysrepocfg("/ietf-routing:routing/control-plane-protocols/control-plane-protocol")
|
routing_data = get_json("/ietf-routing:routing/control-plane-protocols/control-plane-protocol")
|
||||||
if routing_data:
|
if routing_data:
|
||||||
data['_routing'] = routing_data
|
data['_routing'] = routing_data
|
||||||
|
|
||||||
@@ -428,6 +439,7 @@ def ospf(args: List[str]) -> None:
|
|||||||
else:
|
else:
|
||||||
print(f"Unknown OSPF subcommand: {subcommand}")
|
print(f"Unknown OSPF subcommand: {subcommand}")
|
||||||
|
|
||||||
|
|
||||||
def rip(args: List[str]) -> None:
|
def rip(args: List[str]) -> None:
|
||||||
"""Handle show rip [subcommand] [ifname]
|
"""Handle show rip [subcommand] [ifname]
|
||||||
|
|
||||||
@@ -440,8 +452,7 @@ def rip(args: List[str]) -> None:
|
|||||||
Optional:
|
Optional:
|
||||||
ifname - Interface name (for interface subcommand)
|
ifname - Interface name (for interface subcommand)
|
||||||
"""
|
"""
|
||||||
# Fetch operational data from sysrepocfg
|
data = get_json("/ietf-routing:routing/control-plane-protocols/control-plane-protocol")
|
||||||
data = run_sysrepocfg("/ietf-routing:routing/control-plane-protocols/control-plane-protocol")
|
|
||||||
if not data:
|
if not data:
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
@@ -469,10 +480,11 @@ def rip(args: List[str]) -> None:
|
|||||||
else:
|
else:
|
||||||
print(f"Unknown RIP subcommand: {subcommand}")
|
print(f"Unknown RIP subcommand: {subcommand}")
|
||||||
|
|
||||||
|
|
||||||
def routes(args: List[str]):
|
def routes(args: List[str]):
|
||||||
ip_version = args[0] if args and args[0] in ["ipv4", "ipv6"] else "ipv4"
|
ip_version = args[0] if args and args[0] in ["ipv4", "ipv6"] else "ipv4"
|
||||||
|
|
||||||
data = run_sysrepocfg("/ietf-routing:routing/ribs")
|
data = get_json("/ietf-routing:routing/ribs")
|
||||||
if not data:
|
if not data:
|
||||||
print("No route data retrieved.")
|
print("No route data retrieved.")
|
||||||
return
|
return
|
||||||
@@ -481,8 +493,9 @@ def routes(args: List[str]):
|
|||||||
return
|
return
|
||||||
cli_pretty(data, "show-routing-table", "-i", ip_version)
|
cli_pretty(data, "show-routing-table", "-i", ip_version)
|
||||||
|
|
||||||
|
|
||||||
def lldp(args: List[str]):
|
def lldp(args: List[str]):
|
||||||
data = run_sysrepocfg("/ieee802-dot1ab-lldp:lldp")
|
data = get_json("/ieee802-dot1ab-lldp:lldp")
|
||||||
if not data:
|
if not data:
|
||||||
print("No lldp data retrieved.")
|
print("No lldp data retrieved.")
|
||||||
return
|
return
|
||||||
@@ -494,13 +507,13 @@ def lldp(args: List[str]):
|
|||||||
|
|
||||||
def system(args: List[str]) -> None:
|
def system(args: List[str]) -> None:
|
||||||
# Get system state from sysrepo
|
# Get system state from sysrepo
|
||||||
data = run_sysrepocfg("/ietf-system:system-state")
|
data = get_json("/ietf-system:system-state")
|
||||||
if not data:
|
if not data:
|
||||||
print("No system data retrieved.")
|
print("No system data retrieved.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get hardware data (including thermal sensors)
|
# Get hardware data (including thermal sensors)
|
||||||
hardware_data = run_sysrepocfg("/ietf-hardware:hardware")
|
hardware_data = get_json("/ietf-hardware:hardware")
|
||||||
|
|
||||||
# Augment with runtime data
|
# Augment with runtime data
|
||||||
runtime = {}
|
runtime = {}
|
||||||
@@ -596,6 +609,55 @@ def system(args: List[str]) -> None:
|
|||||||
|
|
||||||
cli_pretty(data, "show-system")
|
cli_pretty(data, "show-system")
|
||||||
|
|
||||||
|
|
||||||
|
def nacm(args: List[str]) -> None:
|
||||||
|
"""Handle show nacm [subcommand] [name]
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
(none) - Overview with users, groups, and permissions matrix
|
||||||
|
group NAME - Detailed view of a specific group's rules
|
||||||
|
user NAME - Detailed view of a specific user's permissions
|
||||||
|
"""
|
||||||
|
data = get_json("/ietf-netconf-acm:nacm", "running", quiet=True)
|
||||||
|
if not data:
|
||||||
|
user = os.environ.get('USER', 'unknown')
|
||||||
|
print(f'Error: Access denied because "{user}" NACM authorization failed.')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fetch user data from operational (includes shell and authorized-key from yanger)
|
||||||
|
user_oper = get_json("/ietf-system:system/authentication", quiet=True)
|
||||||
|
|
||||||
|
if user_oper:
|
||||||
|
oper_users = user_oper.get("ietf-system:system", {}).get("authentication", {}).get("user", [])
|
||||||
|
data["ietf-system:system"] = {"authentication": {"user": oper_users}}
|
||||||
|
|
||||||
|
if RAW_OUTPUT:
|
||||||
|
print(json.dumps(data, indent=2))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parse arguments: subcommand and optional name
|
||||||
|
subcommand = args[0] if len(args) > 0 and args[0] else ""
|
||||||
|
name = args[1] if len(args) > 1 else None
|
||||||
|
|
||||||
|
# Route to appropriate formatter
|
||||||
|
if subcommand == "":
|
||||||
|
cli_pretty(data, "show-nacm")
|
||||||
|
elif subcommand == "group":
|
||||||
|
if not name:
|
||||||
|
print("Usage: show nacm group <name>")
|
||||||
|
return
|
||||||
|
data['_group_name'] = name
|
||||||
|
cli_pretty(data, "show-nacm-group")
|
||||||
|
elif subcommand == "user":
|
||||||
|
if not name:
|
||||||
|
print("Usage: show nacm user <name>")
|
||||||
|
return
|
||||||
|
data['_user_name'] = name
|
||||||
|
cli_pretty(data, "show-nacm-user")
|
||||||
|
else:
|
||||||
|
print(f"Unknown NACM subcommand: {subcommand}")
|
||||||
|
|
||||||
|
|
||||||
def execute_command(command: str, args: List[str]):
|
def execute_command(command: str, args: List[str]):
|
||||||
command_mapping = {
|
command_mapping = {
|
||||||
'bfd': bfd,
|
'bfd': bfd,
|
||||||
@@ -605,6 +667,7 @@ def execute_command(command: str, args: List[str]):
|
|||||||
'hardware': hardware,
|
'hardware': hardware,
|
||||||
'interface': interface,
|
'interface': interface,
|
||||||
'lldp': lldp,
|
'lldp': lldp,
|
||||||
|
'nacm': nacm,
|
||||||
'ntp': ntp,
|
'ntp': ntp,
|
||||||
'ospf': ospf,
|
'ospf': ospf,
|
||||||
'rip': rip,
|
'rip': rip,
|
||||||
+2
-2
@@ -156,8 +156,8 @@ cmd_collect()
|
|||||||
collect uptime.txt uptime
|
collect uptime.txt uptime
|
||||||
|
|
||||||
# Configuration files
|
# Configuration files
|
||||||
collect running-config.json sysrepocfg -f json -d running -X
|
collect running-config.json copy running
|
||||||
collect operational-config.json sysrepocfg -f json -d operational -X
|
collect operational-config.json copy operational
|
||||||
|
|
||||||
# Sysrepo YANG modules
|
# Sysrepo YANG modules
|
||||||
if command -v sysrepoctl >/dev/null 2>&1; then
|
if command -v sysrepoctl >/dev/null 2>&1; then
|
||||||
|
|||||||
+11
-6
@@ -129,14 +129,19 @@ char *cfg_adjust(const char *path, const char *template, bool sanitize)
|
|||||||
if (!path_allowed(path))
|
if (!path_allowed(path))
|
||||||
goto err;
|
goto err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* CLI users save to /cfg by default, unless abs. path */
|
||||||
|
if (asprintf(&expanded, "%s%.*s/%s%s",
|
||||||
|
path[0] == '/' ? "" : "/cfg/",
|
||||||
|
dlen, path,
|
||||||
|
basename,
|
||||||
|
strchr(basename, '.') ? "" : ".cfg") < 0)
|
||||||
|
goto err;
|
||||||
|
} else {
|
||||||
|
/* Shell users expect copy to behave more like cp */
|
||||||
|
expanded = strdup(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asprintf(&expanded, "%s%.*s/%s%s",
|
|
||||||
path[0] == '/' ? "" : "/cfg/",
|
|
||||||
dlen, path,
|
|
||||||
basename,
|
|
||||||
strchr(basename, '.') ? "" : ".cfg") < 0)
|
|
||||||
goto err;
|
|
||||||
|
|
||||||
if (sanitize) {
|
if (sanitize) {
|
||||||
resolved = realpath(expanded, NULL);
|
resolved = realpath(expanded, NULL);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <libite/lite.h>
|
#include <libite/lite.h>
|
||||||
|
|
||||||
#define ERRMSG "Error: "
|
#define ERRMSG "Error: "
|
||||||
|
#define DBGMSG "Debug: "
|
||||||
#define INFMSG "Note: "
|
#define INFMSG "Note: "
|
||||||
|
|
||||||
int yorn (const char *fmt, ...);
|
int yorn (const char *fmt, ...);
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"ietf-netconf-acm:nacm": {
|
"ietf-netconf-acm:nacm": {
|
||||||
"enable-nacm": true,
|
"enable-nacm": true,
|
||||||
|
"read-default": "permit",
|
||||||
|
"write-default": "permit",
|
||||||
|
"exec-default": "permit",
|
||||||
"groups": {
|
"groups": {
|
||||||
"group": [
|
"group": [
|
||||||
{
|
{
|
||||||
@@ -8,6 +11,14 @@
|
|||||||
"user-name": [
|
"user-name": [
|
||||||
"admin"
|
"admin"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "operator",
|
||||||
|
"user-name": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "guest",
|
||||||
|
"user-name": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -27,16 +38,61 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "operator-acl",
|
||||||
|
"group": [
|
||||||
|
"operator"
|
||||||
|
],
|
||||||
|
"rule": [
|
||||||
|
{
|
||||||
|
"name": "permit-system-rpcs",
|
||||||
|
"module-name": "ietf-system",
|
||||||
|
"rpc-name": "*",
|
||||||
|
"access-operations": "exec",
|
||||||
|
"action": "permit",
|
||||||
|
"comment": "Operators can reboot, shutdown, and set system time."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "guest-acl",
|
||||||
|
"group": [
|
||||||
|
"guest"
|
||||||
|
],
|
||||||
|
"rule": [
|
||||||
|
{
|
||||||
|
"name": "deny-all-write+exec",
|
||||||
|
"module-name": "*",
|
||||||
|
"access-operations": "create update delete exec",
|
||||||
|
"action": "deny",
|
||||||
|
"comment": "Guests cannot change anything or exec rpcs."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "default-deny-all",
|
"name": "default-deny-all",
|
||||||
"group": ["*"],
|
"group": ["*"],
|
||||||
"rule": [
|
"rule": [
|
||||||
{
|
{
|
||||||
"name": "deny-password-read",
|
"name": "deny-password-access",
|
||||||
"module-name": "ietf-system",
|
|
||||||
"path": "/ietf-system:system/authentication/user/password",
|
"path": "/ietf-system:system/authentication/user/password",
|
||||||
"access-operations": "*",
|
"access-operations": "*",
|
||||||
"action": "deny"
|
"action": "deny",
|
||||||
|
"comment": "No user except admins can access password hashes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deny-keystore-access",
|
||||||
|
"module-name": "ietf-keystore",
|
||||||
|
"access-operations": "*",
|
||||||
|
"action": "deny",
|
||||||
|
"comment": "No user except admins can access cryptographic keys."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deny-truststore-access",
|
||||||
|
"module-name": "ietf-truststore",
|
||||||
|
"access-operations": "*",
|
||||||
|
"action": "deny",
|
||||||
|
"comment": "No user except admins can access trust store."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
factorydir = $(pkgdatadir)/factory.d
|
factorydir = $(pkgdatadir)/factory.d
|
||||||
dist_factory_DATA = 10-nacm.json 10-netconf-server.json \
|
dist_factory_DATA = 10-nacm.json 10-netconf-server.json \
|
||||||
10-infix-services.json 10-system.json
|
10-infix-services.json 10-system.json
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ static int startup_save(sr_session_ctx_t *session, uint32_t sub_id, const char *
|
|||||||
if (systemf("runlevel >/dev/null 2>&1"))
|
if (systemf("runlevel >/dev/null 2>&1"))
|
||||||
return SR_ERR_OK;
|
return SR_ERR_OK;
|
||||||
|
|
||||||
if (systemf("sysrepocfg -X/cfg/startup-config.cfg -d startup -f json"))
|
if (systemf("copy -f startup /cfg/startup-config.cfg"))
|
||||||
return SR_ERR_SYS;
|
return SR_ERR_SYS;
|
||||||
|
|
||||||
return SR_ERR_OK;
|
return SR_ERR_OK;
|
||||||
|
|||||||
+21
-9
@@ -557,10 +557,14 @@ static void del_groups(const char *user, const char **groups)
|
|||||||
/* Users with a valid shell are also allowed CLI access */
|
/* Users with a valid shell are also allowed CLI access */
|
||||||
static void adjust_access(const char *user, const char *shell)
|
static void adjust_access(const char *user, const char *shell)
|
||||||
{
|
{
|
||||||
if (strcmp(shell, "/bin/false"))
|
if (strcmp(shell, "/bin/false")) {
|
||||||
add_group(user, "sys-cli");
|
add_group(user, "klish");
|
||||||
else
|
erasef("/home/%s/.hushlogin", user);
|
||||||
del_group(user, "sys-cli");
|
} else {
|
||||||
|
del_group(user, "klish");
|
||||||
|
/* prevent even motd from showing */
|
||||||
|
touchf("/home/%s/.hushlogin", user);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* XXX: Currently Infix only has admin and non-admins as a group */
|
/* XXX: Currently Infix only has admin and non-admins as a group */
|
||||||
@@ -579,17 +583,25 @@ static bool is_admin_user(sr_session_ctx_t *session, const char *user)
|
|||||||
return false; /* safe default */
|
return false; /* safe default */
|
||||||
|
|
||||||
for (size_t j = 0; j < group_count; j++) {
|
for (size_t j = 0; j < group_count; j++) {
|
||||||
/* Fetch and check rules for each group */
|
const char *group = groups[j].data.string_val;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Note: module-name has default="*", so we must explicitly exclude
|
||||||
|
* rules with 'path' and 'rpc-name' to only match pure module-level
|
||||||
|
* wildcard admin rules.
|
||||||
|
*/
|
||||||
snprintf(xpath, sizeof(xpath), NACM_BASE_"/rule-list[group='%s']/rule"
|
snprintf(xpath, sizeof(xpath), NACM_BASE_"/rule-list[group='%s']/rule"
|
||||||
"[module-name='*'][access-operations='*'][action='permit']",
|
"[module-name='*'][access-operations='*'][action='permit']"
|
||||||
groups[j].data.string_val);
|
"[not(path)][not(rpc-name)]", group);
|
||||||
rc = sr_get_items(session, xpath, 0, 0, &rules, &rule_count);
|
rc = sr_get_items(session, xpath, 0, 0, &rules, &rule_count);
|
||||||
if (rc)
|
if (rc)
|
||||||
continue; /* not found, this is OK */
|
continue; /* not found, this is OK */
|
||||||
|
|
||||||
/* At least one group grants full administrator permissions */
|
/* At least one group grants full administrator permissions */
|
||||||
if (rule_count > 0)
|
if (rule_count > 0) {
|
||||||
|
DEBUG("User '%s' granted admin via group '%s'", user, group);
|
||||||
is_admin = true;
|
is_admin = true;
|
||||||
|
}
|
||||||
|
|
||||||
sr_free_values(rules, rule_count);
|
sr_free_values(rules, rule_count);
|
||||||
}
|
}
|
||||||
@@ -615,7 +627,7 @@ static int is_valid_username(const char *user)
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static char *sys_find_usable_shell(sr_session_ctx_t *sess, char *name)
|
static char *sys_find_usable_shell(sr_session_ctx_t *sess, const char *name)
|
||||||
{
|
{
|
||||||
const char *conf = NULL;
|
const char *conf = NULL;
|
||||||
char *shell = NULL;
|
char *shell = NULL;
|
||||||
|
|||||||
@@ -47,7 +47,43 @@ static const char *cd_home(kcontext_t *ctx)
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int systemf(const char *fmt, ...)
|
/*
|
||||||
|
* Safe subprocess execution without shell interpretation.
|
||||||
|
* Use this instead of system()/systemf() when arguments come from user input.
|
||||||
|
*/
|
||||||
|
static int run(char *const argv[])
|
||||||
|
{
|
||||||
|
pid_t pid;
|
||||||
|
int status;
|
||||||
|
|
||||||
|
pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
execvp(argv[0], argv);
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid < 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (waitpid(pid, &status, 0) < 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (WIFEXITED(status))
|
||||||
|
return WEXITSTATUS(status);
|
||||||
|
|
||||||
|
if (WIFSIGNALED(status)) {
|
||||||
|
errno = EINTR;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Shell command execution - only use with hardcoded commands or when
|
||||||
|
* shell features (pipes, redirects) are needed. Never use with user input.
|
||||||
|
*/
|
||||||
|
static int shellf(const char *fmt, ...)
|
||||||
{
|
{
|
||||||
va_list ap;
|
va_list ap;
|
||||||
char *cmd;
|
char *cmd;
|
||||||
@@ -68,8 +104,6 @@ static int systemf(const char *fmt, ...)
|
|||||||
vsnprintf(cmd, len, fmt, ap);
|
vsnprintf(cmd, len, fmt, ap);
|
||||||
va_end(ap);
|
va_end(ap);
|
||||||
|
|
||||||
// fprintf(stderr, INFMSG "CMD: '%s'\n", cmd);
|
|
||||||
|
|
||||||
rc = system(cmd);
|
rc = system(cmd);
|
||||||
if (rc == -1)
|
if (rc == -1)
|
||||||
return -1;
|
return -1;
|
||||||
@@ -87,6 +121,7 @@ static int systemf(const char *fmt, ...)
|
|||||||
|
|
||||||
int infix_datastore(kcontext_t *ctx)
|
int infix_datastore(kcontext_t *ctx)
|
||||||
{
|
{
|
||||||
|
char *argv[] = { "files", "/cfg", NULL };
|
||||||
const char *ds;
|
const char *ds;
|
||||||
|
|
||||||
ds = kcontext_script(ctx);
|
ds = kcontext_script(ctx);
|
||||||
@@ -104,13 +139,14 @@ int infix_datastore(kcontext_t *ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
done:
|
done:
|
||||||
return systemf("files /cfg");
|
return run(argv);
|
||||||
}
|
}
|
||||||
|
|
||||||
int infix_erase(kcontext_t *ctx)
|
int infix_erase(kcontext_t *ctx)
|
||||||
{
|
{
|
||||||
kpargv_t *pargv = kcontext_pargv(ctx);
|
kpargv_t *pargv = kcontext_pargv(ctx);
|
||||||
const char *path;
|
const char *path;
|
||||||
|
char *argv[4];
|
||||||
|
|
||||||
path = kparg_value(kpargv_find(pargv, "file"));
|
path = kparg_value(kpargv_find(pargv, "file"));
|
||||||
if (!path) {
|
if (!path) {
|
||||||
@@ -120,12 +156,18 @@ int infix_erase(kcontext_t *ctx)
|
|||||||
|
|
||||||
cd_home(ctx);
|
cd_home(ctx);
|
||||||
|
|
||||||
return systemf("erase -s %s", path);
|
argv[0] = "erase";
|
||||||
|
argv[1] = "-s";
|
||||||
|
argv[2] = (char *)path;
|
||||||
|
argv[3] = NULL;
|
||||||
|
|
||||||
|
return run(argv);
|
||||||
}
|
}
|
||||||
|
|
||||||
int infix_files(kcontext_t *ctx)
|
int infix_files(kcontext_t *ctx)
|
||||||
{
|
{
|
||||||
const char *path;
|
const char *path;
|
||||||
|
char *argv[3];
|
||||||
|
|
||||||
cd_home(ctx);
|
cd_home(ctx);
|
||||||
path = kcontext_script(ctx);
|
path = kcontext_script(ctx);
|
||||||
@@ -134,7 +176,11 @@ int infix_files(kcontext_t *ctx)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return systemf("files %s", path);
|
argv[0] = "files";
|
||||||
|
argv[1] = (char *)path;
|
||||||
|
argv[2] = NULL;
|
||||||
|
|
||||||
|
return run(argv);
|
||||||
}
|
}
|
||||||
|
|
||||||
int infix_ifaces(kcontext_t *ctx)
|
int infix_ifaces(kcontext_t *ctx)
|
||||||
@@ -144,12 +190,13 @@ int infix_ifaces(kcontext_t *ctx)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Note: uses shellf() for pipes, but all arguments are hardcoded by callers */
|
||||||
static int firewall_dbus_completion(const char *interface, const char *method, const char *parser)
|
static int firewall_dbus_completion(const char *interface, const char *method, const char *parser)
|
||||||
{
|
{
|
||||||
return systemf("gdbus call --system --dest org.fedoraproject.FirewallD1 "
|
return shellf("gdbus call --system --dest org.fedoraproject.FirewallD1 "
|
||||||
"--object-path /org/fedoraproject/FirewallD1 "
|
"--object-path /org/fedoraproject/FirewallD1 "
|
||||||
"--method org.fedoraproject.FirewallD1.%s.%s 2>/dev/null "
|
"--method org.fedoraproject.FirewallD1.%s.%s 2>/dev/null "
|
||||||
"| %s", interface, method, parser);
|
"| %s", interface, method, parser);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -190,38 +237,52 @@ int infix_firewall_policies(kcontext_t *ctx)
|
|||||||
* - tr converts single to double quotes
|
* - tr converts single to double quotes
|
||||||
* - jq extracts array items
|
* - jq extracts array items
|
||||||
*/
|
*/
|
||||||
|
/* Note: uses shellf() for pipes, but command is hardcoded */
|
||||||
int infix_firewall_services(kcontext_t *ctx)
|
int infix_firewall_services(kcontext_t *ctx)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
return systemf("gdbus call --system --dest org.fedoraproject.FirewallD1 "
|
return shellf("gdbus call --system --dest org.fedoraproject.FirewallD1 "
|
||||||
"--object-path /org/fedoraproject/FirewallD1 "
|
"--object-path /org/fedoraproject/FirewallD1 "
|
||||||
"--method org.fedoraproject.FirewallD1.listServices 2>/dev/null "
|
"--method org.fedoraproject.FirewallD1.listServices 2>/dev/null "
|
||||||
"| sed 's/^(//; s/,)$//' | tr \"'\" '\"' | jq -r '.[]' 2>/dev/null");
|
"| sed 's/^(//; s/,)$//' | tr \"'\" '\"' | jq -r '.[]' 2>/dev/null");
|
||||||
}
|
}
|
||||||
|
|
||||||
int infix_copy(kcontext_t *ctx)
|
int infix_copy(kcontext_t *ctx)
|
||||||
{
|
{
|
||||||
kpargv_t *pargv = kcontext_pargv(ctx);
|
kpargv_t *pargv = kcontext_pargv(ctx);
|
||||||
const char *src, *dst;
|
const char *src, *dst, *remote_user;
|
||||||
char user[256] = "";
|
char *argv[12];
|
||||||
char validate[8] = "";
|
|
||||||
kparg_t *parg;
|
kparg_t *parg;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
src = kparg_value(kpargv_find(pargv, "src"));
|
src = kparg_value(kpargv_find(pargv, "src"));
|
||||||
dst = kparg_value(kpargv_find(pargv, "dst"));
|
dst = kparg_value(kpargv_find(pargv, "dst"));
|
||||||
if (!src || !dst)
|
if (!src || !dst)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
parg = kpargv_find(pargv, "user");
|
/* Ensure we run the copy command as the logged-in user, not root (klishd) */
|
||||||
if (parg)
|
argv[i++] = "doas";
|
||||||
snprintf(user, sizeof(user), "-u %s", kparg_value(parg));
|
argv[i++] = "-u";
|
||||||
|
argv[i++] = (char *)cd_home(ctx);
|
||||||
|
argv[i++] = "copy";
|
||||||
|
argv[i++] = "-s";
|
||||||
|
|
||||||
parg = kpargv_find(pargv, "validate");
|
parg = kpargv_find(pargv, "validate");
|
||||||
if (parg)
|
if (parg)
|
||||||
strlcpy(validate, "-n", sizeof(validate));
|
argv[i++] = "-n";
|
||||||
|
|
||||||
/* Ensure we run the copy command as the logged-in user, not root (klishd) */
|
parg = kpargv_find(pargv, "user");
|
||||||
return systemf("doas -u %s copy -s %s %s %s %s", cd_home(ctx), validate, user, src, dst);
|
if (parg) {
|
||||||
|
remote_user = kparg_value(parg);
|
||||||
|
argv[i++] = "-u";
|
||||||
|
argv[i++] = (char *)remote_user;
|
||||||
|
}
|
||||||
|
|
||||||
|
argv[i++] = (char *)src;
|
||||||
|
argv[i++] = (char *)dst;
|
||||||
|
argv[i] = NULL;
|
||||||
|
|
||||||
|
return run(argv);
|
||||||
}
|
}
|
||||||
|
|
||||||
int infix_shell(kcontext_t *ctx)
|
int infix_shell(kcontext_t *ctx)
|
||||||
@@ -299,13 +360,12 @@ static const char *valid_boot_target(const kparg_t *parg)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
int infix_set_boot_order(kcontext_t *ctx)
|
static int infix_set_boot_order(kcontext_t *ctx)
|
||||||
{
|
{
|
||||||
char tmpfile[] = "/tmp/boot-order-XXXXXX";
|
|
||||||
kpargv_t *pargv = kcontext_pargv(ctx);
|
kpargv_t *pargv = kcontext_pargv(ctx);
|
||||||
const char *targets[3];
|
const char *targets[3];
|
||||||
int fd, rc = 0;
|
char *argv[16];
|
||||||
FILE *fp;
|
int i = 0;
|
||||||
|
|
||||||
targets[0] = valid_boot_target(kpargv_find(pargv, "first"));
|
targets[0] = valid_boot_target(kpargv_find(pargv, "first"));
|
||||||
targets[1] = valid_boot_target(kpargv_find(pargv, "second"));
|
targets[1] = valid_boot_target(kpargv_find(pargv, "second"));
|
||||||
@@ -316,34 +376,41 @@ int infix_set_boot_order(kcontext_t *ctx)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
fd = mkstemp(tmpfile);
|
argv[i++] = "doas";
|
||||||
if (fd == -1)
|
argv[i++] = "-u";
|
||||||
goto fail;
|
argv[i++] = (char *)cd_home(ctx);
|
||||||
|
argv[i++] = "rpc";
|
||||||
|
argv[i++] = "/infix-system:set-boot-order";
|
||||||
|
|
||||||
fp = fdopen(fd, "w");
|
if (targets[0]) {
|
||||||
if (!fp) {
|
argv[i++] = "boot-order";
|
||||||
close(fd);
|
argv[i++] = (char *)targets[0];
|
||||||
unlink(tmpfile);
|
|
||||||
fail:
|
|
||||||
fprintf(stderr, ERRMSG "failed creating temporary file\n");
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
if (targets[1]) {
|
||||||
fputs("{\"infix-system:set-boot-order\":{\"boot-order\":[", fp);
|
argv[i++] = "boot-order";
|
||||||
for (size_t i = 0; i < NELEMS(targets); i++) {
|
argv[i++] = (char *)targets[1];
|
||||||
if (!targets[i])
|
|
||||||
continue;
|
|
||||||
|
|
||||||
fprintf(fp, "%s\"%s\"", i > 0 ? "," : "", targets[i]);
|
|
||||||
}
|
}
|
||||||
fputs("]}}", fp);
|
if (targets[2]) {
|
||||||
|
argv[i++] = "boot-order";
|
||||||
|
argv[i++] = (char *)targets[2];
|
||||||
|
}
|
||||||
|
argv[i] = NULL;
|
||||||
|
|
||||||
fclose(fp);
|
return run(argv);
|
||||||
|
}
|
||||||
|
|
||||||
rc = systemf("sysrepocfg -R %s -fjson 2>&1", tmpfile);
|
int infix_users(kcontext_t *ctx)
|
||||||
unlink(tmpfile);
|
{
|
||||||
|
(void)ctx;
|
||||||
|
return shellf("copy oper -x /system/authentication/user/name "
|
||||||
|
"| jq -r '.\"ietf-system:system\".authentication.user[].name'");
|
||||||
|
}
|
||||||
|
|
||||||
return rc;
|
int infix_groups(kcontext_t *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
return shellf("copy oper -x /nacm/groups "
|
||||||
|
"| jq -r '.\"ietf-netconf-acm:nacm\".groups.group[].name'");
|
||||||
}
|
}
|
||||||
|
|
||||||
int kplugin_infix_fini(kcontext_t *ctx)
|
int kplugin_infix_fini(kcontext_t *ctx)
|
||||||
@@ -363,6 +430,8 @@ int kplugin_infix_init(kcontext_t *ctx)
|
|||||||
kplugin_add_syms(plugin, ksym_new("erase", infix_erase));
|
kplugin_add_syms(plugin, ksym_new("erase", infix_erase));
|
||||||
kplugin_add_syms(plugin, ksym_new("files", infix_files));
|
kplugin_add_syms(plugin, ksym_new("files", infix_files));
|
||||||
kplugin_add_syms(plugin, ksym_new("ifaces", infix_ifaces));
|
kplugin_add_syms(plugin, ksym_new("ifaces", infix_ifaces));
|
||||||
|
kplugin_add_syms(plugin, ksym_new("users", infix_users));
|
||||||
|
kplugin_add_syms(plugin, ksym_new("groups", infix_groups));
|
||||||
kplugin_add_syms(plugin, ksym_new("firewall_zones", infix_firewall_zones));
|
kplugin_add_syms(plugin, ksym_new("firewall_zones", infix_firewall_zones));
|
||||||
kplugin_add_syms(plugin, ksym_new("firewall_policies", infix_firewall_policies));
|
kplugin_add_syms(plugin, ksym_new("firewall_policies", infix_firewall_policies));
|
||||||
kplugin_add_syms(plugin, ksym_new("firewall_services", infix_firewall_services));
|
kplugin_add_syms(plugin, ksym_new("firewall_services", infix_firewall_services));
|
||||||
|
|||||||
@@ -141,6 +141,20 @@
|
|||||||
<ACTION sym="STRING"/>
|
<ACTION sym="STRING"/>
|
||||||
</PTYPE>
|
</PTYPE>
|
||||||
|
|
||||||
|
<PTYPE name="USERS">
|
||||||
|
<COMPL>
|
||||||
|
<ACTION sym="users@infix"/>
|
||||||
|
</COMPL>
|
||||||
|
<ACTION sym="STRING"/>
|
||||||
|
</PTYPE>
|
||||||
|
|
||||||
|
<PTYPE name="GROUPS">
|
||||||
|
<COMPL>
|
||||||
|
<ACTION sym="groups@infix"/>
|
||||||
|
</COMPL>
|
||||||
|
<ACTION sym="STRING"/>
|
||||||
|
</PTYPE>
|
||||||
|
|
||||||
<VIEW name="main">
|
<VIEW name="main">
|
||||||
<HOTKEY key="^D" cmd="exit"/>
|
<HOTKEY key="^D" cmd="exit"/>
|
||||||
|
|
||||||
@@ -238,7 +252,7 @@
|
|||||||
<PARAM name="pwpass" ptype="/STRING" help="Optional clear text password (no spaces)"/>
|
<PARAM name="pwpass" ptype="/STRING" help="Optional clear text password (no spaces)"/>
|
||||||
</SWITCH>
|
</SWITCH>
|
||||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||||
type=${KLISH_PARAM_pwhash:-sha512}
|
type=${KLISH_PARAM_pwhash:-yescrypt}
|
||||||
salt=${KLISH_PARAM_pwsalt:+-S $KLISH_PARAM_pwsalt}
|
salt=${KLISH_PARAM_pwsalt:+-S $KLISH_PARAM_pwsalt}
|
||||||
mkpasswd -m $type $salt $KLISH_PARAM_pwpass
|
mkpasswd -m $type $salt $KLISH_PARAM_pwpass
|
||||||
</ACTION>
|
</ACTION>
|
||||||
@@ -324,6 +338,20 @@
|
|||||||
</ACTION>
|
</ACTION>
|
||||||
</COMMAND>
|
</COMMAND>
|
||||||
|
|
||||||
|
<COMMAND name="nacm" help="Show users and NACM status and groups">
|
||||||
|
<SWITCH name="subcommands" min="0">
|
||||||
|
<COMMAND name="group" help="Show details for a specific NACM group">
|
||||||
|
<PARAM name="name" ptype="/GROUPS" help="Group name"/>
|
||||||
|
<ACTION sym="script" in="tty" out="tty" interrupt="true">show nacm group $KLISH_PARAM_name</ACTION>
|
||||||
|
</COMMAND>
|
||||||
|
<COMMAND name="user" help="Show NACM details for a specific user">
|
||||||
|
<PARAM name="name" ptype="/USERS" help="User name"/>
|
||||||
|
<ACTION sym="script" in="tty" out="tty" interrupt="true">show nacm user $KLISH_PARAM_name</ACTION>
|
||||||
|
</COMMAND>
|
||||||
|
</SWITCH>
|
||||||
|
<ACTION sym="script" in="tty" out="tty" interrupt="true">show nacm</ACTION>
|
||||||
|
</COMMAND>
|
||||||
|
|
||||||
<COMMAND name="ntp" help="Show NTP status">
|
<COMMAND name="ntp" help="Show NTP status">
|
||||||
<SWITCH name="subcommands" min="0">
|
<SWITCH name="subcommands" min="0">
|
||||||
<COMMAND name="source" help="Show NTP source(s)">
|
<COMMAND name="source" help="Show NTP source(s)">
|
||||||
@@ -628,15 +656,15 @@
|
|||||||
</COMMAND>
|
</COMMAND>
|
||||||
|
|
||||||
<COMMAND name="factory-config" help="Show factory-config">
|
<COMMAND name="factory-config" help="Show factory-config">
|
||||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">jq -C . /etc/factory-config.cfg |pager</ACTION>
|
<ACTION sym="script" in="tty" out="tty" interrupt="true">copy factory | jq -C . |pager</ACTION>
|
||||||
</COMMAND>
|
</COMMAND>
|
||||||
|
|
||||||
<COMMAND name="running-config" help="Show running-config">
|
<COMMAND name="running-config" help="Show running-config">
|
||||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">sysrepocfg -X -f json | jq -C . |pager</ACTION>
|
<ACTION sym="script" in="tty" out="tty" interrupt="true">copy running | jq -C . |pager</ACTION>
|
||||||
</COMMAND>
|
</COMMAND>
|
||||||
|
|
||||||
<COMMAND name="startup-config" help="Show startup-config">
|
<COMMAND name="startup-config" help="Show startup-config">
|
||||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">jq -C . /cfg/startup-config.cfg |pager</ACTION>
|
<ACTION sym="script" in="tty" out="tty" interrupt="true">copy startup | jq -C . |pager</ACTION>
|
||||||
</COMMAND>
|
</COMMAND>
|
||||||
|
|
||||||
<COMMAND name="firewall" help="Show firewall status and configuration">
|
<COMMAND name="firewall" help="Show firewall status and configuration">
|
||||||
@@ -646,12 +674,12 @@
|
|||||||
<PARAM name="limit" ptype="/UINT" help="Last N lines, default: all"/>
|
<PARAM name="limit" ptype="/UINT" help="Last N lines, default: all"/>
|
||||||
</SWITCH>
|
</SWITCH>
|
||||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||||
sysrepocfg -X -d operational -x /infix-firewall:firewall -f json -t 60 | /usr/libexec/statd/cli-pretty show-firewall-log $KLISH_PARAM_limit |pager +G
|
copy operational -x /infix-firewall:firewall | /usr/libexec/statd/cli-pretty show-firewall-log $KLISH_PARAM_limit |pager +G
|
||||||
</ACTION>
|
</ACTION>
|
||||||
</COMMAND>
|
</COMMAND>
|
||||||
<COMMAND name="matrix" help="Show firewall zone matrix">
|
<COMMAND name="matrix" help="Show firewall zone matrix">
|
||||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||||
sysrepocfg -X -d operational -x /infix-firewall:firewall -f json -t 60 | /usr/libexec/statd/cli-pretty show-firewall-matrix
|
copy operational -x /infix-firewall:firewall | /usr/libexec/statd/cli-pretty show-firewall-matrix
|
||||||
</ACTION>
|
</ACTION>
|
||||||
</COMMAND>
|
</COMMAND>
|
||||||
<COMMAND name="zone" help="Show firewall zones">
|
<COMMAND name="zone" help="Show firewall zones">
|
||||||
@@ -659,7 +687,7 @@
|
|||||||
<PARAM name="name" ptype="/FIREWALL_ZONES" help="Zone name"/>
|
<PARAM name="name" ptype="/FIREWALL_ZONES" help="Zone name"/>
|
||||||
</SWITCH>
|
</SWITCH>
|
||||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||||
sysrepocfg -X -d operational -x /infix-firewall:firewall -f json -t 60 | /usr/libexec/statd/cli-pretty show-firewall-zone "$KLISH_PARAM_name" |pager
|
copy operational -x /infix-firewall:firewall | /usr/libexec/statd/cli-pretty show-firewall-zone "$KLISH_PARAM_name" |pager
|
||||||
</ACTION>
|
</ACTION>
|
||||||
</COMMAND>
|
</COMMAND>
|
||||||
<COMMAND name="policy" help="Show firewall policies">
|
<COMMAND name="policy" help="Show firewall policies">
|
||||||
@@ -667,7 +695,7 @@
|
|||||||
<PARAM name="name" ptype="/FIREWALL_POLICIES" help="Policy name"/>
|
<PARAM name="name" ptype="/FIREWALL_POLICIES" help="Policy name"/>
|
||||||
</SWITCH>
|
</SWITCH>
|
||||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||||
sysrepocfg -X -d operational -x /infix-firewall:firewall -f json -t 60 | /usr/libexec/statd/cli-pretty show-firewall-policy "$KLISH_PARAM_name" |pager
|
copy operational -x /infix-firewall:firewall | /usr/libexec/statd/cli-pretty show-firewall-policy "$KLISH_PARAM_name" |pager
|
||||||
</ACTION>
|
</ACTION>
|
||||||
</COMMAND>
|
</COMMAND>
|
||||||
<COMMAND name="service" help="Show firewall services">
|
<COMMAND name="service" help="Show firewall services">
|
||||||
@@ -675,12 +703,12 @@
|
|||||||
<PARAM name="name" ptype="/FIREWALL_SERVICES" help="Service name"/>
|
<PARAM name="name" ptype="/FIREWALL_SERVICES" help="Service name"/>
|
||||||
</SWITCH>
|
</SWITCH>
|
||||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||||
sysrepocfg -X -d operational -x /infix-firewall:firewall -f json -t 60 | /usr/libexec/statd/cli-pretty show-firewall-service "$KLISH_PARAM_name" |pager
|
copy operational -x /infix-firewall:firewall | /usr/libexec/statd/cli-pretty show-firewall-service "$KLISH_PARAM_name" |pager
|
||||||
</ACTION>
|
</ACTION>
|
||||||
</COMMAND>
|
</COMMAND>
|
||||||
</SWITCH>
|
</SWITCH>
|
||||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||||
sysrepocfg -X -d operational -x /infix-firewall:firewall -f json -t 60 | /usr/libexec/statd/cli-pretty show-firewall |pager
|
copy operational -x /infix-firewall:firewall | /usr/libexec/statd/cli-pretty show-firewall |pager
|
||||||
</ACTION>
|
</ACTION>
|
||||||
</COMMAND>
|
</COMMAND>
|
||||||
</COMMAND>
|
</COMMAND>
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2025 Richard Alpe
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
.PHONY: all clean distclean install
|
|
||||||
|
|
||||||
all:
|
|
||||||
|
|
||||||
clean:
|
|
||||||
|
|
||||||
distclean: clean
|
|
||||||
|
|
||||||
install:
|
|
||||||
install -D show.py $(DESTDIR)/bin/show
|
|
||||||
install -D bash_completion.d/show $(DESTDIR)/etc/bash_completion.d/show
|
|
||||||
@@ -216,11 +216,12 @@ def format_uptime_seconds(seconds):
|
|||||||
|
|
||||||
class Column:
|
class Column:
|
||||||
"""Column definition for SimpleTable"""
|
"""Column definition for SimpleTable"""
|
||||||
def __init__(self, name, align='left', formatter=None, flexible=False):
|
def __init__(self, name, align='left', formatter=None, flexible=False, min_width=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.align = align
|
self.align = align
|
||||||
self.formatter = formatter
|
self.formatter = formatter
|
||||||
self.flexible = flexible
|
self.flexible = flexible
|
||||||
|
self.min_width = min_width
|
||||||
|
|
||||||
class SimpleTable:
|
class SimpleTable:
|
||||||
"""Simple table formatter that handles ANSI colors correctly and calculates dynamic column widths"""
|
"""Simple table formatter that handles ANSI colors correctly and calculates dynamic column widths"""
|
||||||
@@ -307,6 +308,11 @@ class SimpleTable:
|
|||||||
value_width = self.visible_width(str(formatted_value))
|
value_width = self.visible_width(str(formatted_value))
|
||||||
widths[i] = max(widths[i], value_width)
|
widths[i] = max(widths[i], value_width)
|
||||||
|
|
||||||
|
# Apply column minimum widths
|
||||||
|
for i, column in enumerate(self.columns):
|
||||||
|
if column.min_width:
|
||||||
|
widths[i] = max(widths[i], column.min_width)
|
||||||
|
|
||||||
return widths
|
return widths
|
||||||
|
|
||||||
def _format_header(self, column_widths, styled=True):
|
def _format_header(self, column_widths, styled=True):
|
||||||
@@ -2900,6 +2906,481 @@ def show_ntp_source(json, address=None):
|
|||||||
print(row)
|
print(row)
|
||||||
|
|
||||||
|
|
||||||
|
def _analyze_group_permissions(nacm):
|
||||||
|
"""Analyze NACM rules to determine effective permissions per group.
|
||||||
|
|
||||||
|
NACM rule evaluation order:
|
||||||
|
1. Rule-lists are processed sequentially in configuration order
|
||||||
|
2. Within each rule-list, rules are evaluated sequentially
|
||||||
|
3. First matching rule wins - no further rules are evaluated
|
||||||
|
4. If no rule matches, global defaults apply
|
||||||
|
5. permit-all (module-name=*, access-operations=*) bypasses everything
|
||||||
|
"""
|
||||||
|
read_default = nacm.get('read-default', 'permit') == 'permit'
|
||||||
|
write_default = nacm.get('write-default', 'permit') == 'permit'
|
||||||
|
exec_default = nacm.get('exec-default', 'permit') == 'permit'
|
||||||
|
|
||||||
|
rule_lists = nacm.get('rule-list', [])
|
||||||
|
groups_config = nacm.get('groups', {}).get('group', [])
|
||||||
|
|
||||||
|
# Collect all deny rules that apply to "*" (all groups)
|
||||||
|
# These create restrictions for groups without permit-all
|
||||||
|
global_denials = []
|
||||||
|
for rule_list in rule_lists:
|
||||||
|
if '*' in rule_list.get('group', []):
|
||||||
|
for rule in rule_list.get('rule', []):
|
||||||
|
if rule.get('action') == 'deny':
|
||||||
|
global_denials.append(rule)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for group in groups_config:
|
||||||
|
group_name = group.get('name', '')
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
can_read = read_default
|
||||||
|
can_write = write_default
|
||||||
|
can_exec = exec_default
|
||||||
|
restrictions = []
|
||||||
|
has_permit_all = False
|
||||||
|
|
||||||
|
# Process rule-lists in order (first match wins)
|
||||||
|
for rule_list in rule_lists:
|
||||||
|
list_groups = rule_list.get('group', [])
|
||||||
|
|
||||||
|
# Check if this rule-list applies to this group specifically
|
||||||
|
if group_name not in list_groups:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for rule in rule_list.get('rule', []):
|
||||||
|
action = rule.get('action')
|
||||||
|
access_ops = rule.get('access-operations', '')
|
||||||
|
module_name = rule.get('module-name', '')
|
||||||
|
|
||||||
|
# permit-all pattern: bypasses ALL other rules including global denials
|
||||||
|
if action == 'permit' and module_name == '*' and access_ops == '*':
|
||||||
|
has_permit_all = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# deny pattern for write+exec (like guest)
|
||||||
|
if action == 'deny' and module_name == '*':
|
||||||
|
ops = access_ops.lower()
|
||||||
|
if all(op in ops for op in ['create', 'update', 'delete']):
|
||||||
|
can_write = False
|
||||||
|
if 'exec' in ops:
|
||||||
|
can_exec = False
|
||||||
|
|
||||||
|
if has_permit_all:
|
||||||
|
break
|
||||||
|
|
||||||
|
# If not permit-all, global denials create restrictions
|
||||||
|
# These affect READ too (access-operations: "*" includes read)
|
||||||
|
if not has_permit_all:
|
||||||
|
for rule in global_denials:
|
||||||
|
path = rule.get('path', '')
|
||||||
|
module = rule.get('module-name', '')
|
||||||
|
|
||||||
|
if path:
|
||||||
|
# Extract meaningful part: /ietf-system:system/.../password -> password
|
||||||
|
restriction = path.rstrip('/').split('/')[-1]
|
||||||
|
elif module:
|
||||||
|
# Remove ietf- prefix for brevity: ietf-keystore -> keystore
|
||||||
|
restriction = module.replace('ietf-', '')
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if restriction and restriction not in restrictions:
|
||||||
|
restrictions.append(restriction)
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'group': group_name,
|
||||||
|
'read': can_read,
|
||||||
|
'write': can_write,
|
||||||
|
'exec': can_exec,
|
||||||
|
'restrictions': restrictions,
|
||||||
|
'has_permit_all': has_permit_all
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _nacm_permissions_matrix(nacm, width=None):
|
||||||
|
"""Render NACM group permissions as a colored matrix.
|
||||||
|
|
||||||
|
Symbols: ✓=full access, ⚠=restricted, ✗=denied
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Multi-line string containing the rendered matrix, or None if no groups
|
||||||
|
"""
|
||||||
|
permissions = _analyze_group_permissions(nacm)
|
||||||
|
if not permissions:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Column widths
|
||||||
|
group_col_width = max(len(p['group']) for p in permissions)
|
||||||
|
group_col_width = max(group_col_width, 5) # Minimum for "GROUP"
|
||||||
|
cell_width = 7 # Width for READ/WRITE/EXEC columns
|
||||||
|
|
||||||
|
# Box drawing
|
||||||
|
top_border = "┌" + "─" * (group_col_width + 2) + "┬"
|
||||||
|
top_border += "─" * (cell_width + 2) + "┬"
|
||||||
|
top_border += "─" * (cell_width + 2) + "┬"
|
||||||
|
top_border += "─" * (cell_width + 2) + "┐"
|
||||||
|
|
||||||
|
header_border = "├" + "─" * (group_col_width + 2) + "┼"
|
||||||
|
header_border += "─" * (cell_width + 2) + "┼"
|
||||||
|
header_border += "─" * (cell_width + 2) + "┼"
|
||||||
|
header_border += "─" * (cell_width + 2) + "┤"
|
||||||
|
|
||||||
|
bottom_border = "└" + "─" * (group_col_width + 2) + "┴"
|
||||||
|
bottom_border += "─" * (cell_width + 2) + "┴"
|
||||||
|
bottom_border += "─" * (cell_width + 2) + "┴"
|
||||||
|
bottom_border += "─" * (cell_width + 2) + "┘"
|
||||||
|
|
||||||
|
# Header row
|
||||||
|
header = f"│ {'GROUP':<{group_col_width}} │"
|
||||||
|
header += f" {'READ':^{cell_width}} │"
|
||||||
|
header += f" {'WRITE':^{cell_width}} │"
|
||||||
|
header += f" {'EXEC':^{cell_width}} │"
|
||||||
|
|
||||||
|
# Calculate centering
|
||||||
|
matrix_width = len(top_border)
|
||||||
|
if width and width > matrix_width:
|
||||||
|
padding = (width - matrix_width) // 2
|
||||||
|
indent = " " * padding
|
||||||
|
else:
|
||||||
|
indent = ""
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append(indent + top_border)
|
||||||
|
lines.append(indent + header)
|
||||||
|
lines.append(indent + header_border)
|
||||||
|
|
||||||
|
# Data rows
|
||||||
|
for perm in permissions:
|
||||||
|
group_name = perm['group']
|
||||||
|
has_restrictions = bool(perm['restrictions'])
|
||||||
|
|
||||||
|
# Determine symbols and colors for each cell
|
||||||
|
def get_cell(has_access, has_restrictions):
|
||||||
|
if not has_access:
|
||||||
|
return Decore.red_bg(f" {'✗':^{cell_width}} ")
|
||||||
|
elif has_restrictions:
|
||||||
|
return Decore.yellow_bg(f" {'⚠':^{cell_width}} ")
|
||||||
|
else:
|
||||||
|
return Decore.green_bg(f" {'✓':^{cell_width}} ")
|
||||||
|
|
||||||
|
read_cell = get_cell(perm['read'], has_restrictions)
|
||||||
|
write_cell = get_cell(perm['write'], has_restrictions)
|
||||||
|
exec_cell = get_cell(perm['exec'], has_restrictions)
|
||||||
|
|
||||||
|
row = f"│ {group_name:<{group_col_width}} │{read_cell}│{write_cell}│{exec_cell}│"
|
||||||
|
lines.append(indent + row)
|
||||||
|
|
||||||
|
lines.append(indent + bottom_border)
|
||||||
|
|
||||||
|
# Legend
|
||||||
|
legend_data = [
|
||||||
|
("✓ Full", Decore.green_bg),
|
||||||
|
("⚠ Restricted", Decore.yellow_bg),
|
||||||
|
("✗ Denied", Decore.red_bg)
|
||||||
|
]
|
||||||
|
colorized_parts = [bg_func(f" {text} ") for text, bg_func in legend_data]
|
||||||
|
legend = " ".join(colorized_parts)
|
||||||
|
|
||||||
|
if width:
|
||||||
|
visible_legend = " ".join([f" {text} " for text, _ in legend_data])
|
||||||
|
legend_padding = max(0, (width - len(visible_legend)) // 2)
|
||||||
|
lines.append(" " * legend_padding + legend)
|
||||||
|
else:
|
||||||
|
lines.append(indent + legend)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def show_nacm(json):
|
||||||
|
"""Display users and NACM (Network Configuration Access Control) groups"""
|
||||||
|
min_width = 62
|
||||||
|
|
||||||
|
nacm = json.get("ietf-netconf-acm:nacm", {})
|
||||||
|
if not nacm:
|
||||||
|
print("NACM not configured.")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if nacm:
|
||||||
|
enabled = "yes" if nacm.get("enable-nacm", True) else "no"
|
||||||
|
print(f"{'enabled':<21}: {enabled}")
|
||||||
|
print(f"{'default read access':<21}: {nacm.get('read-default', 'permit')}")
|
||||||
|
print(f"{'default write access':<21}: {nacm.get('write-default', 'deny')}")
|
||||||
|
print(f"{'default exec access':<21}: {nacm.get('exec-default', 'permit')}")
|
||||||
|
print(f"{'denied operations':<21}: {nacm.get('denied-operations', 0)}")
|
||||||
|
print(f"{'denied data writes':<21}: {nacm.get('denied-data-writes', 0)}")
|
||||||
|
print(f"{'denied notifications':<21}: {nacm.get('denied-notifications', 0)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
# Group permissions matrix
|
||||||
|
if nacm:
|
||||||
|
matrix = _nacm_permissions_matrix(nacm, min_width)
|
||||||
|
if matrix:
|
||||||
|
print(matrix)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Users table
|
||||||
|
system = json.get("ietf-system:system", {})
|
||||||
|
users = system.get("authentication", {}).get("user", [])
|
||||||
|
if users:
|
||||||
|
user_table = SimpleTable([
|
||||||
|
Column('USER', min_width=12),
|
||||||
|
Column('SHELL'),
|
||||||
|
Column('LOGIN', flexible=True)
|
||||||
|
], min_width=min_width)
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
name = user.get("name", "")
|
||||||
|
shell_data = user.get("infix-system:shell", "false")
|
||||||
|
shell = shell_data.split(":")[-1] if ":" in shell_data else shell_data
|
||||||
|
|
||||||
|
has_password = bool(user.get("password"))
|
||||||
|
has_keys = bool(user.get("authorized-key"))
|
||||||
|
if has_password and has_keys:
|
||||||
|
login = "password+key"
|
||||||
|
elif has_password:
|
||||||
|
login = "password"
|
||||||
|
elif has_keys:
|
||||||
|
login = "key"
|
||||||
|
else:
|
||||||
|
login = "-"
|
||||||
|
|
||||||
|
user_table.row(name, shell, login)
|
||||||
|
|
||||||
|
user_table.print()
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Groups table
|
||||||
|
groups = nacm.get("groups", {}).get("group", []) if nacm else []
|
||||||
|
if groups:
|
||||||
|
group_table = SimpleTable([
|
||||||
|
Column('GROUP', min_width=12),
|
||||||
|
Column('USERS', flexible=True)
|
||||||
|
], min_width=min_width)
|
||||||
|
|
||||||
|
for group in groups:
|
||||||
|
name = group.get("name", "")
|
||||||
|
members = " ".join(group.get("user-name", []))
|
||||||
|
group_table.row(name, members)
|
||||||
|
|
||||||
|
group_table.print()
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def show_nacm_group(json):
|
||||||
|
"""Display detailed information about a specific NACM group."""
|
||||||
|
group_name = json.get('_group_name')
|
||||||
|
nacm = json.get("ietf-netconf-acm:nacm", {})
|
||||||
|
|
||||||
|
if not nacm:
|
||||||
|
print("NACM not configured.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find the group
|
||||||
|
groups = nacm.get("groups", {}).get("group", [])
|
||||||
|
group = None
|
||||||
|
for g in groups:
|
||||||
|
if g.get("name") == group_name:
|
||||||
|
group = g
|
||||||
|
break
|
||||||
|
|
||||||
|
if not group:
|
||||||
|
print(f"Group '{group_name}' not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Group info
|
||||||
|
members = group.get("user-name", [])
|
||||||
|
members_str = ", ".join(members) if members else "(none)"
|
||||||
|
print(f"{'members':<17}: {members_str}")
|
||||||
|
|
||||||
|
# Analyze permissions
|
||||||
|
permissions = _analyze_group_permissions(nacm)
|
||||||
|
group_perm = None
|
||||||
|
for p in permissions:
|
||||||
|
if p['group'] == group_name:
|
||||||
|
group_perm = p
|
||||||
|
break
|
||||||
|
|
||||||
|
if group_perm:
|
||||||
|
def perm_str(has_access, has_restrictions):
|
||||||
|
if not has_access:
|
||||||
|
return Decore.red("no")
|
||||||
|
elif has_restrictions:
|
||||||
|
return Decore.yellow("restricted")
|
||||||
|
else:
|
||||||
|
return Decore.green("yes")
|
||||||
|
|
||||||
|
has_restrictions = bool(group_perm['restrictions'])
|
||||||
|
print(f"{'read permission':<17}: {perm_str(group_perm['read'], has_restrictions)}")
|
||||||
|
print(f"{'write permission':<17}: {perm_str(group_perm['write'], has_restrictions)}")
|
||||||
|
print(f"{'exec permission':<17}: {perm_str(group_perm['exec'], has_restrictions)}")
|
||||||
|
|
||||||
|
# Find applicable rules, respecting NACM evaluation order
|
||||||
|
# Rule-lists are evaluated in order; first matching rule wins
|
||||||
|
# If a permit-all rule is found, no further rules would be evaluated
|
||||||
|
rule_lists = nacm.get("rule-list", [])
|
||||||
|
applicable_rules = []
|
||||||
|
has_permit_all = group_perm.get('has_permit_all', False) if group_perm else False
|
||||||
|
|
||||||
|
for rule_list in rule_lists:
|
||||||
|
list_groups = rule_list.get("group", [])
|
||||||
|
|
||||||
|
# Check if this rule-list applies to this group specifically (not via "*")
|
||||||
|
if group_name in list_groups:
|
||||||
|
list_name = rule_list.get("name", "")
|
||||||
|
for rule in rule_list.get("rule", []):
|
||||||
|
applicable_rules.append({
|
||||||
|
'list': list_name,
|
||||||
|
'rule': rule,
|
||||||
|
'applies_via': group_name
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check if this is a permit-all rule
|
||||||
|
action = rule.get('action')
|
||||||
|
module_name = rule.get('module-name', '')
|
||||||
|
access_ops = rule.get('access-operations', '')
|
||||||
|
if action == 'permit' and module_name == '*' and access_ops == '*':
|
||||||
|
# permit-all matches everything, stop here
|
||||||
|
break
|
||||||
|
|
||||||
|
if has_permit_all:
|
||||||
|
# Don't process any more rule-lists
|
||||||
|
break
|
||||||
|
|
||||||
|
# Only add rules from "*" group if we don't have permit-all
|
||||||
|
if not has_permit_all:
|
||||||
|
for rule_list in rule_lists:
|
||||||
|
list_groups = rule_list.get("group", [])
|
||||||
|
if "*" in list_groups and group_name not in list_groups:
|
||||||
|
list_name = rule_list.get("name", "")
|
||||||
|
for rule in rule_list.get("rule", []):
|
||||||
|
applicable_rules.append({
|
||||||
|
'list': list_name,
|
||||||
|
'rule': rule,
|
||||||
|
'applies_via': "*"
|
||||||
|
})
|
||||||
|
|
||||||
|
if applicable_rules:
|
||||||
|
print(f"{'applicable rules':<17}: {len(applicable_rules)}")
|
||||||
|
|
||||||
|
for item in applicable_rules:
|
||||||
|
rule = item['rule']
|
||||||
|
rule_name = rule.get('name', '(unnamed)')
|
||||||
|
action = rule.get('action', 'permit')
|
||||||
|
access_ops = rule.get('access-operations', '*')
|
||||||
|
module_name = rule.get('module-name', '')
|
||||||
|
path = rule.get('path', '')
|
||||||
|
rpc_name = rule.get('rpc-name', '')
|
||||||
|
comment = rule.get('comment', '')
|
||||||
|
|
||||||
|
# Format action with color
|
||||||
|
if action == 'permit':
|
||||||
|
action_str = Decore.green("permit")
|
||||||
|
else:
|
||||||
|
action_str = Decore.red("deny")
|
||||||
|
|
||||||
|
# Build target description
|
||||||
|
target = ""
|
||||||
|
if path:
|
||||||
|
target = path
|
||||||
|
elif module_name:
|
||||||
|
target = module_name
|
||||||
|
if rpc_name:
|
||||||
|
target += f" (rpc: {rpc_name})"
|
||||||
|
|
||||||
|
applies_note = f" (via '*')" if item['applies_via'] == '*' else ""
|
||||||
|
|
||||||
|
Decore.title(f"{rule_name}{applies_note}", width=70)
|
||||||
|
print(f"{' action':<13}: {action_str}")
|
||||||
|
print(f"{' operations':<13}: {access_ops}")
|
||||||
|
if target:
|
||||||
|
print(f"{' target':<13}: {target}")
|
||||||
|
if comment:
|
||||||
|
print(f"{' comment':<13}: {comment}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def show_nacm_user(json):
|
||||||
|
"""Display detailed information about a specific user's NACM permissions."""
|
||||||
|
user_name = json.get('_user_name')
|
||||||
|
nacm = json.get("ietf-netconf-acm:nacm", {})
|
||||||
|
system = json.get("ietf-system:system", {})
|
||||||
|
|
||||||
|
# Find the user
|
||||||
|
users = system.get("authentication", {}).get("user", [])
|
||||||
|
user = None
|
||||||
|
for u in users:
|
||||||
|
if u.get("name") == user_name:
|
||||||
|
user = u
|
||||||
|
break
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
print(f"User '{user_name}' not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# User details
|
||||||
|
shell_data = user.get("infix-system:shell", "false")
|
||||||
|
shell = shell_data.split(":")[-1] if ":" in shell_data else shell_data
|
||||||
|
print(f"{'shell':<17}: {shell}")
|
||||||
|
|
||||||
|
has_password = bool(user.get("password"))
|
||||||
|
has_keys = bool(user.get("authorized-key"))
|
||||||
|
if has_password and has_keys:
|
||||||
|
login = "password + SSH key"
|
||||||
|
elif has_password:
|
||||||
|
login = "password"
|
||||||
|
elif has_keys:
|
||||||
|
login = "SSH key"
|
||||||
|
else:
|
||||||
|
login = "(none configured)"
|
||||||
|
print(f"{'login':<17}: {login}")
|
||||||
|
|
||||||
|
# Find which NACM groups this user belongs to
|
||||||
|
if not nacm:
|
||||||
|
print("NACM not configured.")
|
||||||
|
return
|
||||||
|
|
||||||
|
groups = nacm.get("groups", {}).get("group", [])
|
||||||
|
user_groups = []
|
||||||
|
for group in groups:
|
||||||
|
if user_name in group.get("user-name", []):
|
||||||
|
user_groups.append(group.get("name"))
|
||||||
|
|
||||||
|
groups_str = ", ".join(user_groups) if user_groups else "(none)"
|
||||||
|
print(f"{'nacm group':<17}: {groups_str}")
|
||||||
|
|
||||||
|
# Show effective permissions for each group
|
||||||
|
if user_groups:
|
||||||
|
permissions = _analyze_group_permissions(nacm)
|
||||||
|
|
||||||
|
for group_name in user_groups:
|
||||||
|
for p in permissions:
|
||||||
|
if p['group'] == group_name:
|
||||||
|
def perm_str(has_access, has_restrictions):
|
||||||
|
if not has_access:
|
||||||
|
return Decore.red("no")
|
||||||
|
elif has_restrictions:
|
||||||
|
return Decore.yellow("restricted")
|
||||||
|
else:
|
||||||
|
return Decore.green("yes")
|
||||||
|
|
||||||
|
has_restrictions = bool(p['restrictions'])
|
||||||
|
print(f"{'read permission':<17}: {perm_str(p['read'], has_restrictions)}")
|
||||||
|
print(f"{'write permission':<17}: {perm_str(p['write'], has_restrictions)}")
|
||||||
|
print(f"{'exec permission':<17}: {perm_str(p['exec'], has_restrictions)}")
|
||||||
|
break
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("For detailed rules, use: show nacm group <name>")
|
||||||
|
|
||||||
|
|
||||||
def show_system(json):
|
def show_system(json):
|
||||||
"""System information overivew"""
|
"""System information overivew"""
|
||||||
if not json.get("ietf-system:system-state"):
|
if not json.get("ietf-system:system-state"):
|
||||||
@@ -4884,6 +5365,10 @@ def main():
|
|||||||
subparsers.add_parser('show-firewall-log', help='Show firewall log') \
|
subparsers.add_parser('show-firewall-log', help='Show firewall log') \
|
||||||
.add_argument('limit', nargs='?', help='Last N lines, default: all')
|
.add_argument('limit', nargs='?', help='Last N lines, default: all')
|
||||||
|
|
||||||
|
subparsers.add_parser('show-nacm', help='Show NACM status and groups')
|
||||||
|
subparsers.add_parser('show-nacm-group', help='Show NACM group details')
|
||||||
|
subparsers.add_parser('show-nacm-user', help='Show NACM user details')
|
||||||
|
|
||||||
subparsers.add_parser('show-ntp', help='Show NTP status') \
|
subparsers.add_parser('show-ntp', help='Show NTP status') \
|
||||||
.add_argument('-a', '--address', help='Show details for specific address')
|
.add_argument('-a', '--address', help='Show details for specific address')
|
||||||
subparsers.add_parser('show-ntp-tracking', help='Show NTP tracking status')
|
subparsers.add_parser('show-ntp-tracking', help='Show NTP tracking status')
|
||||||
@@ -4947,6 +5432,12 @@ def main():
|
|||||||
show_firewall_service(json_data, args.name)
|
show_firewall_service(json_data, args.name)
|
||||||
elif args.command == "show-firewall-log":
|
elif args.command == "show-firewall-log":
|
||||||
show_firewall_logs(args.limit)
|
show_firewall_logs(args.limit)
|
||||||
|
elif args.command == "show-nacm":
|
||||||
|
show_nacm(json_data)
|
||||||
|
elif args.command == "show-nacm-group":
|
||||||
|
show_nacm_group(json_data)
|
||||||
|
elif args.command == "show-nacm-user":
|
||||||
|
show_nacm_user(json_data)
|
||||||
elif args.command == "show-ntp":
|
elif args.command == "show-ntp":
|
||||||
show_ntp(json_data, args.address)
|
show_ntp(json_data, args.address)
|
||||||
elif args.command == "show-ntp-tracking":
|
elif args.command == "show-ntp-tracking":
|
||||||
|
|||||||
@@ -262,28 +262,75 @@ def add_timezone(out):
|
|||||||
|
|
||||||
|
|
||||||
def add_users(out):
|
def add_users(out):
|
||||||
shadow_output = HOST.run_multiline(["getent", "shadow"], [])
|
# Map shell paths to YANG identity names
|
||||||
users = []
|
shell_map = {
|
||||||
|
"/bin/bash": "infix-system:bash",
|
||||||
|
"/bin/sh": "infix-system:sh",
|
||||||
|
"/usr/bin/clish": "infix-system:clish",
|
||||||
|
"/bin/false": "infix-system:false",
|
||||||
|
"/sbin/nologin": "infix-system:false",
|
||||||
|
"/usr/sbin/nologin": "infix-system:false",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get users from /etc/passwd - include users with 1000 <= uid < 10000 (added by confd)
|
||||||
|
passwd_output = HOST.run_multiline(["getent", "passwd"], [])
|
||||||
|
passwd_users = {}
|
||||||
|
for line in passwd_output:
|
||||||
|
parts = line.split(':')
|
||||||
|
if len(parts) >= 7:
|
||||||
|
username = parts[0]
|
||||||
|
uid = int(parts[2]) if parts[2].isdigit() else 0
|
||||||
|
shell = parts[6].strip()
|
||||||
|
if 1000 <= uid < 10000:
|
||||||
|
passwd_users[username] = shell_map.get(shell, "infix-system:false")
|
||||||
|
|
||||||
|
# Get password hashes from shadow
|
||||||
|
shadow_output = HOST.run_multiline(["getent", "shadow"], [])
|
||||||
|
shadow_hashes = {}
|
||||||
for line in shadow_output:
|
for line in shadow_output:
|
||||||
parts = line.split(':')
|
parts = line.split(':')
|
||||||
if len(parts) < 2:
|
if len(parts) >= 2:
|
||||||
continue
|
username = parts[0]
|
||||||
username = parts[0]
|
password_hash = parts[1]
|
||||||
password_hash = parts[1]
|
# Only include valid password hashes (not locked/disabled)
|
||||||
|
if (password_hash and
|
||||||
|
not password_hash.startswith('*') and
|
||||||
|
not password_hash.startswith('!')):
|
||||||
|
shadow_hashes[username] = password_hash
|
||||||
|
|
||||||
|
# Build user list from passwd users (1000 <= uid < 10000)
|
||||||
|
users = []
|
||||||
|
for username, shell in passwd_users.items():
|
||||||
|
user = {"name": username}
|
||||||
|
if username in shadow_hashes:
|
||||||
|
user["password"] = shadow_hashes[username]
|
||||||
|
user["infix-system:shell"] = shell
|
||||||
|
|
||||||
|
# Read SSH authorized keys from /var/run/sshd/${user}.keys
|
||||||
|
keys_file = f"/var/run/sshd/{username}.keys"
|
||||||
|
keys_content = HOST.read(keys_file)
|
||||||
|
if keys_content:
|
||||||
|
authorized_keys = []
|
||||||
|
for line in keys_content.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith('#'):
|
||||||
|
continue
|
||||||
|
parts = line.split(None, 2)
|
||||||
|
if len(parts) >= 2:
|
||||||
|
algorithm = parts[0]
|
||||||
|
key_data = parts[1]
|
||||||
|
# Use comment as key name, or generate one
|
||||||
|
key_name = parts[2] if len(parts) > 2 else f"{username}-key-{len(authorized_keys)}"
|
||||||
|
authorized_keys.append({
|
||||||
|
"name": key_name,
|
||||||
|
"algorithm": algorithm,
|
||||||
|
"key-data": key_data
|
||||||
|
})
|
||||||
|
if authorized_keys:
|
||||||
|
user["authorized-key"] = authorized_keys
|
||||||
|
|
||||||
# Skip any records that do not pass YANG validation
|
|
||||||
if (not password_hash or
|
|
||||||
password_hash.startswith('0') or
|
|
||||||
password_hash.startswith('*') or
|
|
||||||
password_hash.startswith('!')):
|
|
||||||
continue
|
|
||||||
user = {}
|
|
||||||
user["name"] = username
|
|
||||||
user["password"] = password_hash
|
|
||||||
users.append(user)
|
users.append(user)
|
||||||
|
|
||||||
|
|
||||||
insert(out, "authentication", "user", users)
|
insert(out, "authentication", "user", users)
|
||||||
|
|
||||||
def add_clock(out):
|
def add_clock(out):
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ image::topology.svg[DHCP Server Multiple Subnets topology, align=center, scaledw
|
|||||||
==== Sequence
|
==== Sequence
|
||||||
|
|
||||||
. Set up topology and attach to client and server DUTs
|
. Set up topology and attach to client and server DUTs
|
||||||
. Configure DHCP server and clients
|
. Configure DHCP server
|
||||||
|
. Configure client hostnames
|
||||||
|
. Configure DHCP clients
|
||||||
. Verify DHCP client1 get correct lease
|
. Verify DHCP client1 get correct lease
|
||||||
. Verify DHCP client1 has default route via server
|
. Verify DHCP client1 has default route via server
|
||||||
. Verify DHCP client1 has correct DNS server(s)
|
. Verify DHCP client1 has correct DNS server(s)
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ with infamy.Test() as test:
|
|||||||
client2 = env.attach("client2", "mgmt")
|
client2 = env.attach("client2", "mgmt")
|
||||||
client3 = env.attach("client3", "mgmt")
|
client3 = env.attach("client3", "mgmt")
|
||||||
|
|
||||||
with test.step("Configure DHCP server and clients"):
|
with test.step("Configure DHCP server"):
|
||||||
server.put_config_dicts({
|
server.put_config_dicts({
|
||||||
"ietf-interfaces": {
|
"ietf-interfaces": {
|
||||||
"interfaces": {
|
"interfaces": {
|
||||||
@@ -195,10 +195,44 @@ with infamy.Test() as test:
|
|||||||
}
|
}
|
||||||
}})
|
}})
|
||||||
|
|
||||||
|
with test.step("Configure client hostnames"):
|
||||||
|
# Set hostnames first, before enabling DHCP clients
|
||||||
|
client1.put_config_dicts({
|
||||||
|
"ietf-system": {
|
||||||
|
"system": {
|
||||||
|
"hostname": HOSTNM1,
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
|
||||||
|
client2.put_config_dicts({
|
||||||
|
"ietf-system": {
|
||||||
|
"system": {
|
||||||
|
"hostname": HOSTNM2,
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
|
||||||
|
client3.put_config_dicts({
|
||||||
|
"ietf-system": {
|
||||||
|
"system": {
|
||||||
|
"hostname": HOSTNM3,
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
|
||||||
|
# Wait for hostnames to be applied in operational datastore
|
||||||
|
print("Waiting for client hostnames to take ...")
|
||||||
|
until(lambda: client1.get_data("/ietf-system:system")
|
||||||
|
.get("system", {}).get("hostname") == HOSTNM1)
|
||||||
|
until(lambda: client2.get_data("/ietf-system:system")
|
||||||
|
.get("system", {}).get("hostname") == HOSTNM2)
|
||||||
|
until(lambda: client3.get_data("/ietf-system:system")
|
||||||
|
.get("system", {}).get("hostname") == HOSTNM3)
|
||||||
|
|
||||||
|
with test.step("Configure DHCP clients"):
|
||||||
# All clients request/accept the same options. We do this to
|
# All clients request/accept the same options. We do this to
|
||||||
# both keep fleet configuration simple but also to verify that
|
# both keep fleet configuration simple but also to verify that
|
||||||
# the server is behaving correctly.
|
# the server is behaving correctly.
|
||||||
|
|
||||||
|
print("Enable DHCP clients ...")
|
||||||
client1.put_config_dicts({
|
client1.put_config_dicts({
|
||||||
"ietf-interfaces": {
|
"ietf-interfaces": {
|
||||||
"interfaces": {
|
"interfaces": {
|
||||||
@@ -220,7 +254,6 @@ with infamy.Test() as test:
|
|||||||
},
|
},
|
||||||
"ietf-system": {
|
"ietf-system": {
|
||||||
"system": {
|
"system": {
|
||||||
"hostname": HOSTNM1,
|
|
||||||
"ntp": {"enabled": True},
|
"ntp": {"enabled": True},
|
||||||
}
|
}
|
||||||
}})
|
}})
|
||||||
@@ -246,7 +279,6 @@ with infamy.Test() as test:
|
|||||||
},
|
},
|
||||||
"ietf-system": {
|
"ietf-system": {
|
||||||
"system": {
|
"system": {
|
||||||
"hostname": HOSTNM2,
|
|
||||||
"ntp": {"enabled": True},
|
"ntp": {"enabled": True},
|
||||||
}
|
}
|
||||||
}})
|
}})
|
||||||
@@ -272,7 +304,6 @@ with infamy.Test() as test:
|
|||||||
},
|
},
|
||||||
"ietf-system": {
|
"ietf-system": {
|
||||||
"system": {
|
"system": {
|
||||||
"hostname": HOSTNM3,
|
|
||||||
"ntp": {"enabled": True},
|
"ntp": {"enabled": True},
|
||||||
"dns-resolver": {
|
"dns-resolver": {
|
||||||
"search": [
|
"search": [
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ endif::topdoc[]
|
|||||||
. Configure
|
. Configure
|
||||||
. Reboot and wait for the unit to come back
|
. Reboot and wait for the unit to come back
|
||||||
. Verify user admin is now in wheel group
|
. Verify user admin is now in wheel group
|
||||||
. Verify user admin is now in sys-cli group
|
. Verify user admin is now in sysrepo group
|
||||||
|
|
||||||
|
|
||||||
<<<
|
<<<
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ with infamy.Test() as test:
|
|||||||
if not tgtssh.runsh("grep wheel /etc/group | grep 'admin'"):
|
if not tgtssh.runsh("grep wheel /etc/group | grep 'admin'"):
|
||||||
test.fail()
|
test.fail()
|
||||||
|
|
||||||
with test.step("Verify user admin is now in sys-cli group"):
|
with test.step("Verify user admin is now in sysrepo group"):
|
||||||
if not tgtssh.runsh("grep sys-cli /etc/group | grep 'admin'"):
|
if not tgtssh.runsh("grep sysrepo /etc/group | grep 'admin'"):
|
||||||
test.fail()
|
test.fail()
|
||||||
|
|
||||||
test.succeed()
|
test.succeed()
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"user": [
|
"user": [
|
||||||
{
|
{
|
||||||
"name": "admin",
|
"name": "admin",
|
||||||
"password": "$5$mI/zpOAqZYKLC2WU$i7iPzZiIjOjrBF3NyftS9CCq8dfYwHwrmUK097Jca9A"
|
"password": "$5$mI/zpOAqZYKLC2WU$i7iPzZiIjOjrBF3NyftS9CCq8dfYwHwrmUK097Jca9A",
|
||||||
|
"infix-system:shell": "infix-system:bash"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -39,7 +39,8 @@
|
|||||||
"user": [
|
"user": [
|
||||||
{
|
{
|
||||||
"name": "admin",
|
"name": "admin",
|
||||||
"password": "$5$mI/zpOAqZYKLC2WU$i7iPzZiIjOjrBF3NyftS9CCq8dfYwHwrmUK097Jca9A"
|
"password": "$5$mI/zpOAqZYKLC2WU$i7iPzZiIjOjrBF3NyftS9CCq8dfYwHwrmUK097Jca9A",
|
||||||
|
"infix-system:shell": "infix-system:bash"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
root:x:0:0:root:/root:/bin/bash
|
||||||
|
daemon:x:1:1:daemon:/usr/sbin:/bin/false
|
||||||
|
bin:x:2:2:bin:/bin:/bin/false
|
||||||
|
sys:x:3:3:sys:/dev:/bin/false
|
||||||
|
sync:x:4:100:sync:/bin:/bin/sync
|
||||||
|
mail:x:8:8:mail:/var/spool/mail:/bin/false
|
||||||
|
www-data:x:33:33:www-data:/var/www:/bin/false
|
||||||
|
sysrepo:x:60:60:sysrepo:/var:/bin/false
|
||||||
|
backup:x:34:34:backup:/var/backups:/bin/false
|
||||||
|
nobody:x:65534:65534:nobody:/home:/bin/false
|
||||||
|
yangnobody:x:333666:333666:Unauthenticated operations via RESTCONF:/:/bin/false
|
||||||
|
avahi:x:100:101::/:/bin/false
|
||||||
|
chrony:x:101:102:Time daemon:/run/chrony:/bin/false
|
||||||
|
dbus:x:102:103:DBus messagebus user:/run/dbus:/bin/false
|
||||||
|
frr:x:103:104:FRR user priv:/var/run/frr:/bin/false
|
||||||
|
sshd:x:105:106:SSH drop priv user:/var/empty:/bin/false
|
||||||
|
admin:x:1000:1000:Linux User,,,:/home/admin:/bin/bash
|
||||||
@@ -5,7 +5,7 @@ sys:*:::::::
|
|||||||
sync:*:::::::
|
sync:*:::::::
|
||||||
mail:*:::::::
|
mail:*:::::::
|
||||||
www-data:*:::::::
|
www-data:*:::::::
|
||||||
sys-cli:*:::::::
|
sysrepo:*:::::::
|
||||||
backup:*:::::::
|
backup:*:::::::
|
||||||
nobody:*:::::::
|
nobody:*:::::::
|
||||||
yangnobody:*:::::::
|
yangnobody:*:::::::
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ include::user_admin/Readme.adoc[]
|
|||||||
|
|
||||||
<<<
|
<<<
|
||||||
|
|
||||||
|
include::nacm-basic/Readme.adoc[]
|
||||||
|
|
||||||
|
<<<
|
||||||
|
|
||||||
include::timezone/Readme.adoc[]
|
include::timezone/Readme.adoc[]
|
||||||
|
|
||||||
<<<
|
<<<
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
- name: Add admin user
|
- name: Add admin user
|
||||||
case: user_admin/test.py
|
case: user_admin/test.py
|
||||||
|
|
||||||
|
- name: Basic NACM permissions
|
||||||
|
case: nacm-basic/test.py
|
||||||
|
|
||||||
- name: Set timezone using timezone name
|
- name: Set timezone using timezone name
|
||||||
case: timezone/test.py
|
case: timezone/test.py
|
||||||
|
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
test.adoc
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
=== Basic NACM permissions
|
||||||
|
|
||||||
|
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/system/nacm-basic]
|
||||||
|
|
||||||
|
==== Description
|
||||||
|
|
||||||
|
Test that NACM groups (admin, operator, guest) correctly enforce
|
||||||
|
access control with path-based rules and default policies.
|
||||||
|
|
||||||
|
Creates three user privilege levels from scratch:
|
||||||
|
|
||||||
|
- admin: Full access (permit-all rule)
|
||||||
|
- operator: Can manage interfaces/routing, cannot modify system config
|
||||||
|
- guest: Read-only access (write-default: deny, exec deny rule)
|
||||||
|
|
||||||
|
Verifies that:
|
||||||
|
|
||||||
|
- All users can read configuration (read-default: permit)
|
||||||
|
- Operators can modify interfaces (path-specific permit rule)
|
||||||
|
- Operators cannot modify system configuration (write-default: deny)
|
||||||
|
- Guests cannot modify any configuration (write-default: deny)
|
||||||
|
- Admin can modify all configuration (permit-all rule bypasses defaults)
|
||||||
|
|
||||||
|
==== Topology
|
||||||
|
|
||||||
|
image::topology.svg[Basic NACM permissions topology, align=center, scaledwidth=75%]
|
||||||
|
|
||||||
|
==== Sequence
|
||||||
|
|
||||||
|
. Set up topology and attach to target
|
||||||
|
. Configure NACM groups, rules, and test users
|
||||||
|
. Verify operator can read configuration
|
||||||
|
. Verify operator can modify interface configuration
|
||||||
|
. Verify operator cannot modify system configuration
|
||||||
|
. Verify guest can read configuration
|
||||||
|
. Verify guest cannot modify configuration
|
||||||
|
. Verify admin can modify configuration
|
||||||
|
|
||||||
|
|
||||||
Executable
+257
@@ -0,0 +1,257 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Verify basic NACM permission enforcement
|
||||||
|
|
||||||
|
Test that NACM groups (admin, operator, guest) correctly enforce
|
||||||
|
access control with permissive defaults and targeted denials.
|
||||||
|
|
||||||
|
The NACM design is "permit by default, deny sensitive items":
|
||||||
|
|
||||||
|
- admin: Full unrestricted access (permit-all rule)
|
||||||
|
- operator: Can configure everything EXCEPT passwords, keystore, truststore
|
||||||
|
- guest: Read-only access (explicit deny of create/update/delete/exec)
|
||||||
|
|
||||||
|
Verifies that:
|
||||||
|
|
||||||
|
- Operators can read and modify most configuration (hostname, interfaces)
|
||||||
|
- Operators CANNOT read or write password hashes (protected path)
|
||||||
|
- Guests can read but cannot modify any configuration
|
||||||
|
- Admin can access everything including passwords
|
||||||
|
"""
|
||||||
|
import infamy
|
||||||
|
|
||||||
|
OPERATOR_USER = "oper"
|
||||||
|
OPERATOR_PASS = "oper123"
|
||||||
|
GUEST_USER = "guest"
|
||||||
|
GUEST_PASS = "guest123"
|
||||||
|
|
||||||
|
with infamy.Test() as test:
|
||||||
|
with test.step("Set up topology and attach to target"):
|
||||||
|
env = infamy.Env()
|
||||||
|
target = env.attach("target", "mgmt")
|
||||||
|
|
||||||
|
with test.step("Configure NACM groups, rules, and test users"):
|
||||||
|
operator_hash = "$1$gwU5mRgP$1/ASdRwD5ycqdmWpTKHSa0"
|
||||||
|
guest_hash = "$1$4rdUOhNN$vw3i4FyPvIkzRFwrUXQod1"
|
||||||
|
|
||||||
|
target.put_config_dicts({
|
||||||
|
"ietf-system": {
|
||||||
|
"system": {
|
||||||
|
"authentication": {
|
||||||
|
"user": [{
|
||||||
|
"name": OPERATOR_USER,
|
||||||
|
"password": operator_hash,
|
||||||
|
"shell": "infix-system:bash"
|
||||||
|
}, {
|
||||||
|
"name": GUEST_USER,
|
||||||
|
"password": guest_hash,
|
||||||
|
"shell": "infix-system:bash"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ietf-netconf-acm": {
|
||||||
|
"nacm": {
|
||||||
|
"enable-nacm": True,
|
||||||
|
"read-default": "permit",
|
||||||
|
"write-default": "permit",
|
||||||
|
"exec-default": "permit",
|
||||||
|
"groups": {
|
||||||
|
"group": [{
|
||||||
|
"name": "admin",
|
||||||
|
"user-name": ["admin"]
|
||||||
|
}, {
|
||||||
|
"name": "operator",
|
||||||
|
"user-name": [OPERATOR_USER]
|
||||||
|
}, {
|
||||||
|
"name": "guest",
|
||||||
|
"user-name": [GUEST_USER]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"rule-list": [{
|
||||||
|
"name": "admin-acl",
|
||||||
|
"group": ["admin"],
|
||||||
|
"rule": [{
|
||||||
|
"name": "permit-all",
|
||||||
|
"module-name": "*",
|
||||||
|
"access-operations": "*",
|
||||||
|
"action": "permit",
|
||||||
|
"comment": "Admin has full access"
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
"name": "operator-acl",
|
||||||
|
"group": ["operator"],
|
||||||
|
"rule": [{
|
||||||
|
"name": "permit-system-rpcs",
|
||||||
|
"module-name": "ietf-system",
|
||||||
|
"access-operations": "exec",
|
||||||
|
"action": "permit",
|
||||||
|
"comment": "Operators can reboot, shutdown, and set system time"
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
"name": "guest-acl",
|
||||||
|
"group": ["guest"],
|
||||||
|
"rule": [{
|
||||||
|
"name": "deny-all-exec",
|
||||||
|
"module-name": "*",
|
||||||
|
"access-operations": "create update delete exec",
|
||||||
|
"action": "deny",
|
||||||
|
"comment": "Guests cannot change anything or call rpcs"
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
"name": "default-deny-all",
|
||||||
|
"group": ["*"],
|
||||||
|
"rule": [{
|
||||||
|
"name": "deny-password-access",
|
||||||
|
"path": "/ietf-system:system/authentication/user/password",
|
||||||
|
"access-operations": "*",
|
||||||
|
"action": "deny",
|
||||||
|
"comment": "No user except admins can access password hashes."
|
||||||
|
}, {
|
||||||
|
"name": "deny-keystore-access",
|
||||||
|
"module-name": "ietf-keystore",
|
||||||
|
"access-operations": "*",
|
||||||
|
"action": "deny",
|
||||||
|
"comment": "No user except admins can access cryptographic keys."
|
||||||
|
}, {
|
||||||
|
"name": "deny-truststore-access",
|
||||||
|
"module-name": "ietf-truststore",
|
||||||
|
"access-operations": "*",
|
||||||
|
"action": "deny",
|
||||||
|
"comment": "No user except admins can access trust store."
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
with test.step("Verify operator can read configuration"):
|
||||||
|
# Attach as operator user
|
||||||
|
operator = env.attach("target", "mgmt", username=OPERATOR_USER,
|
||||||
|
password=OPERATOR_PASS, test_reset=False)
|
||||||
|
|
||||||
|
# Operator should be able to read (read-default: permit)
|
||||||
|
ifaces = operator.get_config_dict("/ietf-interfaces:interfaces")
|
||||||
|
num_ifaces = len(ifaces.get('interfaces', {}).get('interface', []))
|
||||||
|
print(f"Operator successfully read {num_ifaces} interfaces")
|
||||||
|
|
||||||
|
with test.step("Verify operator can modify interface configuration"):
|
||||||
|
# Use patch_config() which PATCHes directly to running datastore
|
||||||
|
# This avoids full datastore copy that requires broader permissions
|
||||||
|
operator.patch_config("ietf-interfaces", {
|
||||||
|
"interfaces": {
|
||||||
|
"interface": [{
|
||||||
|
"name": "lo",
|
||||||
|
"description": "Modified by operator"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Verify the change
|
||||||
|
ifaces = operator.get_config_dict("/ietf-interfaces:interfaces")
|
||||||
|
lo_iface = None
|
||||||
|
for iface in ifaces.get('interfaces', {}).get('interface', []):
|
||||||
|
if iface.get('name') == 'lo':
|
||||||
|
lo_iface = iface
|
||||||
|
break
|
||||||
|
assert lo_iface and lo_iface.get('description') == "Modified by operator", \
|
||||||
|
"Operator failed to modify interface"
|
||||||
|
print("Operator successfully modified interface configuration")
|
||||||
|
|
||||||
|
with test.step("Verify operator can modify hostname"):
|
||||||
|
# Operators can now modify most system config (write-default: permit)
|
||||||
|
operator.patch_config("ietf-system", {
|
||||||
|
"system": {
|
||||||
|
"hostname": "operator-test"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Verify the change
|
||||||
|
cfg = operator.get_config_dict("/ietf-system:system")
|
||||||
|
assert cfg.get("system", {}).get("hostname") == "operator-test", \
|
||||||
|
"Operator failed to modify hostname"
|
||||||
|
print("Operator successfully modified hostname")
|
||||||
|
|
||||||
|
with test.step("Verify operator cannot read password hashes"):
|
||||||
|
# Password hashes are protected by deny-password-access rule
|
||||||
|
cfg = operator.get_config_dict("/ietf-system:system")
|
||||||
|
users = cfg.get("system", {}).get("authentication", {}).get("user", [])
|
||||||
|
|
||||||
|
# Check that no user entry contains a password field
|
||||||
|
for user in users:
|
||||||
|
assert "password" not in user, \
|
||||||
|
f"Operator should NOT be able to read password for user '{user.get('name')}'"
|
||||||
|
print("Operator correctly denied read access to password hashes")
|
||||||
|
|
||||||
|
with test.step("Verify operator cannot write password hashes"):
|
||||||
|
# Try to change a password - should fail due to NACM deny rule
|
||||||
|
# Use a valid MD5 hash format to ensure we test NACM, not YANG validation
|
||||||
|
valid_hash = "$1$testsalt$YvPTBnV5RhkWwXLzR7kK/1"
|
||||||
|
try:
|
||||||
|
operator.patch_config("ietf-system", {
|
||||||
|
"system": {
|
||||||
|
"authentication": {
|
||||||
|
"user": [{
|
||||||
|
"name": OPERATOR_USER,
|
||||||
|
"password": valid_hash
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, retries=1)
|
||||||
|
assert False, "Operator should NOT be able to modify passwords!"
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
assert any(keyword in error_msg for keyword in ["403", "Forbidden", "denied", "authorization failed"]), \
|
||||||
|
f"Expected NACM denial, got: {e}"
|
||||||
|
print("Operator correctly denied write access to password hashes")
|
||||||
|
|
||||||
|
with test.step("Verify guest can read configuration"):
|
||||||
|
# Attach as guest user
|
||||||
|
guest = env.attach("target", "mgmt", username=GUEST_USER,
|
||||||
|
password=GUEST_PASS, test_reset=False)
|
||||||
|
|
||||||
|
# Guest should be able to read (read-default: permit)
|
||||||
|
ifaces = guest.get_config_dict("/ietf-interfaces:interfaces")
|
||||||
|
num_ifaces = len(ifaces.get('interfaces', {}).get('interface', []))
|
||||||
|
print(f"Guest successfully read {num_ifaces} interfaces")
|
||||||
|
|
||||||
|
with test.step("Verify guest cannot modify configuration"):
|
||||||
|
# Try to modify hostname - should fail (write-default: deny)
|
||||||
|
# Use patch_config with retries=1 since NACM denials won't succeed on retry
|
||||||
|
try:
|
||||||
|
guest.patch_config("ietf-system", {
|
||||||
|
"system": {
|
||||||
|
"hostname": "hacked"
|
||||||
|
}
|
||||||
|
}, retries=1)
|
||||||
|
assert False, "Guest should NOT be able to modify configuration!"
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
assert any(keyword in error_msg for keyword in ["403", "Forbidden", "denied", "authorization failed"]), \
|
||||||
|
f"Expected NACM denial, got: {e}"
|
||||||
|
print("Guest correctly denied write access")
|
||||||
|
|
||||||
|
with test.step("Verify admin can access passwords"):
|
||||||
|
# Admin should have full access including protected paths
|
||||||
|
cfg = target.get_config_dict("/ietf-system:system")
|
||||||
|
users = cfg.get("system", {}).get("authentication", {}).get("user", [])
|
||||||
|
|
||||||
|
# Admin should be able to see password hashes
|
||||||
|
has_password = any("password" in user for user in users)
|
||||||
|
assert has_password, "Admin should be able to read password hashes"
|
||||||
|
print("Admin successfully read password hashes")
|
||||||
|
|
||||||
|
# Admin can also modify passwords
|
||||||
|
target.put_config_dicts({
|
||||||
|
"ietf-system": {
|
||||||
|
"system": {
|
||||||
|
"hostname": "admin-test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
cfg = target.get_config_dict("/ietf-system:system")
|
||||||
|
assert cfg.get("system", {}).get("hostname") == "admin-test", \
|
||||||
|
"Admin hostname change not applied"
|
||||||
|
print("Admin successfully modified hostname")
|
||||||
|
|
||||||
|
test.succeed()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../infamy/topologies/1x1.dot
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
|
||||||
|
<!-- Title: 1x1 Pages: 1 -->
|
||||||
|
<svg width="424pt" height="45pt"
|
||||||
|
viewBox="0.00 0.00 424.03 45.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 41)">
|
||||||
|
<title>1x1</title>
|
||||||
|
<polygon fill="white" stroke="transparent" points="-4,4 -4,-41 420.03,-41 420.03,4 -4,4"/>
|
||||||
|
<!-- host -->
|
||||||
|
<g id="node1" class="node">
|
||||||
|
<title>host</title>
|
||||||
|
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 100,-36.5 100,-0.5 0,-0.5"/>
|
||||||
|
<text text-anchor="middle" x="25" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||||
|
<polyline fill="none" stroke="black" points="50,-0.5 50,-36.5 "/>
|
||||||
|
<text text-anchor="middle" x="75" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||||
|
</g>
|
||||||
|
<!-- target -->
|
||||||
|
<g id="node2" class="node">
|
||||||
|
<title>target</title>
|
||||||
|
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-36.5 416.03,-36.5 416.03,-0.5 300.03,-0.5"/>
|
||||||
|
<text text-anchor="middle" x="325.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||||
|
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-36.5 "/>
|
||||||
|
<text text-anchor="middle" x="383.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
|
||||||
|
</g>
|
||||||
|
<!-- host--target -->
|
||||||
|
<g id="edge1" class="edge">
|
||||||
|
<title>host:mgmt--target:mgmt</title>
|
||||||
|
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-18.5C100,-18.5 300.03,-18.5 300.03,-18.5"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
+8
-4
@@ -17,10 +17,14 @@ class Server:
|
|||||||
self._create_files(start, end, netmask, ip, router, prefix, hostname)
|
self._create_files(start, end, netmask, ip, router, prefix, hostname)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
#print(self.config_file)
|
"""Clean up config and lease files"""
|
||||||
#os.unlink(self.config_file)
|
try:
|
||||||
#os.unlink(self.leases_file)
|
if os.path.exists(self.config_file):
|
||||||
pass
|
os.unlink(self.config_file)
|
||||||
|
if os.path.exists(self.leases_file):
|
||||||
|
os.unlink(self.leases_file)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.start()
|
self.start()
|
||||||
|
|||||||
+19
-6
@@ -281,7 +281,7 @@ class Device(Transport):
|
|||||||
"""Get Python dictionary version of XML configuration"""
|
"""Get Python dictionary version of XML configuration"""
|
||||||
return self.get_config(xpath).print_dict()
|
return self.get_config(xpath).print_dict()
|
||||||
|
|
||||||
def put_config(self, edit):
|
def put_config(self, edit, retries=3):
|
||||||
"""Send XML configuration over NETCONF"""
|
"""Send XML configuration over NETCONF"""
|
||||||
yang2nc = {
|
yang2nc = {
|
||||||
"none": None,
|
"none": None,
|
||||||
@@ -298,7 +298,7 @@ class Device(Transport):
|
|||||||
f"nc:operation=\"{dst}\"" if dst else "")
|
f"nc:operation=\"{dst}\"" if dst else "")
|
||||||
|
|
||||||
last_error = None
|
last_error = None
|
||||||
for _ in range(0, 3):
|
for _ in range(0, retries):
|
||||||
try:
|
try:
|
||||||
self.ncc.edit_config(xml, default_operation='merge')
|
self.ncc.edit_config(xml, default_operation='merge')
|
||||||
last_error = None
|
last_error = None
|
||||||
@@ -313,7 +313,7 @@ class Device(Transport):
|
|||||||
if last_error is not None:
|
if last_error is not None:
|
||||||
raise last_error
|
raise last_error
|
||||||
|
|
||||||
def put_config_dicts(self, models):
|
def put_config_dicts(self, models, retries=3):
|
||||||
"""PUT full configuration of all models to running-config"""
|
"""PUT full configuration of all models to running-config"""
|
||||||
config = ""
|
config = ""
|
||||||
infer_put_dict(self.name, models)
|
infer_put_dict(self.name, models)
|
||||||
@@ -328,9 +328,9 @@ class Device(Transport):
|
|||||||
lyd = mod.parse_data_dict(models[model], no_state=True, validate=False)
|
lyd = mod.parse_data_dict(models[model], no_state=True, validate=False)
|
||||||
config += lyd.print_mem("xml", with_siblings=True, pretty=False) + "\n"
|
config += lyd.print_mem("xml", with_siblings=True, pretty=False) + "\n"
|
||||||
# print(f"Send new XML config: {config}")
|
# print(f"Send new XML config: {config}")
|
||||||
return self.put_config(config)
|
return self.put_config(config, retries=retries)
|
||||||
|
|
||||||
def put_config_dict(self, modname, edit):
|
def put_config_dict(self, modname, edit, retries=3):
|
||||||
"""Convert Python dictionary to XMl and send as configuration"""
|
"""Convert Python dictionary to XMl and send as configuration"""
|
||||||
try:
|
try:
|
||||||
mod = self.ly.get_module(modname)
|
mod = self.ly.get_module(modname)
|
||||||
@@ -341,7 +341,20 @@ class Device(Transport):
|
|||||||
lyd = mod.parse_data_dict(edit, no_state=True, validate=False)
|
lyd = mod.parse_data_dict(edit, no_state=True, validate=False)
|
||||||
config = lyd.print_mem("xml", with_siblings=True, pretty=False)
|
config = lyd.print_mem("xml", with_siblings=True, pretty=False)
|
||||||
# print(f"Send new XML config: {config}")
|
# print(f"Send new XML config: {config}")
|
||||||
return self.put_config(config)
|
return self.put_config(config, retries=retries)
|
||||||
|
|
||||||
|
def patch_config(self, modname, edit, retries=3):
|
||||||
|
"""Merge configuration for a single model to running-config
|
||||||
|
|
||||||
|
For NETCONF, this is identical to put_config_dict() since
|
||||||
|
edit-config already has proper NACM support.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
modname: YANG module name
|
||||||
|
edit: Configuration dictionary
|
||||||
|
retries: Number of retry attempts on failure (default 3)
|
||||||
|
"""
|
||||||
|
return self.put_config_dict(modname, edit, retries=retries)
|
||||||
|
|
||||||
def call(self, call):
|
def call(self, call):
|
||||||
"""Call RPC, XML version"""
|
"""Call RPC, XML version"""
|
||||||
|
|||||||
+190
-42
@@ -51,8 +51,8 @@ def requests_workaround(method, url, json, headers, auth, verify=False, retry=0)
|
|||||||
request = requests.Request(method, url, json=json, headers=headers,
|
request = requests.Request(method, url, json=json, headers=headers,
|
||||||
auth=auth)
|
auth=auth)
|
||||||
prepared_request = session.prepare_request(request)
|
prepared_request = session.prepare_request(request)
|
||||||
prepared_request.url = prepared_request.url.replace('%25', '%')
|
prepared_request.url = re.sub(r'%25', '%', prepared_request.url)
|
||||||
prepared_request.url = prepared_request.url.replace('%3A', ':')
|
prepared_request.url = re.sub(r'%3a', ':', prepared_request.url, flags=re.IGNORECASE)
|
||||||
response = session.send(prepared_request, verify=verify)
|
response = session.send(prepared_request, verify=verify)
|
||||||
try:
|
try:
|
||||||
# Raise exceptions for HTTP errors
|
# Raise exceptions for HTTP errors
|
||||||
@@ -83,6 +83,10 @@ def requests_workaround_post(url, json, headers, auth, verify=False):
|
|||||||
return requests_workaround('POST', url, json, headers, auth, verify=False)
|
return requests_workaround('POST', url, json, headers, auth, verify=False)
|
||||||
|
|
||||||
|
|
||||||
|
def requests_workaround_patch(url, json, headers, auth, verify=False):
|
||||||
|
return requests_workaround('PATCH', url, json, headers, auth, verify=False)
|
||||||
|
|
||||||
|
|
||||||
def requests_workaround_get(url, headers, auth, verify=False):
|
def requests_workaround_get(url, headers, auth, verify=False):
|
||||||
return requests_workaround('GET', url, None, headers, auth, verify=False)
|
return requests_workaround('GET', url, None, headers, auth, verify=False)
|
||||||
|
|
||||||
@@ -201,7 +205,7 @@ class Device(Transport):
|
|||||||
def get_datastore(self, datastore="operational", path="", parse=True):
|
def get_datastore(self, datastore="operational", path="", parse=True):
|
||||||
"""Get a datastore"""
|
"""Get a datastore"""
|
||||||
dspath = f"/ds/ietf-datastores:{datastore}"
|
dspath = f"/ds/ietf-datastores:{datastore}"
|
||||||
if path is not None:
|
if path is not None and path != "":
|
||||||
dspath = f"{dspath}{path}"
|
dspath = f"{dspath}{path}"
|
||||||
|
|
||||||
url = f"{self.restconf_url}{dspath}"
|
url = f"{self.restconf_url}{dspath}"
|
||||||
@@ -252,65 +256,209 @@ class Device(Transport):
|
|||||||
# Raise exceptions for HTTP errors
|
# Raise exceptions for HTTP errors
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
def get_config_dict(self, modname):
|
def get_config_dict(self, xpath):
|
||||||
"""Get all configuration for module @modname as dictionary"""
|
"""Get all configuration matching @xpath as dictionary"""
|
||||||
ds = self.get_running(modname)
|
# Strip leading slash if present (for compatibility with NETCONF xpath style)
|
||||||
|
xpath = xpath.lstrip('/')
|
||||||
|
|
||||||
|
# Get the whole module's configuration by requesting the module root
|
||||||
|
# The xpath parameter might be in format "module:container" or just "module"
|
||||||
|
if ":" in xpath:
|
||||||
|
model, container = xpath.split(":", 1) # Split only on first colon
|
||||||
|
path = f"/{model}:{container}" # This creates something like /ietf-syslog:syslog
|
||||||
|
else:
|
||||||
|
# If no colon, assume the whole thing is the module name
|
||||||
|
model = xpath
|
||||||
|
path = f"/{model}" # This creates something like /ietf-syslog
|
||||||
|
|
||||||
|
ds = self.get_running(path)
|
||||||
|
if ds is None:
|
||||||
|
return None
|
||||||
ds = json.loads(ds.print_mem("json", with_siblings=True, pretty=False))
|
ds = json.loads(ds.print_mem("json", with_siblings=True, pretty=False))
|
||||||
model, container = modname.split(":")
|
# If we have module:container format, extract the container part from the result
|
||||||
for k, v in ds.items():
|
if ":" in xpath:
|
||||||
return {container: v}
|
_, container = xpath.split(":", 1)
|
||||||
|
for k, v in ds.items():
|
||||||
|
return {container: v}
|
||||||
|
else:
|
||||||
|
# Return the whole result if no specific container was specified
|
||||||
|
return ds
|
||||||
|
|
||||||
def put_config_dicts(self, models):
|
def put_config_dicts(self, models, retries=3):
|
||||||
"""PUT full configuration of all models to running-config"""
|
"""PATCH configuration of all models to running-config
|
||||||
|
|
||||||
|
Uses candidate datastore + copy to running to trigger sysrepo
|
||||||
|
change callbacks, similar to how NETCONF edit-config + commit works.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
models: Dictionary of models to configure
|
||||||
|
retries: Number of retry attempts on failure (default 3)
|
||||||
|
"""
|
||||||
infer_put_dict(self.name, models)
|
infer_put_dict(self.name, models)
|
||||||
running = self.get_running()
|
|
||||||
|
|
||||||
for model in models.keys():
|
# Copy running to candidate first (to preserve existing config)
|
||||||
|
self.copy("running", "candidate")
|
||||||
|
|
||||||
|
# PATCH each model to candidate datastore
|
||||||
|
for model, config in models.items():
|
||||||
try:
|
try:
|
||||||
mod = self.lyctx.get_module(model)
|
mod = self.lyctx.get_module(model)
|
||||||
except libyang.util.LibyangError:
|
except libyang.util.LibyangError:
|
||||||
raise Exception(f"YANG model '{model}' not found on device. "
|
raise Exception(f"YANG model '{model}' not found on device. "
|
||||||
f"Model may not be installed or enabled. "
|
f"Model may not be installed or enabled. "
|
||||||
f"Available models can be checked with get_schema_list()") from None
|
f"Available models can be checked with get_schema_list()") from None
|
||||||
lyd = mod.parse_data_dict(models[model], no_state=True, validate=False)
|
|
||||||
running.merge(lyd)
|
|
||||||
|
|
||||||
cfg = running.print_mem("json", with_siblings=True, pretty=True)
|
# Parse and convert to get proper structure with module prefix
|
||||||
# print(f"PUT new running-config: {cfg}")
|
lyd = mod.parse_data_dict(config, no_state=True, validate=False)
|
||||||
return self.put_datastore("running", json.loads(cfg))
|
patch_data = json.loads(lyd.print_mem("json", with_siblings=True, pretty=False))
|
||||||
|
|
||||||
def put_config_dict(self, modname, edit):
|
# PATCH to candidate datastore
|
||||||
"""Add @edit to running config and put the whole configuration"""
|
url = f"{self.restconf_url}/ds/ietf-datastores:candidate"
|
||||||
|
|
||||||
# This is hacky, refactor when rousette have PATCH support.
|
last_error = None
|
||||||
running = self.get_running()
|
for attempt in range(0, retries):
|
||||||
|
try:
|
||||||
|
response = requests_workaround_patch(
|
||||||
|
url,
|
||||||
|
json=patch_data,
|
||||||
|
headers=self.headers,
|
||||||
|
auth=self.auth,
|
||||||
|
verify=False
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
last_error = None
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
last_error = e
|
||||||
|
if attempt < retries - 1:
|
||||||
|
print(f"Failed PATCH to {url}: {e} Retrying ...")
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
print(f"Failed PATCH to {url}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if last_error is not None:
|
||||||
|
raise last_error
|
||||||
|
|
||||||
|
# Copy candidate to running (acts as "commit", triggers sysrepo callbacks)
|
||||||
|
self.copy("candidate", "running")
|
||||||
|
|
||||||
|
def put_config_dict(self, xpath, edit, retries=3):
|
||||||
|
"""PATCH configuration for a single model to running-config
|
||||||
|
|
||||||
|
Uses candidate datastore + copy to running to trigger sysrepo
|
||||||
|
change callbacks, similar to how NETCONF edit-config + commit works.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
xpath: YANG module name
|
||||||
|
edit: Configuration dictionary
|
||||||
|
retries: Number of retry attempts on failure (default 3)
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
mod = self.lyctx.get_module(modname)
|
mod = self.lyctx.get_module(xpath)
|
||||||
except libyang.util.LibyangError:
|
except libyang.util.LibyangError:
|
||||||
raise Exception(f"YANG model '{modname}' not found on device. "
|
raise Exception(f"YANG model '{xpath}' not found on device. "
|
||||||
|
f"Model may not be installed or enabled. "
|
||||||
|
f"Available models can be checked with get_schema_list()") from None
|
||||||
|
|
||||||
|
# Copy running to candidate first (to preserve existing config)
|
||||||
|
self.copy("running", "candidate")
|
||||||
|
|
||||||
|
# Parse and convert to get proper structure with module prefix
|
||||||
|
lyd = mod.parse_data_dict(edit, no_state=True, validate=False)
|
||||||
|
patch_data = json.loads(lyd.print_mem("json", with_siblings=True, pretty=False))
|
||||||
|
|
||||||
|
# PATCH to candidate datastore
|
||||||
|
url = f"{self.restconf_url}/ds/ietf-datastores:candidate"
|
||||||
|
last_error = None
|
||||||
|
for attempt in range(0, retries):
|
||||||
|
try:
|
||||||
|
response = requests_workaround_patch(
|
||||||
|
url,
|
||||||
|
json=patch_data,
|
||||||
|
headers=self.headers,
|
||||||
|
auth=self.auth,
|
||||||
|
verify=False
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
last_error = None
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
last_error = e
|
||||||
|
if attempt < retries - 1:
|
||||||
|
print(f"Failed PATCH to {url}: {e} Retrying ...")
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
print(f"Failed PATCH to {url}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if last_error is not None:
|
||||||
|
raise last_error
|
||||||
|
|
||||||
|
# Copy candidate to running (acts as "commit", triggers sysrepo callbacks)
|
||||||
|
self.copy("candidate", "running")
|
||||||
|
|
||||||
|
def patch_config(self, xpath, edit, retries=3):
|
||||||
|
"""PATCH configuration directly to running datastore
|
||||||
|
|
||||||
|
This bypasses the candidate datastore, avoiding full datastore
|
||||||
|
copy operations. Useful for NACM-restricted users who only have
|
||||||
|
access to specific paths. Note: may not trigger sysrepo callbacks
|
||||||
|
for all configuration types.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
xpath: YANG module name
|
||||||
|
edit: Configuration dictionary
|
||||||
|
retries: Number of retry attempts on failure (default 3)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
mod = self.lyctx.get_module(xpath)
|
||||||
|
except libyang.util.LibyangError:
|
||||||
|
raise Exception(f"YANG model '{xpath}' not found on device. "
|
||||||
f"Model may not be installed or enabled. "
|
f"Model may not be installed or enabled. "
|
||||||
f"Available models can be checked with get_schema_list()") from None
|
f"Available models can be checked with get_schema_list()") from None
|
||||||
|
|
||||||
for k, _ in edit.items():
|
# Parse and convert to get proper structure with module prefix
|
||||||
module = modname + ":" + k
|
lyd = mod.parse_data_dict(edit, no_state=True, validate=False)
|
||||||
break
|
patch_data = json.loads(lyd.print_mem("json", with_siblings=True, pretty=False))
|
||||||
|
|
||||||
# Ugly hack, but this function should be refactored when patch
|
# PATCH directly to running datastore
|
||||||
# is available in rousette anyway.
|
url = f"{self.restconf_url}/ds/ietf-datastores:running"
|
||||||
rundict = json.loads(running.print_mem("json", with_siblings=True,
|
last_error = None
|
||||||
pretty=False))
|
for attempt in range(0, retries):
|
||||||
if rundict.get(module) is None:
|
try:
|
||||||
rundict[module] = {}
|
response = requests_workaround_patch(
|
||||||
running = self.lyctx.parse_data_mem(json.dumps(rundict), "json",
|
url,
|
||||||
parse_only=True)
|
json=patch_data,
|
||||||
|
headers=self.headers,
|
||||||
|
auth=self.auth,
|
||||||
|
verify=False
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
last_error = None
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
last_error = e
|
||||||
|
# Try to extract detailed error message from response
|
||||||
|
error_detail = str(e)
|
||||||
|
if hasattr(e, 'response') and e.response is not None:
|
||||||
|
try:
|
||||||
|
err_json = e.response.json()
|
||||||
|
if 'ietf-restconf:errors' in err_json:
|
||||||
|
errors = err_json['ietf-restconf:errors'].get('error', [])
|
||||||
|
if errors:
|
||||||
|
error_detail = errors[0].get('error-message', str(e))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if attempt < retries - 1:
|
||||||
|
print(f"Failed PATCH: {error_detail} Retrying ...")
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
print(f"Failed PATCH: {error_detail}")
|
||||||
|
continue
|
||||||
|
|
||||||
change = mod.parse_data_dict(edit, no_state=True, validate=False)
|
if last_error is not None:
|
||||||
running.merge_module(change)
|
raise last_error
|
||||||
cfg = running.print_mem("json", with_siblings=True, pretty=False)
|
|
||||||
# print(f"PUT new running-config: {cfg}")
|
|
||||||
data = json.loads(cfg)
|
|
||||||
|
|
||||||
return self.put_datastore("running", data)
|
|
||||||
|
|
||||||
def call_dict(self, model, call):
|
def call_dict(self, model, call):
|
||||||
pass # Need implementation
|
pass # Need implementation
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ class Transport(ABC):
|
|||||||
def put_config_dict(self, modname, edit):
|
def put_config_dict(self, modname, edit):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def patch_config(self, modname, edit):
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_dict(self, xpath=None):
|
def get_dict(self, xpath=None):
|
||||||
pass
|
pass
|
||||||
|
|||||||
+5
-3
@@ -29,9 +29,11 @@ export INFAMY_ARGS := --transport=netconf
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
test:
|
test:
|
||||||
$(test-dir)/env -r $(base) $(mode) $(binaries) $(pkg-$(ARCH)) $(ninepm) -v $(TESTS)
|
$(test-dir)/env -r $(base) $(mode) $(binaries) $(pkg-$(ARCH)) \
|
||||||
$(ninepm_report) github $(test-dir)/.log/last/result.json
|
sh -c '$(ninepm) -v $(TESTS); \
|
||||||
$(ninepm_report) asciidoc $(test-dir)/.log/last/result.json
|
$(ninepm_report) github $(test-dir)/.log/last/result.json; \
|
||||||
|
$(ninepm_report) asciidoc $(test-dir)/.log/last/result.json; \
|
||||||
|
chmod -R 777 $(test-dir)/.log'
|
||||||
|
|
||||||
test-sh:
|
test-sh:
|
||||||
$(test-dir)/env $(base) $(mode) $(binaries) $(pkg-$(ARCH)) -i /bin/sh
|
$(test-dir)/env $(base) $(mode) $(binaries) $(pkg-$(ARCH)) -i /bin/sh
|
||||||
|
|||||||
Reference in New Issue
Block a user