#!/usr/bin/env bash logfile="/tmp/pvc-install.log" lockfile="/run/pvc-install.lock" if [[ $( whoami ) != "root" ]]; then echo "STOP! This script is designed to run as root within the installer only!" echo "To build a PVC installer ISO file, use './buildiso.sh'." echo "To build a PVC installer PXE root, use './buildpxe.sh'." exit 1 fi echo active_ttys=( $( w | grep "^root" | awk '{ print $2 }' ) ) echo "Active TTYs: ${active_ttys[@]}" this_tty=$( tty | sed -e "s:/dev/::" ) echo "This TTY: ${this_tty}" echo echo "Waiting for consoles to get ready..." if [[ ${#active_ttys[@]} -gt 1 ]]; then if [[ "${active_ttys[@]}" =~ "ttyS" ]]; then if grep -q -E -o "tty[0-9]+" <<<"${this_tty}"; then echo "Found more than one TTY and at least one serial TTY!" echo -n "If you wish to run the installer on this graphical TTY instead of the serial TTY, press enter within 15 seconds... " if ! read -t 15; then echo "timeout." exit 0 fi else echo "Found more than one TTY!" echo -n "Waiting for other TTYs to time out... " sleep $(( 16 + $( grep -E -o '[0-9]+' <<<"${this_tty}" ) )) echo "done." fi else echo "Found more than one graphical TTY!" echo -n "If you wish to run the installer on this graphical TTY, press enter within 60 seconds... " if ! read -t 60 _d; then echo "timeout." echo "To launch the installer again on this TTY, run '/install.sh'." exit 0 fi fi fi if [[ -f ${lockfile} ]]; then echo "Aborting installer due to lockfile presence: $( cat ${lockfile} )." exit 0 fi printf "PID $$ on TTY ${this_tty}" > ${lockfile} echo # Set the target consoles in the installed image target_consoles="" for tty in $( echo -e "$( sed 's/ /\n/g' <<<"${active_ttys[@]}" )" | sort ); do tty_type=${tty%%[0-9]*} # Only use the first of each console type if grep -q "${tty_type}" <<<"${target_consoles}"; then continue fi if [[ ${tty_type} == "ttyS" ]]; then # Add 115200 baud rate tty="${tty},115200n8" fi target_consoles="${target_consoles} console=${tty}" done iso_name="XXDATEXX" target_deploy_user="XXDEPLOYUSERXX" supported_filesystems="ext4 xfs" default_filesystem="ext4" supported_debrelease="bookworm" default_debrelease="bookworm" default_debmirror="http://ftp.debian.org/debian" # Base packages (installed by debootstrap) basepkglist="lvm2,parted,gdisk,sudo,vim,gpg,gpg-agent,openssh-server,vlan,ifenslave,python3,ca-certificates,curl" case $( uname -m ) in x86_64) # If we're in EFI mode, install grub-efi, otherwise install grub-pc if grep -q efivarfs /proc/mounts &>/dev/null; then grub_pkg="grub-efi" else grub_pkg="grub-pc" fi basepkglist="${basepkglist},${grub_pkg},linux-image-amd64" ;; aarch64) basepkglist="${basepkglist},grub-efi-arm64,linux-image-arm64" ;; esac # Supplemental packages (installed in chroot after debootstrap) suppkglist="firmware-linux,firmware-linux-nonfree,firmware-bnx2,firmware-bnx2x,ntp,ipmitool,acpid,acpi-support-base,lsscsi,lsb-release" # Modules to blacklist (known-faulty) target_module_blacklist=( "hpwdt" ) # DANGER - THIS PASSWORD IS PUBLIC # It should be used ONLY immediately after booting the PVC node in a SECURE environment # to facilitate troubleshooting of a failed boot. It should NOT be exposed to the Internet, # and it should NOT be left in place after system configuration. The PVC Ansible deployment # roles will overwrite it by default during configuration. root_password="hCb1y2PF" # Cleanup function for failures or final termination cleanup() { set +o errexit echo -n "Cleaning up target... " umount ${target}/tmp >&2 umount ${target}/run >&2 umount ${target}/sys >&2 umount ${target}/proc >&2 umount ${target}/dev/pts >&2 umount ${target}/dev >&2 umount ${target}/var/lib/ceph >&2 umount ${target}/var/lib/zookeeper >&2 umount ${target}/boot/efi >&2 umount ${target}/boot >&2 umount ${target} >&2 vgchange -an >&2 rmdir ${target} >&2 echo "done." echo -n "Removing lockfile... " rm ${lockfile} echo "done." echo if [[ -z ${DONE} ]]; then case ${install_option} in on) echo "A fatal error occurred!; restarting installer in 15 seconds. Press any key to spawn a shell." if ! read -t 15; then exec ${0} fi ;; *) echo "A fatal error occurred. Use the shell to inspect the log for errors at:" echo -e " ${logfile}" echo "To restart the installer, run '/install.sh'." ;; esac fi } trap cleanup EXIT # Checkin function seed_checkin() ( case ${1} in start) action="install-start" ;; end) action="install-complete" ;; esac curl -X POST \ -H "Content-Type: application/json" \ -d "{\"action\":\"${action}\",\"hostname\":\"${target_hostname}\",\"host_macaddr\":\"${host_macaddr}\",\"host_ipaddr\":\"${host_ipaddr}\",\"bmc_macaddr\":\"${bmc_macaddr}\",\"bmc_ipaddr\":\"${bmc_ipaddr}\"}" \ ${pvcbootstrapd_checkin_uri} >&2 ) # Obtain the preseed options from the kernel command line install_option="" seed_host="" seed_file="" kernel_cmdline=( $( cat /proc/cmdline ) ) for option in ${kernel_cmdline[@]}; do case ${option} in pvcinstall.preseed=*) install_option=${option#pvcinstall.preseed=} ;; pvcinstall.seed_host=*) seed_host=${option#pvcinstall.seed_host=} ;; pvcinstall.seed_file=*) seed_file=${option#pvcinstall.seed_file=} ;; esac done seed_config() { # Get IPMI BMC MAC for checkings bmc_macaddr="$( ipmitool lan print 2>/dev/null | grep 'MAC Address ' | awk '{ print $NF }' )" bmc_ipaddr="$( ipmitool lan print 2>/dev/null | grep 'IP Address ' | awk '{ print $NF }' )" # Perform DHCP on all interfaces to come online for interface in $( ip address | grep '^[0-9]' | grep 'eno\|enp\|ens\|wlp' | awk '{ print $2 }' | tr -d ':' ); do ip link set ${interface} up pgrep dhclient &>/dev/null || dhclient ${interface} >&2 done # Fetch the seed config tftp -m binary "${seed_host}" -c get "${seed_file}" /tmp/install.seed >&2 || exit 1 # Load the variables from the seed config . /tmp/install.seed || exit 1 # Ensure optional configurations are set to defaults if unset if [[ -z ${filesystem} ]]; then filesystem="${default_filesystem}" fi if [[ -z ${debrelease} ]]; then debrelease="${default_debrelease}" fi if [[ -z ${debmirror} ]]; then debmirror="${default_debmirror}" fi # Append the addpkglist to the suppkglist if present if [[ -n ${addpkglist} ]]; then suppkglist="${suppkglist},${addpkglist}" fi # Handle the target interface target_route="$( ip route show to match ${seed_host} | grep 'scope link' )" target_interface="$( grep -E -o 'e[a-z]+[0-9]+[a-z0-9]*' <<<"${target_route}" )" host_macaddr=$( ip -br link show ${target_interface} | awk '{ print $3 }' ) host_ipaddr=$( ip -br address show ${target_interface} | awk '{ print $3 }' | awk -F '/' '{ print $1 }' ) o_target_disk="${target_disk}" # Handle the target disk case "${target_disk}" in /dev/*) # Get the real path of the block device (for /dev/disk/* symlink paths) target_disk="$( realpath ${target_disk} )" ;; detect:*) # Use the detect.py parser to get the target disk from the detect string target_disk="$( /detect.py ${target_disk} )" ;; *) target_disk="" ;; esac if [[ ! -b ${target_disk} ]]; then echo "Invalid disk or disk not found for '${o_target_disk}'!" exit 1 else echo "Found target disk '${target_disk}'" fi echo echo "WARNING! All data on block device ${target_disk} will be wiped!" echo -n "Press any key within 15 seconds to cancel... " if read -t 15; then DONE="c" exit 0 fi echo } interactive_config() { # Stop all existing networking for a fresh slate for interactive config service networking stop &>/dev/null clear echo "--------------------------------------------------------" echo "| PVC Node installer (${iso_name}) |" echo "--------------------------------------------------------" echo echo "This LiveCD will install a PVC node base system ready for bootstrapping with 'pvc-ansible'." echo echo "* NOTE: If you make a mistake and need to restart the installer while answering" echo " the questions below, you may do so by typing ^C to cancel the script," echo " then re-running it by calling /install.sh in the resulting shell." echo echo "--------------------------------------------------------" echo "| Section 1: System Hostname |" echo "--------------------------------------------------------" echo echo "1) Please enter a fully-qualified hostname for the system. This should match the hostname" echo "in the 'pvc-ansible' inventory." while [[ -z ${target_hostname} ]]; do echo echo -n "> " read target_hostname if [[ -z ${target_hostname} ]]; then echo echo "Please enter a hostname." continue fi echo done disks="$( for disk in /dev/sd? /dev/nvme?n?; do if [[ ! -b ${disk} ]]; then continue fi disk_data="$( fdisk -l ${disk} 2>/dev/null )" echo -n "${disk}" echo -en "\t$( grep "^Disk model:" <<<"${disk_data}" | awk '{ $1=""; print $0 }' )" echo -en " $( grep "^Disk ${disk}:" <<<"${disk_data}" | awk '{ $1=""; $2="size:"; print $0 }' )" echo done )" echo "--------------------------------------------------------" echo "| Section 2: Disk setup |" echo "--------------------------------------------------------" echo echo "2a) Please enter the disk to install the PVC base system to. This disk will be WIPED," echo "an LVM PV created on it, and the system installed to this LVM." echo echo "* NOTE: PVC requires a disk of at least 30GB to be installed to, and 100GB is the" echo "recommended minimum size for optimal production partition sizes." echo echo "* NOTE: This disk should be high-performance flash (SSD, etc.)." echo echo "* NOTE: This disk should be a RAID-1 volume configured in hardware, or a very durable" echo "storage device, for maximum redundancy and resiliency." echo echo "Available disks:" echo echo -e "$( sed 's/\(.*\)/ \1/' <<<"${disks[@]}" )" while [[ ! -b ${target_disk} ]]; do echo echo -n "> " read target_disk if [[ ! -b ${target_disk} ]]; then echo echo "Please enter a valid target disk." continue fi blockdev_size_gbytes="$(( $( blockdev --getsize64 ${target_disk} ) / 1024 / 1024 / 1024 - 1))" if [[ ${blockdev_size_gbytes} -lt 30 ]]; then target_disk="" echo echo "The specified disk is too small (<30 GB) to use as a PVC system disk." echo "Please choose an alternative disk." continue fi echo done echo "2b) Please enter an alternate filesystem for the system partitions if desired." echo " Supported: ${supported_filesystems}" echo " Default: ${default_filesystem}" while [[ -z ${filesystem} ]]; do echo echo -n "> " read filesystem if [[ -z ${filesystem} ]]; then filesystem="${default_filesystem}" fi if ! grep -qw "${filesystem}" <<<"${supported_filesystems}"; then filesystem="" echo echo "Please enter a valid filesystem." continue fi echo done echo "--------------------------------------------------------" echo "| Section 3: Networking |" echo "--------------------------------------------------------" echo echo -n "Probing interfaces... " for interface in $( ip address | grep '^[0-9]' | grep 'eno\|enp\|ens\|wlp' | awk '{ print $2 }' | tr -d ':' ); do ip link set ${interface} up done sleep 5 interfaces="$( ip address | grep '^[0-9]' | grep 'eno\|enp\|ens\|wlp' | awk '{ print $2"\t"$3 }' | tr -d ':' )" echo "done." echo "Available interfaces:" echo echo -e "$( sed 's/\(.*\)/ \1/' <<<"${interfaces[@]}" )" echo echo "3a) Please enter the primary network interface for external connectivity. If" echo "no entries are shown here, ensure a cable is connected, then restart the" echo "installer with ^C and '/install.sh'." echo echo "If you want a bonding interface, please enter 'bond' here." while [[ -z ${target_interface} ]]; do echo echo -n "> " read target_interface if [[ -z ${target_interface} ]]; then echo echo "Please enter a valid interface." continue fi if [[ ${target_interface} != "bond" ]] && ! grep -qw "${target_interface}" <<<"${interfaces[@]}"; then target_interface="" echo echo "Please enter a valid interface." continue fi echo done if [[ ${target_interface} == "bond" ]]; then target_interface="" echo "3b) Please enter the name of the bonding interface (e.g. 'bond0'). This" echo "should match the interface you will use in the pvc-ansible configuration if" echo "applicable and MUST start with 'bond'." echo while [[ -z ${target_interface} ]]; do echo -n "> " read target_interface if [[ -z ${target_interface} ]]; then echo echo "Please enter a valid interface." continue fi if ! grep -q '^bond' <<<"${target_interface}"; then echo echo "Please enter a valid interface." continue fi done echo echo "3c) Please enter the bonding mode for the interface (e.g. '802.3ad' or" echo "'active-backup'). This mode must be valid or the networking will fail." echo while [[ -z ${bonding_mode} ]]; do echo -n "> " read bonding_mode if [[ -z ${bonding_mode} ]]; then echo echo "Please enter a valid bonding mode." continue fi done echo echo "3d) Please enter the space-separated slave interfaces from the list above." echo while [[ -z ${slave_interfaces} ]]; do echo -n "> " read slave_interfaces if [[ -z ${slave_interfaces} ]]; then echo echo "Please enter a valid list of slave interfaces." continue fi done echo echo -n "Bringing up bond interface... " ip link add ${target_interface} type bond ip link set ${target_interface} type bond mode ${bonding_mode} for slave_interface in ${slave_interfaces}; do ip link set ${slave_interface} down ip link set ${slave_interface} master ${target_interface} ip link set ${slave_interface} up done ip link set ${target_interface} up echo "done." echo next_prompt_1="3e" next_prompt_2="3f" next_prompt_3="3g" else next_prompt_1="3b" next_prompt_2="3c" next_prompt_3="3g" fi echo -n "${next_prompt_1}) Is a tagged vLAN required for the primary network interface? [y/N] " read vlans_req if [[ ${vlans_req} == 'y' || ${vlans_req} == 'Y' ]]; then echo echo "Please enter the vLAN ID for the interface." while [[ -z ${vlan_id} ]]; do echo echo -n "> " read vlan_id if [[ -z ${vlan_id} ]]; then echo echo "Please enter a numeric vLAN ID." continue fi done echo else vlan_id="" echo fi echo "${next_prompt_2}) Please enter the IP address, in CIDR format [X.X.X.X/YY], of the primary" echo "network interface. Leave blank for DHCP configuration of the interface on boot." echo echo -n "> " read target_ipaddr if [[ -n ${target_ipaddr} ]]; then target_netformat="static" echo echo "${next_prompt_3}) Please enter the default gateway IP address of the primary" echo "network interface." while [[ -z ${target_defgw} ]]; do echo echo -n "> " read target_defgw if [[ -z ${target_defgw} ]]; then echo echo "Please enter a default gateway; the installer requires Internet access." continue fi echo done else target_netformat="dhcp" echo fi echo -n "Bringing up primary network interface in ${target_netformat} mode... " case ${target_netformat} in 'static') if [[ -n ${vlan_id} ]]; then modprobe 8021q >&2 vlan_interface=${target_interface}.${vlan_id} ip link add link ${target_interface} name ${vlan_interface} type vlan id ${vlan_id} >&2 ip link set ${target_interface} up >&2 || true ip link set ${vlan_interface} up >&2 || true ip address add ${target_ipaddr} dev ${vlan_interface} >&2 || true ip route add default via ${target_defgw} >&2 || true formatted_ipaddr="$( sipcalc ${target_ipaddr} | grep -v '(' | awk '/Host address/{ print $NF }' )" formatted_netmask="$( sipcalc ${target_ipaddr} | grep -v '(' | awk '/Network mask/{ print $NF }' )" real_interface="${vlan_interface}" target_interfaces_is="static-vlan" else ip link set ${target_interface} up >&2 || true ip address add ${target_ipaddr} dev ${target_interface} >&2 || true ip route add default via ${target_defgw} >&2 || true formatted_ipaddr="$( sipcalc ${target_ipaddr} | grep -v '(' | awk '/Host address/{ print $NF }' )" formatted_netmask="$( sipcalc ${target_ipaddr} | grep -v '(' | awk '/Network mask/{ print $NF }' )" real_interface="${target_interface}" target_interfaces_is="static-raw" fi cat </etc/resolv.conf nameserver 8.8.8.8 EOF ;; 'dhcp') if [[ -n ${vlan_id} ]]; then modprobe 8021q >&2 vconfig add ${target_interface} ${vlan_id} &>/dev/null vlan_interface=${target_interface}.${vlan_id} dhclient ${vlan_interface} >&2 real_interface="${vlan_interface}" target_interfaces_is="dhcp-vlan" else dhclient ${target_interface} >&2 real_interface="${target_interface}" target_interfaces_is="dhcp-raw" fi ;; esac echo "done." echo echo -n "Waiting for networking to become ready... " while ! ping -q -c 1 8.8.8.8 &>/dev/null; do sleep 1 done echo "done." echo echo "--------------------------------------------------------" echo "| Section 4: Debian Configuration |" echo "--------------------------------------------------------" echo echo "4a) Please enter an alternate Debian release codename for the system if desired." echo " Supported: ${supported_debrelease}" echo " Default: ${default_debrelease}" while [[ -z ${debrelease} ]]; do echo echo -n "> " read debrelease if [[ -z ${debrelease} ]]; then debrelease="${default_debrelease}" fi if ! grep -qw "${debrelease}" <<<"${supported_debrelease}"; then debrelease="" echo echo "Please enter a valid release." continue fi echo done echo "4b) Please enter an HTTP URL for an alternate Debian mirror if desired." echo " Default: ${default_debmirror}" while [[ -z ${debmirror} ]]; do echo echo -n "> " read debmirror if [[ -z ${debmirror} ]]; then debmirror="${default_debmirror}" fi if ! wget -O /dev/null ${debmirror}/dists/${debrelease}/Release &>/dev/null; then debmirror="" echo echo "Please enter a valid Debian mirror URL." continue fi echo echo "Repository mirror '${debmirror}' successfully validated." echo done echo "4c) Please enter any additional packages, comma-separated without spaces," echo "that you require installed in the base system (firmware, etc.). These" echo "must be valid packages or the install will fail!" echo echo -n "> " read addpkglist echo target_keys_method="wget" echo "--------------------------------------------------------" echo "| Section 5: SSH Setup |" echo "--------------------------------------------------------" echo echo "5) Please enter an HTTP URL containing a text list of SSH authorized keys to" echo "fetch. These keys will be allowed access to the deployment user '${target_deploy_user}'" echo "via SSH." echo "" echo "Leave blank to bypass this and use a password instead." echo echo -n "> " read target_keys_path if [[ -z ${target_keys_path} ]]; then echo echo "No SSH keys URL specified. Falling back to password configuration." echo echo "5) Please enter a password (hidden), twice, for the deployment user '${target_deploy_user}'." while [[ -z "${target_password}" ]]; do echo echo -n "> " read -s target_password_1 echo echo -n "> " read -s target_password_2 echo if [[ -n "${target_password_1}" && "${target_password_1}" -eq "${target_password_2}" ]]; then target_password="${target_password_1}" else echo echo "The specified passwords do not match or are empty." fi done else while ! wget -O /dev/null ${target_keys_path} &>/dev/null; do echo echo "Please enter a valid SSH keys URL." echo echo -n "> " read target_keys_path done echo echo "SSH key source '${target_keys_path}' successfully validated." fi echo } case ${install_option} in on) seed_config ;; *) interactive_config ;; esac echo titlestring_text="| Proceeding with installation of host '${target_hostname}'. |" titlestring_len="$(( $( wc -c <<<"${titlestring_text}" ) - 2 ))" for i in $( seq 0 ${titlestring_len} ); do echo -n "-"; done; echo echo "${titlestring_text}" for i in $( seq 0 ${titlestring_len} ); do echo -n "-"; done; echo echo ### Script begins ### echo "LOGFILE: ${logfile}" echo exec 1> >( tee -ia ${logfile} ) exec 2> >( tee -ia ${logfile} >/dev/null ) set -o errexit set -o xtrace case ${install_option} in on) seed_checkin start ;; *) # noop true ;; esac echo -n "Determining partition sizes... " ts=$(date +%s) blockdev_size_bytes="$( blockdev --getsize64 ${target_disk} )" blockdev_size_gbytes="$(( ${blockdev_size_bytes} / 1024 / 1024 / 1024 - 1))" if [[ ${blockdev_size_gbytes} -ge 100 ]]; then # Optimal sized system disk (>=100GB), use large partitions size_root_lv="32" size_ceph_lv="8" size_zookeeper_lv="32" size_swap_lv="16" te=$(date +%s) echo "found large disk (${blockdev_size_gbytes}GB >= 100GB), using optimal partition sizes. [$((te-ts))s]" else # Minimum sized disk (>=30GB), use small partitions size_root_lv="8" size_ceph_lv="4" size_zookeeper_lv="8" size_swap_lv="8" te=$(date +%s) echo "found small disk (${blockdev_size_gbytes}GB < 100GB), using small partition sizes. [$((te-ts))s]" fi echo -n "Unmounting potential partitions on target device... " ts=$(date +%s) for mount in $( mount | grep "${target_disk}" | awk '{ print $3 }' | sort -r ); do umount -f ${mount} >&2 || true done te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Unmounting potential LVM logical volumes on target device... " ts=$(date +%s) for vg in $( pvscan | grep "${target_disk}" | awk '{ print $4 }' ); do for mount in $( mount | grep "/${vg}" | awk '{ print $3 }' | sort -r ); do umount -f ${mount} >&2 || true done done te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Disabing potential LVM volume groups on target device... " ts=$(date +%s) for vg in $( pvscan | grep "${target_disk}" | awk '{ print $4 }' ); do vgchange -an ${vg} >&2 || true sleep 1 vgchange -an ${vg} >&2 || true yes | vgremove -f ${vg} >&2 || true done te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Removing existing LVM physical volumes... " ts=$(date +%s) for pv in $( pvscan | grep "${target_disk}" | awk '{ print $2 }' ); do yes | pvremove -f ${pv} >&2 || true done te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Wiping partition signatures on '${target_disk}'... " ts=$(date +%s) wipefs -a ${target_disk} >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Preparing GPT partitions on '${target_disk}'... " ts=$(date +%s) # New GPT, part 1 32MB BIOS boot, part 2 64MB ESP, part 3 928MB BOOT, part 4 inf LVM PV echo -e "o\ny\nn\n1\n\n32M\nEF02\nn\n2\n\n64M\nEF00\nn\n3\n\n928M\n8300\nn\n4\n\n\n8E00\nw\ny\n" | gdisk ${target_disk} >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Rescanning disks... " ts=$(date +%s) partprobe >&2 || true sleep 5 te=$(date +%s) echo "done. [$((te-ts))s]" if grep --silent '/dev/nvme' <<<"${target_disk}"; then part_lvm="${target_disk}p4" part_boot="${target_disk}p3" part_esp="${target_disk}p2" else part_lvm="${target_disk}4" part_boot="${target_disk}3" part_esp="${target_disk}2" fi echo -n "Creating LVM PV on '${part_lvm}'... " ts=$(date +%s) yes | pvcreate -ffy ${part_lvm} >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Creating LVM VG 'vgx'... " ts=$(date +%s) yes | vgcreate -f vgx ${part_lvm} >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Creating root logical volume (${size_root_lv}GB)... " ts=$(date +%s) yes | lvcreate -L ${size_root_lv}G -n root vgx >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Creating filesystem on root logical volume (${filesystem})... " ts=$(date +%s) yes | mkfs.${filesystem} /dev/vgx/root >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Creating ceph logical volume (${size_ceph_lv}GB)... " ts=$(date +%s) yes | lvcreate -L ${size_ceph_lv}G -n ceph vgx >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Creating filesystem on ceph logical volume (${filesystem})... " ts=$(date +%s) yes | mkfs.${filesystem} /dev/vgx/ceph >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Creating zookeeper logical volume (${size_zookeeper_lv}GB)... " ts=$(date +%s) yes | lvcreate -L ${size_zookeeper_lv}G -n zookeeper vgx >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Creating filesystem on zookeeper logical volume (${filesystem})... " ts=$(date +%s) yes | mkfs.${filesystem} /dev/vgx/zookeeper >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Creating swap logical volume (${size_swap_lv}GB)... " ts=$(date +%s) yes | lvcreate -L ${size_swap_lv}G -n swap vgx >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Creating swap space on swap logical volume... " ts=$(date +%s) yes | mkswap -f /dev/vgx/swap >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Creating filesystem on boot partition (ext2)... " ts=$(date +%s) yes | mkfs.ext2 ${part_boot} >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Creating filesystem on ESP partition (vfat)... " ts=$(date +%s) yes | mkdosfs -F32 ${part_esp} >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Mounting disks on temporary target '${target}'... " ts=$(date +%s) vgchange -ay >&2 target="/tmp/target" mkdir -p ${target} >&2 mount /dev/vgx/root ${target} >&2 mkdir -p ${target}/boot >&2 chattr +i ${target}/boot >&2 mount ${part_boot} ${target}/boot >&2 mkdir -p ${target}/boot/efi >&2 chattr +i ${target}/boot/efi >&2 mount ${part_esp} ${target}/boot/efi >&2 mkdir -p ${target}/var/lib/ceph >&2 chattr +i ${target}/var/lib/ceph >&2 mount /dev/vgx/ceph ${target}/var/lib/ceph >&2 mkdir -p ${target}/var/lib/zookeeper >&2 chattr +i ${target}/var/lib/zookeeper >&2 mount /dev/vgx/zookeeper ${target}/var/lib/zookeeper >&2 mkdir -p ${target}/tmp >&2 chattr +i ${target}/tmp >&2 mount -t tmpfs tmpfs ${target}/tmp >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Running debootstrap install... " ts=$(date +%s) echo "Command: debootstrap --include=${basepkglist} ${debrelease} ${target}/ ${debmirror}" >&2 debootstrap --include=${basepkglist} ${debrelease} ${target}/ ${debmirror} >&2 te=$(date +%s) echo "done. [$((te-ts))s]" case ${debrelease} in buster) non_free="non-free" ;; bullseye) non_free="non-free" # python-is-python3 is critical for bullsye suppkglist="python-is-python3,${suppkglist}" ;; bookworm) # Use non-free-firmware component instead of non-free non_free="non-free-firmware" # python-is-python3 is critical for bullsye suppkglist="python-is-python3,${suppkglist}" ;; esac echo -n "Adding ${non_free} APT component (firmware, etc.)... " ts=$(date +%s) mkdir -p ${target}/etc/apt/sources.list.d/ >&2 echo "deb ${debmirror} ${debrelease} contrib ${non_free}" | tee -a ${target}/etc/apt/sources.list >&2 chroot ${target} apt-get update >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Installing supplemental packages... " ts=$(date +%s) chroot ${target} apt-get install -y --no-install-recommends $( sed 's/,/ /g' <<<"${suppkglist}" ) >&2 te=$(date +%s) echo "done. [$((te-ts))s]" # Determine the bypath name of the specified system disk for disk in /dev/disk/by-path/*; do bypathlink="$( realpath ${disk} | awk -F'/' '{ print $NF }' )" enteredname="$( awk -F'/' '{ print $NF }' <<<"${target_disk}" )" if [[ ${bypathlink} == ${enteredname} ]]; then bypath_disk="${disk}" fi done # Check if TRIM is supported on the root disk (NVMe always is) if grep --silent '/dev/nvme' <<<"${target_disk}"; then extdiscard="discard," elif hdparm -I ${target_disk} | grep --silent "TRIM supported"; then extdiscard="discard," else extdiscard="" fi echo -n "Adding fstab entries... " ts=$(date +%s) echo "# fstab configuration for PVC hypervisor" | tee ${target}/etc/fstab >&2 echo "/dev/mapper/vgx-root / ${filesystem} defaults,${extdiscard}errors=remount-ro 0 1" | tee -a ${target}/etc/fstab >&2 echo "/dev/mapper/vgx-ceph /var/lib/ceph ${filesystem} defaults,${extdiscard}errors=remount-ro 0 2" | tee -a ${target}/etc/fstab >&2 echo "/dev/mapper/vgx-zookeeper /var/lib/zookeeper ${filesystem} defaults,${extdiscard}errors=remount-ro 0 2" | tee -a ${target}/etc/fstab >&2 echo "/dev/mapper/vgx-swap none swap sw 0 0" | tee -a ${target}/etc/fstab >&2 echo "${bypath_disk}-part3 /boot ext2 defaults 0 2" | tee -a ${target}/etc/fstab >&2 echo "${bypath_disk}-part2 /boot/efi vfat umask=0077 0 2" | tee -a ${target}/etc/fstab >&2 echo "tmpfs /tmp tmpfs defaults 0 0" | tee -a ${target}/etc/fstab >&2 te=$(date +%s) echo "done. [$((te-ts))s]" seed_interfaces_segment() { # A seed install is always "dhcp-raw" since the provisioner is always a dedicated, access port target_interfaces_block="auto ${target_interface}\niface ${target_interface} inet dhcp\npost-up /etc/network/pvcprovisionerd.checkin.sh \$IFACE" } interactive_interfaces_segment() { case ${target_interfaces_is} in static-vlan) target_interfaces_block="auto ${vlan_interface}\niface ${vlan_interface} inet ${target_netformat}\n\tvlan_raw_device ${target_interface}\n\taddress ${formatted_ipaddr}\n\tnetmask ${formatted_netmask}\n\tgateway ${target_defgw}" if [[ -n ${slave_interfaces} ]]; then target_interfaces_block="${target_interfaces_block}\n\nauto ${target_interface}\niface ${target_interface} inet manual" fi ;; static-raw) target_interfaces_block="auto ${target_interface}\niface ${target_interface} inet ${target_netformat}\n\taddress ${formatted_ipaddr}\n\tnetmask ${formatted_netmask}\n\tgateway ${target_defgw}" ;; dhcp-vlan) target_interfaces_block="auto ${vlan_interface}\niface ${vlan_interface} inet ${target_netformat}\n\tvlan_raw_device${target_interface}" if [[ -n ${slave_interfaces} ]]; then target_interfaces_block="${target_interfaces_block}\n\nauto ${target_interface}\niface ${target_interface} inet manual" fi ;; dhcp-raw) target_interfaces_block="auto ${target_interface}\niface ${target_interface} inet ${target_netformat}" ;; esac if [[ -n ${slave_interfaces} ]]; then target_interfaces_block="${target_interfaces_block}\n\tbond-mode ${bonding_mode}\n\tbond-slaves ${slave_interfaces}" fi } echo -n "Creating bootstrap interface segment... " ts=$(date +%s) case ${install_option} in on) seed_interfaces_segment ;; *) interactive_interfaces_segment ;; esac te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Adding bootstrap interface segment... " ts=$(date +%s) echo -e "${target_interfaces_block}" | tee ${target}/etc/network/interfaces.d/${target_interface} >&2 te=$(date +%s) echo "done. [$((te-ts))s]" case ${install_option} in on) echo -n "Creating bond interface segment... " ts=$(date +%s) bond_interfaces="$( ip -br link show | grep -E -o '^e[a-z]+[0-9]+[a-z0-9]*' | grep -v "^${target_interface}$" | tr '\n' ' ' )" bond_interfaces_block="auto bond0\niface bond0 inet manual\n\tbond-mode 802.3ad\n\tbond-slaves ${bond_interfaces}\n\tpost-up ip link set mtu 9000 dev \$IFACE" te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Adding bond interface segment... " ts=$(date +%s) echo -e "${bond_interfaces_block}" | tee ${target}/etc/network/interfaces.d/bond0 >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Adding bootstrap interface post-up checkin script... " ts=$(date +%s) cat <&2 #!/usr/bin/env bash target_interface=\${1} pvcbootstrapd_checkin_uri="${pvcbootstrapd_checkin_uri}" host_macaddr=\$( ip -br link show \${target_interface} | awk '{ print \$3 }' ) host_ipaddr=\$( ip -br address show \${target_interface} | awk '{ print \$3 }' | awk -F '/' '{ print \$1 }' ) bmc_macaddr=\$( ipmitool lan print | grep 'MAC Address ' | awk '{ print \$NF }' ) bmc_ipaddr=\$( ipmitool lan print | grep 'IP Address ' | awk '{ print \$NF }' ) if [[ -f /etc/pvc-install.base && -f /etc/pvc-install.pvc ]]; then # The second boot, after Ansible has configured the cluster action="system-boot_configured" else # The first boot, when Ansible has not been run yet action="system-boot_initial" fi sleep 30 curl -X POST \ -H "Content-Type: application/json" \ -d "{\"action\":\"\${action}\",\"hostname\":\"\$( hostname -s )\",\"host_macaddr\":\"\${host_macaddr}\",\"host_ipaddr\":\"\${host_ipaddr}\",\"bmc_macaddr\":\"\${bmc_macaddr}\",\"bmc_ipaddr\":\"\${bmc_ipaddr}\"}" \ \${pvcbootstrapd_checkin_uri} if [[ \${action} == "system-boot_configured" ]]; then # Clean up the bootstrap interface and this script rm /etc/network/interfaces.d/\${target_interface} rm \$0 fi EOF chmod +x ${target}/etc/network/pvcprovisionerd.checkin.sh te=$(date +%s) echo "done. [$((te-ts))s]" ;; *) # noop true ;; esac echo -n "Setting temporary 'root' password... " ts=$(date +%s) echo "root:${root_password}" | chroot ${target} chpasswd >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Adding deployment user... " ts=$(date +%s) mv ${target}/home ${target}/var/home >&2 chroot ${target} useradd -u 200 -d /var/home/${target_deploy_user} -m -s /bin/bash -g operator -G sudo ${target_deploy_user} >&2 chroot ${target} mkdir -p /var/home/${target_deploy_user}/.ssh if [[ -n ${target_keys_path} ]]; then case ${target_keys_method} in wget) wget -O ${target}/var/home/${target_deploy_user}/.ssh/authorized_keys ${target_keys_path} || failed_keys="y" ;; tftp) tftp -m binary "${seed_host}" -c get "${target_keys_path}" ${target}/var/home/${target_deploy_user}/.ssh/authorized_keys || failed_keys="y" ;; esac chroot ${target} chmod 0600 /var/home/${target_deploy_user}/.ssh/authorized_keys chroot ${target} chown -R ${target_deploy_user}:operator /var/home/${target_deploy_user} else echo "${target_deploy_user}:${target_password}" | chroot ${target} chpasswd >&2 fi te=$(date +%s) echo "done. [$((te-ts))s]" if [[ -n ${failed_keys} ]]; then target_password="$( pwgen -s 8 1 )" echo "WARNING: Failed to fetch keys; target deploy user SSH keyauth will fail." echo "Setting temporary random password '${temp_password}' instead." echo "${target_deploy_user}:${target_password}" | chroot ${target} chpasswd >&2 fi echo -n "Setting NOPASSWD for sudo group... " ts=$(date +%s) sed -i 's/^%sudo\tALL=(ALL:ALL) ALL/%sudo\tALL=(ALL:ALL) NOPASSWD: ALL/' ${target}/etc/sudoers te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Setting /etc/issue generator... " ts=$(date +%s) mkdir -p ${target}/etc/network/if-up.d >&2 echo -e "#!/bin/sh IP=\"\$( ip -4 addr show dev ${target_interface} | grep inet | awk '{ print \$2 }' | head -1 )\" cat </etc/issue Debian GNU/Linux 10 \\\\n \\\\l Bootstrap interface IP address: \$IP EOF" | tee ${target}/etc/network/if-up.d/issue-gen >&2 chmod +x ${target}/etc/network/if-up.d/issue-gen 1>&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Generating host rsa and ed25519 keys... " ts=$(date +%s) rm ${target}/etc/ssh/ssh_host_*_key* >&2 chroot ${target} ssh-keygen -t rsa -N "" -f /etc/ssh/ssh_host_rsa_key >&2 chroot ${target} ssh-keygen -t ed25519 -N "" -f /etc/ssh/ssh_host_ed25519_key >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Setting hostname... " ts=$(date +%s) echo "${target_hostname}" | tee ${target}/etc/hostname >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Setting resolv.conf... " ts=$(date +%s) echo "nameserver 8.8.8.8" | tee ${target}/etc/resolv.conf >&2 te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Installing GRUB bootloader... " ts=$(date +%s) mount --bind /dev ${target}/dev >&2 mount --bind /dev/pts ${target}/dev/pts >&2 mount --bind /proc ${target}/proc >&2 mount --bind /sys ${target}/sys >&2 mount --bind /run ${target}/run >&2 if [[ -d /sys/firmware/efi ]]; then bios_target="x86_64-efi" else bios_target="i386-pc" fi cat <&2 GRUB_DEFAULT=0 GRUB_TIMEOUT=5 GRUB_DISTRIBUTOR="Parallel Virtual Cluster (PVC) - Debian" GRUB_CMDLINE_LINUX="noautogroup blacklist=hpwdt ${target_consoles}" GRUB_TERMINAL_INPUT="console serial" GRUB_TERMINAL_OUTPUT="gfxterm serial" GRUB_SERIAL_COMMAND="serial --unit=0 --unit=1 --speed=115200" EOF mount --bind /sys/firmware/efi/efivars ${target}/sys/firmware/efi/efivars chroot ${target} grub-install --force --target=${bios_target} ${target_disk} >&2 chroot ${target} grub-mkconfig -o /boot/grub/grub.cfg >&2 umount ${target}/sys/firmware/efi/efivars te=$(date +%s) echo "done. [$((te-ts))s]" echo -n "Adding module blacklists... " ts=$(date +%s) for module in ${target_module_blacklist[@]}; do echo "blacklist ${module}" >> ${target}/etc/modprobe.d/blacklist.conf done chroot ${target} update-initramfs -u -k all >&2 te=$(date +%s) echo "done. [$((te-ts))s]" DONE="y" seed_postinst() { cleanup echo "Temporary root password: ${root_password}" echo seed_checkin end echo "Rebooting in 10 seconds." sleep 10 sync reboot -f } interactive_postinst() { set +o errexit echo echo -n "Launch a chroot shell in the target environment? (NOTE: no shell prompt) [y/N] " read launch_chroot if [[ ${launch_chroot} == 'y' || ${launch_chroot} == 'Y' ]]; then echo "Type 'exit' or Ctrl+D to exit chroot." chroot ${target} /bin/bash fi cleanup echo "-------------------------------------------------------------------------------------" echo "| PVC node installation finished. Next steps: |" echo "| 1. Press to reboot the system. |" echo "| 2. Boot the system verify SSH access (IP shown on login screen). |" echo "| 3. Proceed with system deployment via PVC Ansible. |" echo "| |" echo "| The INSECURE temporary root password if the system will not boot is: ${root_password} |" echo "-------------------------------------------------------------------------------------" echo read sync reboot -f } case ${install_option} in on) seed_postinst ;; *) interactive_postinst ;; esac