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}