From cc165c5bb9abe6e0a88a85fcd8d8fcf82c0fcfb5 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 8 Oct 2024 16:55:13 +0200 Subject: [PATCH 1/8] confd: replace phys-address deviation with new setting This commit replaces the ietf-interfaces deviation for phys-address, used to set custom MAC address on interfaces, with a more flexible approach which can calculate the new MAC address based on the device's chassi MAC, with or without an added offset. The regression test has been updated to test all variants. Resolves: #680 Signed-off-by: Joachim Wiberg --- src/confd/src/ietf-interfaces.c | 124 +++++++++++++++--- src/confd/yang/confd.inc | 2 +- src/confd/yang/infix-interfaces.yang | 39 +++++- ....yang => infix-interfaces@2024-10-08.yang} | 0 .../iface_phys_address/test.py | 101 ++++++++++++-- 5 files changed, 232 insertions(+), 34 deletions(-) rename src/confd/yang/{infix-interfaces@2024-09-23.yang => infix-interfaces@2024-10-08.yang} (100%) 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() From 98ff33113ad52d20ec3f5757b338f003a7bf31f5 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 9 Oct 2024 03:37:37 +0200 Subject: [PATCH 2/8] confd: bump version for phys-address .cfg migration See issue #680 Signed-off-by: Joachim Wiberg --- package/confd/confd.mk | 2 +- src/confd/configure.ac | 3 ++- .../1.2/10-ietf-interfaces-phys-address.sh | 15 +++++++++++++++ src/confd/share/migrate/1.2/Makefile.am | 2 ++ src/confd/share/migrate/Makefile.am | 2 +- 5 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 src/confd/share/migrate/1.2/10-ietf-interfaces-phys-address.sh create mode 100644 src/confd/share/migrate/1.2/Makefile.am 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/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 From 1852d12e7fa3e3a28237b594b305e747b2751999 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 9 Oct 2024 03:47:58 +0200 Subject: [PATCH 3/8] confd: refactor 30-cfg-migrate for stand-alone use This patch relocates the 30-cfg-migrate init script to confd, where it belongs, renaming it migrate and installing into /usr/sbin. The script has been heavily modified to be more user friendly, as well as useful for automated restore operations, to be able to refresh old backups to modern .cfg file syntax. Example (injected phys-address error): admin@example:~$ migrate -c /etc/factory-config.cfg /etc/factory-config.cfg: has syntax error, requires migrating. Since backups are created with the old confd sytanx version in the name, the backup functionality has been kept in the script. Signed-off-by: Joachim Wiberg --- .../usr/libexec/infix/init.d/30-cfg-migrate | 100 +------- src/confd/bin/Makefile.am | 2 +- src/confd/bin/migrate | 235 ++++++++++++++++++ 3 files changed, 243 insertions(+), 94 deletions(-) create mode 100755 src/confd/bin/migrate 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/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/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 From f79ad4d2d8bd8ec09e9ac1f06815f99ff8f21f6d Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 9 Oct 2024 06:03:43 +0200 Subject: [PATCH 4/8] confd: handle static factory-config syntax error For products with a static factory-config, or customers generating with an older syntax, attempt migration in case of bootstrap failure. Refactor logging to drop '-s'. This prevents duplicate log messages since bootstrap always runs after syslogd has started and all stdout is always logged. Signed-off-by: Joachim Wiberg --- src/confd/bin/bootstrap | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) 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) From 284f79242aa9ec9a7a83a095be4e9b9f446d2c9c Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 9 Oct 2024 15:53:17 +0200 Subject: [PATCH 5/8] doc: add missing VLAN block to overview svg Signed-off-by: Joachim Wiberg --- doc/img/lego.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 026236390b01822527582aa537d04f2a6c41d801 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 9 Oct 2024 15:53:51 +0200 Subject: [PATCH 6/8] doc: add picture to show stacking direction Signed-off-by: Joachim Wiberg --- doc/img/lego-relations.svg | 4 ++++ doc/networking.md | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 doc/img/lego-relations.svg 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/networking.md b/doc/networking.md index b7c1bc196..ec5d0a65d 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. From 025c3ab7f683c19024ba890fd1af7452f2b6b4a2 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 9 Oct 2024 15:54:57 +0200 Subject: [PATCH 7/8] doc: new setting on general interface settings Signed-off-by: Joachim Wiberg --- board/aarch64/r2s/README.md | 2 +- doc/container.md | 2 +- doc/networking.md | 62 +++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) 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/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/networking.md b/doc/networking.md index ec5d0a65d..4e04aa833 100644 --- a/doc/networking.md +++ b/doc/networking.md @@ -77,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 @@ -1058,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`. From 26180859bbaa75089621e5e639c1f2ef455b0c07 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 9 Oct 2024 16:15:26 +0200 Subject: [PATCH 8/8] doc: update changelog Add issue #680, and all others that's gone in during the last week. Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) 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 -------------------------