#!/usr/bin/bash
#
# Write the DHCPv6 client identifier that the xCAT reservation of this node holds.
#
# makedhcp builds the DUID that mac.duid selects, by default a DUID-UUID (RFC 6355) from
# vpd.uuid. A DHCPv6 host reservation carries either a duid or a hw-address, never both. A
# client that sends any other identifier matches no reservation and takes a pool address with
# no boot URL. No network manager derives a DUID from the SMBIOS UUID in the byte order the
# firmware sends: NetworkManager has no such option at all, and systemd-networkd's
# DUIDType=uuid uses the decoded product UUID, which is the other order. So the value is
# written as a literal, for whichever renderer the node has.
#
# This covers a node whose installed OS speaks DHCPv6, which is a diskful or cloned node on a
# family with no static handoff. A diskless or statelite node needs nothing: the kernel command
# line configures its address statically and configeth writes "dhcp6: false", so its image sends
# no DHCPv6 request at all.
#
# The DUID for a given UUID is the same literal as xCAT-test/bats/genesis_duid_uuid.bats and
# genesis_openembedded_duid.bats, which hold the two Genesis images to this byte order, and as
# the reservation in xCAT-test/unit/dhcp_kea_plugin_intent.t.
set -u

LC_ALL=C
export LC_ALL

uuid_file=${XCAT_PRODUCT_UUID_FILE:-/sys/devices/virtual/dmi/id/product_uuid}
nm_dir=${XCAT_NM_CONF_DIR:-/etc/NetworkManager/conf.d}
networkd_dir=${XCAT_NETWORKD_CONF_DIR:-/etc/systemd/networkd.conf.d}

log() {
    logger -t xcat-dhcpv6-duid -- "$*" 2>/dev/null || echo "xcat-dhcpv6-duid: $*"
}

# mac.duid decides the identifier, the same way makedhcp decides it, and mypostscript exports
# it as DHCP6DUID. A node whose reservation holds a canonical-order, a link-layer or a literal
# DUID would match nothing if this script always derived the firmware order. With no value,
# makedhcp reserves the DUID-LL on ppc64le, because SLOF reports no UUID.
duid_setting=$(printf '%s' "${DHCP6DUID:-}" | tr -d '[:space:]' | tr 'A-Z' 'a-z')
if [ -z "$duid_setting" ]; then
    case ${ARCH:-} in
        ppc*) duid_setting=ll ;;
        *)    duid_setting=firmware ;;
    esac
fi

literal=
case $duid_setting in
    firmware|canonical) ;;
    ll)
        # makedhcp builds the DUID-LL from the first address of mac.mac, the netboot NIC.
        mac=${MACMAC:-}
        mac=${mac%%|*}
        mac=${mac%%!*}
        mac=$(printf '%s' "$mac" | tr 'A-F-' 'a-f:')
        if ! printf '%s' "$mac" | grep -qE '^[0-9a-f]{2}(:[0-9a-f]{2}){5,8}$'; then
            log "the duid attribute is ll and there is no usable address in mac.mac; leaving the DHCPv6 client identifier alone"
            exit 0
        fi
        literal=00:03:00:01:$mac
        ;;
    *)
        # A literal DUID is reserved verbatim, so it is written verbatim.
        if ! printf '%s' "$duid_setting" | grep -qE '^[0-9a-f]{2}(:[0-9a-f]{2}){2,129}$'; then
            log "the duid attribute is '$duid_setting', which is not firmware, canonical, ll, or a DUID of colon-separated hexadecimal octets; leaving the DHCPv6 client identifier alone"
            exit 0
        fi
        literal=$duid_setting
        ;;
esac

# Only the two UUID orders read the SMBIOS UUID. makedhcp reserves a literal or a DUID-LL
# without one.
if [ -n "$literal" ]; then
    duid=$literal
