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 Alpine with minimal disk
27//! hexz vm install --iso alpine.iso --disk-size 5G \
28//!   --ram 1G --output alpine.st
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 (= `primary_size`)
36//!
37//! # Performance Notes
38//!
39//! - Installation speed depends on ISO and hardware
40//! - Packing uses LZ4 compression by default (fast)
41//! - CDC parameters are auto-detected via DCAM for optimal deduplication
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 `primary_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    primary_size: String,
79    ram: String,
80    output: PathBuf,
81    no_graphics: bool,
82    vnc: bool,
83) -> Result<()> {
84    println!("Creating temporary raw disk ({})...", primary_size);
85    let temp_dir = tempfile::tempdir()?;
86    let raw_path = temp_dir.path().join("temp_install.raw");
87
88    let status = Command::new("qemu-img")
89        .arg("create")
90        .arg("-f")
91        .arg("raw")
92        .arg(&raw_path)
93        .arg(&primary_size)
94        .status()
95        .context("Failed to create raw disk. Is qemu-img installed?")?;
96
97    if !status.success() {
98        anyhow::bail!("Failed to create raw disk image");
99    }
100
101    println!("Starting Installer. Please install the OS and SHUT DOWN when finished.");
102    println!("NOTE: Networking is DISABLED to ensure a clean, isolated snapshot.");
103
104    let mut cmd = Command::new("qemu-system-x86_64");
105
106    cmd.arg("-m")
107        .arg(&ram)
108        .arg("-enable-kvm")
109        .arg("-smp")
110        .arg(QEMU_SMP_COUNT);
111
112    if vnc {
113        println!("Starting VNC server on display :1 (Port 5901).");
114        println!("Connect via SSH tunnel: ssh -L 5901:localhost:5901 <host>");
115        cmd.arg("-display").arg("vnc=:1");
116    } else if no_graphics {
117        println!("(Running in Headless Serial Mode)");
118        println!("* To exit QEMU: Press 'Ctrl+a' then 'x'");
119        println!(
120            "* IMPORTANT: You may need to edit the kernel boot line in GRUB and add 'console=ttyS0'"
121        );
122
123        cmd.arg("-nographic");
124    }
125
126    let status = cmd
127        .arg("-net")
128        .arg("none")
129        .arg("-cdrom")
130        .arg(&iso)
131        .arg("-drive")
132        .arg(format!("file={},format=raw", raw_path.display()))
133        .status()
134        .context("Failed to run QEMU installer")?;
135
136    if !status.success() {
137        anyhow::bail!("QEMU installer exited with error");
138    }
139
140    println!("Installation finished (VM shut down).");
141    println!("Converting raw disk to Hexz snapshot...");
142
143    pack::run(
144        Some(raw_path.clone()),
145        None,
146        output.clone(),
147        "lz4".to_string(),
148        false,
149        false,
150        DEFAULT_BLOCK_SIZE,
151        None,  // min_chunk (use default)
152        None,  // avg_chunk (use default)
153        None,  // max_chunk (use default)
154        None,  // workers (auto)
155        false, // dcam (use fixed defaults)
156        false, // silent
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}