#!/bin/bash
# Purpose: automatically install Debian + Sipwise C5 platform
################################################################################

set -e
set -E

# Functions

usage() {
  echo "$0 - automatically deploy Debian ${DEBIAN_RELEASE} and (optionally) ngcp ce/pro.

Control installation parameters:

  debugmode                - enable additional debug information
  help|-h|--help           - show this message
  ngcphalt                 - poweroff the server in the end of deployment
  ngcpnw.dhcp              - use DHCP as network configuration in installed system
  ngcpreboot               - reboot the server in the end of deployment
  ngcpstatus=...           - sleep for number of seconds befor deployment.sh is finished
  nocolorlogo              - print the logo in the top of he screen
  noinstall                - do not install neither Debian nor NGCP
  nongcp                   - do not install NGCP but install plain Debian only

Control target system:

  arch=...                 - use specified architecture of debian packages
  debianrelease=...        - install specified Debian release
  debianrepo=...           - hostname of Debian APT repository mirror
  debianrepotransport=...  - use specified transport for Debian repository
  enablevmservices         - add some tricks for installation to VM
  fallbackfssize=...       - size of ngcp-fallback partition. Equal to ngncp-root size if not specified
  ip=...                   - standard Linux kernel ip= boot option
  lowperformance           - add some tuning for low performance systems
  ngcpce                   - install CE Edition
  ngcpcrole=...            - server role (Carrier)
  ngcpeaddr=...            - cluster IP address
  ngcpextnetmask=...       - use the following netmask for external interface
  ngcphostname=...         - hostname of installed system (defaults to ngcp/sp[1,2])
  ngcpinternal             - use internal (mrX.X.X-update) NGCP repo
  ngcpinst                 - force usage of NGCP installer
  ngcpip1=...              - IP address of first node (Pro Edition only)
  ngcpip2=...              - IP address of second node (Pro Edition only)
  ngcpipshared=...         - HA shared IP address
  ngcpmgmt=...             - name of management node
  ngcpnetmask=...          - netmask of ha_int interface
  ngcpnodename=...         - name of the node in terms of spN
  ngcpnomysqlrepl          - skip MySQL sp1<->sp2 replication configuration/check
  ngcpppa                  - use NGCP PPA Debian repository
  ngcppro                  - install Pro Edition
  ngcppxeinstall           - shows that system is deployed via iPXE
  ngcpupload               - run ngcp-prepare-translations in the end of configuration
  ngcpvers=...             - install specific SP/CE version
  ngcpvlanbootint=...      - currently, not used
  ngcpvlanhaint=...        - the ID of the vlan that is used for ha_int interface type
  ngcpvlanrtpext=...       - the ID of the vlan that is used for rtp_ext interface type
  ngcpvlansipext=...       - the ID of the vlan that is used for sip_ext interface type
  ngcpvlansipint=...       - the ID of the vlan that is used for sip_int interface type
  ngcpvlansshext=...       - the ID of the vlan that is used for ssh_ext interface type
  ngcpvlanwebext=...       - the ID of the vlan that is used for web_ext interface type
  noeatmydata              - use noeatmydata program to speed up packages installation
  nopuppetrepeat           - do not repeat puppet deployment in case of errors
  puppetenv=...            - use specified puppet environment
  puppetgitbranch=...      - use specified git branch to get puppet configuration
  puppetgitrepo=...        - clone puppet configuration from specified git repo
  puppetserver=...         - install puppet configuration from specified puppet server
  rootfssize=...           - size of ngcp-root partition
  sipwiserepo=...          - hostname of Sipwise APT repository mirror
  sipwiserepotransport=... - use specified transport for Sipwise repository
  swapfilesize=...         - size of swap file in megabytes
  swraiddestroy            - destroy the currently configured RAID and create a new one
  swraiddisk1=...          - the 1st device which will be used for software RAID
  swraiddisk2=...          - the 2nd device which will be used for software RAID
  targetdisk=...           - use specified disk to place the system
  vagrant                  - add some tricks for creation of vagrant image


The command line options correspond with the available bootoptions.
Command line overrides any present bootoption.

Usage examples:

  # ngcp-deployment ngcpce ngcpnw.dhcp

  # netcardconfig # configure eth0 with static configuration
  # ngcp-deployment ngcppro ngcpnodename=sp1

  # netcardconfig # configure eth0 with static configuration
  # ngcp-deployment ngcppro ngcpnodename=sp2
"
}

### helper functions {{{
get_deploy_status() {
  if [ -r "${STATUS_DIRECTORY}/status" ] ; then
    cat "${STATUS_DIRECTORY}/status"
  else
    echo 'error'
  fi
}

set_deploy_status() {
  [ -n "$1" ] || return 1
  echo "$*" > "${STATUS_DIRECTORY}"/status
}

enable_deploy_status_server() {
  mkdir -p "${STATUS_DIRECTORY}"

  cat > /etc/systemd/system/deployment-status.service << EOF
[Unit]
Description=Deployment Status Server
After=network.target

[Service]
Type=simple
Restart=always
ExecStart=/usr/bin/python3 -m http.server 4242 --directory=/srv/deployment/

[Install]
WantedBy=sysinit.target
EOF
  systemctl daemon-reload
  systemctl restart deployment-status.service
}

# load ":"-separated nfs ip into array BP[client-ip], BP[server-ip], ...
# ip=<client-ip>:<server-ip>:<gw-ip>:<netmask>:<hostname>:<device>:<autoconf>
# $1: Array name (needs "declare -A BP" before call), $2: ip=... string
loadNfsIpArray() {
  [ -n "$1" ] && [ -n "$2" ] || return 0
  local IFS=":"
  local ind=(client-ip server-ip gw-ip netmask hostname device autoconf)
  local i
  for i in $2 ; do
    eval "$1[${ind[n++]}]=$i"
  done
  [ "$n" == "7" ] && return 0 || return 1
}

disable_systemd_tmpfiles_clean() {
  echo "Disabling systemd-tmpfiles-clean.timer"
  systemctl mask systemd-tmpfiles-clean.timer
}

debootstrap_sipwise_key() {
  mkdir -p /etc/debootstrap/pre-scripts/
  cat > /etc/debootstrap/pre-scripts/install-sipwise-key.sh << EOF
#!/bin/bash
# installed via deployment.sh
target_file=sipwise-keyring-bootstrap.gpg
if [[ "${SIPWISE_APT_KEY_PATH}" =~ trusted.gpg\$ ]]; then
  target_file=trusted.gpg
fi
cp ${SIPWISE_APT_KEY_PATH} "\${MNTPOINT}/etc/apt/trusted.gpg.d/\${target_file}"
EOF
  chmod 775 /etc/debootstrap/pre-scripts/install-sipwise-key.sh
}

check_package_version() {
  if [ $# -lt 2 ] ; then
    die "Usage: package_upgrade <package> <version>" >&2
  fi

  local package_name="$1"
  local required_version="$2"
  local present_version

  present_version=$(dpkg-query --show --showformat="\${Version}" "${package_name}")

  if dpkg --compare-versions "${present_version}" lt "${required_version}" ; then
    echo "${package_name} version ${present_version} is older than minimum required version ${required_version}."
    return 1
  fi

  return 0
}

ensure_recent_package_versions() {
  [[ -z "${UPGRADE_PACKAGES[*]}" ]] && return 0

  echo "Ensuring packages are installed in a recent enough version: ${UPGRADE_PACKAGES[*]}"

  # use temporary apt database for speed reasons
  local TMPDIR
  TMPDIR=$(mktemp -d -t ngcp-deployment-recent-tmp.XXXXXXXXXX)
  mkdir -p "${TMPDIR}/statedir/lists/partial" "${TMPDIR}/cachedir/archives/partial"
  local debsrcfile
  debsrcfile=$(mktemp -t ngcp-deployment-recent-debsrc.XXXXXXXXXX)
  echo "deb ${SIPWISE_REPO_TRANSPORT}://${SIPWISE_REPO_HOST}/grml.org grml-testing main" >> "${debsrcfile}"

  DEBIAN_FRONTEND='noninteractive' apt-get \
    -o dir::cache="${TMPDIR}/cachedir" \
    -o dir::state="${TMPDIR}/statedir" \
    -o dir::etc::sourcelist="${debsrcfile}" \
    -o dir::etc::sourceparts=/dev/null \
    -o dir::etc::trustedparts="/etc/apt/trusted.gpg.d/" \
    update

  DEBIAN_FRONTEND='noninteractive' apt-get \
    -o dir::cache="${TMPDIR}/cachedir" \
    -o dir::state="${TMPDIR}/statedir" \
    -o dir::etc::sourcelist="${debsrcfile}" \
    -o dir::etc::sourceparts=/dev/null \
    -o dir::etc::trustedparts="/etc/apt/trusted.gpg.d/" \
    -y --no-install-recommends install "${UPGRADE_PACKAGES[@]}"

  for pkg in "${UPGRADE_PACKAGES[@]}"; do
    if is_package_installed "${pkg}"; then
      echo "Package '${pkg}' was installed correctly."
    else
      die "Error: Package '${pkg}' was not installed correctly, aborting."
    fi
  done
}

die() {
  echo "$@" >&2
  set_deploy_status "error"
  exit 1
}

enable_trace() {
  if "${DEBUG_MODE}" ; then
    set -x
    export PS4='+\t (${BASH_SOURCE##*/}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): } '
  fi
}

disable_trace() {
  if "${DEBUG_MODE}" ; then
    set +x
    export PS4=''
  fi
}

is_package_installed() {
  local pkg="$1"

  if [ "$(dpkg-query -f "\${db:Status-Status} \${db:Status-Eflag}" -W "${pkg}" 2>/dev/null)" = 'installed ok' ]; then
    return 0
  else
    return 1
  fi
}

ensure_packages_installed() {
  declare -a packages=("$@")

  if [[ "${#packages[@]}" -eq 0 ]]; then
    packages=("${ADDITIONAL_PACKAGES[@]}")
  fi

  if [[ "${#packages[@]}" -eq 0 ]]; then
   return 0
  fi

  local install_packages
  install_packages=()
  echo "Ensuring packages installed: ${packages[*]}"
  for pkg in "${packages[@]}"; do
    if is_package_installed "${pkg}"; then
      echo "Package '${pkg}' is already installed, nothing to do."
    else
      echo "Package '${pkg}' is not installed, scheduling..."
      install_packages+=("${pkg}")
    fi
  done

  if [ -z "${install_packages[*]}" ] ; then
    echo "No packages to install, skipping further ensure_packages_installed execution"
    return 0
  fi

  # Use separate apt database and source list because non management node has no internet access
  # so is installed from management node so these additional packages have to be accessible from
  # sipwise repo
  local TMPDIR
  TMPDIR=$(mktemp -d -t ngcp-deployment-ensure-tmp.XXXXXXXXXX)
  mkdir -p "${TMPDIR}/etc/preferences.d" "${TMPDIR}/statedir/lists/partial" \
    "${TMPDIR}/cachedir/archives/partial"
  chown _apt -R "${TMPDIR}"

  local deb_release
  case "${DEBIAN_RELEASE}" in
    buster|bullseye|bookworm|trixie)
      deb_release="${DEBIAN_RELEASE}"
      echo "Using ${deb_release} as Debian repository for ${FUNCNAME[0]}"
      ;;
    *)
      echo "Error: Unsupported Debian release '${DEBIAN_RELEASE}' in ensure_packages_installed()" >&2
      exit 1
      ;;
  esac

  echo "deb ${DEBIAN_URL}/debian/ ${deb_release} main contrib non-free" > \
    "${TMPDIR}/etc/sources.list"

  mkdir -p "${TMPDIR}"/etc/apt/apt.conf.d/
  cat > "${TMPDIR}"/etc/apt/apt.conf.d/73_acquire_retries << EOF
# NGCP_MANAGED_FILE -- deployment.sh
Acquire::Retries "3";
EOF

  DEBIAN_FRONTEND='noninteractive' apt-get \
    -o dir::cache="${TMPDIR}/cachedir" \
    -o dir::state="${TMPDIR}/statedir" \
    -o dir::etc="${TMPDIR}/etc" \
    -o dir::etc::trustedparts="/etc/apt/trusted.gpg.d/" \
    update

  DEBIAN_FRONTEND='noninteractive' apt-get \
    -o dir::cache="${TMPDIR}/cachedir" \
    -o dir::state="${TMPDIR}/statedir" \
    -o dir::etc="${TMPDIR}/etc" \
    -o dir::etc::trustedparts="/etc/apt/trusted.gpg.d/" \
    -o DPkg::Options::=--force-confnew \
    -y --no-install-recommends install "${install_packages[@]}"

  for pkg in "${install_packages[@]}"; do
    if is_package_installed "${pkg}"; then
      echo "Package '${pkg}' was installed correctly."
    else
      die "Error: Package '${pkg}' was not installed correctly, aborting."
    fi
  done
}

status_wait() {
  if [[ -n "${STATUS_WAIT}" ]] && [[ "${STATUS_WAIT}" != 0 ]]; then
    # if ngcpstatus boot option is used wait for a specific so a
    # remote host has a chance to check for deploy status "finished",
    # defaults to 0 seconds otherwise
    echo "Sleeping for ${STATUS_WAIT} seconds (as requested via boot option 'ngcpstatus')"
    sleep "${STATUS_WAIT}"
  fi
}

wait_exit() {
  local e_code="${?}"
  if [[ "${e_code}" -ne 0 ]]; then
    set_deploy_status "error"
  fi
  trap '' 1 2 3 6 15 ERR EXIT
  status_wait
  exit "${e_code}"
}

# check for EFI support or try to enable it
efi_support() {
  # Absence of an EFI runtime does not prevent loading efivarfs since commit
  # 301de9a2055357375a4e1053d9df0f8f3467ff00 which landed in Linux v6.3.
  # Unconditionally load it, in case nothing else attempted it.
  modprobe efivarfs &>/dev/null || true

  if [ -d /sys/firmware/efi ] ; then
    echo "EFI support detected."
    return 0
  fi

  return 1
}

