#!/bin/sh # HCN (Hybrid Cloud Network) dracut module # Discovers HCN configurations from device-tree and sets up bonding # # Workflow: # 1. Check if HCN is enabled (rd.hcn=1, rd.hcn.ip, or rd.hcn.route), exit early if not # 2. Discover HCN devices from device-tree (/proc/device-tree) # - Scans PCI devices (SR-IOV ethernet adapters) # - Scans vdevices (VNIC and Virtual Ethernet) # 3. Build MAPPINGS: bond names -> slave devices with MACs and modes # 4. Process rd.hcn.ip= and rd.hcn.route= parameters and replace slave references with bonds # 5. Carry over the remaining network options of the real kernel command line # (nameserver=, rd.peerdns=, rd.net.dhcp.*) # 6. Generate NetworkManager connections to a temporary directory via nm-initrd-generator # 7. Fix up generated connections to use correct bond masters and naming # 8. Copy connections to /etc/NetworkManager/system-connections for persistence # # This script runs from hcn-init-initrd.service, once udev has discovered the # devices. Much earlier, hcn-cmdline.sh has already told NetworkManager and the # other dracut modules to leave the network alone by writing an "ip=hcn" marker # to /etc/cmdline.d, so nothing configures the bond ports behind our back. # # Usage: # rd.hcn=1 - Create bond connections only (no IP configuration) # rd.hcn.ip= - Create bonds with HCN-specific IP configuration # rd.hcn.route= - Create bonds with HCN-specific route configuration command -v getargs >/dev/null || . /lib/dracut-lib.sh # Helper to read 4 bytes from device-tree and return hex string xdump4() { hexdump -n 4 -ve '/1 "%02x"' "$1" } # Helper to get MAC address from device-tree get_mac() { local dev=$1 if [ -f "$dev/local-mac-address" ]; then hexdump -ve '/1 "%02x:"' "$dev/local-mac-address" | sed 's/:$//' fi } # Number of 3 second waits left for the devices to show up in sysfs after a # migration, 3 minutes in total. # # The budget is global and not per device on purpose: the devices appear in # parallel, so waiting for them one by one would multiply the time spent here # by the number of devices and hcn-init-initrd.service, which allows 5 minutes # for the whole run, would kill the script in the middle of it. HCN_WAIT_RETRIES=60 # Function to discover HCN mapping for a device-tree node # # On success it sets HCN_MAPPING to "bondname devname mac mode" and returns 0. # The result is handed over in a variable instead of being printed because the # function logs its progress with info(), and under systemd info() writes to # stdout, which would end up mixed into the mapping (see carry_over_cmdline). get_dev_hcn() { local dev=$1 local hcnid devname mode mac ofpath HCN_MAPPING="" hcnid=$(xdump4 "$dev/ibm,hcn-id") [ -z "$hcnid" ] && return 1 mode=$(tr -d '\0' <"$dev/ibm,hcn-mode" 2>/dev/null) ofpath=${dev#/proc/device-tree} # Wait for device to appear in sysfs. This might take time after migration. # Every device is looked up at least once, even with the shared wait budget # already spent by the previous ones. devname is reset on each try because # ofpathname can resolve a name for a device that is not in sysfs yet, and # such a name must not be taken as a valid result once the waiting is over. while :; do devname=$(ofpathname -l "$ofpath" 2>/dev/null) if [ -n "$devname" ] && [ -e "/sys/class/net/$devname" ]; then info "parse-hcn: device $devname ready for $ofpath" mac=$(get_mac "$dev") break fi devname="" [ "${HCN_WAIT_RETRIES:-0}" -gt 0 ] || break # The tries are short so that a device that is already there is not waited # for longer than needed, but only one out of five is logged, the messages # also go to the console if [ $((HCN_WAIT_RETRIES % 5)) -eq 0 ]; then info "parse-hcn: waiting for device for $ofpath ($HCN_WAIT_RETRIES tries left)" fi sleep 3 HCN_WAIT_RETRIES=$((HCN_WAIT_RETRIES - 1)) done if [ -z "$devname" ]; then warn "parse-hcn: could not resolve device name for $dev" return 1 fi # The bond mapping: bondname devname mac mode HCN_MAPPING="bond$hcnid $devname ${mac:-none} ${mode:-none}" return 0 } # Get a value from a keyfile (INI format) # Usage: gkeyfile_get
gkeyfile_get() { awk -v sec="$2" -v key="$3" ' $0 ~ "^[[:space:]]*\\[.*\\][[:space:]]*$" { current_sec = $0 gsub(/^[[:space:]]+|[[:space:]]+$/, "", current_sec) current_sec = substr(current_sec, 2, length(current_sec) - 2) in_sec = (current_sec == sec) } in_sec { idx = index($0, "=") if (idx > 0) { k = substr($0, 1, idx - 1) gsub(/^[[:space:]]+|[[:space:]]+$/, "", k) if (k == key) { val = substr($0, idx + 1) gsub(/^[[:space:]]+|[[:space:]]+$|"/, "", val) print val exit } } } ' "$1" } # Check if a key exists in a keyfile (INI format) # Usage: gkeyfile_has
gkeyfile_has() { awk -v sec="$2" -v key="$3" ' $0 ~ "^[[:space:]]*\\[.*\\][[:space:]]*$" { current_sec = $0 gsub(/^[[:space:]]+|[[:space:]]+$/, "", current_sec) current_sec = substr(current_sec, 2, length(current_sec) - 2) in_sec = (current_sec == sec) } in_sec { idx = index($0, "=") if (idx > 0) { k = substr($0, 1, idx - 1) gsub(/^[[:space:]]+|[[:space:]]+$/, "", k) if (k == key) { found = 1 exit } } } END { exit !found } ' "$1" } # Set a value in a keyfile (INI format) # Usage: gkeyfile_set
gkeyfile_set() { if gkeyfile_has "$1" "$2" "$3"; then sed -i "/^\[$2\]/,/^\[/ s|^\([[:space:]]*$3[[:space:]]*=[[:space:]]*\).*|\1$4|" "$1" else sed -i "/^\[$2\]/a $3=$4" "$1" fi } # Parse a NetworkManager connection file and extract key fields # Returns: id|uuid|ifname|master|controller|mac (pipe-separated) parse_nm_connection() { awk -F'=' ' /^[[:space:]]*id[[:space:]]*=/ { gsub(/^[[:space:]]+|[[:space:]]+$|"/, "", $2) id = $2 } /^[[:space:]]*uuid[[:space:]]*=/ { gsub(/^[[:space:]]+|[[:space:]]+$|"/, "", $2) uuid = $2 } /^[[:space:]]*interface-name[[:space:]]*=/ { gsub(/^[[:space:]]+|[[:space:]]+$|"/, "", $2) ifname = $2 } /^[[:space:]]*master[[:space:]]*=/ { gsub(/^[[:space:]]+|[[:space:]]+$|"/, "", $2) master = $2 } /^[[:space:]]*controller[[:space:]]*=/ { gsub(/^[[:space:]]+|[[:space:]]+$|"/, "", $2) controller = $2 } /^[[:space:]]*mac-address[[:space:]]*=/ { gsub(/^[[:space:]]+|[[:space:]]+$|:|"/, "", $2) mac = tolower($2) } END { print id "|" uuid "|" ifname "|" master "|" controller "|" mac } ' "$1" } # Function to fix up NetworkManager connection files # # IMPORTANT: This function should ONLY be called when rd.hcn=1 is set # # nm-initrd-generator creates connections with: # - Interface names as connection IDs (e.g., "eth0") # - UUIDs as master/controller references instead of bond names # - Generic interface-name settings # # We need to transform these to: # - Bond-based connection IDs (e.g., "bond0-eth0") # - Bond names as master/controller references (e.g., "bond0") # - Correct slave-type/port-type set to 'bond' # - Match slaves to correct bonds based on MAPPINGS # # This function only modifies connections that are related to HCN bonds # (either bond interfaces themselves or slaves found in MAPPINGS) fixup_nm_connections() { local conn_dir="${HCN_RUNTIME_CONN_DIR:-$NM_RUNTIME_CONN_DIR}" [ -d "$conn_dir" ] || return 0 local con id uuid ifname master controller mac uuid_map map local found_master found_ifname mapping_info new_id # First pass: build UUID to ID mapping for resolution for con in "$conn_dir"/*.nmconnection; do [ -e "$con" ] || continue IFS='|' read -r id uuid ifname master controller mac <:{dhcp|on|any|dhcp6|auto6|link6}[:[]] # Method-only config with optional MTU: also applies to every bond. # Extract optional MTU field (after the method, could be followed by more colons) temp_ip=${HCN_IP} # Count colons to determine if there are extra fields colons=0 while [ "${temp_ip#*:}" != "$temp_ip" ]; do colons=$((colons + 1)) temp_ip=${temp_ip#*:} done # Format: interface:method[:mtu] # We need at least 1 colon (interface:method), optionally 2 for MTU if [ "$colons" -ge 1 ]; then info "parse-hcn: applying DHCP/auto config with optional MTU to $BONDNAME" NEW_ARGS="$NEW_ARGS ip=$BONDNAME:${HCN_IP#*:}" fi ;; *) # Static IP configuration: a fixed address can belong to only one bond, # so an unqualified static config falls back to the first bond only. [ "$BONDNAME" = "$FIRST_BOND" ] || continue # Count colons to determine format # Standard format: rd.hcn.ip=:[]:::::{none|off|dhcp|on|any|dhcp6|auto6|ibft}[:[][:]] # Minimum fields: client-IP:gateway:netmask:hostname:interface:method (5 colons) # Extended: adds :dns1 and/or :dns2 (up to 7 colons) temp_ip=${HCN_IP} colons=0 while [ "${temp_ip#*:}" != "$temp_ip" ]; do colons=$((colons + 1)) temp_ip=${temp_ip#*:} done if [ "$colons" -ge 5 ]; then # Already has interface field, just need to replace it with bond name # Extract fields: we need to replace the 6th field (interface) with BONDNAME # Preserve any DNS fields that may follow # shellcheck disable=SC2034 IFS=':' read -r f1 f2 f3 f4 f5 f6 f7 f8 f9 < "$HCN_RUNTIME_DIR"/cmdline # Find and execute nm-initrd-generator generator_found=0 for gen in /usr/lib/nm-initrd-generator /usr/libexec/nm-initrd-generator; do [ -x "$gen" ] || continue generator_found=1 info "parse-hcn: calling $gen" rm -f "$HCN_RUNTIME_CONN_DIR"/* # --run-config-dir points to a scratch directory on purpose. The generator # always writes 15-carrier-timeout.conf, and with the default directory # this run would overwrite the one NetworkManager generated from the real # command line, resetting a user supplied rd.net.timeout.carrier. # shellcheck disable=SC2086 if "$gen" -c "$HCN_RUNTIME_CONN_DIR" -r "$HCN_RUNTIME_CONF_DIR" -- $NEW_ARGS; then if [ "$(ls -A "$HCN_RUNTIME_CONN_DIR")" ]; then fixup_nm_connections # Persist these new HCN connections to /etc mkdir -p "$NM_CONF_CONN_DIR" cp -rf "$HCN_RUNTIME_CONN_DIR"/* "$NM_CONF_CONN_DIR" mkdir -p "$NM_RUNTIME_DIR"/initrd : > "$NM_RUNTIME_DIR"/initrd/neednet echo '[ -f /tmp/nm.done ]' > "$hookdir"/initqueue/finished/nm.sh else warn "parse-hcn: nm-initrd-generator did not generate any connections" fi else warn "parse-hcn: nm-initrd-generator failed" fi break done if [ $generator_found -eq 0 ]; then warn "parse-hcn: nm-initrd-generator not found" fi fi