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 @@
+
+
+
+
\ 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 @@
-
\ No newline at end of file
+
\ 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()