cdr2mask() {
  # From https://stackoverflow.com/questions/20762575/explanation-of-convertor-of-cidr-to-netmask-in-linux-shell-netmask2cdir-and-cdir
  # Number of args to shift, 255..255, first non-255 byte, zeroes
  set -- $(( 5 - ("${1}" / 8) )) 255 255 255 255 $(( (255 << (8 - ("${1}" % 8))) & 255 )) 0 0 0
  if [[ "${1}" -gt 1 ]] ; then
    shift "${1}"
  else
    shift
  fi
  echo "${1:-0}.${2:-0}.${3:-0}.${4:-0}"
}

clear_partition_table() {
  local blockdevice="$1"

  if [[ ! -b "${blockdevice}" ]] ; then
    die "Error: ${blockdevice} doesn't look like a valid block device." >&2
  fi

  # make sure parted doesn't fail if LVM is already present
  blockdev --rereadpt "$blockdevice"
  for disk in "$blockdevice"* ; do
    existing_pvs=$(pvs "$disk" -o vg_name --noheadings 2>/dev/null || true)
    if [ -n "$existing_pvs" ] ; then
      for pv in $existing_pvs ; do
        echo "Getting rid of existing VG $pv"
        vgremove -ff "$pv"
      done
    fi

    echo "Removing possibly existing LVM/PV label from $disk"
    pvremove "$disk" --force --force --yes || true
  done

  # ensure we remove signatures from partitions like /dev/nvme1n1p3 first,
  # and only then get rid of signaturs from main blockdevice /dev/nvme1n1
  for partition in $(lsblk --noheadings --output KNAME "${blockdevice}" | grep -v "^${blockdevice#\/dev\/}$" || true) ; do
    [ -b "${partition}" ] || continue
    echo "Wiping disk signatures from partition ${partition}"
    wipefs -a "${partition}"
  done

  echo "Wiping disk signatures from ${blockdevice}"
  wipefs -a "${blockdevice}"

  dd if=/dev/zero of="${blockdevice}" bs=1M count=1
  blockdev --rereadpt "${blockdevice}"
}

get_pvdevice_by_label() {
  local blockdevice="$1"
  if [[ -z "${blockdevice}" ]]; then
    die "Error: need a blockdevice to probe, nothing provided."
  fi
  local partlabel="$2"
  if [[ -z "${partlabel}" ]]; then
    die "Error: need a partlabel to search for, nothing provided."
  fi

  local pvdevice=""
  pvdevice=$(blkid -t PARTLABEL="${partlabel}" -o device "${blockdevice}"* || true)
  echo "${pvdevice}"
}

get_pvdevice_by_label_with_retries() {
  if [[ $# -ne 4 ]]; then
    die "Error: needs 4 arguments: a BLOCKDEVICE to probe, a PARTLABEL to search for, MAX_TRIES and name of variable to return PVDEVICE."
  fi

  local blockdevice="$1"
  if [[ -z "${blockdevice}" ]]; then
    die "Error: need a blockdevice to probe, nothing provided."
  fi
  local partlabel="$2"
  if [[ -z "${partlabel}" ]]; then
    die "Error: need a partlabel to search for, nothing provided."
  fi
  local max_tries="$3"
  if [[ -z "${max_tries}" ]]; then
    die "Error: need max_tries, nothing provided."
  fi
  # return result in this variable
  # shellcheck disable=SC2034
  declare -n ret="$4"

  local pvdevice_local
  for try in $(seq 1 "${max_tries}"); do
    pvdevice_local=$(get_pvdevice_by_label "${blockdevice}" "${partlabel}")
    if [[ -n "${pvdevice_local}" ]]; then
      echo "pvdevice is now available: ${pvdevice_local}"
      # it's a reference to an external variable that sets the return value
      # shellcheck disable=SC2034
      ret="${pvdevice_local}"
      break
    else
      if [[ "${try}" -lt "${max_tries}" ]]; then
        echo "pvdevice not yet available (blockdevice=${blockdevice}, partlabel='${partlabel}'), try #${try} of ${max_tries}, retrying in 1 second..."
        sleep 1s
      else
        die "Error: could not get pvdevice after #${try} tries"
      fi
    fi
  done
}

parted_execution() {
  local blockdevice="$1"

  echo "Creating partition table"
  parted -a optimal -s "${blockdevice}" mklabel gpt

  # BIOS boot with GPT
  parted -a optimal -s "${blockdevice}" mkpart primary 2048s 2M
  parted -a optimal -s "${blockdevice}" set 1 bios_grub on
  parted -a optimal -s "${blockdevice}" "name 1 'BIOS Boot'"

  if efi_support ; then
    # EFI boot with GPT
    parted -a optimal -s "${blockdevice}" mkpart primary 2M 512M
    parted -a optimal -s "${blockdevice}" "name 2 'EFI System'"
    parted -a optimal -s "${blockdevice}" set 2 boot on
  else
    # unused partition if we don't run in an EFI environment
    parted -a optimal -s "${blockdevice}" mkpart primary 2M 512M
  fi

  blockdev --flushbufs "${blockdevice}"

  if efi_support ; then
    echo "Get path of EFI partition"
    local max_tries=60
    EFI_PARTITION=""
    get_pvdevice_by_label_with_retries "${blockdevice}" "EFI System" "${max_tries}" EFI_PARTITION
  fi
}

set_up_partition_table_noswraid() {
  local blockdevice
  blockdevice="/dev/${DISK}"

  clear_partition_table "$blockdevice"

  parted_execution "$blockdevice"
  parted -a optimal -s "${blockdevice}" mkpart primary 512M 100%
  parted -a optimal -s "${blockdevice}" "name 3 'Linux LVM'"
  parted -a optimal -s "${blockdevice}" set 3 lvm on
  blockdev --flushbufs "${blockdevice}"

  local max_tries=60
  local pvdevice
  local partlabel="Linux LVM"
  get_pvdevice_by_label_with_retries "${blockdevice}" "${partlabel}" "${max_tries}" pvdevice

  echo "Creating PV + VG"
  pvcreate -ff -y "${pvdevice}"
  vgcreate "${VG_NAME}" "${pvdevice}"
  vgchange -a y "${VG_NAME}"
}

set_up_partition_table_swraid() {
  local orig_swraid_device raidev1 raidev2 raid_device raid_disks

  # make sure we don't overlook unassembled SW-RAIDs:
  mdadm --assemble --scan --config /dev/null || true # fails if there's nothing to assemble

  # "local" arrays get assembled as /dev/md0 and upwards,
  # whereas "foreign" arrays start ad md127 downwards;
  # since we need to also handle those, identify them:
  raid_device=$(lsblk --list --noheadings --output TYPE,NAME | awk '/^raid/ {print $2}' | head -1)

  # only consider changing SWRAID_DEVICE if we actually identified an RAID array:
  if [[ -n "${raid_device:-}" ]] ; then
    if ! [[ -b /dev/"${raid_device}" ]] ; then
      die "Error: identified SW-RAID device '/dev/${raid_device}' not a valid block device."
    fi

    # identify which disks are part of the RAID array:
    raid_disks=$(lsblk -l -n -s /dev/"${raid_device}" | grep -vw "^${raid_device}" | awk '{print $1}')
    for d in ${raid_disks} ; do
      # compare against expected SW-RAID disks to avoid unexpected behavior:
      if ! printf "%s\n" "$d" | grep -qE "(${SWRAID_DISK1}|${SWRAID_DISK2})" ; then
        die "Error: unexpected disk in RAID array /dev/${raid_device}: $d [expected SW-RAID disks: $SWRAID_DISK1 + $SWRAID_DISK2]"
      fi
    done

    # remember the original setting, so we can use it after mdadm cleanup:
    orig_swraid_device="${SWRAID_DEVICE}"

    echo "NOTE: default SWRAID_DEVICE set to ${SWRAID_DEVICE} though we identified active ${raid_device}"
    SWRAID_DEVICE="/dev/${raid_device}"
    echo "NOTE: will continue with '${SWRAID_DEVICE}' as SWRAID_DEVICE for mdadm cleanup"
  fi

  if [[ -b "${SWRAID_DEVICE}" ]] ; then
    if [[ "${SWRAID_DESTROY}" = "true" ]] ; then
      echo "Wiping signatures from ${SWRAID_DEVICE}"
      wipefs -a "${SWRAID_DEVICE}"

      echo "Removing mdadm device ${SWRAID_DEVICE}"
      mdadm --remove "${SWRAID_DEVICE}"

      echo "Stopping mdadm device ${SWRAID_DEVICE}"
      mdadm --stop "${SWRAID_DEVICE}"

      echo "Zero-ing superblock from /dev/${SWRAID_DISK1}"
      mdadm --zero-superblock "/dev/${SWRAID_DISK1}"

      echo "Zero-ing superblock from /dev/${SWRAID_DISK2}"
      mdadm --zero-superblock "/dev/${SWRAID_DISK2}"
    else
      echo "NOTE: if you are sure you don't need it SW-RAID device any longer, execute:"
      echo "      mdadm --remove ${SWRAID_DEVICE} ; mdadm --stop ${SWRAID_DEVICE}; mdadm --zero-superblock /dev/sd..."
      echo "      (also you can use boot option 'swraiddestroy' to destroy SW-RAID automatically)"
      die "Error: SW-RAID device ${SWRAID_DEVICE} exists already."
    fi
  fi

  if [[ -n "${orig_swraid_device:-}" ]] ; then
    echo "NOTE: modified RAID array detected, setting SWRAID_DEVICE back to original setting '${orig_swraid_device}'"
    SWRAID_DEVICE="${orig_swraid_device}"
  fi

  for disk in "${SWRAID_DISK1}" "${SWRAID_DISK2}" ; do
    if grep -q "$disk" /proc/mdstat ; then
      die "Error: disk $disk seems to be part of an existing SW-RAID setup."
    fi
    clear_partition_table "/dev/${disk}"
  done

  parted_execution "/dev/${SWRAID_DISK1}"

  parted -a optimal -s "/dev/${SWRAID_DISK1}" mkpart primary 512M 100%
  parted -a optimal -s "/dev/${SWRAID_DISK1}" "name 3 'Linux RAID'"
  parted -a optimal -s "/dev/${SWRAID_DISK1}" set 3 raid on

  # clone partitioning from SWRAID_DISK1 to SWRAID_DISK2
  sgdisk "/dev/${SWRAID_DISK1}" -R "/dev/${SWRAID_DISK2}"
  # randomize the disk's GUID and all partitions' unique GUIDs after cloning
  sgdisk -G "/dev/${SWRAID_DISK2}"

  local partlabel="Linux RAID"
  local max_tries=60
  get_pvdevice_by_label_with_retries "/dev/${SWRAID_DISK1}" "${partlabel}" "${max_tries}" raidev1
  get_pvdevice_by_label_with_retries "/dev/${SWRAID_DISK2}" "${partlabel}" "${max_tries}" raidev2

  mdadm --create --run --verbose "${SWRAID_DEVICE}" --level=1 --raid-devices=2 "${raidev1}" "${raidev2}"

  echo "Creating PV + VG on ${SWRAID_DEVICE}"
  pvcreate -ff -y "${SWRAID_DEVICE}"
  vgcreate "${VG_NAME}" "${SWRAID_DEVICE}"
  vgchange -a y "${VG_NAME}"
}

set_up_partition_table() {
  if [[ "${SWRAID}" = "true" ]] ; then
    set_up_partition_table_swraid
  else
    set_up_partition_table_noswraid
  fi
}

create_ngcp_partitions() {
  # root
  echo "Creating LV 'root' with ${ROOTFS_SIZE}"
  lvcreate --yes -n root -L "${ROOTFS_SIZE}" "${VG_NAME}"

  echo "Creating ${FILESYSTEM} filesystem on /dev/${VG_NAME}/root"
  mkfs."${FILESYSTEM}" -FF /dev/"${VG_NAME}"/root

  # used later by installer
  ROOT_FS="/dev/mapper/${VG_NAME}-root"

  # fallback
  if [[ "${FALLBACKFS_SIZE}" != "0" ]]; then
    echo "Creating LV 'fallback' with ${FALLBACKFS_SIZE}"
    lvcreate --yes -n fallback -L "${FALLBACKFS_SIZE}" "${VG_NAME}"

    echo "Creating ${FILESYSTEM} filesystem on /dev/${VG_NAME}/fallback"
    mkfs."${FILESYSTEM}" -FF /dev/"${VG_NAME}"/fallback

    # used later by installer
    FALLBACK_FS="/dev/mapper/${VG_NAME}-fallback"
  fi

  # data
  local vg_free data_size unassigned
  vg_free=$(vgs "${VG_NAME}" -o vg_free --noheadings --nosuffix --units B)
  data_size=$(( vg_free * 18 / 20 )) # 90% of free space (in MB)
  unassigned=$(( vg_free - data_size ))
  # make sure we have 10% or at least 500MB unassigned space
  if [[ "${unassigned}" -lt 524288000 ]] ; then # 500MB
    data_size=$(( vg_free - 524288000 ))
  fi

  local data_size_mb
  data_size_mb=$(( data_size / 1024 / 1024 ))
  echo "Creating LV data with ${data_size_mb}M"
  lvcreate --yes -n data -L "${data_size_mb}M" "${VG_NAME}"

  echo "Creating ${FILESYSTEM} on /dev/${VG_NAME}/data"
  mkfs."${FILESYSTEM}" -FF /dev/"${VG_NAME}"/data

  # used later by installer
  DATA_PARTITION="/dev/mapper/${VG_NAME}-data"
}

create_debian_partitions() {
  # rootfs
  local root_size="${ROOTFS_SIZE:-8G}"
  echo "Creating LV root with ${root_size}"
  lvcreate --yes -n root -L "${root_size}" "${VG_NAME}"

  echo "Creating ${FILESYSTEM} on /dev/${VG_NAME}/root"
  mkfs."${FILESYSTEM}" -FF /dev/"${VG_NAME}"/root

  # used later by installer
  ROOT_FS="/dev/mapper/${VG_NAME}-root"
}

display_partition_table() {
  local blockdevice
  if [[ "${SWRAID}" = "true" ]] ; then
    for disk in "${SWRAID_DISK1}" "${SWRAID_DISK2}" ; do
      echo "Displaying partition table (of SW-RAID disk /dev/$disk) for reference:"
      parted -s "/dev/${disk}" unit GiB print
      lsblk "/dev/${disk}"
    done
  else
    echo "Displaying partition table (of /dev/$disk) for reference:"
    parted -s "/dev/${DISK}" unit GiB print
    lsblk "/dev/${DISK}"
  fi
}

enable_ntp() {
  echo "Displaying original timedatectl status:"
  timedatectl status

  if systemctl cat chrony &>/dev/null ; then
    echo "Ensuring chrony service is running"
    systemctl start chrony

    echo "Enabling NTP synchronization"
    timedatectl set-ntp true || true
  elif systemctl cat ntpsec &>/dev/null ; then
    echo "Ensuring ntpsec service is running"
    systemctl start ntpsec

    echo "Enabling NTP synchronization"
    timedatectl set-ntp true || true
  else
    echo "No ntp service identified, skipping NTP synchronization"
  fi

  echo "Disabling RTC for local time"
  timedatectl set-local-rtc false

  echo "Displaying new timedatectl status:"
  timedatectl status
}

lvm_setup() {
  local saved_options
  saved_options="$(set +o)"
  # be restrictive in what we execute
  set -euo pipefail

  if "$NGCP_INSTALLER" ; then
    VG_NAME="ngcp"
    set_up_partition_table
    create_ngcp_partitions
    display_partition_table
  else
    VG_NAME="vg0"
    set_up_partition_table
    create_debian_partitions
    display_partition_table
  fi

  # used later by installer
  ROOT_FS="/dev/mapper/${VG_NAME}-root"

  # restore original options/behavior
  eval "$saved_options"
}

retrieve_deployment_scripts_fake_uname() {
  local target_path="$1"
  case "${SP_VERSION}" in
    trunk)
      local repos_base_path="${SIPWISE_URL}/autobuild/dists/release-trunk-${DEBIAN_RELEASE}/main/binary-amd64/"
      local deployment_path="${SIPWISE_URL}/autobuild/pool/main/n/ngcp-deployment-iso"
      ;;
    trunk-weekly)
      local repos_base_path="${SIPWISE_URL}/autobuild/release/release-${SP_VERSION}/dists/release-${SP_VERSION}/main/binary-amd64/"
      local deployment_path="${SIPWISE_URL}/autobuild/release/release-${SP_VERSION}/pool/main/n/ngcp-deployment-iso/"
      ;;
    *)
      echo "NOTE: SP_VERSION is unset, assuming we're installing a non-NGCP system"
      echo "NOTE: using fake-uname.so of ngcp-deployment-scripts from release-trunk-${DEBIAN_RELEASE}"
      local repos_base_path="${SIPWISE_URL}/autobuild/dists/release-trunk-${DEBIAN_RELEASE}/main/binary-amd64/"
      local deployment_path="${SIPWISE_URL}/autobuild/pool/main/n/ngcp-deployment-iso"
      ;;
  esac

  wget --timeout=30 -O Packages.gz "${repos_base_path}Packages.gz"
  # sed: display paragraphs matching the "Package: ..." string, then grab string "^Version: " and display the actual version via awk
  # sort -u to avoid duplicates in repositories
  local version
  version=$(zcat Packages.gz | sed "/./{H;\$!d;};x;/Package: ngcp-deployment-scripts/b;d" | awk '/^Version: / {print $2}' | sort -u)

  [ -n "$version" ] || die "Error: ngcp-deployment-scripts version for release-trunk-${DEBIAN_RELEASE} could not be detected."

  # retrieve Debian package
  local deb_package="ngcp-deployment-scripts_${version}_amd64.deb"
  local deployment_scripts_package="${deployment_path}/${deb_package}"
  wget --timeout=30 -O "/root/${deb_package}" "${deployment_scripts_package}"

  # extract Debian package
  dpkg -x "/root/${deb_package}" /root/ngcp-deployment-scripts/

  # finally install extracted fake-uname.so towards target
  if [ -r /root/ngcp-deployment-scripts/usr/lib/ngcp-deployment-scripts/fake-uname.so ] ; then
    echo "Installing fake-uname.so from ${deb_package} to ${target_path}"
    cp /root/ngcp-deployment-scripts/usr/lib/ngcp-deployment-scripts/fake-uname.so "${target_path}" || die "Failed to install fake_uname.so in ${target_path}"
  else
    die "Error: can not access fake-uname.so from /root/${deb_package}"
  fi
}

