Skip to main content

hexz_cli/cmd/vm/
install.rs

1//! OS installation from ISO and conversion to Hexz snapshot.
2//!
3//! This command automates the process of installing an operating system from
4//! an ISO image and converting the result into a Hexz snapshot archive.
5//!
6//! # Installation Workflow
7//!
8//! 1. **Create Virtual Disk**: Generate temporary raw disk image
9//! 2. **Launch Installer**: Boot QEMU with ISO attached
10//! 3. **User Interaction**: User completes OS installation
11//! 4. **Shutdown VM**: User powers off after installation
12//! 5. **Pack Snapshot**: Convert raw disk to compressed `.st` archive
13//! 6. **Cleanup**: Remove temporary raw disk
14//!
15//! # Usage Example
16//!
17//! ```bash
18//! # Install Ubuntu with 20GB disk and 4GB RAM
19//! hexz vm install --iso ubuntu-24.04.iso \
20//!   --disk-size 20G --ram 4G --output ubuntu.st
21//!
22//! # Install with VNC (for headless servers)
23//! hexz vm install --iso debian.iso --disk-size 10G \
24//!   --ram 2G --output debian.st --vnc
25//!
26//! # Install with CDC for better deduplication
27//! hexz vm install --iso alpine.iso --disk-size 5G \
28//!   --ram 1G --output alpine.st --cdc
29//! ```
30//!
31//! # Requirements
32//!
33//! - `qemu-img`: For creating virtual disks
34//! - `qemu-system-x86_64`: For running the installer VM
35//! - Sufficient disk space for temporary raw image (= `disk_size`)
36//!
37//! # Performance Notes
38//!
39//! - Installation speed depends on ISO and hardware
40//! - Packing uses LZ4 compression by default (fast)
41//! - Add `--cdc` for better deduplication (slower but smaller)
42
43use anyhow::{Context, Result};
44use std::path::PathBuf;
45use std::process::Command;
46
47use crate::cmd::data::pack;
48
49/// Number of vCPUs passed to QEMU for the installer VM (2).
50///
51/// **Architectural intent:** Provides enough parallelism for typical
52/// installers without over-provisioning; passed as `-smp` to QEMU.
53const QEMU_SMP_COUNT: &str = "2";
54
55/// Block size in bytes used when creating the snapshot from the raw disk (64 KiB).
56///
57/// **Architectural intent:** Matches the default compression block size for
58/// the create pipeline; changing it affects output layout and compression
59/// granularity.
60const DEFAULT_BLOCK_SIZE: u32 = 65536;
61
62/// Installs an operating system from an ISO and converts it into a snapshot.
63///
64/// **Architectural intent:** Automates the multi-step workflow of creating a
65/// raw disk, running a QEMU-based installer, and feeding the resulting disk
66/// through the standard snapshot creation pipeline.
67///
68/// **Constraints:** Requires `qemu-img` and `qemu-system-x86_64` to be
69/// installed and in `$PATH`. The `disk_size` and `ram` parameters are passed
70/// directly to QEMU tooling and must use size suffixes they understand (for
71/// example `10G`, `4G`).
72///
73/// **Side effects:** Creates and deletes temporary disk images, spawns a full
74/// virtual machine for the duration of installation, and writes the final
75/// snapshot to `output`.
76pub fn run(
77    iso: PathBuf,
78    disk_size: String,
79    ram: String,
80    output: PathBuf,
81    no_graphics: bool,
82    vnc: bool,
83    cdc: bool,
84) -> Result<()> {
85    println!("Creating temporary raw disk ({})...", disk_size);
86    let temp_dir = tempfile::tempdir()?;
87    let raw_path = temp_dir.path().join("temp_install.raw");
88
89    let status = Command::new("qemu-img")
90        .arg("create")
91        .arg("-f")
92        .arg("raw")
93        .arg(&raw_path)
94        .arg(&disk_size)
95        .status()
96        .context("Failed to create raw disk. Is qemu-img installed?")?;
97
98    if !status.success() {
99        anyhow::bail!("Failed to create raw disk image");
100    }
101
102    println!("Starting Installer. Please install the OS and SHUT DOWN when finished.");
103    println!("NOTE: Networking is DISABLED to ensure a clean, isolated snapshot.");
104
105    let mut cmd = Command::new("qemu-system-x86_64");
106
107    cmd.arg("-m")
108        .arg(&ram)
109        .arg("-enable-kvm")
110        .arg("-smp")
111        .arg(QEMU_SMP_COUNT);
112
113    if vnc {
114        println!("Starting VNC server on display :1 (Port 5901).");
115        println!("Connect via SSH tunnel: ssh -L 5901:localhost:5901 <host>");
116        cmd.arg("-display").arg("vnc=:1");
117    } else if no_graphics {
118        println!("(Running in Headless Serial Mode)");
119        println!("* To exit QEMU: Press 'Ctrl+a' then 'x'");
120        println!(
121            "* IMPORTANT: You may need to edit the kernel boot line in GRUB and add 'console=ttyS0'"
122        );
123
124        cmd.arg("-nographic");
125    }
126
127    let status = cmd
128        .arg("-net")
129        .arg("none")
130        .arg("-cdrom")
131        .arg(&iso)
132        .arg("-drive")
133        .arg(format!("file={},format=raw", raw_path.display()))
134        .status()
135        .context("Failed to run QEMU installer")?;
136
137    if !status.success() {
138        anyhow::bail!("QEMU installer exited with error");
139    }
140
141    println!("Installation finished (VM shut down).");
142    println!("Converting raw disk to Hexz snapshot...");
143
144    pack::run(
145        Some(raw_path.clone()),
146        None,
147        output.clone(),
148        "lz4".to_string(),
149        false,
150        false,
151        DEFAULT_BLOCK_SIZE,
152        cdc,
153        16384,
154        65536,
155        131072,
156        false,
157    )?;
158
159    println!("Cleanup complete.");
160    println!("Created: {:?}", output);
161    println!("You can now boot this with: hexz boot {:?}", output);
162
163    Ok(())
164}