diff --git a/README.md b/README.md index 2235f0da5..b3c6b67a1 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,12 @@ functionality: `PROVISIONING_MACS` is provided) - `PROVISIONING_IP` - the specific IP to use (instead of calculating it based on the `PROVISIONING_INTERFACE`) +- `IRONIC_URL_HOSTNAME` - a fully qualified name resolving to an IPv4 and/or IPv6 + address, used for both binding and forming the required URLs; for the latter + purpose only, it can be used in combination with `PROVISIONING_INTERFACE`, which + would instead be used for the former. If the hostname has both IPv4 and IPv6 + records, and both addresses are correctly assigned on the same network interface, + `IRONIC_URL_HOSTNAME` enables a dual-stack ironic image configuration. - `DNSMASQ_EXCEPT_INTERFACE` - interfaces to exclude when providing DHCP address (default `lo`) - `HTTP_PORT` - port used by http server (default `80`) diff --git a/ironic-config/apache2-ipxe.conf.j2 b/ironic-config/apache2-ipxe.conf.j2 index f570e6ea1..9f4b41d92 100644 --- a/ironic-config/apache2-ipxe.conf.j2 +++ b/ironic-config/apache2-ipxe.conf.j2 @@ -1,4 +1,5 @@ -Listen {{ env.IPXE_TLS_PORT }} +Listen 0.0.0.0:{{ env.IPXE_TLS_PORT }} +Listen [::]:{{ env.IPXE_TLS_PORT }} ErrorLog /dev/stderr diff --git a/ironic-config/apache2-vmedia.conf.j2 b/ironic-config/apache2-vmedia.conf.j2 index 62114274f..742cfedf3 100644 --- a/ironic-config/apache2-vmedia.conf.j2 +++ b/ironic-config/apache2-vmedia.conf.j2 @@ -1,4 +1,5 @@ -Listen {{ env.VMEDIA_TLS_PORT }} +Listen 0.0.0.0:{{ env.VMEDIA_TLS_PORT }} +Listen [::]:{{ env.VMEDIA_TLS_PORT }} ErrorLog /dev/stderr diff --git a/ironic-config/httpd-ironic-api.conf.j2 b/ironic-config/httpd-ironic-api.conf.j2 index 3e7731b47..f669de450 100644 --- a/ironic-config/httpd-ironic-api.conf.j2 +++ b/ironic-config/httpd-ironic-api.conf.j2 @@ -12,11 +12,21 @@ {% if env.LISTEN_ALL_INTERFACES | lower == "true" %} -Listen {{ env.IRONIC_LISTEN_PORT }} +Listen 0.0.0.0:{{ env.IRONIC_LISTEN_PORT }} +Listen [::]:{{ env.IRONIC_LISTEN_PORT }} {% else %} -Listen {{ env.IRONIC_URL_HOST }}:{{ env.IRONIC_LISTEN_PORT }} - +{% if env.ENABLE_IPV4 %} +Listen {{ env.IRONIC_IP }}:{{ env.IRONIC_LISTEN_PORT }} +{% endif %} +{% if env.ENABLE_IPV6 %} +Listen [{{ env.IRONIC_IPV6 }}]:{{ env.IRONIC_LISTEN_PORT }} +{% endif %} +{% if env.IRONIC_URL_HOSTNAME is defined and env.IRONIC_URL_HOSTNAME|length %} + +{% else %} + +{% endif %} {% endif %} DocumentRoot "/shared/html" diff --git a/ironic-config/httpd.conf.j2 b/ironic-config/httpd.conf.j2 index 7bebcdd1d..015a85a73 100644 --- a/ironic-config/httpd.conf.j2 +++ b/ironic-config/httpd.conf.j2 @@ -1,8 +1,14 @@ ServerRoot {{ env.HTTPD_DIR }} {%- if env.LISTEN_ALL_INTERFACES | lower == "true" %} -Listen {{ env.HTTP_PORT }} +Listen 0.0.0.0:{{ env.HTTP_PORT }} +Listen [::]:{{ env.HTTP_PORT }} {% else %} -Listen {{ env.IRONIC_URL_HOST }}:{{ env.HTTP_PORT }} +{% if env.ENABLE_IPV4 %} +Listen {{ env.IRONIC_IP }}:{{ env.HTTP_PORT }} +{% endif %} +{% if env.ENABLE_IPV6 %} +Listen [{{ env.IRONIC_IPV6 }}]:{{ env.HTTP_PORT }} +{% endif %} {% endif %} Include /etc/httpd/conf.modules.d/*.conf User apache diff --git a/ironic-config/ironic.conf.j2 b/ironic-config/ironic.conf.j2 index ed48e9c5c..df384f757 100644 --- a/ironic-config/ironic.conf.j2 +++ b/ironic-config/ironic.conf.j2 @@ -25,7 +25,13 @@ rpc_transport = none use_stderr = true # NOTE(dtantsur): the default md5 is not compatible with FIPS mode hash_ring_algorithm = sha256 +{% if env.ENABLE_IPV4 %} my_ip = {{ env.IRONIC_IP }} +{% endif %} +{% if env.ENABLE_IPV6 %} +my_ipv6 = {{ env.IRONIC_IPV6 }} +{% endif %} + host = {{ env.IRONIC_CONDUCTOR_HOST }} # If a path to a certificate is defined, use that first for webserver @@ -68,7 +74,7 @@ port = {{ env.IRONIC_PRIVATE_PORT }} {% endif %} public_endpoint = {{ env.IRONIC_BASE_URL }} {% else %} -host_ip = {% if env.LISTEN_ALL_INTERFACES | lower == "true" %}::{% else %}{{ env.IRONIC_IP }}{% endif %} +host_ip = {{ env.IRONIC_HOST_IP }} port = {{ env.IRONIC_LISTEN_PORT }} {% if env.IRONIC_TLS_SETUP == "true" %} enable_ssl_api = true @@ -186,7 +192,7 @@ cipher_suite_versions = 3,17 # containers are in host networking. auth_strategy = http_basic http_basic_auth_user_file = {{ env.IRONIC_RPC_HTPASSWD_FILE }} -host_ip = {% if env.LISTEN_ALL_INTERFACES | lower == "true" %}::{% else %}{{ env.IRONIC_IP }}{% endif %} +host_ip = {{ env.IRONIC_HOST_IP }} port = {{ env.IRONIC_JSON_RPC_PORT }} {% if env.IRONIC_TLS_SETUP == "true" %} use_ssl = true diff --git a/main-packages-list.txt b/main-packages-list.txt index dc7001478..0278b8357 100644 --- a/main-packages-list.txt +++ b/main-packages-list.txt @@ -12,3 +12,4 @@ sqlite syslinux-nonlinux util-linux xorriso +bind-utils diff --git a/scripts/configure-ironic.sh b/scripts/configure-ironic.sh index 9671c4f81..90adbe02f 100755 --- a/scripts/configure-ironic.sh +++ b/scripts/configure-ironic.sh @@ -49,6 +49,14 @@ export IRONIC_IPA_COLLECTORS=${IRONIC_IPA_COLLECTORS:-default,logs} wait_for_interface_or_ip +if [[ "$(echo "${LISTEN_ALL_INTERFACES}" | tr '[:upper:]' '[:lower:]')" == "true" ]]; then + export IRONIC_HOST_IP="::" +elif [[ -n "${ENABLE_IPV6}" ]]; then + export IRONIC_HOST_IP="${IRONIC_IPV6}" +else + export IRONIC_HOST_IP="${IRONIC_IP}" +fi + # Hostname to use for the current conductor instance. export IRONIC_CONDUCTOR_HOST=${IRONIC_CONDUCTOR_HOST:-${IRONIC_URL_HOST}} @@ -130,4 +138,11 @@ render_j2_config "/etc/ironic/ironic.conf.j2" \ configure_json_rpc_auth # Make sure ironic traffic bypasses any proxies -export NO_PROXY="${NO_PROXY:-},$IRONIC_IP" +export NO_PROXY="${NO_PROXY:-}" + +if [[ -n "${IRONIC_IPV6}" ]]; then + export NO_PROXY="${NO_PROXY},${IRONIC_IPV6}" +fi +if [[ -n "${IRONIC_IP}" ]]; then + export NO_PROXY="${NO_PROXY},${IRONIC_IP}" +fi diff --git a/scripts/ironic-common.sh b/scripts/ironic-common.sh index bf0ccf59f..5a793ed61 100644 --- a/scripts/ironic-common.sh +++ b/scripts/ironic-common.sh @@ -5,9 +5,11 @@ 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:-}" +export 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}" @@ -46,13 +48,7 @@ get_provisioning_interface() return fi - local interface="provisioning" - - if [[ -n "${PROVISIONING_IP}" ]]; then - if ip -br addr show | grep -i " ${PROVISIONING_IP}/" &>/dev/null; then - interface="$(ip -br addr show | grep -i " ${PROVISIONING_IP}/" | cut -f 1 -d ' ' | cut -f 1 -d '@')" - fi - fi + local interface="" for mac in ${PROVISIONING_MACS//,/ }; do if ip -br link show up | grep -i "$mac" &>/dev/null; then @@ -69,38 +65,232 @@ export PROVISIONING_INTERFACE export LISTEN_ALL_INTERFACES="${LISTEN_ALL_INTERFACES:-true}" +get_ip_of_hostname() +{ + if [[ "$#" -ne 2 ]]; then + echo "ERROR: ${FUNCNAME[0]}: two parameters required, $# provided" >&2 + return 1 + fi + + case "$2" in + 4) + QUERY="a";; + 6) + QUERY="aaaa";; + *) + echo "ERROR: ${FUNCNAME[0]}: the second parameter should be <4|6> for A and AAAA records" >&2 + return 1;; + esac + + local HOSTNAME="$1" + + echo "$(nslookup -type=${QUERY} "${HOSTNAME}" | tail -n2 | grep -w "Address:" | cut -d " " -f2)" +} + +get_ip_of_interface() +{ + local IP_VERS + local IP_ADDR + + if [[ $# -gt 2 ]]; then + echo "ERROR: ${FUNCNAME[0]}: too many parameters" >&2 + return 1 + fi + + if [[ $# -eq 2 ]]; then + case "$2" in + 4|6) + IP_VERS="-$2" + ;; + *) + echo "ERROR: ${FUNCNAME[0]}: the second parameter should be [4|6] (or missing for both)" >&2 + return 2 + ;; + esac + fi + + IFACE="$1" + + ip "${IP_VERS[@]}" -br addr show scope global up dev "${IFACE}" | awk '{print $3}' | sed -e 's%/.*%%' | head -n 1 +} + +get_interface_of_ip() +{ + local IP_VERS + local IP_ADDR + + if [[ $# -gt 2 ]]; then + echo "ERROR: ${FUNCNAME[0]}: too many parameters" >&2 + return 1 + fi + + if [[ $# -eq 2 ]]; then + case "$2" in + 4|6) + IP_VERS="-$2" + ;; + *) + echo "ERROR: ${FUNCNAME[0]}: the second parameter should be [4|6] (or missing for both)" >&2 + return 2 + ;; + esac + fi + + IP_ADDR="$1" + + ip "${IP_VERS[@]}" -br addr show scope global | grep -i " ${IP_ADDR}/" | cut -f 1 -d ' ' | cut -f 1 -d '@' +} + +parse_ip_address() +{ + local IP_ADDR + + if [[ $# -ne 1 ]]; then + echo "ERROR: ${FUNCNAME[0]}: please provide a single IP address as input" >&2 + return 1 + fi + + IP_ADDR="$1" + + if ipcalc "${IP_ADDR}" | grep ^INVALID &>/dev/null; then + echo "ERROR: ${FUNCNAME[0]}: Failed to parse ${IP_ADDR}" >&2 + return 2 + fi + + # Convert the address using ipcalc which strips out the subnet. + # For IPv6 addresses, this will give the short-form address + ipcalc "${IP_ADDR}" | grep "^Address:" | awk '{print $2}' +} + # 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 - # Convert the address using ipcalc which strips out the subnet. - # For IPv6 addresses, this will give the short-form address - IRONIC_IP="$(ipcalc "${PROVISIONING_IP}" | grep "^Address:" | awk '{print $2}')" - export IRONIC_IP - until grep -F " ${IRONIC_IP}/" <(ip -br addr show); do - echo "Waiting for ${IRONIC_IP} to be configured on an interface" + # IRONIC_IP already defined overrides everything else + if [[ -n "${IRONIC_IP}" ]]; then + local PARSED_IP + PARSED_IP="$(parse_ip_address "${IRONIC_IP}")" + if [[ -z "${PARSED_IP}" ]]; then + echo "ERROR: PROVISIONING_IP contains an invalid IP address, failed to start ironic" + exit 1 + fi + + if [[ "${PARSED_IP}" =~ .*:.* ]]; then + export IRONIC_IPV6="${PARSED_IP}" + export IRONIC_IP="" + else + export IRONIC_IP="${PARSED_IP}" + fi + elif [[ -n "${PROVISIONING_IP}" ]]; then + # 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 + local PARSED_IP + PARSED_IP="$(parse_ip_address "${PROVISIONING_IP}")" + if [[ -z "${PARSED_IP}" ]]; then + echo "ERROR: PROVISIONING_IP contains an invalid IP address, failed to start ironic" + exit 1 + fi + + 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 "${PARSED_IP}")" sleep 1 done - else - until [[ -n "$IRONIC_IP" ]]; do - echo "Waiting for ${PROVISIONING_INTERFACE} interface to be configured" - IRONIC_IP="$(ip -br add show scope global up dev "${PROVISIONING_INTERFACE}" | awk '{print $3}' | sed -e 's%/.*%%' | head -n 1)" - export IRONIC_IP + + 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 [[ "${PARSED_IP}" =~ .*:.* ]]; then + export IRONIC_IPV6="${PARSED_IP}" + else + export IRONIC_IP="${PARSED_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 - fi - # If the IP contains a colon, then it's an IPv6 address, and the HTTP - # host needs surrounding with brackets - if [[ "$IRONIC_IP" =~ .*:.* ]]; then - export IPV=6 - export IRONIC_URL_HOST="[$IRONIC_IP]" + # Add some debugging output + 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_RECORD + local IPV4_RECORD + + # we should get at least one IP address + 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}\"" + return 1 + fi + + if [[ "$(echo "${LISTEN_ALL_INTERFACES}" | tr '[:upper:]' '[:lower:]')" == "true" ]]; then + local IPV6_IFACE="" + local IPV4_IFACE="" + + until [[ -n "${IPV6_IFACE}" ]] || [[ -n "${IPV4_IFACE}" ]]; do + 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}\"!" + fi + if [[ -n "$IPV4_IFACE" ]]; then + echo "Found ${IPV4_RECORD} on interface \"${IPV4_IFACE}\"!" + 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 + + export IRONIC_IPV6="${IPV6_RECORD}" + export IRONIC_IP="${IPV4_RECORD}" + fi else - export IPV=4 - export IRONIC_URL_HOST="$IRONIC_IP" + echo "ERROR: 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