get_installer_path() {
  if [ -z "$SP_VERSION" ] && ! $TRUNK_VERSION ; then
    INSTALLER=ngcp-installer-latest.deb

    if "$PRO_EDITION" ; then
      INSTALLER_PATH="${SIPWISE_URL}/sppro/"
    else
      INSTALLER_PATH="${SIPWISE_URL}/spce/"
    fi

    return # we don't want to run any further code from this function
  fi

  # use pool directory according for ngcp release
  if "${PRO_EDITION}" || "${CARRIER_EDITION}" ; then
    local installer_package='ngcp-installer-pro'
    local repos_base_path="${SIPWISE_URL}/sppro/${SP_VERSION}/dists/${DEBIAN_RELEASE}/main/binary-amd64/"
    INSTALLER_PATH="${SIPWISE_URL}/sppro/${SP_VERSION}/pool/main/n/ngcp-installer/"
  else
    local installer_package='ngcp-installer-ce'
    local repos_base_path="${SIPWISE_URL}/spce/${SP_VERSION}/dists/${DEBIAN_RELEASE}/main/binary-amd64/"
    INSTALLER_PATH="${SIPWISE_URL}/spce/${SP_VERSION}/pool/main/n/ngcp-installer/"
    if "${NGCP_INTERNAL}"; then
      repos_base_path="${SIPWISE_URL}/autobuild/release/spce/${SP_VERSION}-update/dists/${DEBIAN_RELEASE}/main/binary-amd64/"
      INSTALLER_PATH="${SIPWISE_URL}/autobuild/release/spce/${SP_VERSION}-update/pool/main/n/ngcp-installer/"
      # check if mrX.X.X-update repo has ngcp-installer package
      # if not - fallback to default repo
      local tmp_packages
      tmp_packages=$(mktemp -t ngcp-deployment-installer-path.XXXXXXXXXX)
      wget --timeout=30 -O "${tmp_packages}" "${repos_base_path}Packages"
      local installer_version
      installer_version=$(sed "/./{H;\$!d;};x;/Package: ${installer_package}/b;d" "${tmp_packages}" | awk '/^Version: / {print $2}' | sort -u)
      rm -f "${tmp_packages}"
      if [ -n "${installer_version}" ]; then
        echo "${SP_VERSION}-update contains ngcp-installer, using it, version '${installer_version}'"
      else
        echo "${SP_VERSION}-update does NOT contain ngcp-installer, using default ngcp-installer package"
        repos_base_path="${SIPWISE_URL}/spce/${SP_VERSION}/dists/${DEBIAN_RELEASE}/main/binary-amd64/"
        INSTALLER_PATH="${SIPWISE_URL}/spce/${SP_VERSION}/pool/main/n/ngcp-installer/"
      fi
    fi
  fi

  # use a separate repos for trunk releases
  if $TRUNK_VERSION ; then
    case "${SP_VERSION}" in
      trunk)
        local repos_base_path="${SIPWISE_URL}/autobuild/dists/release-trunk-${DEBIAN_RELEASE}/main/binary-amd64/"
        INSTALLER_PATH="${SIPWISE_URL}/autobuild/pool/main/n/ngcp-installer/"
        ;;
      trunk-weekly)
        local repos_base_path="${SIPWISE_URL}/autobuild/release/release-${SP_VERSION}/dists/release-${SP_VERSION}/main/binary-amd64/"
        INSTALLER_PATH="${SIPWISE_URL}/autobuild/release/release-${SP_VERSION}/pool/main/n/ngcp-installer/"
        ;;
      *) die "Error: unknown TRUNK_VERSION ${SP_VERSION}" ;;
    esac
  fi

  if [ -n "${NGCP_PPA}" ] ; then
    local ppa_tmp_packages
    ppa_tmp_packages=$(mktemp -t ngcp-deployment-installer-path.XXXXXXXXXX)

    echo "NGCP PPA requested, checking ngcp-installer package availability in PPA repo"
    local ppa_repos_base_path="${SIPWISE_URL}/autobuild/dists/${NGCP_PPA}/main/binary-amd64/"
    wget --timeout=30 -O "${ppa_tmp_packages}" "${ppa_repos_base_path}/Packages.gz"

    local installer_ppa_version
    installer_ppa_version=$(zcat "${ppa_tmp_packages}" | sed "/./{H;\$!d;};x;/Package: ${installer_package}/b;d" | awk '/^Version: / {print $2}' | sort -u)
    rm -f "${ppa_tmp_packages}"

    if [ -n "${installer_ppa_version}" ]; then
      echo "NGCP PPA contains ngcp-installer, using it, version '${installer_ppa_version}'"
      local repos_base_path="${ppa_repos_base_path}"
      INSTALLER_PATH="${SIPWISE_URL}/autobuild/pool/main/n/ngcp-installer/"
    else
      echo "NGCP PPA does NOT contain ngcp-installer, using default ngcp-installer package"
    fi
  fi

  wget --timeout=30 -O Packages.gz "${repos_base_path}Packages.gz"
  # sed: display paragraphs matching the "Package: ..." string, then grab string "^Version: " and display the actual version via awk
  # sort -u to avoid duplicates in repositories shipping the ngcp-installer-pro AND ngcp-installer-pro-ha-v3 debs
  local version
  version=$(zcat Packages.gz | sed "/./{H;\$!d;};x;/Package: ${installer_package}/b;d" | awk '/^Version: / {print $2}' | sort -u)

  [ -n "$version" ] || die "Error: installer version for ngcp ${SP_VERSION}, Debian release $DEBIAN_RELEASE with installer package $installer_package could not be detected."

  if "${PRO_EDITION}" || "${CARRIER_EDITION}" ; then
    INSTALLER="ngcp-installer-pro_${version}_all.deb"
  else
    INSTALLER="ngcp-installer-ce_${version}_all.deb"
  fi
}

set_repos() {
  cat > "${TARGET}/etc/apt/sources.list" << EOF
# Please visit /etc/apt/sources.list.d/ instead.
EOF

  cat > "${TARGET}/etc/apt/sources.list.d/debian.list" << EOF
## custom sources.list, deployed via deployment.sh

# Debian repositories
deb ${MIRROR} ${DEBIAN_RELEASE} main contrib non-free
deb ${SEC_MIRROR} ${DEBIAN_RELEASE}-security main contrib non-free
deb ${MIRROR} ${DEBIAN_RELEASE}-updates main contrib non-free
deb ${DBG_MIRROR} ${DEBIAN_RELEASE}-debug main contrib non-free
EOF

  mkdir -p "${TARGET}"/etc/apt/apt.conf.d/
  cat > "${TARGET}"/etc/apt/apt.conf.d/73_acquire_retries << EOF
# NGCP_MANAGED_FILE -- deployment.sh
Acquire::Retries "3";
EOF
}

gen_installer_config() {
  local conf_file
  conf_file="${TARGET}/etc/ngcp-installer/config_deploy.inc"
  truncate -s 0 "${conf_file}"

  if "${CARRIER_EDITION}" ; then
    cat >> "${conf_file}" << EOF
CARRIER=true
PRO=false
CE=false
EOF
  elif "${PRO_EDITION}" ; then
    cat >> "${conf_file}" << EOF
CARRIER=false
PRO=true
CE=false
EOF
  elif "${CE_EDITION}" ; then
    cat >> "${conf_file}" << EOF
CARRIER=false
PRO=false
CE=true
EOF
  fi

  if "${CARRIER_EDITION}" ; then
    cat >> "${conf_file}" << EOF
CROLE="${CROLE}"
VLAN_BOOT_INT="${VLAN_BOOT_INT}"
VLAN_HA_INT="${VLAN_HA_INT}"
VLAN_RTP_EXT="${VLAN_RTP_EXT}"
VLAN_SIP_EXT="${VLAN_SIP_EXT}"
VLAN_SIP_INT="${VLAN_SIP_INT}"
VLAN_SSH_EXT="${VLAN_SSH_EXT}"
VLAN_WEB_EXT="${VLAN_WEB_EXT}"
EOF
  fi
  if "${PRO_EDITION}" ; then
    cat >> "${conf_file}" << EOF
DPL_MYSQL_REPLICATION="${DPL_MYSQL_REPLICATION}"
FILL_APPROX_CACHE="${FILL_APPROX_CACHE}"
HNAME="${NODE_NAME}"
INTERNAL_DEV="${INTERNAL_DEV}"
INTERNAL_NETMASK="${INTERNAL_NETMASK}"
IP1="${IP1}"
IP2="${IP2}"
IP_HA_SHARED="${IP_HA_SHARED}"
MANAGEMENT_IP="${MANAGEMENT_IP}"
NETWORK_DEVICES="${NETWORK_DEVICES}"
TARGET_HOSTNAME="${TARGET_HOSTNAME}"
EOF
  fi

  if "${CE_EDITION}" && "${NGCP_INTERNAL}" ; then
    cat >> "${conf_file}" << EOF
NGCP_INTERNAL=true
EOF
  fi

  cat >> "${conf_file}" << EOF
ADJUST_FOR_LOW_PERFORMANCE="${ADJUST_FOR_LOW_PERFORMANCE}"
DEBUG_MODE="${DEBUG_MODE}"
DEPLOYMENT_SH=true
DHCP="${DHCP}"
EADDR="${EADDR}"
ENABLE_VM_SERVICES="${ENABLE_VM_SERVICES}"
export NGCP_INSTALLER=true
EXTERNAL_DEV="${EXTERNAL_DEV}"
EXTERNAL_NETMASK="${EXTERNAL_NETMASK}"
FALLBACKFS_SIZE="${FALLBACKFS_SIZE}"
FORCE=no
GW="${GW}"
NAMESERVER="$(awk '/^nameserver/ {print $2}' /etc/resolv.conf)"
NGCP_PPA="${NGCP_PPA}"
ORIGIN_INSTALL_DEV="${ORIGIN_INSTALL_DEV}"
ROOTFS_SIZE="${ROOTFS_SIZE}"
SIPWISE_REPO_HOST="${SIPWISE_REPO_HOST}"
SIPWISE_URL="${SIPWISE_URL}"
STATUS_WAIT_SECONDS=${STATUS_WAIT}
SWAPFILE_SIZE_MB="${SWAPFILE_SIZE_MB}"
EOF

  if "${TRUNK_VERSION}" && "${NGCP_UPLOAD}"; then
    echo "NGCPUPLOAD=true" >> "${TARGET}/etc/ngcp-installer/config_deploy.inc"
  else
    echo "NGCPUPLOAD=false" >> "${TARGET}/etc/ngcp-installer/config_deploy.inc"
  fi
}

