diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-connectivity/xcat-genesis-network/files/10-xcat-genesis.conf b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-connectivity/xcat-genesis-network/files/10-xcat-genesis.conf
new file mode 100644
index 000000000..d4a805a0d
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-connectivity/xcat-genesis-network/files/10-xcat-genesis.conf
@@ -0,0 +1,10 @@
+[main]
+dhcp=internal
+dns=default
+plugins=keyfile
+rc-manager=symlink
+
+[connection]
+ipv4.dhcp-timeout=20
+ipv6.dhcp-duid=ll
+ipv6.dhcp-timeout=20
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-connectivity/xcat-genesis-network/xcat-genesis-network_1.0.bb b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-connectivity/xcat-genesis-network/xcat-genesis-network_1.0.bb
new file mode 100644
index 000000000..849a601bb
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-connectivity/xcat-genesis-network/xcat-genesis-network_1.0.bb
@@ -0,0 +1,16 @@
+SUMMARY = "xCAT Genesis network configuration"
+LICENSE = "EPL-1.0"
+LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/EPL-1.0;md5=57f8d5e2b3e98ac6e088986c12bf94e6"
+
+SRC_URI = "file://10-xcat-genesis.conf"
+S = "${UNPACKDIR}"
+
+RDEPENDS:${PN} = "networkmanager-daemon networkmanager-nmcli"
+
+do_install() {
+ install -d ${D}${sysconfdir}/NetworkManager/conf.d
+ install -m 0644 ${UNPACKDIR}/10-xcat-genesis.conf \
+ ${D}${sysconfdir}/NetworkManager/conf.d/10-xcat-genesis.conf
+}
+
+FILES:${PN} = "${sysconfdir}/NetworkManager/conf.d/10-xcat-genesis.conf"
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/00-xcat-genesis.preset b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/00-xcat-genesis.preset
new file mode 100644
index 000000000..c6cf64a3f
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/00-xcat-genesis.preset
@@ -0,0 +1 @@
+disable getty@.service
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-action b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-action
new file mode 100755
index 000000000..e026054d9
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-action
@@ -0,0 +1,273 @@
+#!/bin/bash
+set -euo pipefail
+
+functions_file=${XCAT_GENESIS_FUNCTIONS:-/usr/libexec/xcat/genesis-functions}
+# shellcheck source=genesis-functions
+source "$functions_file"
+
+state_dir=${XCAT_STATE_DIR:-/run/xcat}
+status_command=${XCAT_STATUS_COMMAND:-/usr/libexec/xcat/genesis-status}
+destiny_file=${XCAT_DESTINY_FILE:-$state_dir/destiny}
+metadata_file=${XCAT_METADATA_FILE:-$state_dir/xcat-response.env}
+network_file=${XCAT_NETWORK_FILE:-$state_dir/genesis.env}
+discover_command=${XCAT_DISCOVER_COMMAND:-/usr/libexec/xcat/genesis-discover}
+certificate_command=${XCAT_CERTIFICATE_COMMAND:-/usr/libexec/xcat/genesis-getcert}
+certificate_file=${XCAT_CERTIFICATE_FILE:-/etc/xcat/cert.pem}
+getdestiny_command=${XCAT_GETDESTINY_COMMAND:-getdestiny}
+nextdestiny_command=${XCAT_NEXTDESTINY_COMMAND:-nextdestiny}
+approved_command_dir=${XCAT_ACTION_COMMAND_DIR:-/usr/libexec/xcat/genesis/actions}
+reboot_command=${XCAT_REBOOT_COMMAND:-systemctl}
+poweroff_command=${XCAT_POWEROFF_COMMAND:-systemctl}
+poll_seconds=${XCAT_ACTION_POLL_SECONDS:-15}
+request_timeout=${XCAT_ACTION_REQUEST_TIMEOUT:-20}
+operation_timeout=${XCAT_ACTION_OPERATION_TIMEOUT:-300}
+maximum_steps=${XCAT_ACTION_MAX_STEPS:-0}
+failure_code=ACTION_FAILED
+failure_detail='Genesis action failed'
+failure_recovery='Review Genesis logs and the assigned xCAT action'
+current_action=
+current_target=
+last_verified_seconds=
+
+[[ "$poll_seconds" =~ ^[0-9]+$ ]] || poll_seconds=15
+[[ "$request_timeout" =~ ^[1-9][0-9]*$ ]] || request_timeout=20
+[[ "$operation_timeout" =~ ^[1-9][0-9]*$ ]] || operation_timeout=300
+[[ "$maximum_steps" =~ ^[0-9]+$ ]] || maximum_steps=0
+
+publish_status() {
+ local state=$1 detail=$2
+ local -a fields
+ shift 2
+
+ fields=("ACTION=$current_action" "TARGET=$current_target")
+ [[ -z $last_verified_seconds ]] \
+ || fields+=("VERIFIED_SECONDS=$last_verified_seconds")
+ genesis_publish_status "$status_command" action xcat-genesis-action \
+ "$state" "$detail" "${fields[@]}" "$@"
+}
+
+# shellcheck disable=SC2329
+finish_status() {
+ local result=$?
+
+ trap - EXIT
+ if ((result != 0)); then
+ publish_status FAILED "$failure_detail" \
+ "CODE=$failure_code" "RECOVERY=$failure_recovery"
+ fi
+ exit "$result"
+}
+
+fail_action() {
+ failure_code=$1
+ failure_detail=$2
+ failure_recovery=$3
+ logger -t xcat-genesis-action -- "$failure_detail"
+ exit 1
+}
+
+parse_destiny() {
+ local raw=$1
+
+ if ! genesis_parse_destiny "$raw"; then
+ fail_action INVALID_XCAT_ACTION \
+ 'xCAT returned an invalid action' \
+ 'Correct the assigned node destiny'
+ fi
+ current_action=$XCAT_GENESIS_ACTION
+ current_target=$XCAT_GENESIS_TARGET
+}
+
+read_destiny() {
+ local raw
+
+ [[ -r $destiny_file ]] || fail_action XCAT_ACTION_MISSING \
+ 'No xCAT action is available' \
+ 'Retry registration or assign a node destiny'
+ IFS= read -r raw <"$destiny_file" || true
+ [[ -n $raw && ${#raw} -le 4096 ]] || fail_action XCAT_ACTION_INVALID \
+ 'The assigned xCAT action is empty or too large' \
+ 'Correct the assigned node destiny'
+ parse_destiny "$raw"
+}
+
+store_destiny() {
+ local raw=$1 temporary_file
+
+ [[ -n $raw && ${#raw} -le 4096 ]] || return 1
+ umask 077
+ temporary_file=${destiny_file}.new
+ printf '%s\n' "$raw" >"$temporary_file"
+ mv "$temporary_file" "$destiny_file"
+ parse_destiny "$raw"
+}
+
+request_current_action() {
+ local action
+
+ if ! action=$(timeout "$request_timeout" "$getdestiny_command" \
+ "$XCATDEST" --metadata "$metadata_file" --once); then
+ return 1
+ fi
+ store_destiny "$action" || return 1
+ last_verified_seconds=$(genesis_uptime_seconds)
+}
+
+request_next_action() {
+ local action
+
+ if ! action=$(timeout "$request_timeout" "$nextdestiny_command" \
+ "$XCATDEST" --metadata "$metadata_file" --once); then
+ return 1
+ fi
+ store_destiny "$action" || return 1
+ last_verified_seconds=$(genesis_uptime_seconds)
+}
+
+run_approved_command() {
+ local command_name argument_text executor
+ local -a arguments=()
+
+ command_name=${current_target%%[[:space:]]*}
+ [[ $command_name =~ ^[a-z][a-z0-9._-]{0,63}$ ]] \
+ || fail_action ACTION_COMMAND_INVALID \
+ 'xCAT assigned an invalid command name' \
+ 'Use an action packaged in the Genesis image'
+ executor=$approved_command_dir/$command_name
+ [[ -f $executor && ! -L $executor && -x $executor ]] \
+ || fail_action ACTION_COMMAND_NOT_APPROVED \
+ "The command is not approved by this Genesis image: $command_name" \
+ 'Use a packaged Genesis action or signed system extension'
+ argument_text=${current_target#"$command_name"}
+ argument_text=${argument_text#"${argument_text%%[![:space:]]*}"}
+ if [[ -n $argument_text ]]; then
+ read -r -a arguments <<<"$argument_text"
+ fi
+ publish_status RUNNING "Running approved action $command_name"
+ timeout "$operation_timeout" "$executor" "${arguments[@]}" \
+ || fail_action ACTION_COMMAND_FAILED \
+ "The approved action failed: $command_name" \
+ 'Review Genesis logs and the action parameters'
+}
+
+prepare_network_boot() {
+ if ipmitool mc info >/dev/null 2>&1; then
+ ipmitool chassis bootdev pxe >/dev/null 2>&1 \
+ || logger -t xcat-genesis-action -- \
+ 'unable to set the in-band PXE boot override'
+ fi
+}
+
+trap finish_status EXIT HUP INT TERM
+
+[[ -r $network_file ]] || fail_action ACTION_NETWORK_STATE_MISSING \
+ 'Management network state is unavailable' \
+ 'Wait for Genesis networking or review its failure'
+# shellcheck disable=SC1090
+source "$network_file"
+: "${XCATDEST:?xCAT endpoint is missing from network state}"
+read_destiny
+if [[ $current_action != discover && ! -s $certificate_file ]]; then
+ publish_status RUNNING 'Requesting the node certificate'
+ timeout "$operation_timeout" "$certificate_command" \
+ || fail_action CERTIFICATE_ACTION_FAILED \
+ 'The node could not obtain an xCAT certificate' \
+ 'Check callback port 300 and xCAT credentials policy'
+fi
+
+for ((step = 1; ; step++)); do
+ if ((maximum_steps > 0 && step > maximum_steps)); then
+ exit 0
+ fi
+
+ case "$current_action" in
+ discover)
+ publish_status RUNNING 'Discovering node hardware'
+ timeout "$operation_timeout" "$discover_command" \
+ || fail_action DISCOVERY_ACTION_FAILED \
+ 'xCAT did not accept the node discovery' \
+ 'Review active discovery methods and node matching'
+ publish_status RUNNING 'Loading the discovered node assignment'
+ request_current_action \
+ || fail_action DISCOVERY_ASSIGNMENT_FAILED \
+ 'The discovered node has no xCAT assignment' \
+ 'Review the discovered node definition and chain'
+ timeout "$operation_timeout" "$certificate_command" \
+ || fail_action DISCOVERY_CERTIFICATE_FAILED \
+ 'The discovered node could not obtain a certificate' \
+ 'Check callback port 300 and xCAT credentials policy'
+ ;;
+ standby|shell|offline)
+ publish_status IDLE 'Waiting for an action from xCAT' \
+ "NEXT_RETRY_SECONDS=$poll_seconds"
+ sleep "$poll_seconds"
+ if ! request_current_action; then
+ publish_status DEGRADED 'xCAT did not answer the action poll' \
+ 'CODE=XCAT_ACTION_POLL_FAILED' \
+ 'RECOVERY=Check xcatd and the management network' \
+ "NEXT_RETRY_SECONDS=$poll_seconds"
+ fi
+ ;;
+ osimage|ondiscover)
+ publish_status RUNNING 'Advancing the xCAT action chain'
+ request_next_action \
+ || fail_action XCAT_CHAIN_ADVANCE_FAILED \
+ 'xCAT did not advance the node action chain' \
+ 'Review the node chain and xcatd logs'
+ ;;
+ runcmd)
+ run_approved_command
+ request_next_action \
+ || fail_action XCAT_CHAIN_ADVANCE_FAILED \
+ 'xCAT did not advance after the approved action' \
+ 'Review the node chain and xcatd logs'
+ ;;
+ runimage)
+ fail_action UNSAFE_LEGACY_ACTION \
+ 'Unsigned runimage actions are not supported' \
+ 'Package the operation as a signed Genesis system extension'
+ ;;
+ configraid)
+ fail_action LEGACY_STORAGE_ACTION \
+ 'Legacy configraid actions are not supported' \
+ 'Assign a validated Genesis hardware-provider task'
+ ;;
+ sysclone)
+ fail_action LEGACY_SYSCLONE_ACTION \
+ 'Legacy sysclone actions are not supported' \
+ 'Use an xCAT image workflow supported by this Genesis release'
+ ;;
+ boot|reboot)
+ publish_status RUNNING 'Advancing the chain before reboot'
+ request_next_action \
+ || fail_action XCAT_CHAIN_ADVANCE_FAILED \
+ 'xCAT did not advance the node action chain' \
+ 'Review the node chain and xcatd logs'
+ prepare_network_boot
+ publish_status RUNNING 'Rebooting into the assigned action'
+ "$reboot_command" reboot
+ exit 0
+ ;;
+ install|netboot|statelite)
+ prepare_network_boot
+ publish_status RUNNING 'Rebooting into the assigned image'
+ "$reboot_command" reboot
+ exit 0
+ ;;
+ shutdown)
+ publish_status RUNNING 'Powering off at xCAT request'
+ "$poweroff_command" poweroff
+ exit 0
+ ;;
+ error)
+ fail_action XCAT_ACTION_ERROR \
+ "xCAT rejected the action: ${current_target:-no detail}" \
+ 'Correct the node destiny or action chain'
+ ;;
+ *)
+ fail_action XCAT_ACTION_UNSUPPORTED \
+ "Unsupported xCAT action: $current_action" \
+ 'Assign an action supported by this Genesis release'
+ ;;
+ esac
+done
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-functions b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-functions
new file mode 100644
index 000000000..97819475d
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-functions
@@ -0,0 +1,53 @@
+# Shared Bash helpers for the Genesis runtime.
+
+genesis_publish_status() {
+ local status_command=$1 component=$2 log_tag=$3
+ shift 3
+
+ "$status_command" "$component" "$@" \
+ || logger -t "$log_tag" -- "unable to publish $component status"
+}
+
+genesis_uptime_seconds() {
+ local uptime_file=${1:-${XCAT_UPTIME_FILE:-/proc/uptime}}
+ local seconds=0
+
+ if [[ -r $uptime_file ]]; then
+ read -r seconds _ <"$uptime_file" || seconds=0
+ seconds=${seconds%%.*}
+ [[ $seconds =~ ^[0-9]+$ ]] || seconds=0
+ fi
+ printf '%s\n' "$seconds"
+}
+
+genesis_parse_destiny() {
+ local raw=$1 prefix
+
+ XCAT_GENESIS_ACTION=
+ XCAT_GENESIS_TARGET=
+ [[ -n $raw && ${#raw} -le 4096 ]] || return 1
+
+ prefix=${raw%%=*}
+ XCAT_GENESIS_ACTION=${prefix%%[[:space:]]*}
+ if [[ $raw = *=* ]]; then
+ XCAT_GENESIS_TARGET=${raw#*=}
+ elif [[ $raw = *[[:space:]]* ]]; then
+ XCAT_GENESIS_TARGET=${raw#"$XCAT_GENESIS_ACTION"}
+ XCAT_GENESIS_TARGET=${XCAT_GENESIS_TARGET#"${XCAT_GENESIS_TARGET%%[![:space:]]*}"}
+ fi
+
+ [[ $XCAT_GENESIS_ACTION =~ ^[a-z][a-z0-9_-]*$ ]]
+}
+
+genesis_component_state() {
+ local status_dir=$1 component=$2 key value
+
+ [[ -r $status_dir/$component.env ]] || return 1
+ while IFS='=' read -r key value; do
+ if [[ $key == STATE ]]; then
+ printf '%s\n' "$value"
+ return 0
+ fi
+ done <"$status_dir/$component.env"
+ return 1
+}
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-maintenance-shell b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-maintenance-shell
new file mode 100644
index 000000000..396b50593
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-maintenance-shell
@@ -0,0 +1,39 @@
+#!/bin/bash
+set -u
+
+functions_file=${XCAT_GENESIS_FUNCTIONS:-/usr/libexec/xcat/genesis-functions}
+# shellcheck source=genesis-functions
+source "$functions_file"
+
+status_dir=${XCAT_STATUS_DIR:-/run/xcat/status}
+
+status=STARTING
+for component in network extensions registration; do
+ component_state=$(genesis_component_state "$status_dir" "$component") \
+ || continue
+ if [[ ${component_state} == FAILED ]]; then
+ status=${component_state}
+ break
+ fi
+done
+if [[ ${status} != FAILED ]]; then
+ for component in action registration network; do
+ component_state=$(genesis_component_state "$status_dir" "$component") \
+ || continue
+ status=${component_state}
+ break
+ done
+fi
+
+node=unknown
+[[ -r /proc/sys/kernel/hostname ]] && read -r node &2
+ exit 1
+}
+
+case "$#" in
+ 0) output_file= ;;
+ 2)
+ [ "$1" = '--output' ] || fail 'usage: genesis-metrics [--output FILE]'
+ output_file=$2
+ ;;
+ *) fail 'usage: genesis-metrics [--output FILE]' ;;
+esac
+
+schema=
+registration_state=
+boot_ready_seconds=
+while IFS='=' read -r name value; do
+ case "$name" in
+ SCHEMA) schema=$value ;;
+ STATE) registration_state=$value ;;
+ UPDATED_SECONDS) boot_ready_seconds=$value ;;
+ esac
+done <"$registration_file"
+
+[ "$schema" = 1 ] && [ "$registration_state" = ACTION_RECEIVED ] \
+ || fail 'Genesis has not received an xCAT action'
+case "$boot_ready_seconds" in
+ ''|*[!0-9]*) fail 'registration time is invalid' ;;
+esac
+
+read -r capture_uptime_seconds _ <"$uptime_file"
+capture_uptime_seconds=${capture_uptime_seconds%%.*}
+case "$capture_uptime_seconds" in
+ ''|*[!0-9]*) fail 'uptime is invalid' ;;
+esac
+
+memory_total_kib=
+memory_available_kib=
+while read -r name value _; do
+ case "$name" in
+ MemTotal:) memory_total_kib=$value ;;
+ MemAvailable:) memory_available_kib=$value ;;
+ esac
+done <"$meminfo_file"
+case "$memory_total_kib" in
+ ''|*[!0-9]*) fail 'total memory is invalid' ;;
+esac
+case "$memory_available_kib" in
+ ''|*[!0-9]*) fail 'available memory is invalid' ;;
+esac
+[ "$memory_available_kib" -le "$memory_total_kib" ] \
+ || fail 'available memory exceeds total memory'
+memory_used_kib=$((memory_total_kib - memory_available_kib))
+
+write_metrics() {
+ printf 'SCHEMA=1\n'
+ printf 'BOOT_READY_SECONDS=%s\n' "$boot_ready_seconds"
+ printf 'CAPTURE_UPTIME_SECONDS=%s\n' "$capture_uptime_seconds"
+ printf 'MEMORY_TOTAL_KIB=%s\n' "$memory_total_kib"
+ printf 'MEMORY_AVAILABLE_KIB=%s\n' "$memory_available_kib"
+ printf 'MEMORY_USED_KIB=%s\n' "$memory_used_kib"
+}
+
+if [ -z "$output_file" ]; then
+ write_metrics
+ exit 0
+fi
+
+case "$output_file" in
+ */*) output_dir=${output_file%/*}; [ -n "$output_dir" ] || output_dir=/ ;;
+ *) output_dir=. ;;
+esac
+install -d -m 0755 "$output_dir"
+temporary_file=$(mktemp "$output_dir/.metrics.XXXXXX")
+trap 'rm -f -- "$temporary_file"' EXIT HUP INT TERM
+write_metrics >"$temporary_file"
+chmod 0644 "$temporary_file"
+mv -f -- "$temporary_file" "$output_file"
+trap - EXIT HUP INT TERM
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-network-refresh b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-network-refresh
new file mode 100644
index 000000000..b7ec0a0b5
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-network-refresh
@@ -0,0 +1,55 @@
+#!/bin/bash
+set -euo pipefail
+
+state_dir=${XCAT_STATE_DIR:-/run/xcat}
+network_file=${XCAT_NETWORK_FILE:-$state_dir/genesis.env}
+network_state_command=${XCAT_NETWORK_STATE_COMMAND:-/usr/libexec/xcat/genesis-network-state}
+sys_class_net=${XCAT_SYS_CLASS_NET:-/sys/class/net}
+callback=${1:-restart}
+
+[[ -r $network_file ]] || exit 1
+# shellcheck disable=SC1090
+source "$network_file"
+
+[[ ${XCAT_NETWORK_METHOD:-auto} = auto ]] || exit 0
+: "${XCAT_INTERFACE:?management interface is missing from network state}"
+
+declare -a requested_interfaces=()
+if [[ $callback = 'restart ('*')' ]]; then
+ requested=${callback#restart (}
+ requested=${requested%)}
+ IFS='|' read -r -a requested_interfaces <<<"$requested"
+fi
+if ((${#requested_interfaces[@]} == 0)); then
+ requested_interfaces=("$XCAT_INTERFACE")
+fi
+
+for interface in "${requested_interfaces[@]}"; do
+ [[ $interface =~ ^[A-Za-z0-9_.-]+$ && -e $sys_class_net/$interface ]] \
+ || exit 1
+done
+
+if [[ $callback = 'restart ('*')' ]]; then
+ while IFS=: read -r interface device_type; do
+ [[ $device_type = ethernet ]] || continue
+ keep=0
+ for requested_interface in "${requested_interfaces[@]}"; do
+ if [[ $interface = "$requested_interface" ]]; then
+ keep=1
+ break
+ fi
+ done
+ ((keep == 1)) \
+ || nmcli --wait 10 device disconnect "$interface" >/dev/null 2>&1 \
+ || true
+ done < <(nmcli -t -f DEVICE,TYPE device status 2>/dev/null || true)
+fi
+
+for interface in "${requested_interfaces[@]}"; do
+ nmcli --wait 10 device disconnect "$interface" >/dev/null 2>&1 || true
+done
+for interface in "${requested_interfaces[@]}"; do
+ nmcli --wait 30 device connect "$interface" >/dev/null
+done
+
+"$network_state_command"
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-network-state b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-network-state
new file mode 100644
index 000000000..cd6a8f10f
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-network-state
@@ -0,0 +1,327 @@
+#!/bin/bash
+set -eu
+
+functions_file=${XCAT_GENESIS_FUNCTIONS:-/usr/libexec/xcat/genesis-functions}
+# shellcheck source=genesis-functions
+source "$functions_file"
+
+log() {
+ logger -t xcat-genesis-network-state -- "$*"
+}
+
+status_command=${XCAT_STATUS_COMMAND:-/usr/libexec/xcat/genesis-status}
+failure_code=NETWORK_CONFIGURATION_FAILED
+failure_detail='Network configuration failed'
+failure_recovery='Check the management network and boot parameters'
+
+publish_status() {
+ genesis_publish_status "$status_command" network \
+ xcat-genesis-network-state "$@"
+}
+
+finish_status() {
+ local result=$?
+
+ trap - EXIT
+ if [ "$result" -ne 0 ]; then
+ publish_status FAILED "$failure_detail" \
+ "CODE=$failure_code" "RECOVERY=$failure_recovery"
+ fi
+ exit "$result"
+}
+
+fail_network() {
+ failure_code=$1
+ failure_detail=$2
+ failure_recovery=$3
+ log "$failure_detail"
+ exit 1
+}
+
+trap finish_status EXIT
+publish_status CONFIGURING_NETWORK 'Selecting the management network'
+
+tcp_probe() {
+ local probe_pid probe_wait
+
+ ( exec 3<>"/dev/tcp/$master/$port" ) 2>/dev/null &
+ probe_pid=$!
+ for ((probe_wait = 0; probe_wait < 10; probe_wait++)); do
+ if ! kill -0 "$probe_pid" 2>/dev/null; then
+ wait "$probe_pid"
+ return $?
+ fi
+ sleep 0.1
+ done
+ kill "$probe_pid" 2>/dev/null || true
+ wait "$probe_pid" 2>/dev/null || true
+ return 1
+}
+
+netmask_to_prefix() {
+ local mask=$1 octet prefix=0 partial=0
+ local -a octets
+
+ IFS=. read -r -a octets <<<"$mask"
+ [ "${#octets[@]}" -eq 4 ] || return 1
+
+ for octet in "${octets[@]}"; do
+ [ "$partial" -eq 0 ] || [ "$octet" = 0 ] || return 1
+ case "$octet" in
+ 255) prefix=$((prefix + 8)) ;;
+ 254) prefix=$((prefix + 7)); partial=1 ;;
+ 252) prefix=$((prefix + 6)); partial=1 ;;
+ 248) prefix=$((prefix + 5)); partial=1 ;;
+ 240) prefix=$((prefix + 4)); partial=1 ;;
+ 224) prefix=$((prefix + 3)); partial=1 ;;
+ 192) prefix=$((prefix + 2)); partial=1 ;;
+ 128) prefix=$((prefix + 1)); partial=1 ;;
+ 0) partial=1 ;;
+ *) return 1 ;;
+ esac
+ done
+
+ printf '%s\n' "$prefix"
+}
+
+destination=
+boot_mac=
+host_ip=
+netmask=
+gateway=
+cmdline_file=${XCAT_CMDLINE_FILE:-/proc/cmdline}
+sys_class_net=${XCAT_SYS_CLASS_NET:-/sys/class/net}
+state_dir=${XCAT_STATE_DIR:-/run/xcat}
+uptime_file=${XCAT_UPTIME_FILE:-/proc/uptime}
+declare -a cmdline_arguments
+
+read -r -a cmdline_arguments <"$cmdline_file"
+for argument in "${cmdline_arguments[@]}"; do
+ case "$argument" in
+ xcatd=*) destination=${argument#xcatd=} ;;
+ BOOTIF=*) boot_mac=${argument#BOOTIF=} ;;
+ hostip=*|ipaddr=*) host_ip=${argument#*=} ;;
+ netmask=*) netmask=${argument#netmask=} ;;
+ gateway=*) gateway=${argument#gateway=} ;;
+ esac
+done
+
+[ -n "$destination" ] || fail_network INVALID_BOOT_PARAMETERS \
+ 'xcatd is missing from the kernel command line' \
+ 'Add xcatd=
: to the kernel command line'
+
+if [[ "$destination" =~ ^\[([^]]+)\]:([0-9]+)$ ]]; then
+ master=${BASH_REMATCH[1]}
+ port=${BASH_REMATCH[2]}
+elif [[ "$destination" =~ ^([^:]+):([0-9]+)$ ]]; then
+ master=${BASH_REMATCH[1]}
+ port=${BASH_REMATCH[2]}
+elif [[ "$destination" = *:* ]]; then
+ master=$destination
+ port=3001
+ destination="[$master]:$port"
+else
+ master=$destination
+ port=3001
+ destination="$master:$port"
+fi
+
+[[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ] || {
+ fail_network INVALID_XCAT_ENDPOINT "Invalid xcatd port: $port" \
+ 'Correct the xcatd endpoint in the boot parameters'
+}
+
+preferred_interface=
+if [ -n "$boot_mac" ]; then
+ boot_mac=${boot_mac#01-}
+ boot_mac=$(printf '%s' "$boot_mac" | tr 'A-Z-' 'a-z:')
+ for address_file in "$sys_class_net"/*/address; do
+ [ -r "$address_file" ] || continue
+ if [ "$(<"$address_file")" = "$boot_mac" ]; then
+ preferred_interface=${address_file%/address}
+ preferred_interface=${preferred_interface##*/}
+ break
+ fi
+ done
+ [ -n "$preferred_interface" ] || fail_network BOOT_INTERFACE_NOT_FOUND \
+ "BOOTIF device not found: $boot_mac" \
+ 'Check the PXE interface and BOOTIF value'
+fi
+
+network_method=auto
+if [ -n "$host_ip" ] || [ -n "$netmask" ] || [ -n "$gateway" ]; then
+ network_method=static
+ [ -n "$host_ip" ] && [ -n "$netmask" ] && [ -n "$gateway" ] \
+ || fail_network INVALID_STATIC_NETWORK \
+ 'Static networking requires hostip, netmask, and gateway' \
+ 'Supply all static network boot parameters or remove them'
+ [ -n "$preferred_interface" ] || fail_network INVALID_STATIC_NETWORK \
+ 'Static networking requires BOOTIF' \
+ 'Add BOOTIF for the management interface'
+
+ if [[ "$host_ip" = */* ]]; then
+ address=$host_ip
+ elif [[ "$netmask" =~ ^[0-9]+$ ]]; then
+ address="$host_ip/$netmask"
+ elif [[ "$host_ip" = *:* ]]; then
+ fail_network INVALID_STATIC_NETWORK \
+ "Invalid IPv6 prefix length: $netmask" \
+ 'Use an IPv6 prefix length from 0 to 128'
+ else
+ prefix=$(netmask_to_prefix "$netmask") \
+ || fail_network INVALID_STATIC_NETWORK \
+ "Invalid IPv4 netmask: $netmask" \
+ 'Use a valid dotted netmask or prefix length'
+ address="$host_ip/$prefix"
+ fi
+
+ nmcli connection delete xcat-genesis-static >/dev/null 2>&1 || true
+ if [[ "$host_ip" = *:* ]]; then
+ network_arguments=(
+ ipv4.method disabled
+ ipv6.method manual
+ ipv6.addresses "$address"
+ ipv6.gateway "$gateway"
+ )
+ else
+ network_arguments=(
+ ipv4.method manual
+ ipv4.addresses "$address"
+ ipv4.gateway "$gateway"
+ ipv6.method auto
+ )
+ fi
+ if ! nmcli connection add type ethernet ifname "$preferred_interface" \
+ con-name xcat-genesis-static "${network_arguments[@]}" \
+ connection.autoconnect yes >/dev/null; then
+ fail_network STATIC_NETWORK_REJECTED \
+ "NetworkManager rejected the static configuration on $preferred_interface" \
+ 'Check the address, gateway, and interface'
+ fi
+ if ! nmcli --wait 20 connection up xcat-genesis-static >/dev/null; then
+ fail_network STATIC_NETWORK_FAILED \
+ "Static network activation failed on $preferred_interface" \
+ 'Check link state and the static network boot parameters'
+ fi
+fi
+
+selected_interface=
+source_address=
+publish_status WAITING_FOR_LINK "Waiting for a route to $destination" \
+ 'ATTEMPT=1' 'ATTEMPT_LIMIT=60'
+for ((attempt = 0; attempt < 60; attempt++)); do
+ if [[ "$master" = *:* ]]; then
+ route=$(ip -6 -o route get "$master" 2>/dev/null || true)
+ elif [[ "$master" =~ ^[0-9]+(\.[0-9]+){3}$ ]]; then
+ route=$(ip -4 -o route get "$master" 2>/dev/null || true)
+ else
+ route=$(ip -4 -o route get "$master" 2>/dev/null \
+ || ip -6 -o route get "$master" 2>/dev/null || true)
+ fi
+ route_interface=$(awk '{for (i=1; i<=NF; i++) if ($i == "dev") print $(i+1)}' <<<"$route")
+ route_source=$(awk '{for (i=1; i<=NF; i++) if ($i == "src") print $(i+1)}' <<<"$route")
+
+ if [ -n "$route_interface" ]; then
+ state=$(nmcli -g GENERAL.STATE device show "$route_interface" 2>/dev/null || true)
+ if [[ "$state" = 100* ]]; then
+ if [ -z "$preferred_interface" ] || \
+ [ "$route_interface" = "$preferred_interface" ] || \
+ [ "$attempt" -ge 20 ]; then
+ selected_interface=$route_interface
+ source_address=$route_source
+ break
+ fi
+ fi
+ fi
+ publish_status WAITING_FOR_LINK "Waiting for a route to $destination" \
+ "ATTEMPT=$((attempt + 1))" 'ATTEMPT_LIMIT=60' \
+ 'NEXT_RETRY_SECONDS=1'
+ sleep 1
+done
+
+[ -n "$selected_interface" ] || fail_network MANAGEMENT_ROUTE_UNAVAILABLE \
+ "No interface can route to $master" \
+ 'Check link, DHCP or static settings, VLANs, and routing'
+
+master_reachable=0
+publish_status CONTACTING_XCAT "Contacting xCAT at $destination" \
+ 'ATTEMPT=1' 'ATTEMPT_LIMIT=10'
+for ((attempt = 0; attempt < 10; attempt++)); do
+ if tcp_probe; then
+ master_reachable=1
+ break
+ fi
+ publish_status CONTACTING_XCAT "Contacting xCAT at $destination" \
+ "ATTEMPT=$((attempt + 1))" 'ATTEMPT_LIMIT=10' \
+ 'NEXT_RETRY_SECONDS=1'
+ sleep 1
+done
+
+[ "$master_reachable" -eq 1 ] || fail_network XCAT_ENDPOINT_UNREACHABLE \
+ "Cannot connect to $destination" \
+ 'Check xcatd, firewalls, routing, and the configured port'
+
+route_gateway=$(awk '{for (i=1; i<=NF; i++) if ($i == "via") print $(i+1)}' <<<"$route")
+source_prefixed_address=
+if [[ "$source_address" = *:* ]]; then
+ address_family=-6
+else
+ address_family=-4
+fi
+while read -r candidate; do
+ if [ "${candidate%%/*}" = "$source_address" ]; then
+ source_prefixed_address=$candidate
+ break
+ fi
+done < <(ip "$address_family" -o address show dev "$selected_interface" \
+ scope global 2>/dev/null | awk '{print $4}')
+[ -n "$source_prefixed_address" ] || source_prefixed_address=$source_address
+
+actual_gateway=${route_gateway:-$gateway}
+dns_servers=
+while IFS= read -r dns_line; do
+ IFS='|' read -r -a dns_candidates <<<"$dns_line"
+ for dns_server in "${dns_candidates[@]}"; do
+ dns_server=${dns_server//[[:space:]]/}
+ [ -n "$dns_server" ] || continue
+ if [ -n "$dns_servers" ]; then
+ dns_servers="$dns_servers,$dns_server"
+ else
+ dns_servers=$dns_server
+ fi
+ done
+done < <(nmcli --terse --escape no -g IP4.DNS,IP6.DNS \
+ device show "$selected_interface" \
+ 2>/dev/null || true)
+link_state=unknown
+[ ! -r "$sys_class_net/$selected_interface/operstate" ] \
+ || read -r link_state <"$sys_class_net/$selected_interface/operstate"
+mac_address=
+[ ! -r "$sys_class_net/$selected_interface/address" ] \
+ || read -r mac_address <"$sys_class_net/$selected_interface/address"
+verified_seconds=$(genesis_uptime_seconds "$uptime_file")
+
+mkdir -p "$state_dir"
+chmod 0755 "$state_dir"
+umask 077
+state_file=$state_dir/genesis.env
+temporary_file=${state_file}.new
+{
+ printf 'XCATDEST=%s\n' "$destination"
+ printf 'XCATMASTER=%s\n' "$master"
+ printf 'XCATPORT=%s\n' "$port"
+ printf 'XCAT_INTERFACE=%s\n' "$selected_interface"
+ printf 'XCAT_SOURCE_ADDRESS=%s\n' "$source_address"
+ printf 'XCAT_SOURCE_PREFIXED_ADDRESS=%s\n' "$source_prefixed_address"
+ printf 'XCAT_GATEWAY=%s\n' "$actual_gateway"
+ printf 'XCAT_DNS_SERVERS=%s\n' "$dns_servers"
+ printf 'XCAT_NETWORK_METHOD=%s\n' "$network_method"
+ printf 'XCAT_LINK_STATE=%s\n' "$link_state"
+ printf 'XCAT_MAC_ADDRESS=%s\n' "$mac_address"
+ printf 'XCAT_VERIFIED_SECONDS=%s\n' "$verified_seconds"
+} >"$temporary_file"
+mv "$temporary_file" "$state_file"
+
+publish_status READY "Management network ready on $selected_interface" \
+ "VERIFIED_SECONDS=$verified_seconds"
+log "using $selected_interface to reach $destination"
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-register b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-register
new file mode 100644
index 000000000..3de7dc2d3
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-register
@@ -0,0 +1,120 @@
+#!/bin/bash
+set -eu
+
+functions_file=${XCAT_GENESIS_FUNCTIONS:-/usr/libexec/xcat/genesis-functions}
+# shellcheck source=genesis-functions
+source "$functions_file"
+
+state_dir=${XCAT_STATE_DIR:-/run/xcat}
+cmdline_file=${XCAT_CMDLINE_FILE:-/proc/cmdline}
+status_command=${XCAT_STATUS_COMMAND:-/usr/libexec/xcat/genesis-status}
+metadata_file=$state_dir/xcat-response.env
+kernel_destiny=
+failure_code=XCAT_REGISTRATION_FAILED
+failure_detail='xCAT registration failed'
+failure_recovery='Check xcatd, certificates, and the management network'
+maximum_attempts=${XCAT_REGISTRATION_ATTEMPTS:-6}
+retry_seconds=${XCAT_REGISTRATION_RETRY_SECONDS:-5}
+request_timeout=${XCAT_REGISTRATION_REQUEST_TIMEOUT:-10}
+declare -a cmdline_arguments
+
+[[ "$maximum_attempts" =~ ^[1-9][0-9]*$ ]] \
+ || maximum_attempts=6
+[[ "$retry_seconds" =~ ^[0-9]+$ ]] \
+ || retry_seconds=5
+[[ "$request_timeout" =~ ^[1-9][0-9]*$ ]] \
+ || request_timeout=10
+
+publish_status() {
+ genesis_publish_status "$status_command" registration \
+ xcat-genesis-register "$@"
+}
+
+finish_status() {
+ local result=$?
+
+ trap - EXIT
+ if [ "$result" -ne 0 ]; then
+ publish_status FAILED "$failure_detail" \
+ "CODE=$failure_code" "RECOVERY=$failure_recovery"
+ fi
+ exit "$result"
+}
+
+trap finish_status EXIT
+
+read -r -a cmdline_arguments <"$cmdline_file"
+for argument in "${cmdline_arguments[@]}"; do
+ case "$argument" in
+ destiny=*) kernel_destiny=${argument#destiny=} ;;
+ esac
+done
+
+selected_destiny=
+for ((attempt = 1; attempt <= maximum_attempts; attempt++)); do
+ publish_status CONTACTING_XCAT 'Requesting an action from xCAT' \
+ "ATTEMPT=$attempt" "ATTEMPT_LIMIT=$maximum_attempts"
+ if [ -n "$kernel_destiny" ]; then
+ if timeout "$request_timeout" getdestiny "$XCATDEST" \
+ --metadata "$metadata_file" --once >/dev/null; then
+ selected_destiny=$kernel_destiny
+ break
+ fi
+ elif selected_destiny=$(timeout "$request_timeout" getdestiny "$XCATDEST" \
+ --metadata "$metadata_file" --once); then
+ break
+ fi
+ selected_destiny=
+ if [ "$attempt" -lt "$maximum_attempts" ]; then
+ publish_status CONTACTING_XCAT 'xCAT has not answered yet' \
+ "ATTEMPT=$attempt" "ATTEMPT_LIMIT=$maximum_attempts" \
+ "NEXT_RETRY_SECONDS=$retry_seconds"
+ sleep "$retry_seconds"
+ fi
+done
+
+[ -n "$selected_destiny" ] || {
+ failure_code=XCAT_RESPONSE_UNAVAILABLE
+ failure_detail="No valid response from xCAT after $maximum_attempts attempts"
+ failure_recovery='Check xcatd, certificates, and the assigned node action'
+ logger -t xcat-genesis-register -- "$failure_detail"
+ exit 1
+}
+
+genesis_parse_destiny "$selected_destiny" || {
+ failure_code=INVALID_XCAT_ACTION
+ failure_detail='xCAT returned an invalid action'
+ failure_recovery='Correct the node destiny on the xCAT management node'
+ logger -t xcat-genesis-register -- "$failure_detail"
+ exit 1
+}
+selected_action=$XCAT_GENESIS_ACTION
+selected_target=$XCAT_GENESIS_TARGET
+
+confirmed_node=
+if [ -r "$metadata_file" ]; then
+ while IFS='=' read -r name value; do
+ case "$name" in
+ XCAT_NODE_NAME) confirmed_node=$value ;;
+ esac
+ done <"$metadata_file"
+fi
+
+verified_seconds=$(genesis_uptime_seconds)
+
+umask 077
+temporary_file=$state_dir/destiny.new
+printf '%s\n' "$selected_destiny" >"$temporary_file"
+mv "$temporary_file" "$state_dir/destiny"
+
+declare -a status_fields
+status_fields=(
+ "VERIFIED_SECONDS=$verified_seconds"
+ "ACTION=$selected_action"
+ "TARGET=$selected_target"
+)
+[ -z "$confirmed_node" ] || status_fields+=("NODE_NAME=$confirmed_node")
+publish_status ACTION_RECEIVED "Action $selected_action received" \
+ "${status_fields[@]}"
+logger -t xcat-genesis-register -- \
+ "received action from $XCATDEST: $selected_action"
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-status b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-status
new file mode 100755
index 000000000..5ddd86f85
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/genesis-status
@@ -0,0 +1,110 @@
+#!/bin/sh
+set -eu
+
+LC_ALL=C
+export LC_ALL
+
+status_dir=${XCAT_STATUS_DIR:-/run/xcat/status}
+uptime_file=${XCAT_UPTIME_FILE:-/proc/uptime}
+
+fail() {
+ printf 'genesis-status: %s\n' "$*" >&2
+ exit 1
+}
+
+[ "$#" -ge 2 ] \
+ || fail 'usage: genesis-status COMPONENT STATE [DETAIL] [NAME=VALUE ...]'
+
+component=$1
+state=$2
+shift 2
+detail=${1:-}
+[ "$#" -eq 0 ] || shift
+
+case "$component" in
+ ''|*[!a-z0-9-]*) fail 'invalid component' ;;
+esac
+[ "${#component}" -le 32 ] || fail 'invalid component'
+
+case "$state" in
+ STARTING|IDLE|WAITING_FOR_LINK|CONFIGURING_NETWORK|CONTACTING_XCAT|ACTION_RECEIVED|RUNNING|READY|DEGRADED|FAILED) ;;
+ *) fail 'invalid state' ;;
+esac
+
+detail=$(printf '%s' "$detail" \
+ | tr '\r\n\t' ' ' \
+ | tr -cd '\040-\176' \
+ | cut -c 1-160)
+
+uptime=0
+if [ -r "$uptime_file" ]; then
+ read -r uptime _ <"$uptime_file" || uptime=0
+ uptime=${uptime%%.*}
+ case "$uptime" in
+ ''|*[!0-9]*) uptime=0 ;;
+ esac
+fi
+
+started_seconds=$uptime
+status_file=$status_dir/$component.env
+if [ -r "$status_file" ]; then
+ previous_schema=
+ previous_state=
+ previous_started=
+ while IFS='=' read -r name value; do
+ case "$name" in
+ SCHEMA) previous_schema=$value ;;
+ STATE) previous_state=$value ;;
+ STARTED_SECONDS) previous_started=$value ;;
+ esac
+ done <"$status_file"
+ case "$previous_started" in
+ ''|*[!0-9]*) ;;
+ *)
+ if [ "$previous_schema" = 1 ] && [ "$previous_state" = "$state" ]; then
+ started_seconds=$previous_started
+ fi
+ ;;
+ esac
+fi
+
+install -d -m 0755 "$status_dir"
+temporary_file=$(mktemp "$status_dir/.${component}.XXXXXX")
+trap 'rm -f -- "$temporary_file"' EXIT HUP INT TERM
+{
+ printf 'SCHEMA=1\n'
+ printf 'STATE=%s\n' "$state"
+ printf 'DETAIL=%s\n' "$detail"
+ printf 'STARTED_SECONDS=%s\n' "$started_seconds"
+ printf 'UPDATED_SECONDS=%s\n' "$uptime"
+ seen_fields=' '
+ for field in "$@"; do
+ case "$field" in
+ *=*) name=${field%%=*}; value=${field#*=} ;;
+ *) fail 'invalid field' ;;
+ esac
+ case "$name" in
+ CODE|RECOVERY|ATTEMPT|ATTEMPT_LIMIT|NEXT_RETRY_SECONDS|VERIFIED_SECONDS|NODE_NAME|ACTION|TARGET|PROGRESS_PERCENT) ;;
+ *) fail 'invalid field name' ;;
+ esac
+ case "$seen_fields" in
+ *" $name "*) fail 'duplicate field name' ;;
+ esac
+ seen_fields="$seen_fields$name "
+ value=$(printf '%s' "$value" \
+ | tr '\r\n\t' ' ' \
+ | tr -cd '\040-\176' \
+ | cut -c 1-240)
+ case "$name" in
+ ATTEMPT|ATTEMPT_LIMIT|NEXT_RETRY_SECONDS|VERIFIED_SECONDS|PROGRESS_PERCENT)
+ case "$value" in
+ ''|*[!0-9]*) fail 'invalid numeric field' ;;
+ esac
+ ;;
+ esac
+ printf '%s=%s\n' "$name" "$value"
+ done
+} >"$temporary_file"
+chmod 0644 "$temporary_file"
+mv -f -- "$temporary_file" "$status_dir/$component.env"
+trap - EXIT HUP INT TERM
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/xcat-genesis-action.service b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/xcat-genesis-action.service
new file mode 100644
index 000000000..bf30395c1
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/xcat-genesis-action.service
@@ -0,0 +1,16 @@
+[Unit]
+Description=Execute the assigned xCAT Genesis action
+Requires=xcat-genesis-register.service
+After=xcat-genesis-register.service
+StartLimitIntervalSec=0
+
+[Service]
+Type=simple
+EnvironmentFile=/run/xcat/genesis.env
+ExecStart=/usr/libexec/xcat/genesis-action
+Restart=on-failure
+RestartSec=10
+TimeoutStartSec=infinity
+
+[Install]
+WantedBy=multi-user.target
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/xcat-genesis-metrics.service b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/xcat-genesis-metrics.service
new file mode 100644
index 000000000..0185ee1fe
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/xcat-genesis-metrics.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=Record xCAT Genesis boot metrics
+ConditionKernelCommandLine=xcatd
+Requires=xcat-genesis-register.service
+After=xcat-genesis-register.service
+
+[Service]
+Type=oneshot
+ExecStart=/usr/libexec/xcat/genesis-metrics --output /run/xcat/metrics.env
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/xcat-genesis-network-ready.target b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/xcat-genesis-network-ready.target
new file mode 100644
index 000000000..c4412d622
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/xcat-genesis-network-ready.target
@@ -0,0 +1,8 @@
+[Unit]
+Description=xCAT Genesis management network is ready
+ConditionKernelCommandLine=xcatd
+Requires=xcat-genesis-network-state.service
+After=xcat-genesis-network-state.service
+
+[Install]
+WantedBy=multi-user.target
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/xcat-genesis-network-state.service b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/xcat-genesis-network-state.service
new file mode 100644
index 000000000..41f22e0ad
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/xcat-genesis-network-state.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=Select the xCAT Genesis management network
+ConditionKernelCommandLine=xcatd
+Wants=NetworkManager.service
+After=NetworkManager.service
+Before=xcat-genesis-network-ready.target
+
+[Service]
+Type=oneshot
+ExecStart=/usr/libexec/xcat/genesis-network-state
+RemainAfterExit=yes
+TimeoutStartSec=120
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/xcat-genesis-register.service b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/xcat-genesis-register.service
new file mode 100644
index 000000000..2e5c839c4
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/xcat-genesis-register.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=Register Genesis with xCAT
+ConditionKernelCommandLine=xcatd
+Requires=xcat-genesis-network-ready.target xcat-genesis-extensions.service
+After=xcat-genesis-network-ready.target xcat-genesis-extensions.service
+
+[Service]
+Type=oneshot
+EnvironmentFile=/run/xcat/genesis.env
+ExecStart=/usr/libexec/xcat/genesis-register
+RemainAfterExit=yes
+TimeoutStartSec=90
+
+[Install]
+WantedBy=multi-user.target
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/xcat-genesis-init_1.0.bb b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/xcat-genesis-init_1.0.bb
new file mode 100644
index 000000000..8d643a4a9
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/xcat-genesis-init_1.0.bb
@@ -0,0 +1,83 @@
+SUMMARY = "xCAT Genesis systemd units"
+LICENSE = "EPL-1.0"
+LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/EPL-1.0;md5=57f8d5e2b3e98ac6e088986c12bf94e6"
+
+SRC_URI = "file://00-xcat-genesis.preset \
+ file://genesis-functions \
+ file://genesis-maintenance-shell \
+ file://genesis-action \
+ file://genesis-metrics \
+ file://genesis-network-refresh \
+ file://genesis-network-state \
+ file://genesis-register \
+ file://genesis-status \
+ file://xcat-genesis-action.service \
+ file://xcat-genesis-metrics.service \
+ file://xcat-genesis-network-ready.target \
+ file://xcat-genesis-network-state.service \
+ file://xcat-genesis-register.service \
+"
+S = "${UNPACKDIR}"
+
+inherit systemd
+
+RDEPENDS:${PN} = "bash coreutils ipmitool iproute2 networkmanager-nmcli xcat-genesis-discovery xcat-genesis-protocol"
+
+SYSTEMD_SERVICE:${PN} = "xcat-genesis-action.service \
+ xcat-genesis-metrics.service \
+ xcat-genesis-network-ready.target \
+ xcat-genesis-register.service \
+"
+SYSTEMD_AUTO_ENABLE = "enable"
+
+do_install() {
+ install -d ${D}${libexecdir}/xcat
+ install -m 0644 ${UNPACKDIR}/genesis-functions \
+ ${D}${libexecdir}/xcat/genesis-functions
+ install -m 0755 ${UNPACKDIR}/genesis-maintenance-shell \
+ ${D}${libexecdir}/xcat/genesis-maintenance-shell
+ install -m 0755 ${UNPACKDIR}/genesis-action \
+ ${D}${libexecdir}/xcat/genesis-action
+ install -m 0755 ${UNPACKDIR}/genesis-metrics \
+ ${D}${libexecdir}/xcat/genesis-metrics
+ install -m 0755 ${UNPACKDIR}/genesis-network-refresh \
+ ${D}${libexecdir}/xcat/genesis-network-refresh
+ install -m 0755 ${UNPACKDIR}/genesis-network-state \
+ ${D}${libexecdir}/xcat/genesis-network-state
+ install -m 0755 ${UNPACKDIR}/genesis-register \
+ ${D}${libexecdir}/xcat/genesis-register
+ install -m 0755 ${UNPACKDIR}/genesis-status \
+ ${D}${libexecdir}/xcat/genesis-status
+
+ install -d ${D}${systemd_system_unitdir}
+ install -m 0644 ${UNPACKDIR}/xcat-genesis-action.service \
+ ${D}${systemd_system_unitdir}/xcat-genesis-action.service
+ install -m 0644 ${UNPACKDIR}/xcat-genesis-metrics.service \
+ ${D}${systemd_system_unitdir}/xcat-genesis-metrics.service
+ install -m 0644 ${UNPACKDIR}/xcat-genesis-network-ready.target \
+ ${D}${systemd_system_unitdir}/xcat-genesis-network-ready.target
+ install -m 0644 ${UNPACKDIR}/xcat-genesis-network-state.service \
+ ${D}${systemd_system_unitdir}/xcat-genesis-network-state.service
+ install -m 0644 ${UNPACKDIR}/xcat-genesis-register.service \
+ ${D}${systemd_system_unitdir}/xcat-genesis-register.service
+
+ install -d ${D}${systemd_unitdir}/system-preset
+ install -m 0644 ${UNPACKDIR}/00-xcat-genesis.preset \
+ ${D}${systemd_unitdir}/system-preset/00-xcat-genesis.preset
+}
+
+FILES:${PN} = "${libexecdir}/xcat/genesis-maintenance-shell \
+ ${libexecdir}/xcat/genesis-functions \
+ ${libexecdir}/xcat/genesis-action \
+ ${libexecdir}/xcat/genesis-metrics \
+ ${libexecdir}/xcat/genesis-network-refresh \
+ ${libexecdir}/xcat/genesis-network-state \
+ ${libexecdir}/xcat/genesis-register \
+ ${libexecdir}/xcat/genesis-status \
+ ${systemd_system_unitdir}/xcat-genesis-action.service \
+ ${systemd_system_unitdir}/xcat-genesis-metrics.service \
+ ${systemd_system_unitdir}/xcat-genesis-network-ready.target \
+ ${systemd_system_unitdir}/xcat-genesis-network-state.service \
+ ${systemd_system_unitdir}/xcat-genesis-register.service \
+ ${systemd_unitdir}/system-preset/00-xcat-genesis.preset \
+"
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/genesis-credential-callback b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/genesis-credential-callback
new file mode 100755
index 000000000..86d6cdebb
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/genesis-credential-callback
@@ -0,0 +1,12 @@
+#!/bin/bash
+set -eu
+
+request=
+IFS= read -r request || true
+if [[ $request != *CREDOKBYYOU?* ]]; then
+ logger -t xcat-genesis-credential-callback -- \
+ 'ignored an invalid credential check'
+ exit 1
+fi
+
+printf '%s\n' CREDOKBYME
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/genesis-discover b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/genesis-discover
new file mode 100755
index 000000000..28453d65f
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/genesis-discover
@@ -0,0 +1,317 @@
+#!/bin/bash
+set -euo pipefail
+
+state_dir=${XCAT_STATE_DIR:-/run/xcat}
+status_command=${XCAT_STATUS_COMMAND:-/usr/libexec/xcat/genesis-status}
+network_file=${XCAT_NETWORK_FILE:-$state_dir/genesis.env}
+response_file=${XCAT_DISCOVERY_RESPONSE_FILE:-$state_dir/discovery-response}
+network_refresh_command=${XCAT_NETWORK_REFRESH_COMMAND:-/usr/libexec/xcat/genesis-network-refresh}
+sys_root=${XCAT_SYS_ROOT:-}
+proc_root=${XCAT_PROC_ROOT:-/proc}
+key_dir=${XCAT_KEY_DIR:-/etc/xcat}
+maximum_attempts=${XCAT_DISCOVERY_ATTEMPTS:-36}
+retry_seconds=${XCAT_DISCOVERY_RETRY_SECONDS:-5}
+ipmitool_command=${XCAT_IPMITOOL_COMMAND:-ipmitool}
+lldp_command=${XCAT_LLDP_COMMAND:-lldpcli}
+failure_code=DISCOVERY_FAILED
+failure_detail='Node discovery failed'
+failure_recovery='Check xCAT discovery policy and the management network'
+temporary_dir=
+
+[[ "$maximum_attempts" =~ ^[1-9][0-9]*$ ]] || maximum_attempts=36
+[[ "$retry_seconds" =~ ^[0-9]+$ ]] || retry_seconds=5
+
+publish_status() {
+ "$status_command" discovery "$@" \
+ || logger -t xcat-genesis-discover -- 'unable to publish discovery status'
+}
+
+# shellcheck disable=SC2329
+finish_status() {
+ local result=$?
+
+ trap - EXIT
+ [[ -z $temporary_dir ]] || rm -rf -- "$temporary_dir"
+ if ((result != 0)); then
+ publish_status FAILED "$failure_detail" \
+ "CODE=$failure_code" "RECOVERY=$failure_recovery"
+ fi
+ exit "$result"
+}
+
+fail_discovery() {
+ failure_code=$1
+ failure_detail=$2
+ failure_recovery=$3
+ logger -t xcat-genesis-discover -- "$failure_detail"
+ exit 1
+}
+
+xml_escape() {
+ local value=$1
+
+ value=${value//&/\&}
+ value=${value//\<}
+ value=${value//>/\>}
+ value=${value//\"/\"}
+ value=${value//\'/\'}
+ printf '%s' "$value"
+}
+
+append_element() {
+ local name=$1 value=${2:-}
+
+ [[ -n $value ]] || return 0
+ printf '<%s>%s%s>\n' "$name" "$(xml_escape "$value")" "$name" \
+ >>"$unsigned_packet"
+}
+
+read_value() {
+ local value_file=$1
+
+ [[ -r $value_file ]] || return 1
+ tr -d '\000\r\n' <"$value_file" | head -c 512
+}
+
+send_packet() {
+ if [[ -n ${XCAT_DISCOVERY_SEND_COMMAND:-} ]]; then
+ "$XCAT_DISCOVERY_SEND_COMMAND" "$compressed_packet" "$XCATMASTER" "$XCATPORT"
+ return
+ fi
+
+ /usr/libexec/xcat/genesis-udp-send \
+ "$compressed_packet" "$XCATMASTER" "$XCATPORT"
+}
+
+trap finish_status EXIT HUP INT TERM
+
+[[ -r $network_file ]] || fail_discovery DISCOVERY_NETWORK_STATE_MISSING \
+ 'Management network state is unavailable' \
+ 'Wait for Genesis networking or review its failure'
+# shellcheck disable=SC1090
+source "$network_file"
+: "${XCATMASTER:?xCAT master is missing from network state}"
+: "${XCATPORT:?xCAT port is missing from network state}"
+: "${XCAT_INTERFACE:?management interface is missing from network state}"
+
+install -d -m 0700 "$key_dir"
+if [[ ! -s $key_dir/privkey.pem ]]; then
+ umask 077
+ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
+ -out "$key_dir/privkey.pem" >/dev/null 2>&1 \
+ || fail_discovery DISCOVERY_KEY_FAILED \
+ 'Unable to create the discovery identity' \
+ 'Review entropy and OpenSSL diagnostics'
+fi
+openssl pkey -in "$key_dir/privkey.pem" -noout >/dev/null 2>&1 \
+ || fail_discovery DISCOVERY_KEY_INVALID \
+ 'The discovery identity is invalid' \
+ 'Replace the damaged Genesis runtime state and retry'
+
+temporary_dir=$(mktemp -d "${TMPDIR:-/tmp}/genesis-discovery.XXXXXX")
+unsigned_packet=$temporary_dir/request.xml
+signed_packet=$temporary_dir/request-signed.xml
+signature_file=$temporary_dir/signature
+compressed_packet=$temporary_dir/request.xml.gz
+
+architecture=$(uname -m)
+case "$architecture" in
+ i?86) architecture=x86 ;;
+ armv7l) architecture=armv7hf ;;
+esac
+
+product_name=$(read_value "$sys_root/sys/devices/virtual/dmi/id/product_name" || true)
+system_vendor=$(read_value "$sys_root/sys/devices/virtual/dmi/id/sys_vendor" || true)
+serial=$(read_value "$sys_root/sys/devices/virtual/dmi/id/product_serial" || true)
+uuid=$(read_value "$sys_root/sys/devices/virtual/dmi/id/product_uuid" || true)
+machine_type=
+[[ -z $system_vendor && -z $product_name ]] \
+ || machine_type=${system_vendor:+$system_vendor:}$product_name
+platform=$(awk -F ':' \
+ '/^platform[[:space:]]*:/ {sub(/^[[:space:]]+/, "", $2); print $2; exit}' \
+ "$proc_root/cpuinfo" 2>/dev/null || true)
+if [[ -z $product_name ]]; then
+ product_name=$(read_value "$proc_root/device-tree/model" || true)
+ [[ -n $product_name ]] \
+ || product_name=$(read_value \
+ "$sys_root/sys/firmware/devicetree/base/model" || true)
+ machine_type=${product_name##*,}
+ serial=$(read_value "$proc_root/device-tree/system-id" || true)
+ [[ -n $serial ]] \
+ || serial=$(read_value \
+ "$sys_root/sys/firmware/devicetree/base/serial-number" || true)
+ serial=${serial##*,}
+fi
+machine_type=${machine_type//./}
+serial=${serial//./}
+node_type=
+case "$system_vendor $product_name" in
+ *KVM*|*QEMU*|*VMware*|*VirtualBox*|*Bochs*|*Hyper-V*) node_type=virtual ;;
+esac
+
+cpu_count=$(awk '/^(processor|cpu)[[:space:]]*:/ {count++} END {print count + 0}' \
+ "$proc_root/cpuinfo" 2>/dev/null || printf '0')
+cpu_type=$(awk -F ':' \
+ '/^(model name|cpu)[[:space:]]*:/ {sub(/^[[:space:]]+/, "", $2); print $2; exit}' \
+ "$proc_root/cpuinfo" 2>/dev/null || true)
+memory=$(awk '/^MemTotal:/ {printf "%.0fMB", $2 / 1024; exit}' \
+ "$proc_root/meminfo" 2>/dev/null || true)
+disk_size=$(lsblk -b -dn -o NAME,SIZE,TYPE 2>/dev/null \
+ | awk '$3 == "disk" {printf "%s%s:%.0fGB", separator, $1, $2 / 1073741824; separator=","}')
+
+printf '%s\n' '' 'findme' \
+ '1' >"$unsigned_packet"
+append_element arch "$architecture"
+append_element nodetype "$node_type"
+append_element cpucount "$cpu_count"
+append_element cputype "$cpu_type"
+append_element memory "$memory"
+append_element disksize "$disk_size"
+append_element mtm "$machine_type"
+append_element serial "$serial"
+append_element platform "$platform"
+
+if [[ -e $sys_root/dev/ipmi0 || -e $sys_root/dev/ipmi/0 || -e $sys_root/dev/ipmidev/0 ]]; then
+ append_element bmcinband 1
+fi
+if command -v "$ipmitool_command" >/dev/null 2>&1 \
+ && "$ipmitool_command" mc info >/dev/null 2>&1; then
+ lan_channel=$("$ipmitool_command" sol info 2>/dev/null \
+ | awk -F ':' '/Payload Channel/ {gsub(/[[:space:]]/, "", $2); print $2; exit}')
+ bmc_data=$("$ipmitool_command" lan print "$lan_channel" 2>/dev/null \
+ || "$ipmitool_command" lan print 2>/dev/null || true)
+ bmc_source=$(awk -F ':' \
+ '/IP Address Source/ {sub(/^[[:space:]]+/, "", $2); print $2; exit}' \
+ <<<"$bmc_data")
+ bmc_address=$(awk -F ':' \
+ '/^IP Address[[:space:]]*:/ {sub(/^[[:space:]]+/, "", $2); print $2; exit}' \
+ <<<"$bmc_data")
+ bmc_mac=$(awk -F ':' \
+ '/MAC Address/ {sub(/^[[:space:]]+/, "", $2); print $2 ":" $3 ":" $4 ":" $5 ":" $6 ":" $7; exit}' \
+ <<<"$bmc_data")
+ [[ $bmc_source != *Static* ]] || append_element bmc "$bmc_address"
+ append_element bmcmac "$bmc_mac"
+fi
+
+shopt -s nullglob
+first_mac=
+for interface_dir in "$sys_root/sys/class/net"/*; do
+ interface=${interface_dir##*/}
+ [[ $interface != lo && -r $interface_dir/address ]] || continue
+ mac_address=$(read_value "$interface_dir/address" || true)
+ [[ -n $mac_address ]] || continue
+ driver=unknown
+ if [[ -L $interface_dir/device/driver ]]; then
+ driver=$(basename "$(readlink -f "$interface_dir/device/driver")")
+ fi
+ normalized_mac=$(printf '%s' "$mac_address" | tr 'a-f' 'A-F')
+ [[ -n $first_mac ]] || first_mac=${normalized_mac//:/}
+ ipv4_address=$(ip -4 -o address show dev "$interface" scope global 2>/dev/null \
+ | awk 'NR == 1 {print $4}')
+ ipv6_address=$(ip -6 -o address show dev "$interface" scope global 2>/dev/null \
+ | awk 'NR == 1 {print $4}')
+ address=${ipv4_address:-$ipv6_address}
+ pci_slot=$(awk -F= '$1 == "PCI_SLOT_NAME" {print $2; exit}' \
+ "$interface_dir/device/uevent" 2>/dev/null || true)
+ mac_record="$driver|$interface|$normalized_mac|$address"
+ append_element mac "$mac_record"
+ printf '%s\n' '' >>"$unsigned_packet"
+ append_element devname "$interface"
+ append_element driver "$driver"
+ append_element ip4address "$ipv4_address"
+ append_element ip6address "$ipv6_address"
+ append_element hwaddr "$normalized_mac"
+ append_element pcidev "$pci_slot"
+ location=$(read_value "$interface_dir/device/physical_slot" || true)
+ [[ -n $location ]] \
+ || location=$(read_value "$interface_dir/device/label" || true)
+ append_element location "$location"
+ if command -v "$lldp_command" >/dev/null 2>&1; then
+ lldp_data=$("$lldp_command" show neighbors ports "$interface" \
+ details -f keyvalue 2>/dev/null || true)
+ switch_name=$(awk -F= \
+ '/\.chassis\.name=/ {print substr($0, index($0, "=") + 1); exit}' \
+ <<<"$lldp_data")
+ switch_address=$(awk -F= \
+ '/\.chassis\.mgmt-ip=/ {print substr($0, index($0, "=") + 1); exit}' \
+ <<<"$lldp_data")
+ switch_description=$(awk -F= \
+ '/\.chassis\.descr=/ {print substr($0, index($0, "=") + 1); exit}' \
+ <<<"$lldp_data")
+ switch_port=$(awk -F= \
+ '/\.port\.descr=/ {print substr($0, index($0, "=") + 1); exit}' \
+ <<<"$lldp_data")
+ append_element switchname "$switch_name"
+ append_element switchaddr "$switch_address"
+ append_element switchdesc "$switch_description"
+ append_element switchport "$switch_port"
+ fi
+ printf '%s\n' '' >>"$unsigned_packet"
+done
+
+if [[ -z $uuid ]]; then
+ uuid=${machine_type:-$architecture}-${serial:-unknown}-${first_mac:-unknown}
+ uuid=$(printf '%s' "$uuid" | tr '[:upper:]' '[:lower:]')
+fi
+append_element uuid "$uuid"
+
+public_key=$(openssl pkey -in "$key_dir/privkey.pem" -pubout 2>/dev/null \
+ | awk '!/PUBLIC KEY/ {printf "%s", $0}')
+append_element xcatpubkey "$public_key"
+printf '%s\n' '' '' '' \
+ >>"$unsigned_packet"
+
+openssl dgst -sha512 -sign "$key_dir/privkey.pem" \
+ -out "$signature_file" "$unsigned_packet" \
+ || fail_discovery DISCOVERY_SIGNATURE_FAILED \
+ 'Unable to sign the discovery inventory' \
+ 'Review the Genesis discovery identity'
+signature=$(openssl base64 -A -in "$signature_file")
+awk -v signature="$signature" \
+ '/<\/sha512sig>/ {print signature} {print}' \
+ "$unsigned_packet" >"$signed_packet"
+gzip -9 -c "$signed_packet" >"$compressed_packet"
+packet_size=$(wc -c <"$compressed_packet")
+((packet_size <= 60000)) || fail_discovery DISCOVERY_PACKET_TOO_LARGE \
+ "Discovery inventory is too large: $packet_size bytes" \
+ 'Review the attached device inventory'
+
+rm -f -- "$response_file"
+for ((attempt = 1; attempt <= maximum_attempts; attempt++)); do
+ publish_status RUNNING 'Sending hardware inventory to xCAT' \
+ "ATTEMPT=$attempt" "ATTEMPT_LIMIT=$maximum_attempts"
+ if ! send_packet; then
+ logger -t xcat-genesis-discover -- 'unable to send discovery inventory'
+ fi
+ for ((waited = 0; waited < retry_seconds; waited++)); do
+ if [[ -s $response_file ]]; then
+ response=$(head -n 1 "$response_file")
+ case "$response" in
+ restart|restart\ \(*\))
+ publish_status CONFIGURING_NETWORK \
+ 'Applying the discovered network identity'
+ "$network_refresh_command" "$response" \
+ || fail_discovery DISCOVERY_NETWORK_REFRESH_FAILED \
+ 'The discovered network identity could not be applied' \
+ 'Check the new DHCP reservation and management interface'
+ publish_status READY 'xCAT accepted the discovered node' \
+ 'PROGRESS_PERCENT=100'
+ logger -t xcat-genesis-discover -- \
+ 'xCAT accepted the discovery inventory'
+ exit 0
+ ;;
+ processed)
+ fail_discovery DISCOVERY_NOT_MATCHED \
+ 'xCAT received the node but did not match it' \
+ 'Review active discovery methods and node definitions'
+ ;;
+ processing) rm -f -- "$response_file" ;;
+ esac
+ fi
+ sleep 1
+ done
+done
+
+fail_discovery DISCOVERY_TIMEOUT \
+ "xCAT did not accept the node after $maximum_attempts attempts" \
+ 'Check discovery status, node matching, and callback connectivity'
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/genesis-discovery-callback b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/genesis-discovery-callback
new file mode 100755
index 000000000..c868ac0fd
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/genesis-discovery-callback
@@ -0,0 +1,24 @@
+#!/bin/bash
+set -eu
+
+state_dir=${XCAT_STATE_DIR:-/run/xcat}
+response_file=${XCAT_DISCOVERY_RESPONSE_FILE:-$state_dir/discovery-response}
+
+response=
+IFS= read -r response || true
+case "$response" in
+ restart|restart\ \(*\)|processing|processed) ;;
+ *)
+ logger -t xcat-genesis-discovery-callback -- \
+ 'ignored an invalid discovery callback'
+ exit 1
+ ;;
+esac
+
+mkdir -p "$state_dir"
+chmod 0755 "$state_dir"
+umask 077
+temporary_file=${response_file}.new
+printf '%s\n' "$response" >"$temporary_file"
+mv "$temporary_file" "$response_file"
+printf '%s\n' 'callback received'
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/genesis-getcert b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/genesis-getcert
new file mode 100755
index 000000000..b6cace3a1
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/genesis-getcert
@@ -0,0 +1,145 @@
+#!/bin/bash
+set -euo pipefail
+
+state_dir=${XCAT_STATE_DIR:-/run/xcat}
+status_command=${XCAT_STATUS_COMMAND:-/usr/libexec/xcat/genesis-status}
+network_file=${XCAT_NETWORK_FILE:-$state_dir/genesis.env}
+metadata_file=${XCAT_METADATA_FILE:-$state_dir/xcat-response.env}
+key_dir=${XCAT_KEY_DIR:-/etc/xcat}
+request_timeout=${XCAT_CREDENTIAL_REQUEST_TIMEOUT:-20}
+failure_code=CERTIFICATE_REQUEST_FAILED
+failure_detail='Unable to obtain an xCAT client certificate'
+failure_recovery='Check node identity, callback port 300, and xCAT credentials policy'
+temporary_dir=
+
+[[ "$request_timeout" =~ ^[1-9][0-9]*$ ]] || request_timeout=20
+
+publish_status() {
+ "$status_command" credentials "$@" \
+ || logger -t xcat-genesis-getcert -- 'unable to publish credential status'
+}
+
+# shellcheck disable=SC2329
+finish_status() {
+ local result=$?
+
+ trap - EXIT
+ [[ -z $temporary_dir ]] || rm -rf -- "$temporary_dir"
+ if ((result != 0)); then
+ publish_status FAILED "$failure_detail" \
+ "CODE=$failure_code" "RECOVERY=$failure_recovery"
+ fi
+ exit "$result"
+}
+
+fail_certificate() {
+ failure_code=$1
+ failure_detail=$2
+ failure_recovery=$3
+ logger -t xcat-genesis-getcert -- "$failure_detail"
+ exit 1
+}
+
+trap finish_status EXIT HUP INT TERM
+
+[[ -r $network_file ]] || fail_certificate CERTIFICATE_NETWORK_STATE_MISSING \
+ 'Management network state is unavailable' \
+ 'Wait for Genesis networking or review its failure'
+# shellcheck disable=SC1090
+source "$network_file"
+: "${XCATDEST:?xCAT endpoint is missing from network state}"
+
+node_name=
+if [[ -r $metadata_file ]]; then
+ while IFS='=' read -r name value; do
+ case "$name" in
+ XCAT_NODE_NAME) node_name=$value ;;
+ esac
+ done <"$metadata_file"
+fi
+[[ $node_name =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,254}$ ]] \
+ || fail_certificate CERTIFICATE_NODE_IDENTITY_MISSING \
+ 'xCAT has not confirmed the node name' \
+ 'Complete discovery and refresh the node assignment'
+
+install -d -m 0700 "$key_dir"
+if [[ ! -s $key_dir/privkey.pem ]]; then
+ umask 077
+ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
+ -out "$key_dir/privkey.pem" >/dev/null 2>&1 \
+ || fail_certificate CERTIFICATE_IDENTITY_KEY_FAILED \
+ 'Unable to create the Genesis identity key' \
+ 'Review entropy and OpenSSL diagnostics'
+fi
+if [[ ! -s $key_dir/certkey.pem ]]; then
+ umask 077
+ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 \
+ -out "$key_dir/certkey.pem" >/dev/null 2>&1 \
+ || fail_certificate CERTIFICATE_KEY_FAILED \
+ 'Unable to create the client certificate key' \
+ 'Review entropy and OpenSSL diagnostics'
+fi
+openssl pkey -in "$key_dir/privkey.pem" -noout >/dev/null 2>&1 \
+ || fail_certificate CERTIFICATE_IDENTITY_KEY_INVALID \
+ 'The Genesis identity key is invalid' \
+ 'Replace the damaged Genesis runtime state and retry'
+openssl pkey -in "$key_dir/certkey.pem" -noout >/dev/null 2>&1 \
+ || fail_certificate CERTIFICATE_KEY_INVALID \
+ 'The client certificate key is invalid' \
+ 'Replace the damaged Genesis runtime state and retry'
+
+temporary_dir=$(mktemp -d "${TMPDIR:-/tmp}/genesis-certificate.XXXXXX")
+csr_file=$temporary_dir/client.csr
+unsigned_request=$temporary_dir/request.xml
+signed_request=$temporary_dir/request-signed.xml
+signature_file=$temporary_dir/signature
+response_file=$temporary_dir/response.xml
+certificate_file=$temporary_dir/client.pem
+
+openssl req -new -key "$key_dir/certkey.pem" -out "$csr_file" \
+ -subj "/CN=$node_name" >/dev/null 2>&1 \
+ || fail_certificate CERTIFICATE_CSR_FAILED \
+ 'Unable to create the client certificate request' \
+ 'Review the client key and confirmed node name'
+
+{
+ printf '%s\n' '' 'getcredentials' \
+ 'x509cert' '300' ''
+ cat "$csr_file"
+ printf '%s\n' '' '' '' ''
+} >"$unsigned_request"
+openssl dgst -sha512 -sign "$key_dir/privkey.pem" \
+ -out "$signature_file" "$unsigned_request" \
+ || fail_certificate CERTIFICATE_SIGNATURE_FAILED \
+ 'Unable to sign the client certificate request' \
+ 'Review the Genesis identity key'
+signature=$(openssl base64 -A -in "$signature_file")
+awk -v signature="$signature" \
+ '/<\/sha512sig>/ {print signature} {print}' \
+ "$unsigned_request" >"$signed_request"
+
+publish_status RUNNING 'Requesting an xCAT client certificate' \
+ "NODE_NAME=$node_name"
+if ! timeout "$request_timeout" openssl s_client -connect "$XCATDEST" \
+ -quiet 2>/dev/null <"$signed_request" >"$response_file"; then
+ fail_certificate CERTIFICATE_RESPONSE_UNAVAILABLE \
+ 'xCAT did not answer the client certificate request' \
+ 'Check xcatd and callback connectivity to port 300'
+fi
+awk '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/' \
+ "$response_file" >"$certificate_file"
+[[ -s $certificate_file ]] \
+ || fail_certificate CERTIFICATE_RESPONSE_INVALID \
+ 'xCAT returned no client certificate' \
+ 'Check node identity and xCAT credentials policy'
+openssl x509 -in "$certificate_file" -noout >/dev/null 2>&1 \
+ || fail_certificate CERTIFICATE_RESPONSE_INVALID \
+ 'xCAT returned an invalid client certificate' \
+ 'Review xcatd certificate service diagnostics'
+
+chmod 0600 "$certificate_file"
+mv -f -- "$certificate_file" "$key_dir/cert.pem"
+publish_status READY 'xCAT client certificate installed' \
+ "NODE_NAME=$node_name" 'PROGRESS_PERCENT=100'
+logger -t xcat-genesis-getcert -- \
+ "installed an xCAT client certificate for $node_name"
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/genesis-udp-send.c b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/genesis-udp-send.c
new file mode 100644
index 000000000..4b54258dd
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/genesis-udp-send.c
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: EPL-1.0
+
+#define _POSIX_C_SOURCE 200112L
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+enum {
+ SOURCE_PORT = 301,
+ MAX_DATAGRAM = 65507,
+};
+
+int main(int argc, char **argv)
+{
+ struct addrinfo hints = { 0 };
+ struct addrinfo *addresses = NULL;
+ unsigned char packet[MAX_DATAGRAM + 1];
+ char *end = NULL;
+ long port;
+ FILE *input;
+ size_t length;
+ int error;
+ int last_error = 0;
+ int socket_fd;
+ int result = EXIT_FAILURE;
+
+ if (argc != 4) {
+ fprintf(stderr, "usage: %s packet host port\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ errno = 0;
+ port = strtol(argv[3], &end, 10);
+ if (errno || !end || *end || port < 1 || port > UINT16_MAX) {
+ fprintf(stderr, "invalid destination port: %s\n", argv[3]);
+ return EXIT_FAILURE;
+ }
+
+ input = fopen(argv[1], "rb");
+ if (!input) {
+ perror("unable to open discovery packet");
+ return EXIT_FAILURE;
+ }
+ length = fread(packet, 1, sizeof(packet), input);
+ if (ferror(input) || length == 0 || length > MAX_DATAGRAM) {
+ fprintf(stderr, "invalid discovery packet size\n");
+ fclose(input);
+ return EXIT_FAILURE;
+ }
+ fclose(input);
+
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_DGRAM;
+ error = getaddrinfo(argv[2], argv[3], &hints, &addresses);
+ if (error != 0) {
+ fprintf(stderr, "unable to resolve discovery endpoint: %s\n",
+ gai_strerror(error));
+ return EXIT_FAILURE;
+ }
+
+ for (struct addrinfo *address = addresses; address; address = address->ai_next) {
+ struct sockaddr_storage source = { 0 };
+ socklen_t source_length;
+
+ if (address->ai_family == AF_INET) {
+ struct sockaddr_in *source4 = (struct sockaddr_in *)&source;
+ source4->sin_family = AF_INET;
+ source4->sin_addr.s_addr = htonl(INADDR_ANY);
+ source4->sin_port = htons(SOURCE_PORT);
+ source_length = sizeof(*source4);
+ } else if (address->ai_family == AF_INET6) {
+ struct sockaddr_in6 *source6 = (struct sockaddr_in6 *)&source;
+ source6->sin6_family = AF_INET6;
+ source6->sin6_addr = in6addr_any;
+ source6->sin6_port = htons(SOURCE_PORT);
+ source_length = sizeof(*source6);
+ } else {
+ continue;
+ }
+
+ socket_fd = socket(address->ai_family, address->ai_socktype,
+ address->ai_protocol);
+ if (socket_fd < 0) {
+ last_error = errno;
+ continue;
+ }
+ if (bind(socket_fd, (struct sockaddr *)&source, source_length) < 0) {
+ last_error = errno;
+ close(socket_fd);
+ continue;
+ }
+ if (sendto(socket_fd, packet, length, 0, address->ai_addr,
+ address->ai_addrlen) == (ssize_t)length) {
+ close(socket_fd);
+ result = EXIT_SUCCESS;
+ break;
+ }
+ last_error = errno;
+ close(socket_fd);
+ }
+ if (result != EXIT_SUCCESS && last_error) {
+ errno = last_error;
+ perror("unable to send discovery packet");
+ }
+
+ freeaddrinfo(addresses);
+ return result;
+}
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/xcat-genesis-credential.socket b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/xcat-genesis-credential.socket
new file mode 100644
index 000000000..bc901307d
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/xcat-genesis-credential.socket
@@ -0,0 +1,9 @@
+[Unit]
+Description=Answer xCAT credential checks
+
+[Socket]
+ListenStream=300
+Accept=yes
+
+[Install]
+WantedBy=sockets.target
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/xcat-genesis-credential@.service b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/xcat-genesis-credential@.service
new file mode 100644
index 000000000..7e94c86ad
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/xcat-genesis-credential@.service
@@ -0,0 +1,9 @@
+[Unit]
+Description=Handle an xCAT credential check
+
+[Service]
+Type=oneshot
+ExecStart=/usr/libexec/xcat/genesis-credential-callback
+StandardInput=socket
+StandardOutput=socket
+StandardError=journal
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/xcat-genesis-discovery.socket b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/xcat-genesis-discovery.socket
new file mode 100644
index 000000000..5936b28a1
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/xcat-genesis-discovery.socket
@@ -0,0 +1,9 @@
+[Unit]
+Description=Receive xCAT discovery callbacks
+
+[Socket]
+ListenStream=3001
+Accept=yes
+
+[Install]
+WantedBy=sockets.target
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/xcat-genesis-discovery@.service b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/xcat-genesis-discovery@.service
new file mode 100644
index 000000000..d1aa3476d
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/files/xcat-genesis-discovery@.service
@@ -0,0 +1,9 @@
+[Unit]
+Description=Handle an xCAT discovery callback
+
+[Service]
+Type=oneshot
+ExecStart=/usr/libexec/xcat/genesis-discovery-callback
+StandardInput=socket
+StandardOutput=socket
+StandardError=journal
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/xcat-genesis-discovery_1.0.bb b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/xcat-genesis-discovery_1.0.bb
new file mode 100644
index 000000000..52b1bd8b9
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-discovery/xcat-genesis-discovery_1.0.bb
@@ -0,0 +1,65 @@
+SUMMARY = "xCAT Genesis node discovery"
+LICENSE = "EPL-1.0"
+LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/EPL-1.0;md5=57f8d5e2b3e98ac6e088986c12bf94e6"
+
+SRC_URI = "file://genesis-discover \
+ file://genesis-udp-send.c \
+ file://genesis-discovery-callback \
+ file://genesis-getcert \
+ file://genesis-credential-callback \
+ file://xcat-genesis-discovery.socket \
+ file://xcat-genesis-discovery@.service \
+ file://xcat-genesis-credential.socket \
+ file://xcat-genesis-credential@.service \
+"
+S = "${UNPACKDIR}"
+
+inherit systemd
+
+do_compile() {
+ ${CC} ${CPPFLAGS} ${CFLAGS} -std=c17 -Wall -Wextra -Wpedantic -Werror \
+ ${LDFLAGS} ${UNPACKDIR}/genesis-udp-send.c \
+ -o genesis-udp-send
+}
+
+RDEPENDS:${PN} = "bash coreutils gzip iproute2 openssl-bin util-linux-lsblk"
+
+SYSTEMD_SERVICE:${PN} = "xcat-genesis-discovery.socket \
+ xcat-genesis-credential.socket \
+"
+SYSTEMD_AUTO_ENABLE = "enable"
+
+do_install() {
+ install -d ${D}${libexecdir}/xcat
+ install -m 0755 ${UNPACKDIR}/genesis-discover \
+ ${D}${libexecdir}/xcat/genesis-discover
+ install -m 0755 ${B}/genesis-udp-send \
+ ${D}${libexecdir}/xcat/genesis-udp-send
+ install -m 0755 ${UNPACKDIR}/genesis-discovery-callback \
+ ${D}${libexecdir}/xcat/genesis-discovery-callback
+ install -m 0755 ${UNPACKDIR}/genesis-getcert \
+ ${D}${libexecdir}/xcat/genesis-getcert
+ install -m 0755 ${UNPACKDIR}/genesis-credential-callback \
+ ${D}${libexecdir}/xcat/genesis-credential-callback
+
+ install -d ${D}${systemd_system_unitdir}
+ install -m 0644 ${UNPACKDIR}/xcat-genesis-discovery.socket \
+ ${D}${systemd_system_unitdir}/xcat-genesis-discovery.socket
+ install -m 0644 ${UNPACKDIR}/xcat-genesis-discovery@.service \
+ ${D}${systemd_system_unitdir}/xcat-genesis-discovery@.service
+ install -m 0644 ${UNPACKDIR}/xcat-genesis-credential.socket \
+ ${D}${systemd_system_unitdir}/xcat-genesis-credential.socket
+ install -m 0644 ${UNPACKDIR}/xcat-genesis-credential@.service \
+ ${D}${systemd_system_unitdir}/xcat-genesis-credential@.service
+}
+
+FILES:${PN} = "${libexecdir}/xcat/genesis-discover \
+ ${libexecdir}/xcat/genesis-udp-send \
+ ${libexecdir}/xcat/genesis-discovery-callback \
+ ${libexecdir}/xcat/genesis-getcert \
+ ${libexecdir}/xcat/genesis-credential-callback \
+ ${systemd_system_unitdir}/xcat-genesis-discovery.socket \
+ ${systemd_system_unitdir}/xcat-genesis-discovery@.service \
+ ${systemd_system_unitdir}/xcat-genesis-credential.socket \
+ ${systemd_system_unitdir}/xcat-genesis-credential@.service \
+"
diff --git a/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-protocol/xcat-genesis-protocol_1.0.bb b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-protocol/xcat-genesis-protocol_1.0.bb
new file mode 100644
index 000000000..bb0721469
--- /dev/null
+++ b/xCAT-genesis-builder/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-protocol/xcat-genesis-protocol_1.0.bb
@@ -0,0 +1,18 @@
+SUMMARY = "xCAT Genesis protocol clients"
+LICENSE = "EPL-1.0"
+LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/EPL-1.0;md5=57f8d5e2b3e98ac6e088986c12bf94e6"
+
+FILESEXTRAPATHS:prepend := "${THISDIR}/../../../../../xCAT-genesis-scripts/usr/bin:"
+
+SRC_URI = "file://getdestiny \
+ file://nextdestiny \
+"
+S = "${UNPACKDIR}"
+
+RDEPENDS:${PN} = "bash openssl-bin"
+
+do_install() {
+ install -d ${D}${bindir}
+ install -m 0755 ${UNPACKDIR}/getdestiny ${D}${bindir}/getdestiny
+ install -m 0755 ${UNPACKDIR}/nextdestiny ${D}${bindir}/nextdestiny
+}
diff --git a/xCAT-genesis-scripts/usr/bin/getdestiny b/xCAT-genesis-scripts/usr/bin/getdestiny
index 6f3a44f01..9fd2685da 100755
--- a/xCAT-genesis-scripts/usr/bin/getdestiny
+++ b/xCAT-genesis-scripts/usr/bin/getdestiny
@@ -1,30 +1,111 @@
#!/bin/bash
-if [ -z "$XCATDEST" ]; then
- XCATDEST=$1
-fi
-echo "
-getdestiny
-300
-" > /tmp/destreq.xml
-if [ -f /tmp/destiny.xml ]; then rm /tmp/destiny.xml; fi
-while [ ! -f /tmp/destiny.xml ] || grep error /tmp/destiny.xml; do
- if [ -f /tmp/destiny.xml ]; then
- timer=60
- while [ $timer -gt 0 ]; do
- echo -en "Retrying in $timer seconds \r" >&2
- sleep 1
- timer=$(($timer-1));
- done
- fi
- echo " " >&2
- if [ -f /etc/xcat/cert.pem -a -f /etc/xcat/certkey.pem ]; then #use client cert if available
- openssl s_client -key /etc/xcat/certkey.pem -cert /etc/xcat/cert.pem -connect "$XCATDEST" -quiet 2> /dev/null < /tmp/destreq.xml > /tmp/destiny.xml
- else
- openssl s_client -connect "$XCATDEST" -quiet 2> /dev/null < /tmp/destreq.xml > /tmp/destiny.xml
- fi
-done
-rm /tmp/destreq.xml
-DESTINY="$(grep '' /tmp/destiny.xml | awk -F'>' '{print $2}'|awk -F'<' '{print $1}')"
-rm /tmp/destiny.xml
-echo "$DESTINY"
+set -u
+metadata_file=
+metadata_temporary=
+once=0
+destination=${XCATDEST:-}
+
+usage() {
+ printf '%s\n' 'Usage: getdestiny [ENDPOINT] [--metadata FILE] [--once]' >&2
+ exit 2
+}
+
+while (($# > 0)); do
+ case "$1" in
+ --metadata)
+ (($# >= 2)) || usage
+ metadata_file=$2
+ shift 2
+ ;;
+ --once)
+ once=1
+ shift
+ ;;
+ --)
+ shift
+ (($# <= 1)) || usage
+ if (($# == 1)); then
+ destination=$1
+ fi
+ break
+ ;;
+ -*) usage ;;
+ *)
+ if [[ -z $destination ]]; then
+ destination=$1
+ fi
+ shift
+ ;;
+ esac
+done
+
+[[ -n $destination ]] || usage
+
+temporary_dir=$(mktemp -d "${TMPDIR:-/tmp}/getdestiny.XXXXXX") || exit 1
+request_file=$temporary_dir/request.xml
+response_file=$temporary_dir/response.xml
+trap 'rm -rf -- "$temporary_dir"; [[ -z $metadata_temporary ]] || rm -f -- "$metadata_temporary"' EXIT HUP INT TERM
+
+printf '%s\n' \
+ '' \
+ 'getdestiny' \
+ '300' \
+ '' >"$request_file"
+
+request_destiny() {
+ if [[ -f /etc/xcat/cert.pem && -f /etc/xcat/certkey.pem ]]; then
+ openssl s_client -key /etc/xcat/certkey.pem \
+ -cert /etc/xcat/cert.pem -connect "$destination" -quiet \
+ 2>/dev/null <"$request_file" >"$response_file"
+ else
+ openssl s_client -connect "$destination" -quiet \
+ 2>/dev/null <"$request_file" >"$response_file"
+ fi
+ [[ -s $response_file ]] && ! grep -q '' "$response_file" \
+ && grep -q '' "$response_file"
+}
+
+while ! request_destiny; do
+ if ((once)); then
+ exit 1
+ fi
+ for ((timer = 60; timer > 0; timer--)); do
+ printf 'Retrying in %s seconds \r' "$timer" >&2
+ sleep 1
+ done
+ printf '%*s\r' 48 '' >&2
+done
+
+extract_element() {
+ local element=$1
+
+ sed -n "s:.*<$element>\([^<]*\)$element>.*:\1:p" "$response_file" \
+ | head -n 1 | tr -cd '\40-\176'
+}
+
+destiny=$(extract_element destiny)
+[[ -n $destiny ]] || exit 1
+
+if [[ -n $metadata_file ]]; then
+ metadata_dir=${metadata_file%/*}
+ if [[ $metadata_dir == "$metadata_file" ]]; then
+ metadata_dir=.
+ elif [[ -z $metadata_dir ]]; then
+ metadata_dir=/
+ fi
+ [[ -d $metadata_dir ]] || exit 1
+ metadata_temporary=$(mktemp "$metadata_dir/.getdestiny.XXXXXX") || exit 1
+ {
+ printf 'XCAT_NODE_NAME=%s\n' "$(extract_element name)"
+ printf 'XCAT_DESTINY=%s\n' "$destiny"
+ printf 'XCAT_IMAGE_SERVER=%s\n' "$(extract_element imgserver)"
+ printf 'XCAT_KERNEL=%s\n' "$(extract_element kernel)"
+ printf 'XCAT_INITRD=%s\n' "$(extract_element initrd)"
+ printf 'XCAT_KERNEL_COMMAND_LINE=%s\n' "$(extract_element kcmdline)"
+ } >"$metadata_temporary"
+ chmod 0600 "$metadata_temporary"
+ mv -f -- "$metadata_temporary" "$metadata_file"
+fi
+
+printf '%s\n' "$destiny"
diff --git a/xCAT-genesis-scripts/usr/bin/nextdestiny b/xCAT-genesis-scripts/usr/bin/nextdestiny
index 298826afb..de523290b 100755
--- a/xCAT-genesis-scripts/usr/bin/nextdestiny
+++ b/xCAT-genesis-scripts/usr/bin/nextdestiny
@@ -1,36 +1,115 @@
#!/bin/bash
-if [ -z "$XCATDEST" ]; then
- XCATDEST=$1
-fi
-echo "
-nextdestiny
-300
-" > /tmp/destreq.xml
-if [ -f /tmp/destiny.xml ]; then rm /tmp/destiny.xml; fi
-timer=0
-while [ ! -f /tmp/destiny.xml ]; do
- while [ $timer -gt 0 ]; do
- echo -en "No destiny command received, retrying in $timer seconds \r" >&2
- sleep 1
- timer=$(($timer-1));
- done
- echo " " >&2;
- if [ -f /etc/xcat/cert.pem -a -f /etc/xcat/certkey.pem ]; then #use client cert if available
- openssl s_client -key /etc/xcat/certkey.pem -cert /etc/xcat/cert.pem -connect "$XCATDEST" -quiet 2> /dev/null < /tmp/destreq.xml > /tmp/destiny.xml
- else
- openssl s_client -connect "$XCATDEST" -quiet 2> /dev/null < /tmp/destreq.xml > /tmp/destiny.xml
- fi
- timer=60
+set -u
+
+metadata_file=
+metadata_temporary=
+once=0
+destination=${XCATDEST:-}
+
+usage() {
+ printf '%s\n' 'Usage: nextdestiny [ENDPOINT] [--metadata FILE] [--once]' >&2
+ exit 2
+}
+
+while (($# > 0)); do
+ case "$1" in
+ --metadata)
+ (($# >= 2)) || usage
+ metadata_file=$2
+ shift 2
+ ;;
+ --once)
+ once=1
+ shift
+ ;;
+ --)
+ shift
+ (($# <= 1)) || usage
+ if (($# == 1)); then
+ destination=$1
+ fi
+ break
+ ;;
+ -*) usage ;;
+ *)
+ if [[ -z $destination ]]; then
+ destination=$1
+ fi
+ shift
+ ;;
+ esac
done
-rm /tmp/destreq.xml
-DESTINY="$(grep '' /tmp/destiny.xml | awk -F'>' '{print $2}'|awk -F'<' '{print $1}')"
-if [ -z "$DESTINY" ]; then
- ERROR="$(grep '' /tmp/destiny.xml | awk -F'>' '{print $2}'|awk -F'<' '{print $1}')"
- if [ ! -z "$ERROR" ]; then
- DESTINY="error=$ERROR"
- else
- DESTINY="error=No destiny command received"
- fi
+
+[[ -n $destination ]] || usage
+
+temporary_dir=$(mktemp -d "${TMPDIR:-/tmp}/nextdestiny.XXXXXX") || exit 1
+request_file=$temporary_dir/request.xml
+response_file=$temporary_dir/response.xml
+trap 'rm -rf -- "$temporary_dir"; [[ -z $metadata_temporary ]] || rm -f -- "$metadata_temporary"' EXIT HUP INT TERM
+
+printf '%s\n' \
+ '' \
+ 'nextdestiny' \
+ '300' \
+ '' >"$request_file"
+
+request_destiny() {
+ if [[ -f /etc/xcat/cert.pem && -f /etc/xcat/certkey.pem ]]; then
+ openssl s_client -key /etc/xcat/certkey.pem \
+ -cert /etc/xcat/cert.pem -connect "$destination" -quiet \
+ 2>/dev/null <"$request_file" >"$response_file"
+ else
+ openssl s_client -connect "$destination" -quiet \
+ 2>/dev/null <"$request_file" >"$response_file"
+ fi
+ [[ -s $response_file ]] && \
+ { grep -q '' "$response_file" || grep -q '' "$response_file"; }
+}
+
+while ! request_destiny; do
+ if ((once)); then
+ exit 1
+ fi
+ for ((timer = 60; timer > 0; timer--)); do
+ printf 'Retrying in %s seconds \r' "$timer" >&2
+ sleep 1
+ done
+ printf '%*s\r' 48 '' >&2
+done
+
+extract_element() {
+ local element=$1
+
+ sed -n "s:.*<$element>\([^<]*\)$element>.*:\1:p" "$response_file" \
+ | head -n 1 | tr -cd '\40-\176'
+}
+
+destiny=$(extract_element destiny)
+if [[ -z $destiny ]]; then
+ error=$(extract_element error)
+ [[ -n $error ]] || error='No destiny command received'
+ destiny="error=$error"
fi
-rm /tmp/destiny.xml
-echo "$DESTINY"
+
+if [[ -n $metadata_file ]]; then
+ metadata_dir=${metadata_file%/*}
+ if [[ $metadata_dir == "$metadata_file" ]]; then
+ metadata_dir=.
+ elif [[ -z $metadata_dir ]]; then
+ metadata_dir=/
+ fi
+ [[ -d $metadata_dir ]] || exit 1
+ metadata_temporary=$(mktemp "$metadata_dir/.nextdestiny.XXXXXX") || exit 1
+ {
+ printf 'XCAT_NODE_NAME=%s\n' "$(extract_element name)"
+ printf 'XCAT_DESTINY=%s\n' "$destiny"
+ printf 'XCAT_IMAGE_SERVER=%s\n' "$(extract_element imgserver)"
+ printf 'XCAT_KERNEL=%s\n' "$(extract_element kernel)"
+ printf 'XCAT_INITRD=%s\n' "$(extract_element initrd)"
+ printf 'XCAT_KERNEL_COMMAND_LINE=%s\n' "$(extract_element kcmdline)"
+ } >"$metadata_temporary"
+ chmod 0600 "$metadata_temporary"
+ mv -f -- "$metadata_temporary" "$metadata_file"
+fi
+
+printf '%s\n' "$destiny"