diff --git a/board/aarch64/r2s/README.md b/board/aarch64/r2s/README.md index 46304984d..dc55770e4 100644 --- a/board/aarch64/r2s/README.md +++ b/board/aarch64/r2s/README.md @@ -128,7 +128,7 @@ what provides a default, the same default MAC addresses to Linux: This is important in case you want to run multiple R2S devices on the same LAN. Meaning you either have to change the MAC address in the -U-Boot environment (below), or modify your `phys-address` setting in +U-Boot environment (below), or use the `custom-phys-address` setting in Infix for the interface(s). Break into U-Boot using Ctrl-C at power-on, preferably when the text diff --git a/board/common/rootfs/usr/libexec/infix/init.d/30-cfg-migrate b/board/common/rootfs/usr/libexec/infix/init.d/30-cfg-migrate index 496ac7ea3..527135d36 100755 --- a/board/common/rootfs/usr/libexec/infix/init.d/30-cfg-migrate +++ b/board/common/rootfs/usr/libexec/infix/init.d/30-cfg-migrate @@ -1,102 +1,16 @@ #!/bin/sh -# Run .cfg migration jq scripts to backup and transform older .cfg files -ident=$(basename "$0") - -MIGRATIONS_DIR="/usr/share/confd/migrate" +# Check if /cfg/startup-config.cfg needs to be migrated to new syntax. +# Backup of the original is created in /cfg/backup/ for old versions, +# the migrate tool inserts old version in name before .cfg extension. CONFIG_FILE="/cfg/startup-config.cfg" -BACKUP_DIR="/cfg/backup" - -note() -{ - logger -I $$ -k -p user.notice -t "$ident" "$1" -} - -err() -{ - logger -I $$ -k -p user.err -t "$ident" "$1" -} - -file_version() -{ - jq -r ' - if .["infix-meta:meta"] | has("version") then - .["infix-meta:meta"]["version"] - else - "0.0" - end - ' "$1" -} - -atoi() -{ - echo "$1" | awk -F. '{print $1 * 1000 + $2}' -} +BACKUP_FILE="/cfg/backup/startup-config.cfg" +mkdir -p "$(dirname "$BACKUP_FILE")" if [ ! -f "$CONFIG_FILE" ]; then - # Nothing to migrate note "No $(basename "$CONFIG_FILE" .cfg) yet, likely factory reset." exit 0 -fi - -cfg_version=$(file_version "$CONFIG_FILE") -current_version=$(atoi "$cfg_version") - -# Find the latest version by examining the highest numbered directory -sys_version=$(find "$MIGRATIONS_DIR" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -n1 | xargs -n1 basename) -latest_version=$(atoi "$sys_version") - -# Check for downgrade -if [ "$current_version" -gt "$latest_version" ]; then - err "Configuration file $CONFIG_FILE version ($cfg_version) is newer than the latest supported version ($sys_version). Exiting." - exit 1 -fi - -# If the current version is already the latest, exit the script -if [ "$current_version" -eq "$latest_version" ]; then - note "Configuration is already at the latest version ($sys_version). No migration needed." +elif migrate -cq "$CONFIG_FILE"; then exit 0 fi -note "Configuration file $CONFIG_FILE is of version $cfg_version, migrating ..." - -# Create the backup directory if it doesn't exist -mkdir -p "$BACKUP_DIR" - -# Create a backup of the current configuration file -nm=$(basename "$CONFIG_FILE" .cfg) -BACKUP_FILE="$BACKUP_DIR/${nm}-${cfg_version}.cfg" -if cp "$CONFIG_FILE" "$BACKUP_FILE"; then - note "Backup created: $BACKUP_FILE" -else - err "Failed creating backup: $BACKUP_FILE" - exit 1 -fi - -# Apply the scripts for each version directory in sequence -for version_dir in $(find "$MIGRATIONS_DIR" -mindepth 1 -maxdepth 1 -type d | sort -V); do - dir=$(basename "$version_dir") - version=$(atoi "$dir") - - # Step by step upgrade file to latest version - if [ "$current_version" -lt "$version" ]; then - note "Applying migrations for version $dir ..." - - # Apply all scripts in the version directory in order - for script in $(find "$version_dir" -type f -name '*.sh' | sort -V); do - note "Calling $script for $CONFIG_FILE ..." - sh "$script" "$CONFIG_FILE" - done - - # File now at $version ... - current_version="$version" - fi -done - -# Update the JSON file to the latest version -if jq --arg version "$sys_version" '.["infix-meta:meta"] = {"infix-meta:version": $version}' "$CONFIG_FILE" \ - > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"; then - note "Configuration updated to version $sys_version." -else - err "Failed updating configuration to version $sys_version!" - exit 1 -fi +migrate -i -b "$BACKUP_FILE" "$CONFIG_FILE" diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 8da14ba64..4dc2d0981 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -5,14 +5,41 @@ All notable changes to the project are documented in this file. [v24.10.0][] - UNRELEASED ------------------------- + +**News:** this release contains *breaking YANG changes* in custom MAC +addresses for interfaces! For details, see below issue #680. + ### Changes -- OSPF: Add limitation to only allow one interface per area. +- Update CONTRIBUTING.md for scaling core team and helping external + contributors understand the development process, issue #672 +- OSPF: Add limitation to only allow one interface per area +- The default builds now include the curiOS nftables container image, + which can be used for advanced firewall setups. For an introduction + see ### Fixes -- Fix #499 by adding a NACM rule to factory config, which by default - deny everyone to read the user password hash. +- Fix #499: add an NACM rule to factory config, which by default + deny everyone to read user password hash(es) +- Fix #663: internal Ethernet interfaces shown in CLI tab completion +- Fix #674: CLI `show interfaces` display internal Ethernet interfaces, + regression introduced late in v24.09 release cycle +- Fix #676: port dropped from bridge when changing its VLAN membership + from tagged to untagged +- Fix #680: replace deviation for `phys-address` in ietf-interfaces.yang + with `custom-phys-address` to allow for constructing more free-form + MAC addresses based on the chassis MAC (a.k.a., base MAC) address. + For more information, see the YANG model, a few examples are listed in + the updated documentation. + The syntax will be automatically updated in the `startup-config` and + `factory-config` -- make sure to verify the changes and update any + static `factory-config` used for your products +- Fix #690: CLI `show ip route` command stops working after 24 hours, + this includes all operational data in ietf-routing:/routing/ribs. +- Fix #697: password is not always set for new users, bug introduced + in v24.06.0 when replacing Augeas with native user handling - Fix BFD in OSPF, previously you could not enable BFD on a single - interface without it was enabled on all interfaces. + interface without it was enabled on all interfaces + [v24.09.0][] - 2024-09-30 ------------------------- diff --git a/doc/container.md b/doc/container.md index b39c3b9fe..d7cd64378 100644 --- a/doc/container.md +++ b/doc/container.md @@ -420,7 +420,7 @@ line where we declare the `ntpd` end as a container network interface: admin@example:/config/interface/veth0/> end admin@example:/config/> edit interface ntpd admin@example:/config/interface/ntpd/> set ipv4 address 192.168.0.2 prefix-length 24 - admin@example:/config/interface/ntpd/> set phys-address 00:c0:ff:ee:00:01 + admin@example:/config/interface/ntpd/> set custom-phys-address static 00:c0:ff:ee:00:01 admin@example:/config/interface/ntpd/> set container-network > Notice how you can also set a custom MAC address at the same time. diff --git a/doc/img/lego-relations.svg b/doc/img/lego-relations.svg new file mode 100644 index 000000000..3daf80579 --- /dev/null +++ b/doc/img/lego-relations.svg @@ -0,0 +1,4 @@ + + + +
ip
vlan
eth
vlan
ip
ip
bridge
eth
Upper
to
lower
Lower
to
Upper
Only one master
eth
ip
\ No newline at end of file diff --git a/doc/img/lego.svg b/doc/img/lego.svg index e2bf87bf2..e63a1bc47 100644 --- a/doc/img/lego.svg +++ b/doc/img/lego.svg @@ -1,4 +1,4 @@ -
lag
eth
veth
veth
bridge
ip
lo
\ No newline at end of file +
lag
eth
veth
veth
bridge
ip
vlan
lo
\ No newline at end of file diff --git a/doc/networking.md b/doc/networking.md index b7c1bc196..4e04aa833 100644 --- a/doc/networking.md +++ b/doc/networking.md @@ -14,8 +14,34 @@ Infix to exploit the unique features not available in IEEE models. ## Interface LEGO® +The network building blocks available in Linux are akin to the popular +LEGO® bricks. + ![Linux Networking Blocks](img/lego.svg) +There are two types of relationships that can link two blocks together: + + 1. **Lower-to-upper**: Visually represented by an extruding square + connected upwards to a square socket. An interface _can only have + a single_ lower-to-upper relationship, i.e., it can be attached to + a single upper interface like a bridge or a LAG. In `iproute2` + parlance, this corresponds to the interface's `master` setting + 2. **Upper-to-lower**: Visually represented by an extruding semicircle + connected downwards to a semicircle socket. The lower interface in + these relationships _accepts multiple_ upper-to-lower relationships + from different upper blocks. E.g., multiple VLANs and IP address + blocks can be connected to the same lower interface + +![Stacking order dependencies](img/lego-relations.svg) + +An interface may simultaneously have a _lower-to-upper_ relation to some +other interface, and be the target of one or more _upper-to-lower_ +relationships. It is valid, for example, for a physical port to be +attached to a bridge, but also have a VLAN interface stacked on top of +it. In this example, traffic assigned to the VLAN in question would be +diverted to the VLAN interface before entering the bridge, while all +other traffic would be bridged as usual. + | **Type** | **Yang Model** | **Description** | | -------- | ----------------- | ------------------------------------------------------------- | | bridge | infix-if-bridge | SW implementation of an IEEE 802.1Q bridge | @@ -27,6 +53,7 @@ Infix to exploit the unique features not available in IEEE models. | | ieee802-ethernet-interface | | | veth | infix-if-veth | Virtual Ethernet pair, typically one end is in a container | + ## Data Plane The blocks you choose, and how you connect them, defines your data plane. @@ -50,6 +77,65 @@ possible to share with a container. Meaning, all the building blocks used on the left hand side can also be used freely on the right hand side as well. + +### General + +General interface settings include `type`, `enable`, custom MAC address, +and text `description`. Other settings have their own sections, below. + +The `type` is important to set when configuring devices remotely because +unlike the CLI, a NETCONF or RESTCONF session cannot guess the interface +type for you. The operating system provides an override of the +available interface types. + +An `enabled` interface can be inspected using the operational datastore, +nodes `admin-state` and `oper-state` show the status, . Possible values +are listed in the YANG model. + +The `custom-phys-address` can be used to set an interface's MAC address. +This is an extension to the ietf-interfaces YANG model, which defines +`phys-address` as read-only[^4]. The following shows the different +configuration options. + +> **Note:** there is no validation or safety checks performed by the +> system when using `custom-phys-address`. In particular the `offset` +> variant can be dangerous to use -- pay attention to the meaning of +> bits in the upper-most octet: local bit, multicast/group, etc. + +#### Fixed custom MAC + +``` +admin@example:/config/> edit interface veth0a +admin@example:/config/interface/veth0a/> set custom-phys-address static 00:ab:00:11:22:33 + +=> 00:ab:00:11:22:33 +``` + +#### Chassis MAC + +Chassis MAC, sometimes also referred to as base MAC. In these two +examples it is `00:53:00:c0:ff:ee`. + +``` +admin@example:/config/> edit interface veth0a +admin@example:/config/interface/veth0a/> set custom-phys-address chassis + +=> 00:53:00:c0:ff:ee +``` + +#### Chassis MAC, with offset + +When constructing a derived address it is recommended to set the locally +administered bit. Same chassis MAC as before. + +``` +admin@example:/config/> edit interface veth0a +admin@example:/config/interface/veth0a/> set custom-phys-address chassis offset 02:00:00:00:00:02 + +=> 02:53:00:c0:ff:f0 +``` + + ### Bridging This is the most central part of the system. A bridge is a switch, and @@ -1031,3 +1117,6 @@ currently supported, namely `ipv4` and `ipv6`. mapping the low-order 23-bits of the IP address in the low-order 23 bits of the Ethernet address 01:00:5E:00:00:00. Meaning, more than one IP multicast group maps to the same MAC multicast group. +[^4]: A YANG deviation was previously used to make it possible to set + `phys-address`, but this has been replaced with the more flexible + `custom-phys-address`. diff --git a/package/confd/confd.mk b/package/confd/confd.mk index 80b24e9fa..70f4b8e96 100644 --- a/package/confd/confd.mk +++ b/package/confd/confd.mk @@ -4,7 +4,7 @@ # ################################################################################ -CONFD_VERSION = 1.1 +CONFD_VERSION = 1.2 CONFD_SITE_METHOD = local CONFD_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/confd CONFD_LICENSE = BSD-3-Clause diff --git a/src/confd/bin/Makefile.am b/src/confd/bin/Makefile.am index 6a87c5e8b..fd7c12d37 100644 --- a/src/confd/bin/Makefile.am +++ b/src/confd/bin/Makefile.am @@ -1,3 +1,3 @@ pkglibexec_SCRIPTS = bootstrap error load gen-service gen-hostname \ gen-interfaces gen-motd gen-hardware gen-version -sbin_SCRIPTS = dagger +sbin_SCRIPTS = dagger migrate diff --git a/src/confd/bin/bootstrap b/src/confd/bin/bootstrap index 684e0792f..e2473f07f 100755 --- a/src/confd/bin/bootstrap +++ b/src/confd/bin/bootstrap @@ -22,6 +22,17 @@ ######################################################################## STATUS="" +# Log functions +critical() +{ + logger -i -p user.crit -t bootstrap "$1" 2>/dev/null || echo "$1" +} + +err() +{ + logger -i -p user.err -t bootstrap "$1" 2>/dev/null || echo "$1" +} + # When logging errors, generating /etc/issue* or /etc/banner (SSH) . /etc/os-release @@ -33,8 +44,7 @@ if [ "$1" = "-f" ] && [ -f "$2" ]; then fi if [ ! -f "$RC" ]; then - logger -sik -p user.error -t bootstrap "Missing rc file $RC" 2>/dev/null \ - || echo "Missing rc file $RC" + err "Missing rc file $RC" exit 99 fi @@ -72,7 +82,7 @@ collate() # Report error on console, syslog, and set login banners for getty + ssh console_error() { - logger -p user.crit -t bootstrap "$1" + critical "$1" # shellcheck disable=SC3037 /bin/echo -e "\n\n\e[31mCRITICAL BOOTSTRAP ERROR\n$1\e[0m\n" > /dev/console @@ -157,11 +167,20 @@ else fi rc=$? +# Ensure 'admin' group users always have access chgrp wheel "$CFG_PATH_" chmod g+w "$CFG_PATH_" + +# Ensure factory-config has correct syntax +if ! migrate -cq "$INIT_DATA"; then + if migrate -iq -b "${INIT_DATA%.*}.bak" "$INIT_DATA"; then + err "${INIT_DATA}: found and fixed old syntax!" + fi +fi + if ! sysrepoctl -z "$INIT_DATA"; then rc=$? - logger -sik -p user.error "Failed loading factory-default datastore" + err "Failed loading factory-default datastore" else # Clear running-config so we can load/create startup in the next step temp=$(mktemp) diff --git a/src/confd/bin/migrate b/src/confd/bin/migrate new file mode 100755 index 000000000..b72d00775 --- /dev/null +++ b/src/confd/bin/migrate @@ -0,0 +1,235 @@ +#!/bin/sh +# Run .cfg migration jq scripts to backup and transform older syntax. +scripts="/usr/share/confd/migrate" +ident=$(basename "$0") + +usage() +{ + echo "usage: $0 [-chiq] [-b /path/backup/file.ext] [/path/to/config.cfg]" + echo + echo "options:" + echo " -b FILE Create backup FILE, appends detected version before .ext" + echo " -c Check only, returns false when migration is not needed" + echo " -h This help text" + echo " -i Edit file in-place instead of sending to stdout, like sed" + echo " -q Quiet, skip normal log messages, only errors are logged" + echo + echo "By default, this script reads a .cfg file on stdin, or as the first" + echo "non-option argument, then migrates it to a new syntax on stdout." +} + +# shellcheck disable=SC2317 +cleanup() +{ + rm -f "$tmp" +} + +note() +{ + if [ -n "$quiet" ]; then + return + fi + logger -I $$ -k -p user.notice -t "$ident" "$1" +} + +err() +{ + logger -I $$ -k -p user.err -t "$ident" "$1" +} + +# Convert human-readable version to integer level +atoi() +{ + echo "$1" | awk -F. '{print $1 * 1000 + $2}' +} + +file_version() +{ + jq -r ' + if .["infix-meta:meta"] | has("infix-meta:version") then + .["infix-meta:meta"]["infix-meta:version"] + else + "0.0" + end + ' "$1" +} + +# Find the latest version by examining the highest numbered directory +confd_version() +{ + find "$scripts" -mindepth 1 -maxdepth 1 -type d \ + | sort -V | tail -n1 | xargs -n1 basename +} + +# Update meta data with the latest version +meta_version() +{ + if jq --arg version "$sys_version" '.["infix-meta:meta"] = {"infix-meta:version": $version}' "$2" \ + > "${2}.tmp" && mv "${2}.tmp" "$2"; then + note "$1: configuration updated to version $sys_version." + return 0 + fi + + err "$1: failed updating configuration to version $sys_version!" + return 1 +} + +# Apply the scripts for each version directory in sequence +migrate() +{ + note "$1: migrating from version $cfg_version" + + for version_dir in $(find "$scripts" -mindepth 1 -maxdepth 1 -type d | sort -V); do + dir=$(basename "$version_dir") + version=$(atoi "$dir") + + # Step by step upgrade file to latest version + if [ "$cfg_level" -lt "$version" ]; then + note "Applying migrations for version $dir ..." + + # Apply all scripts in the version directory in order + for script in $(find "$version_dir" -type f -name '*.sh' | sort -V); do + note "$1: calling $script ..." + sh "$script" "$2" + done + + # File now at $version ... + cfg_level="$version" + fi + done +} + +# Try migrating a copy, then diff the files, for factory-config check +diff() +{ + CFG=$1 + TMP=$(mktemp) + + cp -p "$CFG" "$TMP" + quiet=1 + migrate "(diff)" "$TMP" + cmp -s "$CFG" "$TMP" + rc=$? + rm -f "$TMP" + + return $rc +} + + +tmp=$(mktemp) +chmod 600 "$tmp" + +trap cleanup INT HUP TERM EXIT + +OPTS=$(getopt -o b:chiq -- "$@") +eval set -- "$OPTS" + +while [ -n "$1" ]; do + case $1 in + -b) + bak=$2 + shift + ;; + -c) + check=1 + ;; + -h) + usage + exit 0 + ;; + -i) + inplace=1 + ;; + -q) + quiet=1 + ;; + --) + shift + break + ;; + *) + # Likely file argument + break + ;; + esac + shift +done + +if [ -n "$1" ] && [ -f "$1" ]; then + # Copy to tempfile to allow user to > same file + orig="$1" + cp -p "$1" "$tmp" + shift +elif [ -t 0 ]; then + orig="(stdin)" + cat > "$tmp" +else + usage + exit 1 +fi + +if ! jq empty "$tmp" 2>/dev/null; then + err "$tmp invalid JSON format!" + exit 1 +fi + +cfg_version=$(file_version "$tmp") +sys_version=$(confd_version) + +cfg_level=$(atoi "$cfg_version") +sys_level=$(atoi "$sys_version") + +# Check for downgrade +if [ "$cfg_level" -gt "$sys_level" ]; then + err "$orig: version is newer ($cfg_version) than supported ($sys_version). Exiting." + exit 1 +fi + +# If the current version is already the latest, exit the script +if [ "$cfg_level" -eq "$sys_level" ]; then + exit 0 +else + if [ -n "$check" ]; then + # We may be called to check a file without meta:version (factory) + if [ "$cfg_version" = "0.0" ]; then + if diff "$tmp"; then + # File is OK, despite lacking meta:version + exit 0 + fi + msg="$orig: has syntax error, requires migrating." + else + msg="$orig: version $cfg_version, requires migrating." + fi + if [ -t 0 ]; then + echo "$msg" + else + note "$msg" + fi + exit 1 + fi +fi + +if [ -n "$bak" ]; then + fil="${bak%.*}" + ext="${bak##*.}" + bak="${fil}-${cfg_version}.${ext}" + if cp -p "$tmp" "$bak" 2>/dev/null; then + note "$orig: backup created: $bak" + else + err "$orig: failed creating backup: $bak" + exit 1 + fi +fi + +if ! migrate "$orig" "$tmp"; then + exit 1 +fi +meta_version "$orig" "$tmp" + +if [ -n "$inplace" ] && [ "$orig" != "(stdin)" ]; then + cp -p "$tmp" "$orig" +else + cat "$tmp" +fi + +exit 0 diff --git a/src/confd/configure.ac b/src/confd/configure.ac index 9f8ea9832..639ba8b69 100644 --- a/src/confd/configure.ac +++ b/src/confd/configure.ac @@ -1,6 +1,6 @@ AC_PREREQ(2.61) # confd version is same as system YANG model version, step on breaking changes -AC_INIT([confd], [1.1], [https://github.com/kernelkit/infix/issues]) +AC_INIT([confd], [1.2], [https://github.com/kernelkit/infix/issues]) AM_INIT_AUTOMAKE(1.11 foreign subdir-objects) AM_SILENT_RULES(yes) @@ -15,6 +15,7 @@ AC_CONFIG_FILES([ share/migrate/Makefile share/migrate/1.0/Makefile share/migrate/1.1/Makefile + share/migrate/1.2/Makefile src/Makefile yang/Makefile ]) diff --git a/src/confd/share/migrate/1.2/10-ietf-interfaces-phys-address.sh b/src/confd/share/migrate/1.2/10-ietf-interfaces-phys-address.sh new file mode 100644 index 000000000..97233aae5 --- /dev/null +++ b/src/confd/share/migrate/1.2/10-ietf-interfaces-phys-address.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Migrate phys-address -> custom-phys-address with static option + +file=$1 +temp=${file}.tmp + +jq '( + .["ietf-interfaces:interfaces"].interface[] |= + if has("phys-address") then + .["infix-interfaces:custom-phys-address"] = { "static": .["phys-address"] } | del(.["phys-address"]) + else + . + end +)' "$file" > "$temp" && +mv "$temp" "$file" diff --git a/src/confd/share/migrate/1.2/Makefile.am b/src/confd/share/migrate/1.2/Makefile.am new file mode 100644 index 000000000..5171e8b41 --- /dev/null +++ b/src/confd/share/migrate/1.2/Makefile.am @@ -0,0 +1,2 @@ +migratedir = $(pkgdatadir)/migrate/1.2 +dist_migrate_DATA = 10-ietf-interfaces-phys-address.sh diff --git a/src/confd/share/migrate/Makefile.am b/src/confd/share/migrate/Makefile.am index 62da76223..df1d8ba9d 100644 --- a/src/confd/share/migrate/Makefile.am +++ b/src/confd/share/migrate/Makefile.am @@ -1,2 +1,2 @@ -SUBDIRS = 1.0 1.1 +SUBDIRS = 1.0 1.1 1.2 migratedir = $(pkgdatadir)/migrate diff --git a/src/confd/src/ietf-interfaces.c b/src/confd/src/ietf-interfaces.c index 537c162b9..db4d69399 100644 --- a/src/confd/src/ietf-interfaces.c +++ b/src/confd/src/ietf-interfaces.c @@ -431,15 +431,110 @@ static int netdag_gen_link_mtu(FILE *ip, struct lyd_node *dif) return 0; } +static void calc_mac(const char *base_mac, const char *mac_offset, char *buf, size_t len) +{ + uint8_t base[6], offset[6], result[6]; + int carry = 0, i; + + sscanf(base_mac, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", + &base[0], &base[1], &base[2], &base[3], &base[4], &base[5]); + + sscanf(mac_offset, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", + &offset[0], &offset[1], &offset[2], &offset[3], &offset[4], &offset[5]); + + for (i = 5; i >= 0; i--) { + int sum = base[i] + offset[i] + carry; + + result[i] = sum & 0xFF; + carry = (sum > 0xFF) ? 1 : 0; + } + + snprintf(buf, len, "%02x:%02x:%02x:%02x:%02x:%02x", + result[0], result[1], result[2], result[3], result[4], result[5]); +} + +/* + * Get child value from a diff parent, only returns value if not + * deleted. In which case the deleted flag may be set. + */ +static const char *get_val(struct lyd_node *parent, char *name, int *deleted) +{ + const char *value = NULL; + struct lyd_node *node; + + node = lydx_get_child(parent, name); + if (node) { + if (lydx_get_op(node) == LYDX_OP_DELETE) { + if (deleted) + *deleted = 1; + return NULL; + } + + value = lyd_get_value(node); + } + + return value; +} + +/* + * Locate custom-phys-address, adjust for any offset, and return pointer + * to a static string. (Which will be overwritten on subsequent calls.) + * + * The 'deleted' flag will be set if any of the nodes in the subtree are + * deleted. Used when restoring permaddr and similar. + */ +static char *get_phys_addr(struct lyd_node *parent, int *deleted) +{ + struct lyd_node *node, *cpa; + static char mac[18]; + struct json_t *j; + const char *ptr; + + cpa = lydx_get_descendant(lyd_child(parent), "custom-phys-address", NULL); + if (!cpa || lydx_get_op(cpa) == LYDX_OP_DELETE) { + if (cpa && deleted) + *deleted = 1; + return NULL; + } + + ptr = get_val(cpa, "static", deleted); + if (ptr) { + strlcpy(mac, ptr, sizeof(mac)); + return mac; + } + + node = lydx_get_child(cpa, "chassis"); + if (!node || lydx_get_op(node) == LYDX_OP_DELETE) { + if (node && deleted) + *deleted = 1; + return NULL; + } + + j = json_object_get(confd.root, "mac-address"); + if (!j) { + WARN("cannot set chassis based MAC, not found."); + return NULL; + } + + ptr = json_string_value(j); + strlcpy(mac, ptr, sizeof(mac)); + + ptr = get_val(node, "offset", deleted); + if (ptr) + calc_mac(mac, ptr, mac, sizeof(mac)); + + return mac; +} + static int netdag_gen_link_addr(FILE *ip, struct lyd_node *cif, struct lyd_node *dif) { const char *ifname = lydx_get_cattr(dif, "name"); - const char *mac = NULL; - struct lyd_node *node; + const char *mac; + int deleted = 0; char buf[32]; - node = lydx_get_child(dif, "phys-address"); - if (lydx_get_op(node) == LYDX_OP_DELETE) { + mac = get_phys_addr(dif, &deleted); + if (!mac && deleted) { FILE *fp; /* @@ -455,8 +550,6 @@ static int netdag_gen_link_addr(FILE *ip, struct lyd_node *cif, struct lyd_node if (mac && !strcmp(mac, "null")) return 0; } - } else { - mac = lyd_get_value(node); } if (!mac || !strlen(mac)) { @@ -1344,9 +1437,8 @@ static int netdag_gen_bridge(sr_session_ctx_t *session, struct dagger *net, stru * addrgenmode eui64 with random mac, issue #357. */ if (add) { - const char *mac; + const char *mac = get_phys_addr(cif, NULL); - mac = lydx_get_cattr(cif, "phys-address"); if (!mac) { struct json_t *j; @@ -1449,16 +1541,17 @@ static int netdag_gen_veth(struct dagger *net, struct lyd_node *dif, return ERR_IFACE(cif, err, "Unable to add dep \"%s\" to %s", peer, ifname); } else { char ifname_args[64] = "", peer_args[64] = ""; + const char *mac; dagger_skip_iface(net, peer); - node = lydx_get_child(dif, "phys-address"); - if (node) - snprintf(ifname_args, sizeof(ifname_args), "address %s", lyd_get_value(node)); + mac = get_phys_addr(dif, NULL); + if (mac) + snprintf(ifname_args, sizeof(ifname_args), "address %s", mac); node = lydx_find_by_name(lyd_parent(cif), "interface", peer); - if (node && (node = lydx_get_child(node, "phys-address"))) - snprintf(peer_args, sizeof(peer_args), "address %s", lyd_get_value(node)); + if (node && (mac = get_phys_addr(node, NULL))) + snprintf(peer_args, sizeof(peer_args), "address %s", mac); fprintf(ip, "link add dev %s %s type veth peer %s %s\n", ifname, ifname_args, peer, peer_args); @@ -1612,10 +1705,9 @@ static int netdag_gen_afspec_set(sr_session_ctx_t *session, struct dagger *net, static bool is_phys_addr_deleted(struct lyd_node *dif) { - struct lyd_node *node; + int deleted = 0; - node = lydx_get_child(dif, "phys-address"); - if (node && lydx_get_op(node) == LYDX_OP_DELETE) + if (!get_phys_addr(dif, &deleted) && deleted) return true; return false; diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 40ef9a2af..6d0bfe610 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -37,7 +37,7 @@ MODULES=( "ieee802-ethernet-interface@2019-06-21.yang" "infix-ethernet-interface@2024-02-27.yang" "infix-factory-default@2023-06-28.yang" - "infix-interfaces@2024-09-23.yang -e vlan-filtering" + "infix-interfaces@2024-10-08.yang -e vlan-filtering" # from rousette "ietf-restconf@2017-01-26.yang" diff --git a/src/confd/yang/infix-interfaces.yang b/src/confd/yang/infix-interfaces.yang index 6f807882f..2a0533fda 100644 --- a/src/confd/yang/infix-interfaces.yang +++ b/src/confd/yang/infix-interfaces.yang @@ -9,6 +9,9 @@ module infix-interfaces { import ietf-interfaces { prefix if; } + import ietf-yang-types { + prefix yang; + } include infix-if-base; include infix-if-bridge; @@ -20,6 +23,11 @@ module infix-interfaces { contact "kernelkit@googlegroups.com"; description "Linux bridge and lag extensions for ietf-interfaces."; + revision 2024-10-08 { + description "Replace writable phy-address with custom-phys-address."; + reference "internal"; + } + revision 2024-09-23 { description "Drop interfaces-state deviation, already marked deprecated."; reference "internal"; @@ -71,9 +79,34 @@ module infix-interfaces { } } - deviation "/if:interfaces/if:interface/if:phys-address" { - deviate replace { - config true; + augment "/if:interfaces/if:interface" { + description "Custom phys-address management, static or derived from chassis MAC."; + + container custom-phys-address { + description "Override the default physical address."; + + choice type { + description "Choose between static MAC address or chassis-derived MAC."; + + case static { + leaf static { + description "Statically configured interface address on protocol sub-layer, e.g., MAC."; + type yang:phys-address; + } + } + + case chassis { + container chassis { + description "Derive physical address from chassis MAC address."; + presence "Enable chassis-derived address."; + + leaf offset { + description "Static offset added to the chassis MAC address."; + type yang:phys-address; + } + } + } + } } } } diff --git a/src/confd/yang/infix-interfaces@2024-09-23.yang b/src/confd/yang/infix-interfaces@2024-10-08.yang similarity index 100% rename from src/confd/yang/infix-interfaces@2024-09-23.yang rename to src/confd/yang/infix-interfaces@2024-10-08.yang diff --git a/test/case/ietf_interfaces/iface_phys_address/test.py b/test/case/ietf_interfaces/iface_phys_address/test.py index 1a4554e88..8d5147b64 100755 --- a/test/case/ietf_interfaces/iface_phys_address/test.py +++ b/test/case/ietf_interfaces/iface_phys_address/test.py @@ -2,30 +2,64 @@ """ Custom MAC address on interface -Test possibility to set and remove custom mac address on interfaces +Verify support for setting and removing a custom MAC address on interfaces. +Both static MAC address and derived from the chassis MAC with, or without, +an offset applied. """ -import copy + import infamy import infamy.iface as iface - from infamy.util import until + +def calc_mac(base_mac, mac_offset): + """Add mac_offset to base_mac and return result.""" + base = [int(x, 16) for x in base_mac.split(':')] + offset = [int(x, 16) for x in mac_offset.split(':')] + result = [0] * 6 + carry = 0 + + for i in range(5, -1, -1): + total = base[i] + offset[i] + carry + result[i] = total & 0xFF + carry = 1 if total > 0xFF else 0 + + return ':'.join(f'{x:02x}' for x in result) + + +def reset_mac(tgt, port, mac): + """Reset DUT interface MAC address to default.""" + node = "infix-interfaces:custom-phys-address" + xpath = iface.get_iface_xpath(port, node) + tgt.delete_xpath(xpath) + with test.step("Verify target:data MAC address is reset to default"): + until(lambda: iface.get_phys_address(tgt, tport) == mac) + + with infamy.Test() as test: + CMD = "jq -r '.[\"mac-address\"]' /run/system.json" + with test.step("Initialize"): env = infamy.Env() target = env.attach("target", "mgmt") + tgtssh = env.attach("target", "mgmt", "ssh") _, tport = env.ltop.xlate("target", "data") pmac = iface.get_phys_address(target, tport) - cmac = "02:01:00:c0:ff:ee" - print(f"Target iface {tport} original mac {pmac}") + cmac = tgtssh.runsh(CMD).stdout.strip() + STATIC = "02:01:00:c0:ff:ee" + OFFSET = "00:00:00:00:ff:aa" + + print(f"Chassis MAC address target: {cmac}") + print(f"Default MAC address of {tport} : {pmac}") - with test.step("Set custom MAC address to '02:01:00:c0:ff:ee' on target:mgmt"): - print(f"Intitial MAC address: {pmac}") + with test.step("Set target:data static MAC address '02:01:00:c0:ff:ee'"): config = { "interfaces": { "interface": [{ "name": f"{tport}", - "phys-address": f"{cmac}" + "custom-phys-address": { + "static": f"{STATIC}" + } }] } } @@ -33,14 +67,53 @@ with test.step("Verify target:mgmt has MAC address '02:01:00:c0:ff:ee'"): mac = iface.get_phys_address(target, tport) - print(f"Target iface {tport} current mac: {mac}") + print(f"Current MAC: {mac}, should be: {STATIC}") + assert mac == STATIC + + with test.step("Reset target:mgmt MAC address to default"): + reset_mac(target, tport, pmac) + + with test.step("Set target:data to chassis MAC"): + config = { + "interfaces": { + "interface": [{ + "name": f"{tport}", + "custom-phys-address": { + "chassis": {} + } + }] + } + } + target.put_config_dict("ietf-interfaces", config) + + with test.step("Verify target:data has chassis MAC"): + mac = iface.get_phys_address(target, tport) + print(f"Current MAC: {mac}, should be: {cmac}") assert mac == cmac - with test.step("Remove custom MAC address '02:01:00:c0:ff:ee'"): - xpath=iface.get_iface_xpath(tport, "phys-address") - target.delete_xpath(xpath) + with test.step("Set target:data to chassis MAC + offset"): + print(f"Setting chassis MAC {cmac} + offset {OFFSET}") + config = { + "interfaces": { + "interface": [{ + "name": f"{tport}", + "custom-phys-address": { + "chassis": { + "offset": f"{OFFSET}" + } + } + }] + } + } + target.put_config_dict("ietf-interfaces", config) + + with test.step("Verify target:data has chassis MAC + offset"): + mac = iface.get_phys_address(target, tport) + BMAC = calc_mac(cmac, OFFSET) + print(f"Current MAC: {mac}, should be: {BMAC} (calculated)") + assert mac == BMAC - with test.step("Verify that target:mgmt has the original MAC address again"): - until(lambda: iface.get_phys_address(target, tport) == pmac) + with test.step("Reset target:mgmt MAC address to default"): + reset_mac(target, tport, pmac) test.succeed()