install_vbox_guest_additions_iso() {
  echo "Downloading virtualbox-guest-additions ISO"

  local virtualbox_dir="${TARGET}/usr/share/virtualbox"
  local virtualbox_iso="VBoxGuestAdditions_7.2.0.iso"
  local virtualbox_iso_checksum="43f7a1045cad0aab40e3af906fea37244ba6873b91b4e227245a14e51b399abd" # sha256
  local virtualbox_iso_url_path="/files/${virtualbox_iso}"

  mkdir -p "${virtualbox_dir}"
  # hardcode filename here, so we can re-use it within vagrant_configuration()
  vbox_isofile="${virtualbox_dir}/VBoxGuestAdditions.iso"
  wget --retry-connrefused --no-verbose -c -O "${vbox_isofile}" "${SIPWISE_URL}${virtualbox_iso_url_path}"

  if ! echo "${virtualbox_iso_checksum} ${vbox_isofile}" | sha256sum --check ; then
    die "Error: failed to compute checksum for ${virtualbox_iso}. Exiting."
  fi
}

vbox_adjust_vboxadd_service() {
  mkdir -p "${TARGET}"/etc/systemd/system/vboxadd.service.d
  mkdir -p "${TARGET}"/etc/systemd/system/vboxadd-service.service.d

  cat > "${TARGET}"/etc/systemd/system/vboxadd.service.d/override.conf << EOF
# deployed via deployment-iso's deployment.sh
[Unit]
ConditionVirtualization=oracle
EOF

  cat > "${TARGET}"/etc/systemd/system/vboxadd-service.service.d/override.conf << EOF
# deployed via deployment-iso's deployment.sh
[Unit]
ConditionVirtualization=oracle
EOF
}

vagrant_configuration() {
  # bzip2, linux-headers-amd64 and make are required for VirtualBox Guest Additions installer
  # less + sudo are required for Vagrant itself
  echo "Installing software for VirtualBox Guest Additions installer"
  if ! grml-chroot "${TARGET}" apt-get -y install bzip2 less libxmu6 linux-headers-amd64 make sudo ; then
    die "Error: failed to install 'bzip2 less libxmu6 linux-headers-amd64 make sudo' packages."
  fi

  vagrant_ssh_pub_key='/var/tmp/id_rsa_sipwise.pub'
  echo "Fetching Sipwise vagrant public key from builder.mgm.sipwise.com"
  if ! wget -O "${vagrant_ssh_pub_key}" http://builder.mgm.sipwise.com/vagrant-ngcp/id_rsa_sipwise.pub ; then
    die "Error: failed to wget public Sipwise SSH key for Vagrant boxes"
  fi

  case "${DEBIAN_RELEASE}" in
    trixie)
      # we need a newer virtualbox-guest-additions-iso version than present in Debian/trixie
      install_vbox_guest_additions_iso
      ;;
    *)
      if ! grml-chroot "${TARGET}" apt-get -y install 'virtualbox-guest-additions-iso' ; then
        die "Error: failed to install 'virtualbox-guest-additions-iso' package."
      fi
      ;;
  esac

  if "$NGCP_INSTALLER" ; then
    local SIPWISE_HOME="/nonexistent"
    # it's necessary to use chroot instead of grml-chroot in variable=$() calls
    # as grml-chroot also prints "Writing /etc/debian_chroot ..." line
    # which breaks output
    SIPWISE_HOME=$(chroot "${TARGET}" getent passwd 'sipwise' | cut -d':' -f6)
    if [[ ! -d "${TARGET}/${SIPWISE_HOME}" ]] ; then
      die "Error: cannot determine home of 'sipwise' user, it does not exist or not a directory: ${TARGET}/${SIPWISE_HOME}"
    fi

    # TODO: move PATH adjustment to ngcp-installer (ngcpcfg should have a template here)
    if ! grep -q '^# Added for Vagrant' "${TARGET}/${SIPWISE_HOME}/.profile" 2>/dev/null ; then
      echo "Adjusting PATH configuration for user Sipwise"
      echo "# Added for Vagrant" >> "${TARGET}/${SIPWISE_HOME}/.profile"
      echo "PATH=\$PATH:/sbin:/usr/sbin" >> "${TARGET}/${SIPWISE_HOME}/.profile"
    fi

    echo "Adjusting ssh configuration for user sipwise (add Vagrant SSH key)"
    mkdir -p "${TARGET}/${SIPWISE_HOME}/.ssh/"
    cat "${vagrant_ssh_pub_key}" >> "${TARGET}/${SIPWISE_HOME}/.ssh/sipwise_vagrant_key"
    grml-chroot "${TARGET}" chown sipwise:sipwise "${SIPWISE_HOME}/.ssh" "${SIPWISE_HOME}/.ssh/sipwise_vagrant_key"
    grml-chroot "${TARGET}" chmod 0600 "${SIPWISE_HOME}/.ssh/sipwise_vagrant_key"
  fi

  echo "Adjusting ssh configuration for user root"
  mkdir -p "${TARGET}/root/.ssh/"
  cat "${vagrant_ssh_pub_key}" >> "${TARGET}/root/.ssh/sipwise_vagrant_key"
  grml-chroot "${TARGET}" chmod 0600 /root/.ssh/sipwise_vagrant_key
  sed -i 's|^[#\s]*\(AuthorizedKeysFile.*\)$|\1 %h/.ssh/sipwise_vagrant_key|g' "${TARGET}/etc/ssh/sshd_config"

  # see https://github.com/mitchellh/vagrant/issues/1673
  # and https://bugs.launchpad.net/ubuntu/+source/xen-3.1/+bug/1167281
  if ! grep -q 'adjusted for Vagrant' "${TARGET}/root/.profile" ; then
    echo "Adding workaround for annoying bug 'stdin: is not a tty' Vagrant message"
    sed -ri -e "s/mesg\s+n/# adjusted for Vagrant\ntty -s \&\& mesg n/" "${TARGET}/root/.profile"
  fi

  # shellcheck disable=SC2010
  KERNELHEADERS=$(basename "$(ls -d "${TARGET}"/usr/src/linux-headers*amd64 | grep -v -- -rt-amd64 | sort -u -r -V | head -1)")
  if [ -z "$KERNELHEADERS" ] ; then
    die "Error: no kernel headers found for building the VirtualBox Guest Additions kernel module."
  fi
  KERNELVERSION=${KERNELHEADERS##linux-headers-}
  if [ -z "$KERNELVERSION" ] ; then
    die "Error: no kernel version could be identified."
  fi

  local VIRTUALBOX_DIR="${TARGET}/usr/share/virtualbox"
  local VIRTUALBOX_ISO="VBoxGuestAdditions.iso"
  local vbox_isofile="${VIRTUALBOX_DIR}/${VIRTUALBOX_ISO}"

  if [ ! -r "$vbox_isofile" ] ; then
    die "Error: could not find $vbox_isofile"
  fi

  # ensure we don't try to run vbox* services outside of VirtualBox environments
  vbox_adjust_vboxadd_service

  mkdir -p "${TARGET}/media/cdrom"
  mountpoint "${TARGET}/media/cdrom" >/dev/null && umount "${TARGET}/media/cdrom"
  mount -t iso9660 "${vbox_isofile}" "${TARGET}/media/cdrom/"
  # avoid "ERROR: ld.so: object '/usr/lib/ngcp-deployment-scripts/fake-uname.so' from LD_PRELOAD cannot be preloaded: ignored."
  # messages caused by the host system when running grml-chroot process
  mkdir -p /usr/lib/ngcp-deployment-scripts/
  if [ -r /mnt/usr/lib/ngcp-deployment-scripts/fake-uname.so ] ; then
    cp /mnt/usr/lib/ngcp-deployment-scripts/fake-uname.so /usr/lib/ngcp-deployment-scripts/
    FAKE_UNAME='/usr/lib/ngcp-deployment-scripts/fake-uname.so'
  else
    echo "File /mnt/usr/lib/ngcp-deployment-scripts/fake-uname.so does not exist (building base image without ngcp?)"
    echo "Trying to retrieve fake-uname.so from ngcp-deployment-scripts of release-trunk..."
    retrieve_deployment_scripts_fake_uname /tmp/
    # we don't have /usr/lib/ngcp-deployment-scripts/, so use it
    # via /tmp as that's automatically cleaned during reboot
    cp /tmp/fake-uname.so /mnt/tmp/
    FAKE_UNAME='/tmp/fake-uname.so'
  fi

  local vbox_systemd_workaround=false
  if ! [ -d "${TARGET}/run/systemd/system" ] ; then
    echo "Creating /run/systemd/system for systemd detection within VBoxLinuxAdditions"
    mkdir -p "${TARGET}/run/systemd/system"
    vbox_systemd_workaround=true
  fi

  grml-chroot "${TARGET}" env \
    UTS_RELEASE="${KERNELVERSION}" LD_PRELOAD="${FAKE_UNAME}" \
    /media/cdrom/VBoxLinuxAdditions.run --nox11 || true
  tail -10 "${TARGET}/var/log/vboxadd-install.log"
  umount "${TARGET}/media/cdrom/"

  if "${vbox_systemd_workaround}" ; then
    echo "Removing /run/systemd/system workaround for VBoxLinuxAdditions again"
    rmdir "${TARGET}/run/systemd/system" || true
  fi

  # VBoxLinuxAdditions.run chooses /usr/lib64 as soon as this directory exists, which
  # is the case for our PRO systems shipping the heartbeat-2 package; then the
  # symlink /sbin/mount.vboxsf points to the non-existing /usr/lib64/VBoxGuestAdditions/mount.vboxsf
  # file instead of pointing to /usr/lib/x86_64-linux-gnu/VBoxGuestAdditions/mount.vboxsf
  if ! grml-chroot "${TARGET}" readlink -f /sbin/mount.vboxsf ; then
    echo "Installing mount.vboxsf symlink to work around /usr/lib64 issue"
    ln -sf /usr/lib/x86_64-linux-gnu/VBoxGuestAdditions/mount.vboxsf "${TARGET}/sbin/mount.vboxsf"
  fi

  if [ -d "${TARGET}/etc/.git" ]; then
    echo "Commit /etc/* changes using etckeeper"
    grml-chroot "${TARGET}" etckeeper commit "Vagrant/VirtualBox changes on /etc/*"
  fi
}

check_puppet_rc() {
  local _puppet_rc="$1"
  local _expected_rc="$2"

  if [ "${_puppet_rc}" != "${_expected_rc}" ] ; then
    # an exit code of '0' happens for 'puppet agent --enable' only,
    # an exit code of '2' means there were changes,
    # an exit code of '4' means there were failures during the transaction,
    # an exit code of '6' means there were both changes and failures.
    set_deploy_status "error"
  fi
}

check_puppet_rerun() {
  local repeat=1

  if ! "${NO_PUPPET_REPEAT}" && [ "$(get_deploy_status)" = "error" ] ; then
    echo "Do you want to [r]epeat puppet run or [c]ontinue?"
    while true; do
      read -r a
      case "${a,,}" in
        r)
          echo "Repeating puppet run."
          repeat=0
          set_deploy_status "puppet"
          break
          ;;
        c)
          echo "Continue without repeating puppet run."
          set_deploy_status "puppet"
          break
          ;;
        * ) echo -n "Please answer 'r' to repeat or 'c' to continue: " ;;
      esac
      unset a
    done
  fi

  return "${repeat}"
}

check_puppetserver_time() {
  while true; do
    offset=$(ntpdate -q "$PUPPET_SERVER" | sed -n '1s/.*offset \(.*\),.*/\1/p' | tr -d -)
    seconds=${offset%.*}
    if (( seconds < 10 )) ; then
      echo "All OK. Time offset between $PUPPET_SERVER and current server is $seconds seconds only."
      break
    elif "${NO_PUPPET_REPEAT}"; then
      echo "WARNING: time offset between $PUPPET_SERVER and current server is $seconds seconds."
      echo "(ignoring due to boot option nopuppetrepeat)"
      break
    else
      echo "WARNING: time difference between the current server and $PUPPET_SERVER is ${seconds} seconds (bigger than 10 seconds)."
      echo "Please synchronize time and press any key to recheck or [c]ontinue with puppet run."
      read -r a
      case "${a,,}" in
        c)
          echo "Continue ignoring time offset check."
          break
          ;;
        * ) echo -n "Rechecking the offset..." ;;
      esac
      unset a
    fi
  done
}