else
    # makedhcp applies the policy to vpd.uuid, and mypostscript exports it as VPDUUID.
    # product_uuid is not always the same value: a KVM domain that an earlier mkvm defined
    # holds the byte swap of vpd.uuid.
    uuid=$(printf '%s' "${VPDUUID:-}" | tr -d -c '[:xdigit:]' | tr 'A-F' 'a-f')
    if [ "${#uuid}" -ne 32 ]; then
        if [ ! -r "$uuid_file" ]; then
            log "no SMBIOS UUID at $uuid_file, leaving the DHCPv6 client identifier alone"
            exit 0
        fi

        uuid=$(tr -d -c '[:xdigit:]' <"$uuid_file" | tr 'A-F' 'a-f')
        if [ "${#uuid}" -ne 32 ]; then
            log "no usable SMBIOS UUID at $uuid_file, leaving the DHCPv6 client identifier alone"
            exit 0
        fi
    fi

    # makedhcp refuses the nil UUID, because nodeimport writes it to every PowerKVM node, and
    # reserves the hardware address instead. A DUID built from it would match no reservation.
    case $uuid in
        00000000000000000000000000000000)
            log "SMBIOS UUID is the nil UUID, leaving the DHCPv6 client identifier alone"
            exit 0
            ;;
    esac

    # SMBIOS 2.6 and later store the first three UUID fields little-endian, and EDK2 copies
    # those bytes into its DUID. The UUID text is in canonical order, so the first three fields
    # go back into firmware order here. makedhcp, doxcat and genesis-duid do the same swap. The
    # canonical setting keeps the order of the text.
    if [ "$duid_setting" = canonical ]; then
        octets=${uuid:0:2}:${uuid:2:2}:${uuid:4:2}:${uuid:6:2}
        octets=$octets:${uuid:8:2}:${uuid:10:2}
        octets=$octets:${uuid:12:2}:${uuid:14:2}
    else
        octets=${uuid:6:2}:${uuid:4:2}:${uuid:2:2}:${uuid:0:2}
        octets=$octets:${uuid:10:2}:${uuid:8:2}
        octets=$octets:${uuid:14:2}:${uuid:12:2}
    fi
    for offset in 16 18 20 22 24 26 28 30; do
        octets=$octets:${uuid:$offset:2}
    done
    duid=00:04:$octets
fi

# systemd-networkd composes the DUID from DUIDType and DUIDRawData: the first two octets are
# the type, and the raw data is everything after them.
case ${duid:0:5} in
    00:01) networkd_type=link-layer-time ;;
    00:02) networkd_type=vendor ;;
    00:03) networkd_type=link-layer ;;
    00:04) networkd_type=uuid ;;
    *)     networkd_type=$((16#${duid:0:2}${duid:3:2})) ;;
esac
networkd_raw=${duid:6}

written=0

# NetworkManager takes the whole DUID, including its type. conf.d is read in name order
# and a later file wins, so this name sorts after the packaged defaults.
if [ -d "$nm_dir" ] || [ -d "${nm_dir%/*}" ]; then
    if mkdir -p "$nm_dir" 2>/dev/null &&
       printf '%s\n' '[connection]' "ipv6.dhcp-duid=$duid" >"$nm_dir/15-xcat-dhcpv6-duid.conf"; then
        log "NetworkManager DHCPv6 client identifier is $duid"
        written=$((written + 1))
    else
        log "unable to write $nm_dir/15-xcat-dhcpv6-duid.conf"
    fi
fi

if [ -d "$networkd_dir" ] || [ -d /run/systemd/network ] || [ -d "${networkd_dir%/*}" ]; then
    if mkdir -p "$networkd_dir" 2>/dev/null &&
       printf '%s\n' '[DHCPv6]' "DUIDType=$networkd_type" "DUIDRawData=$networkd_raw" >"$networkd_dir/15-xcat-dhcpv6-duid.conf"; then
        log "systemd-networkd DHCPv6 client identifier is $duid"
        written=$((written + 1))
    else
        log "unable to write $networkd_dir/15-xcat-dhcpv6-duid.conf"
    fi
fi

if [ "$written" -eq 0 ]; then
    log "no network manager configuration directory found; the DHCPv6 client identifier is unset"
    exit 1
fi
exit 0
