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}