puppet_install_from_git() {
  local repeat

  : "${PUPPET_GIT_REPO?ERROR: variable 'PUPPET_GIT_REPO' is NOT defined, cannot continue.}"
  : "${PUPPET_LOCAL_GIT?ERROR: variable 'PUPPET_LOCAL_GIT' is NOT defined, cannot continue.}"
  : "${PUPPET_GIT_BRANCH?ERROR: variable 'PUPPET_GIT_BRANCH' is NOT defined, cannot continue.}"

  echo "Searching for Hiera rescue device by label '${PUPPET_RESCUE_LABEL}'..."
  local PUPPET_RESCUE_DRIVE
  PUPPET_RESCUE_DRIVE=$(blkid | grep -E "LABEL=\"${PUPPET_RESCUE_LABEL}" | head -1 | awk -F: '{print $1}')

  if [ -n "${PUPPET_RESCUE_DRIVE}" ] ; then
    echo "Found Hiera rescue device: '${PUPPET_RESCUE_DRIVE}'"
  else
    die "ERROR: No USB device found matching label '${PUPPET_RESCUE_LABEL}', cannot continue!"
  fi

  echo "Searching for Hiera rescue device type..."
  local DEVICE_TYPE
  DEVICE_TYPE=$(blkid | grep -E "LABEL=\"${PUPPET_RESCUE_LABEL}" | head -1 | sed 's/.*TYPE="\(.*\)".*/\1/')

  if [ -n "${DEVICE_TYPE}" ] ; then
    echo "Hiera rescue device type is:'${DEVICE_TYPE}'"
  else
    die "ERROR: Cannot detect device type for device '${PUPPET_RESCUE_LABEL}', cannot continue!"
  fi

  echo "Copying data from device '${PUPPET_RESCUE_DRIVE}' (mounted into '${PUPPET_RESCUE_PATH}', type '${DEVICE_TYPE}')"
  mkdir -p "${PUPPET_RESCUE_PATH}"
  mount -t "${DEVICE_TYPE}" -o ro "${PUPPET_RESCUE_DRIVE}" "${PUPPET_RESCUE_PATH}"
  mkdir -p "${TARGET}/etc/puppetlabs/code/hieradata/"
  chmod 0700 "${TARGET}/etc/puppetlabs/code/hieradata/"
  cp -a "${PUPPET_RESCUE_PATH}"/hieradata/* "${TARGET}/etc/puppetlabs/code/hieradata/"
  mkdir -p ~/.ssh
  cp "${PUPPET_RESCUE_PATH}"/hieradata/defaults.d/id_rsa_r10k ~/.ssh/
  chmod 600 ~/.ssh/id_rsa_r10k
  umount -f "${PUPPET_RESCUE_PATH}"
  rmdir "${PUPPET_RESCUE_PATH}"

  echo "Cloning Puppet git repository from '${PUPPET_GIT_REPO}' to '${PUPPET_LOCAL_GIT}' (branch '${PUPPET_GIT_BRANCH}')"
  echo 'ssh -i ~/.ssh/id_rsa_r10k -o PubkeyAcceptedKeyTypes=+ssh-rsa -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $*' > ssh
  chmod +x ssh
  if ! GIT_SSH="${PWD}/ssh" git clone --depth 1 -b "${PUPPET_GIT_BRANCH}" "${PUPPET_GIT_REPO}" "${PUPPET_LOCAL_GIT}" ; then
    die "ERROR: Cannot clone git repository, see the error above, cannot continue!"
  fi
  rm "${PWD}/ssh"

  local PUPPET_CODE_PATH
  PUPPET_CODE_PATH="/etc/puppetlabs/code/environments/${PUPPET}"

  echo "Creating empty Puppet environment ${TARGET}/${PUPPET_CODE_PATH}"
  mkdir -p "${TARGET}/${PUPPET_CODE_PATH}"
  chmod 0755 "${TARGET}/${PUPPET_CODE_PATH}"

  echo "Deploying Puppet code from Git repository to ${TARGET}/${PUPPET_CODE_PATH}"
  cp -a "${PUPPET_LOCAL_GIT}"/* "${TARGET}/${PUPPET_CODE_PATH}"
  rm -rf "${PUPPET_LOCAL_GIT:?}"

  repeat=true
  while $repeat ; do
    repeat=false
    echo "Initializing Hiera config..."
    grml-chroot "${TARGET}" puppet apply --test --modulepath="${PUPPET_CODE_PATH}/modules" \
          -e "include puppet::hiera" 2>&1 | tee -a /tmp/puppet.log
    check_puppet_rc "${PIPESTATUS[0]}" "2"
    check_puppet_rerun && repeat=true
  done

  repeat=true
  while $repeat ; do
    repeat=false
    echo "Running Puppet core deployment..."
    grml-chroot "${TARGET}" puppet apply --test --modulepath="${PUPPET_CODE_PATH}/modules" --tags core,apt \
          "${PUPPET_CODE_PATH}/manifests/site.pp" 2>&1 | tee -a /tmp/puppet.log
    check_puppet_rc "${PIPESTATUS[0]}" "2"
    check_puppet_rerun && repeat=true
  done

  repeat=true
  while $repeat ; do
    repeat=false
    echo "Running Puppet main deployment..."
    grml-chroot "${TARGET}" puppet apply --test --modulepath="${PUPPET_CODE_PATH}/modules" \
          "${PUPPET_CODE_PATH}/manifests/site.pp" 2>&1 | tee -a /tmp/puppet.log
    check_puppet_rc "${PIPESTATUS[0]}" "2"
    check_puppet_rerun && repeat=true
  done

  return 0
}

puppet_install_from_puppet() {
  local repeat

  check_puppetserver_time

  repeat=true
  while $repeat ; do
    repeat=false
    echo "Running Puppet core deployment..."
    grml-chroot "${TARGET}" puppet agent --test --tags core,apt 2>&1 | tee -a /tmp/puppet.log
    check_puppet_rc "${PIPESTATUS[0]}" "2"
    check_puppet_rerun && repeat=true
  done

  repeat=true
  while $repeat ; do
    repeat=false
    echo "Running Puppet main deployment..."
    grml-chroot "${TARGET}" puppet agent --test 2>&1 | tee -a /tmp/puppet.log
    check_puppet_rc "${PIPESTATUS[0]}" "2"
    check_puppet_rerun && repeat=true
  done

  return 0
}

set_custom_grub_boot_options() {
  echo "Adjusting default GRUB boot options (enabling net.ifnames=0)"
  sed -i 's/^GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"/GRUB_CMDLINE_LINUX_DEFAULT="\1 net.ifnames=0"/' "${TARGET}/etc/default/grub"

  echo "Invoking update-grub"
  grml-chroot "${TARGET}" update-grub

  if [ -d "${TARGET}/etc/.git" ]; then
    echo "Commit /etc/default/grub changes using etckeeper"
    grml-chroot "${TARGET}" etckeeper commit "/etc/default/grub changes"
  fi
}

get_ping_host() {
  local route

  route="$(route -n | awk '/^0\.0\.0\.0/{print $2}')"

  if [ -n "${route:-}" ] ; then
    ping_host="${route}"
    echo "Default route identified, using host ${ping_host}"
  else
    ping_host="${SIPWISE_REPO_HOST:-deb.sipwise.com}"
    echo "Default route identified, using host ${ping_host} instead"
  fi
}

wait_for_network_online() {
  local tries="${1:-30}"

  echo "Trying reach host ${ping_host} via ICMP/ping to check connectivity"
  while ! ping -O -D -c 1 -i 1 -W 1 "${ping_host}" ; do
    if [ "${tries}" -gt 0 ] ; then
      tries=$((tries-1))
      echo "Retrying ping to ${ping_host} again ($tries tries left)..."
      sleep 1
    else
      echo "WARN: couldn't reach host ${ping_host} via ICMP/ping, continuing anyway"
      break
    fi
  done
}

# Main script

INSTALL_LOG='/tmp/deployment-installer-debug.log'
exec  > >(tee -a $INSTALL_LOG    )
exec 2> >(tee -a $INSTALL_LOG >&2)

# ensure we can interact with stdin
grml_autoconfig_active=false
case "$(systemctl is-active grml-autoconfig)" in
  active|activating)
    grml_autoconfig_active=true
    ;;
esac

if "${grml_autoconfig_active}" ; then
  if systemctl cat grml-autoconfig.service | grep -q StandardInput=null ; then
    echo "Looks like we're running under systemd with /dev/null for stdin."
    echo "Re-executing with usage of /dev/tty1 for stdin"
    exec < /dev/tty1
  fi
fi

# set version to git commit ID
SCRIPT_VERSION="%SCRIPT_VERSION%"

# not set? then fall back to timestamp of execution
if [ -z "$SCRIPT_VERSION" ] || [ "$SCRIPT_VERSION" = '%SCRIPT_VERSION%' ] ; then
  SCRIPT_VERSION=$(date +%s) # seconds since 1970-01-01 00:00:00 UTC
fi

# Never ever execute the script outside of a
# running Grml live system because partitioning
# disks might destroy data. Seriously.
if ! [ -r /etc/grml_cd ] ; then
  echo "Not running inside Grml, better safe than sorry. Sorry." >&2
  exit 1
fi

# better safe than sorry
export LC_ALL=C
export LANG=C

# avoid SHELL being set but not available, causing needrestart failure, see #788819
unset SHELL

# defaults
ADDITIONAL_PACKAGES=(git augeas-tools gdisk qemu-guest-agent)
ADJUST_FOR_LOW_PERFORMANCE=false
ARCH=$(dpkg --print-architecture)
CARRIER_EDITION=false
CE_EDITION=false
CROLE=''
DEBIAN_RELEASE='trixie'
DEBIAN_REPO_HOST="debian.sipwise.com"
DEBIAN_REPO_TRANSPORT="https"
DEBUG_MODE=false
DHCP=false
DPL_MYSQL_REPLICATION=true
EADDR=''
EATMYDATA=true
ENABLE_VM_SERVICES=false
EXTERNAL_NETMASK=''
FALLBACKFS_SIZE=''
FILESYSTEM="ext4"
FILL_APPROX_CACHE=true
HALT=false
INTERACTIVE=true
INTERNAL_DEV='eth1'
INTERNAL_NETMASK='255.255.255.248'
IP1='192.168.255.251'
IP2='192.168.255.252'
IP_HA_SHARED='192.168.255.250'
IP_LINE=''
LOGO=true
NGCP_INSTALLER=false
NGCP_INTERNAL=false
NGCP_PXE_INSTALL=false
NGCP_UPLOAD=false
NODE_NAME=''
NO_PUPPET_REPEAT=false
PRO_EDITION=false
PUPPET=''
PUPPET_GIT_BRANCH=master
PUPPET_GIT_REPO=''
PUPPET_LOCAL_GIT="${TARGET}/tmp/puppet.git"
PUPPET_RESCUE_LABEL="SIPWRESCUE*"
PUPPET_RESCUE_PATH="/mnt/rescue_drive"
PUPPET_SERVER='puppet.mgm.sipwise.com'
REBOOT=false
RETRIEVE_MGMT_CONFIG=false
ROOTFS_SIZE="10G"
SIPWISE_REPO_HOST="deb.sipwise.com"
SIPWISE_REPO_TRANSPORT="https"
STATUS_DIRECTORY='/srv/deployment/'
STATUS_WAIT=0
SWAPFILE_SIZE_MB=""
SWAPFILE_SIZE_MB_MAX="16384"
SWAPFILE_SIZE_MB_MIN="4096"
SWRAID_DESTROY=false
SWRAID_DEVICE="/dev/md0"
SWRAID=false
TARGET='/mnt'
TRUNK_VERSION=false
VAGRANT=false
VLAN_BOOT_INT=2
VLAN_HA_INT=1721
VLAN_RTP_EXT=1722
VLAN_SIP_EXT=1719
VLAN_SIP_INT=1720
VLAN_SSH_EXT=300
VLAN_WEB_EXT=1718

# trap signals: 1 SIGHUP, 2 SIGINT, 3 SIGQUIT, 6 SIGABRT, 15 SIGTERM
trap 'wait_exit;' 1 2 3 6 15 ERR EXIT

echo "Host IP: $(ip-screen)"
echo "Deployment version: $SCRIPT_VERSION"

enable_deploy_status_server

set_deploy_status "checkBootParam"

declare -a PARAMS=()
CMD_LINE=$(cat /proc/cmdline)
IFS=" " read -r -a PARAMS <<< "${CMD_LINE}"
PARAMS+=("$@")

for param in "${PARAMS[@]}" ; do
  case "${param}" in
    arch=*)
      ARCH="${param//arch=/}"
    ;;
    debianrelease=*)
      DEBIAN_RELEASE="${param//debianrelease=/}"
    ;;
    debianrepo=*)
      DEBIAN_REPO_HOST="${param//debianrepo=/}"
    ;;
    debianrepotransport=*)
      DEBIAN_REPO_TRANSPORT="${param//debianrepotransport=/}"
    ;;
    debugmode)
      DEBUG_MODE=true
      enable_trace
      echo "CMD_LINE: ${CMD_LINE}"
    ;;
    enablevmservices)
      ENABLE_VM_SERVICES=true
    ;;
    help|-help)
      usage
      exit 0
    ;;
    lowperformance)
      ADJUST_FOR_LOW_PERFORMANCE=true
    ;;
    ngcpce)
      CE_EDITION=true
      TARGET_HOSTNAME='spce'
      NGCP_INSTALLER=true
      NODE_NAME='spce'
    ;;
    ngcpcrole=*)
      CARRIER_EDITION=true
      CROLE="${param//ngcpcrole=/}"
    ;;
    ngcpeaddr=*)
      EADDR="${param//ngcpeaddr=/}"
    ;;
    ngcpextnetmask=*)
      EXTERNAL_NETMASK="${param//ngcpextnetmask=/}"
    ;;
    ngcphalt)
      HALT=true
    ;;
    ngcphostname=*)
      TARGET_HOSTNAME="${param//ngcphostname=/}"
    ;;
    ngcpinst)
      NGCP_INSTALLER=true
    ;;
    ngcpinternal)
      NGCP_INTERNAL=true
    ;;
    ngcpip1=*)
      IP1="${param//ngcpip1=/}"
    ;;
    ngcpip2=*)
      IP2="${param//ngcpip2=/}"
    ;;
    ngcpipshared=*)
      IP_HA_SHARED="${param//ngcpipshared=/}"
    ;;
    *ngcpnetmask=*)
      INTERNAL_NETMASK="${param//ngcpnetmask=/}"
    ;;
    ngcpnw.dhcp)
      DHCP=true
    ;;
    ngcpppa=*)
      NGCP_PPA="${param//ngcpppa=/}"
    ;;
    ngcppro)
      PRO_EDITION=true
      NGCP_INSTALLER=true
    ;;
    ngcpreboot)
      REBOOT=true
    ;;
    ngcpnodename=*)
      NODE_NAME="${param//ngcpnodename=/}"
    ;;
    ngcpstatus=*)
      STATUS_WAIT="${param//ngcpstatus=/}"
    ;;
    ngcpvers=*)
      SP_VERSION="${param//ngcpvers=/}"
    ;;
    ngcpvlanbootint=*)
      VLAN_BOOT_INT="${param//ngcpvlanbootint=/}"
    ;;
    ngcpvlanhaint=*)
      VLAN_HA_INT="${param//ngcpvlanhaint=/}"
    ;;
    ngcpvlanrtpext=*)
      VLAN_RTP_EXT="${param//ngcpvlanrtpext=/}"
    ;;
    ngcpvlansipext=*)
      VLAN_SIP_EXT="${param//ngcpvlansipext=/}"
    ;;
    ngcpvlansipint=*)
      VLAN_SIP_INT="${param//ngcpvlansipint=/}"
    ;;
    ngcpvlansshext=*)
      VLAN_SSH_EXT="${param//ngcpvlansshext=/}"
    ;;
    ngcpvlanwebext=*)
      VLAN_WEB_EXT="${param//ngcpvlanwebext=/}"
    ;;
    nocolorlogo)
      LOGO=false
    ;;
    noeatmydata)
      EATMYDATA=false
    ;;
    noinstall)
      echo "Exiting as requested via bootoption noinstall."
      exit 0
    ;;
    nongcp)
      NGCP_INSTALLER=false
    ;;
    targetdisk=*)
      TARGET_DISK="${param//targetdisk=/}"
    ;;
    swraiddestroy)
      SWRAID_DESTROY=true
    ;;
    swraiddisk1=*)
      SWRAID_DISK1="${param//swraiddisk1=/}"
      SWRAID_DISK1="${SWRAID_DISK1#/dev/}"
    ;;
    swraiddisk2=*)
      SWRAID_DISK2="${param//swraiddisk2=/}"
      SWRAID_DISK2="${SWRAID_DISK2#/dev/}"
    ;;
    swapfilesize=*)
      SWAPFILE_SIZE_MB="${param//swapfilesize=/}"
    ;;
    vagrant)
      VAGRANT=true
    ;;
    ngcpmgmt=*)
      MANAGEMENT_IP="${param//ngcpmgmt=/}"
      RETRIEVE_MGMT_CONFIG=true
    ;;
    puppetenv=*)
      # we expected to get the environment for puppet
      PUPPET="${param//puppetenv=/}"
    ;;
    puppetserver=*)
      PUPPET_SERVER="${param//puppetserver=/}"
    ;;
    puppetgitrepo=*)
      PUPPET_GIT_REPO="${param//puppetgitrepo=/}"
    ;;
    puppetgitbranch=*)
      PUPPET_GIT_BRANCH="${param//puppetgitbranch=/}"
    ;;
    rootfssize=*)
      ROOTFS_SIZE="${param//rootfssize=/}"
    ;;
    fallbackfssize=*)
      FALLBACKFS_SIZE="${param//fallbackfssize=/}"
    ;;
    sipwiserepo=*)
      SIPWISE_REPO_HOST="${param//sipwiserepo=/}"
    ;;
    ngcpnomysqlrepl)
      DPL_MYSQL_REPLICATION=false
    ;;
    sipwiserepotransport=*)
      SIPWISE_REPO_TRANSPORT="${param//sipwiserepotransport=/}"
    ;;
    ngcppxeinstall)
      NGCP_PXE_INSTALL=true
    ;;
    ip=*)
      IP_LINE="${param//ip=/}"
    ;;
    ngcpupload)
      NGCP_UPLOAD=true
    ;;
    nopuppetrepeat)
      NO_PUPPET_REPEAT=true
    ;;
    *)
      echo "Parameter ${param} not defined in script, skipping"
    ;;
  esac
done


disable_systemd_tmpfiles_clean

# configure static network in installed system?
if pgrep dhclient &>/dev/null ; then
  DHCP=true
fi

if [[ ${SP_VERSION} =~ ^trunk ]]; then
  TRUNK_VERSION=true
fi

# if TARGET_DISK environment variable is set accept it
if [ -n "$TARGET_DISK" ] ; then
  export DISK="${TARGET_DISK}"
else # otherwise try to find sane default
  for i in /sys/block/sd* /sys/block/vd* /sys/block/nvme*n* ; do
    # device might not be a HDD, but a CDROM or a removable device
    if grep -q 0 "${i}/removable" 2>/dev/null; then
      DISK=$(basename "$i")
      export DISK
      break
    fi
  done
  echo "NOTE: used autodetection code to assigning disk '${DISK}'"
fi

if [[ -n "${SWRAID_DISK1}" ]] && [[ -z "${SWRAID_DISK2}" ]] ; then
  die "Error: swraiddisk1 is set, but swraiddisk2 is unset."
elif [[ -z "${SWRAID_DISK1}" ]] && [[ -n "${SWRAID_DISK2}" ]] ; then
  die "Error: swraiddisk2 is set, but swraiddisk1 is unset."
elif [[ -n "${SWRAID_DISK1}" ]] && [[ -n "${SWRAID_DISK2}" ]] ; then
  echo "Identified valid boot options for Software RAID setup."
  SWRAID=true
else
  [[ -z "${DISK}" ]] && die "Error: No non-removable disk suitable for installation found"
fi

# disk information and set GRUB target
if [[ "${SWRAID}" = "true" ]] ; then
  DISK_INFO="Software-RAID with $SWRAID_DISK1 $SWRAID_DISK2"
  GRUB_DISK="/dev/${SWRAID_DISK1}"  # we iterate over the disks in swraidinstallgrub
else
  DISK_INFO="/dev/$DISK"
  GRUB_DISK="/dev/$DISK"
fi

## detect environment {{{
CHASSIS="No physical chassis found"
if dmidecode| grep -q 'Rack Mount Chassis' ; then
  CHASSIS="Running in Rack Mounted Chassis."
elif dmidecode| grep -q 'Location In Chassis: Not Specified'; then
  :
elif dmidecode| grep -q 'Location In Chassis'; then
  CHASSIS="Running in blade chassis $(dmidecode| awk '/Location In Chassis: Slot/ {print $4}')"
fi

if "${CE_EDITION}"; then
  NGCP_INSTALLER_EDITION_STR="Sipwise C5:        CE"
elif "${CARRIER_EDITION}"; then
  NGCP_INSTALLER_EDITION_STR="Sipwise C5:        CARRIER"
elif "${PRO_EDITION}"; then
  NGCP_INSTALLER_EDITION_STR="Sipwise C5:        PRO"
elif ! "${NGCP_INSTALLER}"; then
  # installing plain debian without NGCP
  NGCP_INSTALLER_EDITION_STR=""
elif "${PUPPET}" ; then
  NGCP_INSTALLER_EDITION_STR="PUPPET"
else
  echo "Error: Could not determine 'edition' (spce, sppro, carrier)."
  exit 1
fi

DEBIAN_URL="${DEBIAN_REPO_TRANSPORT}://${DEBIAN_REPO_HOST}"
SIPWISE_URL="${SIPWISE_REPO_TRANSPORT}://${SIPWISE_REPO_HOST}"

FALLBACKFS_SIZE="${FALLBACKFS_SIZE:-${ROOTFS_SIZE}}"

## }}}

if [ -n "$NETSCRIPT" ] ; then
  echo "Automatic deployment via bootoption netscript detected."
  INTERACTIVE=false
fi

ensure_packages_installed

# MT#60284 ensure qemu-guest-agent is running if it's available in VM
if [ -e /dev/virtio-ports/org.qemu.guest_agent.0 ] ; then
  echo "Guest Agent VirtIO device detected, starting qemu-guest-agent service"
  systemctl start qemu-guest-agent
fi

if ! "$NGCP_INSTALLER" ; then
  CARRIER_EDITION=false
  PRO_EDITION=false
  CE_EDITION=false
  unset NODE_NAME
fi

set_deploy_status "getconfig"

# when using ip=....:$HOSTNAME:eth0:off file /etc/hosts doesn't contain the
# hostname by default, avoid warning/error messages in the host system
# and use it for IP address check in pro edition
if [ -z "$TARGET_HOSTNAME" ] ; then
  if "$PRO_EDITION" ; then
    TARGET_HOSTNAME="${NODE_NAME}"
  elif "$CE_EDITION" ; then
    TARGET_HOSTNAME="spce"
  fi

  # if we don't install ngcp ce/pro but
  # $HOSTNAME is set via ip=.... then
  # take it, otherwise fall back to safe default
  if [ -z "$TARGET_HOSTNAME" ] ; then
    if [ -n "$HOSTNAME" ] ; then
      TARGET_HOSTNAME="$HOSTNAME"
    else
      TARGET_HOSTNAME="debian"
    fi
  fi
fi
[ -z "$HOSTNAME" ] && HOSTNAME="nohostname"
if [ -n "$TARGET_HOSTNAME" ] ; then
  HOSTNAME="$TARGET_HOSTNAME"
fi
export HOSTNAME

# get install device from "ip=<client-ip:<srv-ip>:..." boot arg
if [[ -n "${IP_LINE}" ]]; then
  declare -A IP_ARR
  if loadNfsIpArray IP_ARR "${IP_LINE}" ; then
    INSTALL_DEV=${IP_ARR[device]}
    EXT_GW=${IP_ARR[gw-ip]}
    [[ "${IP_ARR[autoconf]}" == 'dhcp' ]] && DHCP=true
  fi
fi

# Get current IP information, programmatically with jq(1)
ensure_packages_installed 'jq'
## try ipv4
INSTALL_DEV=$(ip -4 -j route show | jq -r '.[] | select(.dst == "default") | .dev')
if [[ -z "${INSTALL_DEV}" ]]; then
  ## try ipv6
  INSTALL_DEV=$(ip -6 -j route show | jq -r '.[] | select(.dst == "default") | .gateway')
  INSTALL_IP=$(ip -6 -j addr show dev "${INSTALL_DEV}" | jq -r '.[0].addr_info[0].local')
else
  INSTALL_IP=$(ip -4 -j addr show dev "${INSTALL_DEV}" | jq -r '.[0].addr_info[0].local')
  install_ip_netmask=$(ip -4 -j addr show "${INSTALL_DEV}" | jq -r '.[0].addr_info[0].prefixlen')
  current_netmask="$( cdr2mask "${install_ip_netmask}" )"
  EXTERNAL_NETMASK="${EXTERNAL_NETMASK:-${current_netmask}}"
  unset external_ip_data current_netmask
  GW="$(ip route show dev "${INSTALL_DEV}" | awk '/^default via/ {print $3; exit}')"
fi
echo "INSTALL_IP is ${INSTALL_IP}"
EXTERNAL_DEV="${INSTALL_DEV}"
EXTERNAL_DEV="n${EXTERNAL_DEV}" # rename eth*->neth*
EXTERNAL_IP="${INSTALL_IP}"
EADDR="${EXTERNAL_IP:-${EADDR}}"
MANAGEMENT_IP="${MANAGEMENT_IP:-${IP_HA_SHARED}}"
INTERNAL_DEV="n${INTERNAL_DEV}" # rename eth*->neth*
ORIGIN_INSTALL_DEV="n${INSTALL_DEV}" # rename eth*->neth*
if [[ -n "${EXT_GW}" ]]; then
  GW="${EXT_GW}"
fi

set_deploy_status "settings"

### echo settings
[ -n "$SP_VERSION" ] && SP_VERSION_STR=$SP_VERSION || SP_VERSION_STR="<latest>"

echo "Deployment Settings:

  Install ngcp:      $NGCP_INSTALLER
  $NGCP_INSTALLER_EDITION_STR"

echo "
  Target disk:       $DISK_INFO
  Target Hostname:   $TARGET_HOSTNAME
  Installer version: $SP_VERSION_STR
  Install NW iface:  $INSTALL_DEV
  Install IP:        $INSTALL_IP
  Use DHCP in host:  $DHCP
  Use 'eatmydata':   $EATMYDATA

  Installing in chassis? $CHASSIS

" | tee -a /tmp/installer-settings.txt

if "$PRO_EDITION" ; then
  echo "
  Node name:         $NODE_NAME
  Host Role Carrier: $CROLE

  External NW iface: $EXTERNAL_DEV
  Ext host IP:       $EXTERNAL_IP
  Ext cluster IP:    $EADDR
  Internal NW iface: $INTERNAL_DEV
  Int sp1 host IP:   $IP1
  Int sp2 host IP:   $IP2
  Int sp shared IP:  $IP_HA_SHARED
  Int netmask:       $INTERNAL_NETMASK
  MGMT address:      $MANAGEMENT_IP
" | tee -a /tmp/installer-settings.txt
fi

if "$INTERACTIVE" ; then
  echo "WARNING: Execution will override any existing data!"
  echo "Settings OK? y/N"
  read -r a
  if [[ "${a,,}" != "y" ]] ; then
    echo "Exiting as requested."
    exit 2
  fi
  unset a
fi
## }}}

##### all parameters set #######################################################

set_deploy_status "start"

# measure time of installation procedure - everyone loves stats!
start_seconds=$(cut -d . -f 1 /proc/uptime)

if "$LOGO" ; then
  disable_trace
  GRML_INFO=$(cat /etc/grml_version)
  IP_INFO=$(ip-screen)
  CPU_INFO=$(lscpu | awk '/^CPU\(s\)/ {print $2}')
  RAM_INFO=$(/usr/bin/gawk '/MemTotal/{print $2}' /proc/meminfo)
  DATE_INFO=$(date)
  INSTALLER_TYPE="Install CE: ${CE_EDITION} PRO: ${PRO_EDITION} [${NODE_NAME}] Carrier: ${CARRIER_EDITION} [${NODE_NAME}] [${CROLE}]"
  if [ -n "$NGCP_PPA" ] ; then
    PPA_INFO="| PPA: ${NGCP_PPA} "
  fi
  if efi_support ; then
    EFI_INFO="| EFI support"
  else
    EFI_INFO="| no EFI support"
  fi
  if [[ "${SWRAID}" = "true" ]] ; then
    SWRAID_INFO="| SW-RAID support [${SWRAID_DISK1} + ${SWRAID_DISK2}]"
  fi
  # color
  echo -ne "\ec\e[1;32m"
  clear
  #print logo
  echo "+++ Grml-Sipwise Deployment +++"
  echo ""
  echo "$GRML_INFO"
  echo "Host IP(s): $IP_INFO | Deployment version: $SCRIPT_VERSION"
  echo "$CPU_INFO CPU(s) | ${RAM_INFO}kB RAM | $CHASSIS"
  echo ""
  echo "Install ngcp: $NGCP_INSTALLER | $INSTALLER_TYPE"
  echo "Installing $SP_VERSION_STR platform | Debian: $DEBIAN_RELEASE $EFI_INFO $SWRAID_INFO $PPA_INFO"
  echo "Install IP: $INSTALL_IP | Started deployment at $DATE_INFO"
  # number of lines
  echo -ne "\e[10;0r"
  # reset color
  echo -ne "\e[9B\e[1;m"
  enable_trace
fi

# relevant only while deployment, will be overridden later
if [ -n "$HOSTNAME" ] ; then
  cat > /etc/hosts << EOF
127.0.0.1       grml    localhost
::1     ip6-localhost ip6-loopback grml
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts

127.0.0.1 ${NODE_NAME} ${HOSTNAME}
${INSTALL_IP} ${NODE_NAME} ${HOSTNAME}
EOF
fi

# remote login ftw
service ssh start >/dev/null &
echo "root:sipwise" | chpasswd

# date/time settings, so live system has proper date set
enable_ntp

## partition disk
set_deploy_status "disksetup"

if "$NGCP_INSTALLER" ; then
  VG_NAME="ngcp"
else
  VG_NAME="vg0"
fi

lvm_setup

# otherwise e2fsck fails with "need terminal for interactive repairs"
echo FSCK=no >>/etc/debootstrap/config

echo "Clean the default /etc/debootstrap/packages"
echo > /etc/debootstrap/packages

cat >> /etc/debootstrap/packages << EOF
# we want to have LVM support everywhere
lvm2
EOF

cat >> /etc/debootstrap/packages << EOF
# we want to always have fsck.ext4 available, e.g. for usage in initramfs
e2fsprogs
EOF

if ! "$NGCP_INSTALLER" ; then

  echo "Install some packages to be able to login on the Debian plain system"
  cat >> /etc/debootstrap/packages << EOF
# to be able to login on the Debian plain system via SSH
openssh-server
EOF

  if [[ "${SWRAID}" = "true" ]] ; then
    cat >> /etc/debootstrap/packages << EOF
# required for Software-RAID support on plain debian and Puppet recovery
grub-pc
EOF
  fi

else # "$NGCP_INSTALLER" = true

  echo "Install some essential packages for NGCP bootstrapping"
  # WARNING: consider to add NGCP packages to NGCP metapackage!
  cat >> /etc/debootstrap/packages << EOF
# to be able to retrieve files, starting with Debian/buster no longer present by default
wget
# required for Software-RAID support
mdadm
EOF

fi # if ! "$NGCP_INSTALLER" ; then

# NOTE: we use the debian.sipwise.com CNAME by intention here
# to avoid conflicts with apt-pinning, preferring deb.sipwise.com
# over official Debian
MIRROR="${DEBIAN_URL}/debian/"
SEC_MIRROR="${DEBIAN_URL}/debian-security/"
DBG_MIRROR="${DEBIAN_URL}/debian-debug/"

# MT#60283 Newer Grml-Sipwise deployment ISOs no longer ship
# sipwise-keyring-bootstrap.gpg (see commit 8647b3d)
if [ -r /etc/apt/trusted.gpg.d/sipwise-archive-keyring.gpg ] ; then  # >= mr13.4 / sipwise20250616
  SIPWISE_APT_KEY_PATH="/etc/apt/trusted.gpg.d/sipwise-archive-keyring.gpg"
elif [ -r /etc/apt/trusted.gpg.d/sipwise-keyring-bootstrap.gpg ] ; then  # before mr13.4
  SIPWISE_APT_KEY_PATH="/etc/apt/trusted.gpg.d/sipwise-keyring-bootstrap.gpg"
else
  echo "Error: couldn't find /etc/apt/trusted.gpg.d/sipwise* key for Debian repository usage." >&2
  exit 1
fi

set_deploy_status "debootstrap"

mkdir -p /etc/debootstrap/etc/apt/
echo "Setting up /etc/debootstrap/etc/apt/sources.list"
cat > /etc/debootstrap/etc/apt/sources.list << EOF
# Set up via deployment.sh for grml-debootstrap usage
deb ${MIRROR} ${DEBIAN_RELEASE} main contrib non-free
deb ${SEC_MIRROR} ${DEBIAN_RELEASE}-security main contrib non-free
deb ${MIRROR} ${DEBIAN_RELEASE}-updates main contrib non-free
deb ${DBG_MIRROR} ${DEBIAN_RELEASE}-debug main contrib non-free
EOF

if [[ ! -x "$(which mmdebstrap)" ]]; then
  die "Can't find mmdebstrap"
fi

ADDITIONAL_PACKAGES+=(mmdebstrap)
ensure_packages_installed
export DEBOOTSTRAP=mmdebstrap  # for usage with grml-debootstrap

# install only "Essential:yes" packages plus apt (explicitly included in minbase variant),
# systemd + network related packages
pkg_eatmydata=""
if "${EATMYDATA}"; then
  pkg_eatmydata=",eatmydata"
fi

DEBOPT_OPTIONS=()
pkg_usrmerge=""
case "${DEBIAN_RELEASE}" in
  stretch|buster|bullseye)
    # don't install usrmerge before Debian/bookworm AKA v12; instead invoke with
    # --no-merged-usr, which is a no-op in mmdebstrap (at least until v1.2.1),
    # but force its usage to not be surprised if that default should ever change
    DEBOPT_OPTIONS+=("--no-merged-usr")
    ;;
  *)
    # to use a merged-/usr (expected for Debian/bookworm and newer), we need to
    # tell mmdebstrap to include the usrmerge package
    pkg_usrmerge=",usrmerge"
    ;;
esac

DEBOPT_OPTIONS+=("--variant=minbase --include=systemd,systemd-sysv,init,zstd,isc-dhcp-client,ifupdown,ca-certificates,qemu-guest-agent${pkg_eatmydata}${pkg_usrmerge}")
# TT#61152 Add configuration Acquire::Retries=3, for apt to retry downloads
DEBOPT_OPTIONS+=("--aptopt='Acquire::Retries=3'")

if [[ -n "${EFI_PARTITION}" ]] ; then
  if efi_support ; then
    echo "EFI support present, enabling EFI support within grml-debootstrap"
    EFI_OPTION="--efi ${EFI_PARTITION}"

    # ensure we force creation of a proper FAT filesystem
    echo "Creating FAT filesystem on EFI partition ${EFI_PARTITION}"
    mkfs.fat -F32 -n "EFI" "${EFI_PARTITION}"
  else
    echo "EFI support NOT present, not enabling EFI support within grml-debootstrap"
  fi
fi

# Add sipwise key into chroot
debootstrap_sipwise_key

# install Debian
# shellcheck disable=SC2086
echo y | grml-debootstrap \
  --arch "${ARCH}" \
  --grub "${GRUB_DISK}" \
  --filesystem "${FILESYSTEM}" \
  --hostname "${TARGET_HOSTNAME}" \
  --mirror "$MIRROR" \
  --debopt "${DEBOPT_OPTIONS[*]}" \
  --keep_src_list \
  --defaultinterfaces \
  -r "$DEBIAN_RELEASE" \
  -t "$ROOT_FS" \
  $EFI_OPTION \
  $PRE_SCRIPTS_OPTION \
  $POST_SCRIPTS_OPTION \
  --password 'sipwise' 2>&1 | tee -a /tmp/grml-debootstrap.log

if [ "${PIPESTATUS[1]}" != "0" ]; then
  die "Error during installation of Debian ${DEBIAN_RELEASE}. Find details via: mount $ROOT_FS $TARGET ; ls $TARGET/debootstrap/*.log"
fi

sync
mount "$ROOT_FS" "$TARGET"

if [ -n "${DATA_PARTITION}" ] ; then
  mkdir -p "${TARGET}/ngcp-data"
fi

if [ -n "${FALLBACK_FS}" ] ; then
  mkdir -p "${TARGET}/ngcp-fallback"
fi

# TT#56903: mmdebstrap 0.4.1-2 does not properly remove this dir.
if [ -d "${TARGET}/var/lib/apt/lists/auxfiles" ]; then
  echo "Removing apt's > 1.6 auxfiles directory"
  rmdir "${TARGET}/var/lib/apt/lists/auxfiles"
fi

# MT#7805
if "$NGCP_INSTALLER" ; then
  cat << EOT | augtool --root="$TARGET"
insert opt after /files/etc/fstab/*[file="/"]/opt[last()]
set /files/etc/fstab/*[file="/"]/opt[last()] noatime
save
EOT
fi

# TT#41500: Make sure the timezone setup is coherent
grml-chroot "${TARGET}" dpkg-reconfigure --frontend=noninteractive tzdata

# TT#122950 Avoid time consuming "building database of manual pages"
echo 'man-db man-db/auto-update boolean false' | grml-chroot "${TARGET}" debconf-set-selections

# provide usable /ngcp-data partition
if [ -n "${DATA_PARTITION}" ] ; then
  echo "Enabling ngcp-data partition ${DATA_PARTITION} via /etc/fstab"
  cat >> "${TARGET}/etc/fstab" << EOF
${DATA_PARTITION} /ngcp-data               auto           noatime               0  0
EOF

  # Make sure /ngcp-data is mounted inside chroot
  # (some package might need to create folders structure on .postinst)
  grml-chroot "${TARGET}" mount /ngcp-data
fi

# provide usable /ngcp-fallback read-only partition
if [ -n "${FALLBACK_FS}" ] ; then
  echo "Enabling ngcp-fallback partition ${FALLBACK_FS} via /etc/fstab"
  cat >> "${TARGET}/etc/fstab" << EOF
${FALLBACK_FS} /ngcp-fallback               auto          ro,noatime,nofail     0  0
EOF
fi

if [[ -z "${SWAPFILE_SIZE_MB}" ]]; then
  # TT#11444 Calculate size of swapfile
  ramsize_mb="$(( $(awk '/^MemTotal:/ {print $2}' /proc/meminfo) / 1024))"
  SWAPFILE_SIZE_MB="$(( ramsize_mb / 2))"
  if [[ "${SWAPFILE_SIZE_MB}" -lt "${SWAPFILE_SIZE_MB_MIN}" ]]; then
    SWAPFILE_SIZE_MB="${SWAPFILE_SIZE_MB_MIN}"
  elif [[ "${SWAPFILE_SIZE_MB}" -gt "${SWAPFILE_SIZE_MB_MAX}" ]]; then
    SWAPFILE_SIZE_MB="${SWAPFILE_SIZE_MB_MAX}"
  fi
  SWAPFILE_SIZE_MB="${SWAPFILE_SIZE_MB}M"
  unset ramsize_mb
fi

# get rid of automatically installed packages
grml-chroot "${TARGET}" apt-get --purge -y autoremove

# purge removed packages
# it's necessary to use chroot instead of grml-chroot in variable=$() calls
# as grml-chroot also prints "Writing /etc/debian_chroot ..." line
# which breaks output
mapfile -t removed_packages < <(chroot "${TARGET}" dpkg --list | awk '/^rc/ {print $2}')
if [ ${#removed_packages[@]} -ne 0 ]; then
  grml-chroot "${TARGET}" dpkg --purge "${removed_packages[@]}"
fi

# make sure `hostname` and `hostname --fqdn` return data from chroot
grml-chroot "${TARGET}" hostname -F /etc/hostname

# make sure installations of packages works, will be overridden later again
cat > $TARGET/etc/hosts << EOF
127.0.0.1       localhost
127.0.0.1       ${HOSTNAME}. ${HOSTNAME}

::1             ip6-localhost ip6-loopback
fe00::0         ip6-localnet
ff00::0         ip6-mcastprefix
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters
EOF

if ! "$NGCP_INSTALLER" ; then
  set_custom_grub_boot_options
fi

if "$NGCP_INSTALLER" ; then
  set_deploy_status "ngcp-installer"

  echo "Searching for proper ngcp-installer package ..."
  get_installer_path

  echo "Generating debian/sipwise APT repos ..."
  set_repos

  echo "Installing package ngcp-installer: ${INSTALLER}"
  grml-chroot "${TARGET}" wget "${INSTALLER_PATH}${INSTALLER}"
  grml-chroot "${TARGET}" dpkg -i "${INSTALLER}"
  grml-chroot "${TARGET}" rm -f "${INSTALLER}"

  echo "Generating ngcp-installer configs ..."
  gen_installer_config

  echo "Generating ngcp-installer run script ..."
  cat > "${TARGET}/tmp/ngcp-installer-deployment.sh" << "EOT"
#!/bin/bash

echo -n "Running ngcp-installer via grml-chroot, starting at: "
date +'%F %T %Z'
ngcp_installer_start=$(date +'%s')

ngcp_installer_cmd="ngcp-installer"
if command -v eatmydata &>/dev/null; then
  echo "Running ngcp-installer with 'eatmydata'"
  ngcp_installer_cmd="eatmydata ${ngcp_installer_cmd}"
else
  echo "Running ngcp-installer without 'eatmydata'"
fi
${ngcp_installer_cmd} 2>&1
RC=$?

echo -n "Finishing ngcp-installer at: "
date +'%F %T %Z'
ngcp_installer_end=$(date +'%s')

echo "ngcp-installer total run: $((ngcp_installer_end - ngcp_installer_start)) seconds"

if [ "${RC}" = "0" ]; then
  echo "OK, ngcp-installer finished successfully, continue netscript deployment."
else
  echo "ERROR: Fatal error while running ngcp-installer (exit code '${RC}')!"
  exit ${RC}
fi
EOT

  echo "Execute ngcp-installer inside deployment chroot environment ..."
  if grml-chroot "${TARGET}" /bin/bash /tmp/ngcp-installer-deployment.sh ; then
    echo "ngcp-installer finished successfully"

    echo "Trying to identify ping_host"
    get_ping_host

    # Check the current method of external interface
    # If it is manual - we need to reconfigure /e/n/i to get working network configuration after the reboot
    method=$( sed -rn "s/^iface ${INSTALL_DEV} inet ([A-Za-z]+)/\1/p" < /etc/network/interfaces )
    netcardconf="/usr/sbin/netcardconfig"
    if [[ "${method}" == 'manual' ]]; then
      if "${DHCP}" ; then
        NET_DEV="${INSTALL_DEV}" METHOD='dhcp' "${netcardconf}"
      else
        if "${PRO_EDITION}" && "${NGCP_PXE_INSTALL}" ; then
          NET_DEV="${INSTALL_DEV}" METHOD='static' IPADDR="${INSTALL_IP}" NETMASK="${EXTERNAL_NETMASK}" \
          "${netcardconf}"
        else
          NET_DEV="${INSTALL_DEV}" METHOD='static' IPADDR="${INSTALL_IP}" NETMASK="${EXTERNAL_NETMASK}" \
          GATEWAY="${GW}" "${netcardconf}"
        fi
      fi
    fi
    echo "Copying /etc/network/interfaces ..."
    cp /etc/network/interfaces "${TARGET}/etc/network/"
    sed -i '/iface lo inet dhcp/d' "${TARGET}/etc/network/interfaces"
    # renaming eth*->neth* done below, to also do it for non-ngcp installations
    unset method netcardconf
  else
    die "Error during installation of ngcp. Find details at: ${TARGET}/var/log/ngcp-installer.log"
  fi

  echo "Checking for network connectivity (workaround for e.g. ice network drive issue)"
  wait_for_network_online 15

  echo "Generating udev persistent net rules ..."
  grml-chroot "${TARGET}" /usr/sbin/ngcp-initialize-udev-rules-net

  echo "Temporary files cleanup ..."
  find "${TARGET}/var/log" -type f -size +0 -not -name \*.ini -not -name ngcp-installer.log -exec sh -c ":> \${1}" sh {} \;
  :>$TARGET/run/utmp
  :>$TARGET/run/wtmp

  echo "Backup grml-debootstrap.log for later investigation ..."
  if [ -r /tmp/grml-debootstrap.log ] ; then
    cp /tmp/grml-debootstrap.log "${TARGET}"/var/log/
  fi

  # network interfaces need to be renamed eth*->neth* with mr9.5 / Debian
  # bullseye, and not left with grml-bootstrap defaults
  echo "Renaming eth*->neth* in /etc/network/interfaces ..."
  sed -i '/eth[0-9]/ s|eth|neth|g' "${TARGET}/etc/network/interfaces"
  echo "Content of resulting /etc/network/interfaces:"
  tail -v -n +0 "${TARGET}/etc/network/interfaces"
  echo "========"
fi

if [[ -n "${MANAGEMENT_IP}" ]] && "${RETRIEVE_MGMT_CONFIG}" ; then
  echo "Retrieving public key from management node"
  cat > "${TARGET}/tmp/retrieve_authorized_keys.sh" << EOT
#!/bin/bash
set -e
mkdir -p /root/.ssh
wget --timeout=30 -O /root/.ssh/authorized_keys "http://${MANAGEMENT_IP}:3000/ssh/id_rsa_pub"
chmod 600 /root/.ssh/authorized_keys
EOT
  grml-chroot "${TARGET}" /bin/bash /tmp/retrieve_authorized_keys.sh
fi

# MT#61265 avoid "penalty: failed authentication" in automated SSH/SCP actions in Jenkins jobs
case "${DEBIAN_RELEASE}" in
  trixie)
    echo "Adjusting /etc/ssh/sshd_config for Debian release '${DEBIAN_RELEASE}'"
    echo '# added by deployment.sh' >> "${TARGET}"/etc/ssh/sshd_config

    echo "Disabling PerSourcePenalties in /etc/ssh/sshd_config"
    echo 'PerSourcePenalties no'    >> "${TARGET}"/etc/ssh/sshd_config

    echo '# end of deployment.sh changes' >> "${TARGET}"/etc/ssh/sshd_config
    ;;
esac

if "$VAGRANT" ; then
  echo "Bootoption vagrant present, executing vagrant_configuration."
  vagrant_configuration
fi

if [ -n "$PUPPET" ] ; then
  set_deploy_status "puppet"

  echo "Setting hostname to ${IP_ARR[hostname]}"
  echo "${IP_ARR[hostname]}" > "${TARGET}/etc/hostname"
  grml-chroot "${TARGET}" hostname -F /etc/hostname

  grml-chroot "${TARGET}" apt-get -y install resolvconf libnss-myhostname

  if [ ! -x "${TARGET}/usr/bin/dirmngr" ] ; then
    echo  "Installing dirmngr on Debian ${DEBIAN_RELEASE}, otherwise the first puppet run fails: 'Could not find a suitable provider for apt_key'"
    grml-chroot "${TARGET}" apt-get -y install dirmngr
  fi

  # puppetlabs doesn't provide packages for Debian/trixie yet, so use
  # the AIO packages from the bookworm repos for now,
  puppet_deb_release="${DEBIAN_RELEASE}"
  case "${DEBIAN_RELEASE}" in
    trixie)
      puppet_deb_release="bookworm"
      echo "WARN: enabling ${puppet_deb_release} puppetlabs repository for ${DEBIAN_RELEASE} (see PA-4995)"
      ;;
  esac

  # we need apt pinning, otherwise we get puppet-agent from Debian/trixie
  echo "WARN: installing apt preferences for 'puppet-agent' package from upstream"
  cat > ${TARGET}/etc/apt/preferences.d/puppetlabs << EOF
Explanation: use puppet-agent from upstream puppetlabs
Package: puppet-agent
Pin: release o=Puppetlabs
Pin-Priority: 600
EOF

  echo "Installing 'puppet-agent' with dependencies"
  cat >> ${TARGET}/etc/apt/sources.list.d/puppetlabs.list << EOF
deb ${DEBIAN_URL}/puppetlabs/ ${puppet_deb_release} main puppet dependencies
EOF

  puppet_gpg='/root/puppet.gpg'
  if [[ ! -f "${puppet_gpg}" ]]; then
    die "Can't find ${puppet_gpg} file"
  fi

  if echo "7b4ed31e1028f921b5c965df0a42e508  /root/puppet.gpg" | md5sum -c ; then
    echo "Identified outdated puppetlabs key, fetching key (see MT#60283)"
    wget https://deb.sipwise.com/files/puppetlabs-pubkey-2025.gpg -O "${TARGET}/etc/apt/trusted.gpg.d/puppet.asc"
  elif echo "d6368c2df370ff2093831daad16d9eeb  /root/puppet.gpg" | md5sum -c ; then
    echo "Puppetlabs key seems to be in 'Public-Key (old)' format, installing as puppet.asc"
    cp "${puppet_gpg}" "${TARGET}/etc/apt/trusted.gpg.d/puppet.asc"
  else
    echo "Installing Puppetlabs key as puppet.gpg"
    cp "${puppet_gpg}" "${TARGET}/etc/apt/trusted.gpg.d/"
  fi

  grml-chroot "${TARGET}" apt-get update
  grml-chroot "${TARGET}" apt-get -y install puppet-agent openssh-server lsb-release ntpsec-ntpdate

  # Fix Facter error while running in chroot, facter fails if /etc/mtab is absent:
  grml-chroot "${TARGET}" ln -s /proc/self/mounts /etc/mtab || true

  cat > ${TARGET}/etc/puppetlabs/puppet/puppet.conf<< EOF
# This file has been created by deployment.sh
[main]
server=${PUPPET_SERVER}
environment=${PUPPET}
EOF

  if [ -f "${TARGET}/etc/profile.d/puppet-agent.sh" ] ; then
    echo "Exporting Puppet 4 new PATH (otherwise /opt/puppetlabs/bin/puppet is not found)"
    # shellcheck disable=SC1091
    source "${TARGET}/etc/profile.d/puppet-agent.sh"
  fi

  if [ -n "${PUPPET_GIT_REPO}" ] ; then
    echo "Installing from Puppet Git repository using 'puppet apply'"
    puppet_install_from_git
  else
    echo "Installing from Puppet server '${PUPPET_SERVER}' using 'puppet agent'"
    puppet_install_from_puppet
  fi

fi # if [ -n "$PUPPET" ] ; then


if [[ "${SWRAID}" = "true" ]] ; then
  if efi_support ; then
    set_deploy_status "swraidinstallefigrub"
    grml-chroot "${TARGET}" mount /boot/efi

    # if efivarfs kernel module is loaded, but efivars isn't,
    # then we need to mount efivarfs for efibootmgr usage
    if ! ls /sys/firmware/efi/efivars/* &>/dev/null ; then
      echo "Mounting efivarfs on /sys/firmware/efi/efivars"
      grml-chroot "${TARGET}" mount -t efivarfs efivarfs /sys/firmware/efi/efivars
    fi

    if efibootmgr | grep -q 'NGCP Fallback' ; then
      echo "Deleting existing NGCP Fallback entry from EFI boot manager"
      efi_entry=$(efibootmgr | awk '/ NGCP Fallback$/ {print $1; exit}' | sed 's/^Boot//; s/\*$//')
      efibootmgr -b "$efi_entry" -B
    fi

    echo "Adding NGCP Fallback entry to EFI boot manager"
    efibootmgr --create --disk "/dev/${SWRAID_DISK2}" -p 2 -w --label 'NGCP Fallback' --load '\EFI\debian\grubx64.efi'
  fi

  set_deploy_status "swraidinstallgrub"
  for disk in "${SWRAID_DISK1}" "${SWRAID_DISK2}" ; do
    grml-chroot "${TARGET}" grub-install "/dev/$disk"
  done

  grml-chroot "${TARGET}" update-grub
fi

if [ -r "${INSTALL_LOG}" ] && [ -d "${TARGET}"/var/log/ ] ; then
  set_deploy_status "copylogfiles"
  cp "${INSTALL_LOG}" "${TARGET}"/var/log/
  sync
fi

# unmount /ngcp-data partition inside chroot (if available)
if [ -n "${DATA_PARTITION}" ] ; then
  grml-chroot "${TARGET}" umount /ngcp-data
fi

# don't leave any mountpoints
sync

umount ${TARGET}/sys/firmware/efi/efivars || true
umount ${TARGET}/boot/efi || true
umount ${TARGET}/proc    || true
umount ${TARGET}/sys     || true
umount ${TARGET}/dev/pts || true
umount ${TARGET}/dev     || true
sync

# unmount chroot - what else?
umount $TARGET || umount -l $TARGET # fall back if a process is still being active

# make sure no device mapper handles are open, otherwise
# rereading partition table won't work
dmsetup remove_all || true

declare efidev1 efidev2
if [[ "${SWRAID}" = "true" ]] ; then
  if efi_support ; then
    set_deploy_status "swraidclonegrub"
    partlabel="EFI System"
    max_tries=60
    get_pvdevice_by_label_with_retries "/dev/${SWRAID_DISK1}" "${partlabel}" "${max_tries}" efidev1
    get_pvdevice_by_label_with_retries "/dev/${SWRAID_DISK2}" "${partlabel}" "${max_tries}" efidev2

    echo "Cloning EFI partition from ${efidev1} to ${efidev2}"
    dd if="${efidev1}" of="${efidev2}" bs=10M
  fi

  echo "Ensuring ${SWRAID_DEVICE} is stopped"
  mdadm --stop "${SWRAID_DEVICE}" || true
fi

if ! blockdev --rereadpt "/dev/${DISK}" ; then
  echo "Something on disk /dev/${DISK} (mountpoint $TARGET) seems to be still active, debugging output follows:"
  systemctl status || true
fi

# party time! who brings the whiskey?
echo "Installation finished. \o/"
echo
echo

[ -n "$start_seconds" ] && SECONDS="$(( $(cut -d . -f 1 /proc/uptime) - start_seconds))" || SECONDS="unknown"
echo "Successfully finished deployment process [$(date) - running ${SECONDS} seconds]"

if [ "$(get_deploy_status)" != "error" ] ; then
  set_deploy_status "finished"
fi

status_wait

# do not prompt when running in automated mode
if "$REBOOT" ; then
  echo "Rebooting system as requested via ngcpreboot"
  systemctl reboot
fi

if "$HALT" ; then
  echo "Halting system as requested via ngcphalt"
  systemctl poweroff
fi

if "$INTERACTIVE" ; then
  echo "Do you want to [r]eboot or [h]alt the system now? (Press any other key to cancel.)"
  unset a
  read -r a
  case "${a,,}" in
    r)
      echo "Rebooting system as requested."
      systemctl reboot
    ;;
    h)
      echo "Halting system as requested."
      systemctl poweroff
    ;;
    *)
      echo "Not halting system as requested. Please do not forget to reboot."
      ;;
  esac
fi

## END OF FILE #################################################################1
