isoloom-render 0.11.1

What every Isoloom backend renders with: the backend port, the builders (HCL, Ruby, PowerShell, Dockerfile, the Ansible inventory), a VM's set-up steps, the network pieces and the containers' shared parts.
Documentation
//! A VM's disks: the root file system grown into a disk made bigger than its image's
//! ([`Step::DiskGrow`]), and its data disks formatted and mounted ([`Step::Disks`]), as a root
//! shell script for Linux and as PowerShell for Windows.
//!
//! A platform names an attached data disk its own way (NVMe ids on AWS, LUNs on Azure, by-id
//! links elsewhere) and may attach it after the machine boots, so the step doesn't ask: it waits
//! for a blank disk of the size asked for and formats it with the disk's name as its label.
//! Blank disks of one size are interchangeable, and once formatted a disk is found by its label,
//! so the step is idempotent, and its fstab line mounts it at every boot.
//!
//! [`Step::DiskGrow`]: isoloom_plan::Step::DiskGrow
//! [`Step::Disks`]: isoloom_plan::Step::Disks

use isoloom_plan::DataDisk;
use isoloom_spec::shell::quote;

use super::PKG;
use crate::powershell;

/// How long a disk may take to be attached: five-second tries.
const TRIES: u32 = 60;

/// The shell function that formats (when blank) and mounts one disk: `disk <label> <fs> <mount>
/// <bytes>`. A blank disk is a whole disk of that size (to the GiB) with no partition and no
/// signature.
const DISK_SH: &str = r#"disk() {
  label=$1; fs=$2; at=$3; bytes=$4
  mkdir -p "$at"
  if ! blkid -L "$label" >/dev/null 2>&1; then
    i=0; dev=
    while [ -z "$dev" ]; do
      for d in $(lsblk -dnbo NAME,SIZE,TYPE | awk -v b="$bytes" '$3 == "disk" && $2 >= b && $2 < b + 1073741824 { print $1 }'); do
        if [ "$(lsblk -n "/dev/$d" | wc -l)" -eq 1 ] && ! blkid -p "/dev/$d" >/dev/null 2>&1; then dev=/dev/$d; break; fi
      done
      if [ -z "$dev" ]; then
        i=$((i+1)); if [ "$i" -ge TRIES ]; then echo "disk $label: no blank disk of $bytes bytes is attached" >&2; exit 1; fi
        sleep 5
      fi
    done
    if [ "$fs" = xfs ]; then command -v mkfs.xfs >/dev/null || pkg xfsprogs; mkfs.xfs -q -L "$label" "$dev"; else mkfs.ext4 -q -L "$label" "$dev"; fi
    udevadm settle 2>/dev/null || true
  fi
  grep -q "^LABEL=$label " /etc/fstab || printf 'LABEL=%s %s %s defaults,nofail 0 2\n' "$label" "$at" "$fs" >> /etc/fstab
  mountpoint -q "$at" || mount "$at"
}
"#;

/// The data disks on Linux; `None` when every disk is raw (nothing to format or mount).
pub(super) fn linux(disks: &[DataDisk]) -> Option<String> {
    let lines: String = disks
        .iter()
        .filter_map(|d| {
            let at = d.mount.as_deref()?;
            Some(format!("disk {} {} {} {}\n", quote(&d.name), d.fs.id(), quote(at), d.bytes()))
        })
        .collect();
    (!lines.is_empty()).then(|| format!("set -e\n{PKG}\n{}{lines}", DISK_SH.replace("TRIES", &TRIES.to_string())))
}

/// The data disks on Windows: each brought online, partitioned and formatted NTFS at its drive
/// letter, once (a volume with its label is there already after that).
pub(super) fn windows(disks: &[DataDisk]) -> Option<String> {
    let lines: Vec<String> = disks
        .iter()
        .filter_map(|d| {
            let letter = d.mount.as_deref()?.trim_end_matches(':');
            let label = powershell::quote(&d.name);
            let missing = powershell::quote(&format!("disk {}: no blank disk of {} bytes is attached", d.name, d.bytes()));
            Some(format!(
                "if (-not (Get-Volume -FileSystemLabel {label} -ErrorAction SilentlyContinue)) {{ $i = 0; do {{ $d = Get-Disk | Where-Object {{ $_.PartitionStyle -eq 'RAW' -and $_.Size -ge {b} -and $_.Size -lt {b} + 1GB }} | Select-Object -First 1; if (-not $d) {{ if (++$i -ge {TRIES}) {{ throw {missing} }}; Start-Sleep 5 }} }} until ($d); if ($d.IsOffline) {{ Set-Disk -Number $d.Number -IsOffline $false }}; Initialize-Disk -Number $d.Number -PartitionStyle GPT -PassThru | New-Partition -UseMaximumSize -DriveLetter {letter} | Format-Volume -FileSystem NTFS -NewFileSystemLabel {label} -Confirm:$false | Out-Null }}",
                b = d.bytes(),
            ))
        })
        .collect();
    (!lines.is_empty()).then(|| lines.join("\n"))
}

