#!/usr/bin/bash set -euxo pipefail # Export IRONIC_IP to avoid needing to lean on IRONIC_URL_HOST for consumption in # e.g. dnsmasq configuration export IRONIC_IP="${IRONIC_IP:-}" IRONIC_IPV6="${IRONIC_IPV6:-}" PROVISIONING_INTERFACE="${PROVISIONING_INTERFACE:-}" PROVISIONING_IP="${PROVISIONING_IP:-}" PROVISIONING_MACS="${PROVISIONING_MACS:-}" IRONIC_URL_HOSTNAME="${IRONIC_URL_HOSTNAME:-}" IPXE_CUSTOM_FIRMWARE_DIR="${IPXE_CUSTOM_FIRMWARE_DIR:-/shared/custom_ipxe_firmware}" CUSTOM_CONFIG_DIR="${CUSTOM_CONFIG_DIR:-/conf}" CUSTOM_DATA_DIR="${CUSTOM_DATA_DIR:-/data}" export DNSMASQ_CONF_DIR="${CUSTOM_CONFIG_DIR}/dnsmasq" export DNSMASQ_DATA_DIR="${CUSTOM_DATA_DIR}/dnsmasq" export DNSMASQ_TEMP_DIR="${CUSTOM_CONFIG_DIR}/dnsmasq" export HTTPD_DIR="${CUSTOM_CONFIG_DIR}/httpd" export HTTPD_CONF_DIR="${HTTPD_DIR}/conf" export HTTPD_CONF_DIR_D="${HTTPD_DIR}/conf.d" export IRONIC_CONF_DIR="${CUSTOM_CONFIG_DIR}/ironic" export IRONIC_DB_DIR="${CUSTOM_DATA_DIR}/db" export IRONIC_GEN_CERT_DIR="${CUSTOM_DATA_DIR}/auto_gen_certs" export IRONIC_TMP_DATA_DIR="${CUSTOM_DATA_DIR}/tmp" export PROBE_CONF_DIR="${CUSTOM_CONFIG_DIR}/probes" mkdir -p "${IRONIC_CONF_DIR}" "${PROBE_CONF_DIR}" "${HTTPD_CONF_DIR}" \ "${HTTPD_CONF_DIR_D}" "${DNSMASQ_CONF_DIR}" "${DNSMASQ_TEMP_DIR}" \ "${IRONIC_DB_DIR}" "${IRONIC_GEN_CERT_DIR}" "${DNSMASQ_DATA_DIR}" \ "${IRONIC_TMP_DATA_DIR}" export HTPASSWD_FILE="${IRONIC_CONF_DIR}/htpasswd" export LOCAL_DB_URI="sqlite:///${IRONIC_DB_DIR}/ironic.sqlite" export IRONIC_USE_MARIADB=${IRONIC_USE_MARIADB:-false} get_ip_of_hostname() { if [[ "$#" -ne 2 ]]; then echo "${FUNCNAME}: two parameters required, $# provided" >&2 return 1 fi case $2 in 4) QUERY="a";; 6) QUERY="aaaa";; *) echo "${FUNCNAME}: the second parameter should be [a|aaaa] for A and AAAA records" return 1;; esac local HOSTNAME=$1 echo $(nslookup -type=${QUERY} "${HOSTNAME}" | tail -n2 | grep -w "Address:" | cut -d " " -f2) } get_interface_of_ip() { local IP_VERS="" if [[ "$#" -gt 2 ]]; then echo "${FUNCNAME}: too many parameters" >&2 return 1 fi if [[ "$#" -eq 2 ]]; then case $2 in 4|6) local IP_VERS="-${2}" ;; *) echo "${FUNCNAME}: the second parameter should be [4|6] (or missing for both)" >&2 return 2 ;; esac fi local IP_ADDR=$1 # Convert the address using ipcalc which strips out the subnet. # For IPv6 addresses, this will give the short-form address IP_ADDR="$(ipcalc "${IP_ADDR}" | grep "^Address:" | awk '{print $2}')" echo $(ip ${IP_VERS} -br addr show scope global | grep -i " ${IP_ADDR}/" | cut -f 1 -d ' ' | cut -f 1 -d '@') } get_ip_of_interface() { local IP_VERS="" if [[ "$#" -gt 2 ]]; then echo "${FUNCNAME}: too many parameters" >&2 return 1 fi if [[ "$#" -eq 2 ]]; then case $2 in 4|6) local IP_VERS="-${2}" ;; *) echo "${FUNCNAME}: the second parameter should be [4|6] (or missing for both)" >&2 return 2 ;; esac fi local IFACE=$1 echo $(ip ${IP_VERS} -br addr show scope global up dev ${IFACE} | awk '{print $3}' | sed -e 's%/.*%%' | head -n 1) } get_provisioning_interface() { if [[ -n "$PROVISIONING_INTERFACE" ]]; then # don't override the PROVISIONING_INTERFACE if one is provided echo "$PROVISIONING_INTERFACE" return fi local interface="" for mac in ${PROVISIONING_MACS//,/ }; do if ip -br link show up | grep -i "$mac" &>/dev/null; then interface="$(ip -br link show up | grep -i "$mac" | cut -f 1 -d ' ' | cut -f 1 -d '@')" break fi done echo "$interface" } PROVISIONING_INTERFACE="$(get_provisioning_interface)" export PROVISIONING_INTERFACE export LISTEN_ALL_INTERFACES="${LISTEN_ALL_INTERFACES:-true}" # Wait for the interface or IP to be up, sets $IRONIC_IP wait_for_interface_or_ip() { # If $PROVISIONING_IP is specified, then we wait for that to become # available on an interface, otherwise we look at $PROVISIONING_INTERFACE # for an IP if [[ -n "${PROVISIONING_IP}" ]]; then local IFACE_OF_IP="" until [[ -n "$IFACE_OF_IP" ]]; do echo "Waiting for ${PROVISIONING_IP} to be configured on an interface..." IFACE_OF_IP="$(get_interface_of_ip "${PROVISIONING_IP}")" sleep 1 done echo "Found $PROVISIONING_IP on interface \"${IFACE_OF_IP}\"!" export PROVISIONING_INTERFACE="$IFACE_OF_IP" # If the IP contains a colon, then it's an IPv6 address if [[ "$PROVISIONING_IP" =~ .*:.* ]]; then export IRONIC_IPV6="$PROVISIONING_IP" export IRONIC_IP="" else export IRONIC_IP="$PROVISIONING_IP" fi elif [[ -n "${IRONIC_IP}" ]]; then if [[ "$IRONIC_IP" =~ .*:.* ]]; then export IRONIC_IPV6="$IRONIC_IP" export IRONIC_IP="" fi elif [[ -n "${PROVISIONING_INTERFACE}" ]]; then until [[ -n "$IRONIC_IPV6" ]] || [[ -n "$IRONIC_IP" ]]; do echo "Waiting for ${PROVISIONING_INTERFACE} interface to be configured..." IRONIC_IPV6="$(get_ip_of_interface "${PROVISIONING_INTERFACE}" 6)" sleep 1 IRONIC_IP="$(get_ip_of_interface "${PROVISIONING_INTERFACE}" 4)" sleep 1 done if [[ -n "$IRONIC_IPV6" ]]; then echo "Found $IRONIC_IPV6 on interface \"${PROVISIONING_INTERFACE}\"!" export IRONIC_IPV6 fi if [[ -n "$IRONIC_IP" ]]; then echo "Found $IRONIC_IP on interface \"${PROVISIONING_INTERFACE}\"!" export IRONIC_IP fi elif [[ -n "$IRONIC_URL_HOSTNAME" ]]; then local IPV6_IFACE="" local IPV4_IFACE="" # we should get at least one IP address until [[ -n "$IPV6_IFACE" ]] || [[ -n "$IPV4_IFACE" ]]; do local IPV6_RECORD="" local IPV4_RECORD="" IPV6_RECORD="$(get_ip_of_hostname "${IRONIC_URL_HOSTNAME}" 6)" IPV4_RECORD="$(get_ip_of_hostname "${IRONIC_URL_HOSTNAME}" 4)" # We couldn't get any IP if [[ -z "$IPV4_RECORD" ]] && [[ -z "$IPV6_RECORD" ]]; then echo "${FUNCNAME}: no valid IP found for hostname ${IRONIC_URL_HOSTNAME}" >&2 return 1 fi echo "Waiting for ${IPV6_RECORD} to be configured on an interface" IPV6_IFACE="$(get_interface_of_ip "${IPV6_RECORD}" 6)" sleep 1 echo "Waiting for ${IPV4_RECORD} to be configured on an interface" IPV4_IFACE="$(get_interface_of_ip "${IPV4_RECORD}" 4)" sleep 1 done # Add some debugging output if [[ -n "$IPV6_IFACE" ]]; then echo "Found $IPV6_RECORD on interface \"${IPV6_IFACE}\"!" export IRONIC_IPV6="$IPV6_RECORD" fi if [[ -n "$IPV4_IFACE" ]]; then echo "Found $IPV4_RECORD on interface \"${IPV4_IFACE}\"!" export IRONIC_IP="$IPV4_RECORD" fi # Make sure both IPs are asigned to the same interface if [[ -n "$IPV6_IFACE" ]] && [[ -n "$IPV4_IFACE" ]] && [[ "$IPV6_IFACE" != "$IPV4_IFACE" ]]; then echo "Warning, the IPv4 and IPv6 addresses from \"${HOSTNAME}\" are assigned to different " \ "interfaces (\"${IPV6_IFACE}\" and \"${IPV4_IFACE}\")" >&2 fi else echo "Cannot determine an interface or an IP for binding and creating URLs" return 1 fi # Define the URLs based on the what we have found, # prioritize IPv6 for IRONIC_URL_HOST if [[ -n "$IRONIC_IP" ]]; then export ENABLE_IPV4=yes export IRONIC_URL_HOST="$IRONIC_IP" fi if [[ -n "$IRONIC_IPV6" ]]; then export ENABLE_IPV6=yes export IRONIC_URL_HOST="[${IRONIC_IPV6}]" # The HTTP host needs surrounding with brackets fi # Once determined if we have IPv4 and/or IPv6, override the hostname if provided if [[ -n "$IRONIC_URL_HOSTNAME" ]]; then IRONIC_URL_HOST=$IRONIC_URL_HOSTNAME fi # Avoid having to construct full URL multiple times while allowing # the override of IRONIC_HTTP_URL for environments in which IRONIC_IP # is unreachable from hosts being provisioned. export IRONIC_HTTP_URL="${IRONIC_HTTP_URL:-http://${IRONIC_URL_HOST}:${HTTP_PORT}}" export IRONIC_TFTP_URL="${IRONIC_TFTP_URL:-tftp://${IRONIC_URL_HOST}}" export IRONIC_BASE_URL=${IRONIC_BASE_URL:-"${IRONIC_SCHEME}://${IRONIC_URL_HOST}:${IRONIC_ACCESS_PORT}"} } render_j2_config() { python3.11 -c 'import os; import sys; import jinja2; sys.stdout.write(jinja2.Template(sys.stdin.read()).render(env=os.environ))' < "$1" > "$2" } run_ironic_dbsync() { if [[ "${IRONIC_USE_MARIADB}" == "true" ]]; then # It's possible for the dbsync to fail if mariadb is not up yet, so # retry until success until ironic-dbsync --config-file "${IRONIC_CONF_DIR}/ironic.conf" upgrade; do echo "WARNING: ironic-dbsync failed, retrying" sleep 1 done else # SQLite does not support some statements. Fortunately, we can just # create the schema in one go if not already created, instead of going # through an upgrade cp "/var/lib/ironic/ironic.sqlite" "${IRONIC_DB_DIR}/ironic.sqlite" DB_VERSION="$(ironic-dbsync --config-file "${IRONIC_CONF_DIR}/ironic.conf" version)" if [[ "${DB_VERSION}" == "None" ]]; then ironic-dbsync --config-file "${IRONIC_CONF_DIR}/ironic.conf" create_schema fi fi } # Use the special value "unix" for unix sockets export IRONIC_PRIVATE_PORT=${IRONIC_PRIVATE_PORT:-unix} export IRONIC_ACCESS_PORT=${IRONIC_ACCESS_PORT:-6385} export IRONIC_LISTEN_PORT=${IRONIC_LISTEN_PORT:-$IRONIC_ACCESS_PORT} export IRONIC_ENABLE_DISCOVERY=${IRONIC_ENABLE_DISCOVERY:-${IRONIC_INSPECTOR_ENABLE_DISCOVERY:-false}}