/// Grows the root file system into a disk made bigger than the image's: its partition when it
/// is the disk's last, LVM's physical and logical volumes (Ubuntu's images), then the file
/// system (ext4 or XFS). Best effort and idempotent: with nothing to grow, nothing changes.
pub(super) const GROW_ROOT_SH: &str = r#"set -e
part_grow() {
  dev=$(readlink -f "$1")
  disk=/dev/$(lsblk -no PKNAME "$dev" | head -n1)
  num=$(cat "/sys/class/block/$(basename "$dev")/partition")
  if command -v growpart >/dev/null; then growpart "$disk" "$num" >/dev/null || true
  else echo ", +" | sfdisk --no-reread -N "$num" "$disk" >/dev/null 2>&1 && partx -u "$disk" || true; fi
}
fs_grow() { resize2fs "$1" >/dev/null 2>&1 || xfs_growfs / >/dev/null 2>&1 || true; }
src=$(findmnt -n -o SOURCE /)
case "$(lsblk -no TYPE "$src" | head -n1)" in
  lvm)
    vg=$(lvs --noheadings -o vg_name "$src" | tr -d ' ')
    for pv in $(pvs --noheadings -o pv_name -S vg_name="$vg" | tr -d ' '); do part_grow "$pv"; pvresize "$pv" >/dev/null; done
    lvextend -l +100%FREE "$src" >/dev/null 2>&1 || true
    fs_grow "$src" ;;
  part) part_grow "$src"; fs_grow "$src" ;;
esac
"#;

/// The same on Windows: C: to the largest size its disk allows.
pub(super) const GROW_ROOT_PS1: &str = "$max = (Get-PartitionSupportedSize -DriveLetter C).SizeMax; if ((Get-Partition -DriveLetter C).Size -lt $max) { Resize-Partition -DriveLetter C -Size $max }";

#[cfg(test)]
mod tests {
    use isoloom_spec::model::Filesystem;

    use super::*;

    fn disk(name: &str, fs: Filesystem, mount: Option<&str>) -> DataDisk {
        DataDisk {
            name: name.into(),
            key: format!("db-{name}"),
            size_gb: 2,
            fs,
            mount: mount.map(String::from),
        }
    }

    #[test]
    fn a_disk_waits_for_a_blank_disk_then_mounts_it_by_its_label() {
        let sh = linux(&[disk("data", Filesystem::Ext4, Some("/data"))]).expect("a disk to mount");
        assert!(sh.starts_with("set -e\npkg() {"), "{sh}");
        assert!(sh.ends_with("}\ndisk 'data' ext4 '/data' 2147483648\n"), "{sh}");
        assert!(sh.contains("blkid -L \"$label\"") && sh.contains("LABEL=%s %s %s defaults,nofail 0 2"), "{sh}");
    }

    #[test]
    fn each_formatted_disk_is_found_by_size_then_by_label() {
        let sh = linux(&[disk("data", Filesystem::Xfs, Some("/data")), disk("blk", Filesystem::Raw, None)]).unwrap();
        assert!(sh.contains("disk 'data' xfs '/data' 2147483648\n"), "{sh}");
        assert!(!sh.contains("disk blk"));
        assert!(sh.contains("-ge 60 ]"));
        assert_eq!(linux(&[disk("blk", Filesystem::Raw, None)]), None);
    }

    #[test]
    fn a_windows_disk_gets_its_drive_letter() {
        let ps = windows(&[disk("logs", Filesystem::Ntfs, Some("E:")), disk("raw", Filesystem::Raw, None)]).unwrap();
        assert_eq!(ps.lines().count(), 1, "a raw disk is left alone: {ps}");
        assert!(ps.contains("-DriveLetter E ") && ps.contains("-NewFileSystemLabel 'logs'"), "{ps}");
        assert!(ps.contains("$_.Size -ge 2147483648 -and $_.Size -lt 2147483648 + 1GB"), "{ps}");
        assert!(ps.contains("throw 'disk logs: no blank disk of 2147483648 bytes is attached'"), "{ps}");
        assert_eq!(windows(&[disk("raw", Filesystem::Raw, None)]), None);
    }
}