use anyhow::{bail, Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
fn shell_escape(s: &str) -> String {
if s.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '/')
{
return s.to_string();
}
let escaped = s.replace('\'', "'\\''");
format!("'{}'", escaped)
}
use crate::commands::qemu_img;
use crate::vm::qemu_config::{PortForward, PortProtocol};
use crate::wizard_types::{CreateWizardState, DiskAction, WizardQemuConfig};
pub enum InstallMedia<'a> {
None,
Iso(Option<&'a str>),
RecoveryImage(Option<&'a str>),
}
fn generate_uuid() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let seed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let mut state = seed as u64;
let mut bytes = [0u8; 16];
for byte in &mut bytes {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
*byte = (state >> 33) as u8;
}
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
format!(
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5],
bytes[6], bytes[7],
bytes[8], bytes[9],
bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]
)
}
fn generate_serial() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let seed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let seed = seed ^ (std::process::id() as u128) ^ (seed >> 64);
let chars: Vec<char> = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ".chars().collect();
let mut state = seed as u64;
let mut serial = String::with_capacity(12);
for _ in 0..12 {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let idx = ((state >> 33) as usize) % chars.len();
serial.push(chars[idx]);
}
serial
}
const OVMF_SEARCH_PATHS: &[&str] = &[
"/usr/share/edk2/x64/OVMF_CODE.4m.fd",
"/usr/share/edk2-ovmf/x64/OVMF_CODE.4m.fd",
"/usr/share/OVMF/x64/OVMF_CODE.4m.fd",
"/usr/share/ovmf/x64/OVMF_CODE.4m.fd",
"/usr/share/edk2-ovmf/x64/OVMF_CODE.fd",
"/usr/share/edk2/x64/OVMF_CODE.fd",
"/usr/share/OVMF/OVMF_CODE.fd",
"/usr/share/OVMF/OVMF_CODE_4M.fd",
"/usr/share/edk2/ovmf/OVMF_CODE.fd",
"/usr/share/edk2/ovmf/OVMF_CODE.cc.fd",
"/usr/share/qemu/ovmf-x86_64.bin",
"/usr/share/qemu/ovmf-x86_64-code.bin",
"/run/libvirt/nix-ovmf/OVMF_CODE.fd",
"/usr/share/ovmf/OVMF_CODE.fd",
"/usr/share/qemu/OVMF_CODE.fd",
"/usr/share/ovmf/x64/OVMF_CODE.fd",
];
fn find_ovmf_code_path() -> Option<String> {
for path in OVMF_SEARCH_PATHS {
if Path::new(path).exists() {
return Some(path.to_string());
}
}
None
}
const OVMF_SECBOOT_SEARCH_PATHS: &[&str] = &[
"/usr/share/edk2/x64/OVMF_CODE.secboot.4m.fd",
"/usr/share/OVMF/x64/OVMF_CODE.secboot.4m.fd",
"/usr/share/ovmf/x64/OVMF_CODE.secboot.4m.fd",
"/usr/share/edk2-ovmf/x64/OVMF_CODE.secboot.fd",
"/usr/share/OVMF/OVMF_CODE_4M.secboot.fd",
"/usr/share/OVMF/OVMF_CODE_4M.ms.fd",
"/usr/share/OVMF/OVMF_CODE.secboot.fd",
"/usr/share/OVMF/OVMF_CODE.ms.fd",
"/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd",
"/usr/share/ovmf/OVMF_CODE.secboot.fd",
"/usr/share/qemu/OVMF_CODE.secboot.fd",
];
fn find_ovmf_secboot_code_path() -> Option<String> {
for path in OVMF_SECBOOT_SEARCH_PATHS {
if Path::new(path).exists() {
return Some(path.to_string());
}
}
None
}
fn find_ovmf_secboot_vars_template() -> Option<String> {
let search_paths = [
"/usr/share/OVMF/OVMF_VARS_4M.ms.fd",
"/usr/share/OVMF/OVMF_VARS.ms.fd",
"/usr/share/edk2/ovmf/OVMF_VARS.secboot.fd",
];
for path in search_paths {
if Path::new(path).exists() {
return Some(path.to_string());
}
}
find_ovmf_vars_template()
}
#[derive(Debug)]
pub struct CreatedVm {
#[allow(dead_code)]
pub path: PathBuf,
pub launch_script: PathBuf,
#[allow(dead_code)]
pub disk_image: PathBuf,
}
pub fn create_vm(library_path: &Path, state: &CreateWizardState) -> Result<CreatedVm> {
if state.vm_name.trim().is_empty() {
bail!("VM name cannot be empty");
}
if state.folder_name.is_empty() {
bail!("Folder name cannot be empty");
}
if state.use_existing_disk {
if state.existing_disk_path.is_none() {
bail!("No existing disk selected");
}
let path = state.existing_disk_path.as_ref().unwrap();
if !path.exists() {
bail!("Selected disk does not exist: {}", path.display());
}
} else if state.disk_size_gb == 0 {
bail!("Disk size must be greater than 0");
}
let vm_dir = create_vm_directory(library_path, &state.folder_name)?;
let disk_filename = format!("{}.qcow2", state.folder_name);
let disk_path = if state.use_existing_disk {
handle_existing_disk(
&vm_dir,
&disk_filename,
state.existing_disk_path.as_ref().unwrap(),
&state.existing_disk_action,
)?
} else {
create_disk_image(&vm_dir, &disk_filename, state.disk_size_gb)?
};
let mut qemu_config = state.qemu_config.clone();
if let Some(ref rom_path) = state.bios_rom_path {
let rom_filename = rom_path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "rom.bin".to_string());
let dest = vm_dir.join(&rom_filename);
fs::copy(rom_path, &dest).with_context(|| {
format!(
"Failed to copy ROM file from {} to {}",
rom_path.display(),
dest.display()
)
})?;
qemu_config.bios_path = Some(PathBuf::from(&rom_filename));
}
let script_content = generate_launch_script_with_os(
&state.vm_name,
&disk_filename,
state.iso_path.as_deref(),
state.is_recovery_image,
&qemu_config,
state.selected_os.as_deref(),
state.floppy_path.as_deref(),
);
let launch_script_path = write_launch_script(&vm_dir, &script_content)?;
write_vm_metadata(&vm_dir, &state.vm_name, state.selected_os.as_deref(), None)?;
Ok(CreatedVm {
path: vm_dir,
launch_script: launch_script_path,
disk_image: disk_path,
})
}
fn handle_existing_disk(
vm_dir: &Path,
filename: &str,
source: &Path,
action: &DiskAction,
) -> Result<PathBuf> {
let dest = vm_dir.join(filename);
match action {
DiskAction::Copy => {
fs::copy(source, &dest).with_context(|| {
format!(
"Failed to copy disk from {} to {}",
source.display(),
dest.display()
)
})?;
}
DiskAction::Move => {
if fs::rename(source, &dest).is_err() {
fs::copy(source, &dest).with_context(|| {
format!(
"Failed to copy disk from {} to {}",
source.display(),
dest.display()
)
})?;
fs::remove_file(source).with_context(|| {
format!(
"Failed to remove original disk after copying: {}",
source.display()
)
})?;
}
}
}
Ok(dest)
}
pub fn write_vm_metadata(
vm_dir: &Path,
display_name: &str,
os_profile: Option<&str>,
notes: Option<&str>,
) -> Result<()> {
let metadata_path = vm_dir.join("vm-curator.toml");
let mut content = String::new();
content.push_str("# VM Curator metadata\n\n");
content.push_str(&format!(
"display_name = \"{}\"\n",
display_name.replace('"', "\\\"")
));
if let Some(profile) = os_profile {
content.push_str(&format!("os_profile = \"{}\"\n", profile));
}
if let Some(notes_text) = notes {
if notes_text.contains('\n') {
content.push_str(&format!("notes = '''\n{}'''\n", notes_text));
} else {
content.push_str(&format!(
"notes = \"{}\"\n",
notes_text.replace('"', "\\\"")
));
}
}
fs::write(&metadata_path, content)
.with_context(|| format!("Failed to write VM metadata: {}", metadata_path.display()))?;
Ok(())
}
pub fn create_vm_directory(library_path: &Path, folder_name: &str) -> Result<PathBuf> {
let vm_dir = library_path.join(folder_name);
if vm_dir.exists() {
bail!("VM directory already exists: {}", vm_dir.display());
}
fs::create_dir_all(&vm_dir)
.with_context(|| format!("Failed to create VM directory: {}", vm_dir.display()))?;
Ok(vm_dir)
}
pub fn create_disk_image(vm_dir: &Path, filename: &str, size_gb: u32) -> Result<PathBuf> {
let disk_path = vm_dir.join(filename);
let size_str = format!("{}G", size_gb);
qemu_img::create_disk(&disk_path, &size_str)
.with_context(|| format!("Failed to create disk image: {}", disk_path.display()))?;
Ok(disk_path)
}
fn is_windows_10_or_11(os_profile: Option<&str>) -> bool {
matches!(os_profile, Some("windows-10") | Some("windows-11"))
}
fn is_windows_11(os_profile: Option<&str>) -> bool {
matches!(os_profile, Some("windows-11"))
}
fn is_intel_macos(os_profile: Option<&str>, emulator: &str) -> bool {
if !emulator.contains("x86_64") {
return false;
}
os_profile.is_some_and(|p| p.starts_with("macos-") || p.starts_with("mac-osx-"))
}
#[allow(dead_code)]
fn is_modern_macos(os_profile: Option<&str>) -> bool {
matches!(
os_profile,
Some("macos-big-sur")
| Some("macos-monterey")
| Some("macos-ventura")
| Some("macos-sonoma")
| Some("macos-sequoia")
| Some("macos-tahoe")
)
}
fn generate_smbios_options() -> String {
let uuid = generate_uuid();
let system_serial = generate_serial();
let board_serial = generate_serial();
format!(
r#"# SMBIOS configuration (unique per VM to avoid Windows corporate detection)
SMBIOS_OPTS=(
-smbios "type=1,manufacturer=QEMU,product=Standard PC,version=1.0,serial={system_serial},uuid={uuid}"
-smbios "type=2,manufacturer=QEMU,product=Standard PC,version=1.0,serial={board_serial}"
)
"#,
system_serial = system_serial,
uuid = uuid,
board_serial = board_serial,
)
}
fn generate_tpm_functions() -> String {
r#"# TPM 2.0 emulation functions (required for Windows 11)
TPM_DIR="$VM_DIR/tpm"
init_tpm() {
if [[ ! -d "$TPM_DIR" ]]; then
echo "Initializing TPM state directory..."
mkdir -p "$TPM_DIR"
swtpm_setup --tpmstate "$TPM_DIR" \
--tpm2 \
--create-ek-cert \
--create-platform-cert \
--allow-signing \
--decryption \
--overwrite
fi
}
start_tpm() {
# Initialize TPM if needed
init_tpm
# Kill any existing swtpm for this VM
pkill -f "swtpm.*$TPM_DIR" 2>/dev/null || true
sleep 0.5
echo "Starting TPM emulator..."
swtpm socket \
--tpmstate dir="$TPM_DIR" \
--ctrl type=unixio,path="$TPM_DIR/swtpm-sock" \
--tpm2 \
--daemon
sleep 1
if [[ ! -S "$TPM_DIR/swtpm-sock" ]]; then
echo "Error: TPM socket not created"
exit 1
fi
}
stop_tpm() {
pkill -f "swtpm.*$TPM_DIR" 2>/dev/null || true
}
# Cleanup TPM on exit
cleanup() {
stop_tpm
}
trap cleanup EXIT
"#
.to_string()
}
fn generate_ovmf_vars_setup(needs_secboot: bool) -> String {
let ovmf_vars_template = if needs_secboot {
find_ovmf_secboot_vars_template()
.unwrap_or_else(|| "/usr/share/OVMF/OVMF_VARS.fd".to_string())
} else {
find_ovmf_vars_template().unwrap_or_else(|| "/usr/share/OVMF/OVMF_VARS.fd".to_string())
};
format!(
r#"# UEFI variables (writable copy per VM)
OVMF_VARS_TEMPLATE="{template}"
OVMF_VARS="$VM_DIR/OVMF_VARS.fd"
# Create a writable copy of OVMF_VARS if it doesn't exist
if [[ ! -f "$OVMF_VARS" ]]; then
if [[ -f "$OVMF_VARS_TEMPLATE" ]]; then
echo "Creating UEFI variables file..."
cp "$OVMF_VARS_TEMPLATE" "$OVMF_VARS"
else
echo "Warning: OVMF_VARS template not found at $OVMF_VARS_TEMPLATE"
echo "UEFI variables may not persist across reboots"
fi
fi
"#,
template = ovmf_vars_template
)
}
fn find_ovmf_vars_template() -> Option<String> {
let search_paths = [
"/usr/share/edk2/x64/OVMF_VARS.4m.fd",
"/usr/share/edk2-ovmf/x64/OVMF_VARS.4m.fd",
"/usr/share/OVMF/x64/OVMF_VARS.4m.fd",
"/usr/share/edk2-ovmf/x64/OVMF_VARS.fd",
"/usr/share/edk2/x64/OVMF_VARS.fd",
"/usr/share/OVMF/OVMF_VARS.fd",
"/usr/share/OVMF/OVMF_VARS_4M.fd",
"/usr/share/edk2/ovmf/OVMF_VARS.fd",
"/usr/share/ovmf/OVMF_VARS.fd",
"/usr/share/qemu/OVMF_VARS.fd",
];
for path in search_paths {
if Path::new(path).exists() {
return Some(path.to_string());
}
}
None
}
pub fn generate_launch_script_with_os(
vm_name: &str,
disk_filename: &str,
iso_path: Option<&Path>,
is_recovery_image: bool,
config: &WizardQemuConfig,
os_profile: Option<&str>,
floppy_path: Option<&Path>,
) -> String {
let mut script = String::new();
let is_windows = is_windows_10_or_11(os_profile);
let is_intel_macos_vm = is_intel_macos(os_profile, &config.emulator);
let needs_tpm = config.tpm || is_windows_11(os_profile);
let needs_uefi = config.uefi || is_windows_11(os_profile);
script.push_str("#!/bin/bash\n\n");
script.push_str(&format!("# {} VM Launch Script\n", vm_name));
script.push_str(&format!(
"# {} CPUs, {}MB RAM, {} graphics, {} disk interface\n",
config.cpu_cores, config.memory_mb, config.vga, config.disk_interface
));
if is_windows {
script.push_str("# Windows VM with unique SMBIOS identifiers\n");
}
if is_intel_macos_vm {
script.push_str("# macOS VM with Apple SMC emulation\n");
}
if needs_tpm {
script.push_str("# TPM 2.0 enabled (requires swtpm package)\n");
}
if needs_tpm && needs_uefi {
script.push_str("# Secure Boot enabled (OVMF secboot + SMM)\n");
}
script.push_str("# Generated by vm-curator\n\n");
script.push_str("VM_DIR=\"$(dirname \"$(readlink -f \"$0\")\")\"\n");
script.push_str(&format!("DISK=\"$VM_DIR/{}\"\n", disk_filename));
if is_recovery_image {
if let Some(path) = iso_path {
script.push_str(&format!(
"RECOVERY_IMG={}\n",
shell_escape(&path.display().to_string())
));
} else {
script.push_str("RECOVERY_IMG=\"\"\n");
}
} else {
if let Some(iso) = iso_path {
script.push_str(&format!(
"ISO={}\n",
shell_escape(&iso.display().to_string())
));
} else {
script.push_str("ISO=\"\"\n");
}
}
if let Some(floppy) = floppy_path {
script.push_str(&format!(
"FLOPPY={}\n",
shell_escape(&floppy.display().to_string())
));
}
if let Some(ref bios_path) = config.bios_path {
let filename = bios_path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| bios_path.display().to_string());
script.push_str(&format!("ROM=\"$VM_DIR/{}\"\n", filename));
}
script.push('\n');
if is_intel_macos_vm && needs_uefi && config.bios_path.is_some() {
script.push_str(
r#"# Verify OpenCore bootloader exists
if [[ ! -f "$ROM" ]]; then
echo "Error: OpenCore bootloader not found at $ROM"
echo "Download from: https://github.com/kholia/OSX-KVM"
echo "Place OpenCore.qcow2 in: $VM_DIR/"
exit 1
fi
"#,
);
}
if is_windows {
script.push_str(&generate_smbios_options());
}
if needs_uefi {
script.push_str(&generate_ovmf_vars_setup(needs_tpm));
}
if needs_tpm {
script.push_str(&generate_tpm_functions());
}
script.push_str("show_help() {\n");
script.push_str(" echo \"Usage: $0 [OPTIONS]\"\n");
script.push_str(" echo \"\"\n");
script.push_str(" echo \"Options:\"\n");
script.push_str(" echo \" --install Boot from installation media\"\n");
script.push_str(" echo \" --cdrom <iso> Boot with specified ISO as CD-ROM\"\n");
script.push_str(" echo \" --recovery <dmg> Boot with recovery image (DMG)\"\n");
script.push_str(" echo \" --floppy <img> Boot with specified floppy image\"\n");
script.push_str(" echo \" (no options) Normal boot from hard disk\"\n");
script.push_str("}\n\n");
let floppy_ref = if floppy_path.is_some() {
Some("\"$FLOPPY\"")
} else {
None
};
let base_cmd = build_qemu_command_with_os(
config,
disk_filename,
&InstallMedia::None,
os_profile,
floppy_ref,
);
let install_cmd = if is_recovery_image {
build_qemu_command_with_os(
config,
disk_filename,
&InstallMedia::RecoveryImage(None),
os_profile,
floppy_ref,
)
} else {
build_qemu_command_with_os(
config,
disk_filename,
&InstallMedia::Iso(None),
os_profile,
floppy_ref,
)
};
script.push_str("case \"$1\" in\n");
script.push_str(" --install)\n");
if is_recovery_image {
script.push_str(
" if [[ -z \"$RECOVERY_IMG\" ]] || [[ ! -f \"$RECOVERY_IMG\" ]]; then\n",
);
script.push_str(" echo \"Error: Recovery image not found at $RECOVERY_IMG\"\n");
script.push_str(
" echo \"Please edit this script to set the path or use --recovery\"\n",
);
script.push_str(" exit 1\n");
script.push_str(" fi\n");
script.push_str(" echo \"Booting from recovery image...\"\n");
} else {
script.push_str(" if [[ -z \"$ISO\" ]] || [[ ! -f \"$ISO\" ]]; then\n");
script.push_str(" echo \"Error: Installation ISO not found at $ISO\"\n");
script.push_str(
" echo \"Please edit this script to set the ISO path or use --cdrom\"\n",
);
script.push_str(" exit 1\n");
script.push_str(" fi\n");
script.push_str(" echo \"Booting from installation ISO...\"\n");
}
if needs_tpm {
script.push_str(" start_tpm\n");
}
script.push_str(&format!(" {}\n", install_cmd));
script.push_str(" ;;\n");
script.push_str(" --cdrom)\n");
script.push_str(" if [[ -z \"$2\" ]] || [[ ! -f \"$2\" ]]; then\n");
script.push_str(" echo \"Error: Please specify a valid ISO file\"\n");
script.push_str(" exit 1\n");
script.push_str(" fi\n");
script.push_str(" echo \"Booting with CD-ROM: $2\"\n");
if needs_tpm {
script.push_str(" start_tpm\n");
}
let cdrom_cmd = build_qemu_command_with_os(
config,
disk_filename,
&InstallMedia::Iso(Some("\"$2\"")),
os_profile,
floppy_ref,
);
script.push_str(&format!(" {}\n", cdrom_cmd));
script.push_str(" ;;\n");
script.push_str(" --recovery)\n");
script.push_str(" if [[ -z \"$2\" ]] || [[ ! -f \"$2\" ]]; then\n");
script.push_str(" echo \"Error: Please specify a valid DMG file\"\n");
script.push_str(" exit 1\n");
script.push_str(" fi\n");
script.push_str(" echo \"Booting with recovery image: $2\"\n");
if needs_tpm {
script.push_str(" start_tpm\n");
}
let recovery_cmd = build_qemu_command_with_os(
config,
disk_filename,
&InstallMedia::RecoveryImage(Some("\"$2\"")),
os_profile,
floppy_ref,
);
script.push_str(&format!(" {}\n", recovery_cmd));
script.push_str(" ;;\n");
script.push_str(" --floppy)\n");
script.push_str(" if [[ -z \"$2\" ]] || [[ ! -f \"$2\" ]]; then\n");
script.push_str(" echo \"Error: Please specify a valid floppy image file\"\n");
script.push_str(" exit 1\n");
script.push_str(" fi\n");
script.push_str(" echo \"Booting with floppy: $2\"\n");
if needs_tpm {
script.push_str(" start_tpm\n");
}
let floppy_cmd = build_qemu_command_with_os(
config,
disk_filename,
&InstallMedia::None,
os_profile,
Some("\"$2\""),
);
script.push_str(&format!(" {}\n", floppy_cmd));
script.push_str(" ;;\n");
script.push_str(" --help|-h)\n");
script.push_str(" show_help\n");
script.push_str(" exit 0\n");
script.push_str(" ;;\n");
script.push_str(" \"\")\n");
script.push_str(&format!(" echo \"Booting {}...\"\n", vm_name));
if needs_tpm {
script.push_str(" start_tpm\n");
}
script.push_str(&format!(" {}\n", base_cmd));
script.push_str(" ;;\n");
script.push_str(" *)\n");
script.push_str(" echo \"Unknown option: $1\"\n");
script.push_str(" show_help\n");
script.push_str(" exit 1\n");
script.push_str(" ;;\n");
script.push_str("esac\n");
script
}
pub(crate) const SPICE_AGENT_ARGS: &[&str] = &[
"-device virtio-serial-pci",
"-chardev spicevmc,id=spicechannel0,name=vdagent",
"-device virtserialport,chardev=spicechannel0,name=com.redhat.spice.0",
];
fn build_qemu_command_with_os(
config: &WizardQemuConfig,
_disk_filename: &str,
install_media: &InstallMedia,
os_profile: Option<&str>,
floppy_path: Option<&str>,
) -> String {
let mut args: Vec<String> = Vec::new();
let is_windows = is_windows_10_or_11(os_profile);
let is_intel_macos_vm = is_intel_macos(os_profile, &config.emulator);
let needs_tpm = config.tpm || is_windows_11(os_profile);
let needs_uefi = config.uefi || is_windows_11(os_profile);
args.push(config.emulator.clone());
if config.enable_kvm {
args.push("-enable-kvm".to_string());
}
if config.bios_path.is_some() && !(is_intel_macos_vm && needs_uefi) {
args.push("-bios \"$ROM\"".to_string());
}
if let Some(ref machine) = config.machine {
let safe_machine = shell_escape(machine);
let needs_secboot = needs_tpm && needs_uefi;
let mut machine_opts = vec![safe_machine.to_string()];
if config.enable_kvm {
machine_opts.push("accel=kvm".to_string());
}
if needs_secboot {
machine_opts.push("smm=on".to_string());
}
args.push(format!("-machine {}", machine_opts.join(",")));
}
if let Some(ref cpu_model) = config.cpu_model {
args.push(format!("-cpu {}", shell_escape(cpu_model)));
}
args.push(format!(
"-smp {},sockets=1,cores={},threads=1",
config.cpu_cores, config.cpu_cores
));
args.push(format!("-m {}M", config.memory_mb));
if is_windows {
args.push("\"${SMBIOS_OPTS[@]}\"".to_string());
}
if is_intel_macos_vm {
args.push("-device \"isa-applesmc,osk=ourhardworkbythesewordsguardedpleasedontsteal(c)AppleComputerInc\"".to_string());
args.push("-smbios type=2".to_string());
}
if needs_uefi {
let needs_secboot = needs_tpm;
let ovmf_code = if needs_secboot {
find_ovmf_secboot_code_path()
.or_else(find_ovmf_code_path)
.unwrap_or_else(|| "/usr/share/OVMF/OVMF_CODE.fd".to_string())
} else {
find_ovmf_code_path().unwrap_or_else(|| "/usr/share/OVMF/OVMF_CODE.fd".to_string())
};
args.push(format!(
"-drive if=pflash,format=raw,readonly=on,file={}",
ovmf_code
));
args.push("-drive if=pflash,format=raw,file=\"$OVMF_VARS\"".to_string());
if needs_secboot {
args.push("-global driver=cfi.pflash01,property=secure,value=on".to_string());
}
}
let machine_name = config.machine.as_deref().unwrap_or("");
match machine_name {
"q800" => {
args.push("-drive file=\"$DISK\",format=qcow2,if=none,id=hd0".to_string());
args.push("-device scsi-hd,drive=hd0".to_string());
}
"mac99" => {
args.push("-drive file=\"$DISK\",format=qcow2,if=none,id=hd0".to_string());
args.push("-device ide-hd,drive=hd0,bus=ide.0".to_string());
}
_ => {
if is_intel_macos_vm && needs_uefi {
args.push("-device ich9-ahci,id=sata".to_string());
if config.bios_path.is_some() {
args.push("-drive file=\"$ROM\",format=qcow2,if=none,id=oc".to_string());
args.push("-device ide-hd,drive=oc,bus=sata.0".to_string());
}
let disk_bus = if config.bios_path.is_some() {
"sata.1"
} else {
"sata.0"
};
args.push("-drive file=\"$DISK\",format=qcow2,if=none,id=maindisk".to_string());
args.push(format!("-device ide-hd,drive=maindisk,bus={}", disk_bus));
} else {
let disk_if = if config.disk_interface == "sata" {
"ide"
} else {
&config.disk_interface
};
args.push(format!(
"-drive file=\"$DISK\",format=qcow2,if={},index=0,media=disk",
shell_escape(disk_if)
));
}
}
}
match install_media {
InstallMedia::None => {}
InstallMedia::Iso(custom_path) => {
let iso_ref = custom_path.unwrap_or("\"$ISO\"");
match machine_name {
"q800" => {
args.push(format!(
"-drive file={},format=raw,if=none,id=cd0,media=cdrom",
iso_ref
));
args.push("-device scsi-cd,drive=cd0".to_string());
}
"mac99" => {
args.push(format!(
"-drive file={},format=raw,if=none,id=cd0,media=cdrom",
iso_ref
));
args.push("-device ide-cd,drive=cd0,bus=ide.1".to_string());
}
_ => {
if is_intel_macos_vm && needs_uefi {
let iso_bus = if config.bios_path.is_some() {
"sata.3"
} else {
"sata.2"
};
args.push(format!(
"-drive file={},format=raw,if=none,id=cd0,media=cdrom",
iso_ref
));
args.push(format!("-device ide-cd,drive=cd0,bus={}", iso_bus));
} else {
args.push(format!("-drive file={},media=cdrom,index=1", iso_ref));
args.push("-boot d".to_string());
}
}
}
if !is_intel_macos_vm || !needs_uefi {
if machine_name == "q800" || machine_name == "mac99" {
args.push("-boot d".to_string());
}
}
}
InstallMedia::RecoveryImage(custom_path) => {
let dmg_ref = custom_path.unwrap_or("\"$RECOVERY_IMG\"");
if is_intel_macos_vm && needs_uefi {
let recovery_bus = if config.bios_path.is_some() {
"sata.2"
} else {
"sata.1"
};
args.push(format!(
"-drive file={},snapshot=on,if=none,id=recovery",
dmg_ref
));
args.push(format!(
"-device ide-hd,drive=recovery,bus={}",
recovery_bus
));
} else {
args.push(format!(
"-drive file={},snapshot=on,format=dmg,if=ide,index=2,media=disk",
dmg_ref
));
}
}
}
if let Some(floppy_ref) = floppy_path {
args.push(format!("-fda {}", floppy_ref));
if matches!(install_media, InstallMedia::Iso(_)) {
if let Some(pos) = args.iter().position(|a| a == "-boot d") {
args[pos] = "-boot a".to_string();
}
}
}
if config.gl_acceleration && config.vga == "virtio" {
args.push("-device virtio-vga-gl".to_string());
} else {
args.push(format!("-vga {}", shell_escape(&config.vga)));
}
if config.gl_acceleration {
args.push(format!("-display {},gl=on", shell_escape(&config.display)));
} else {
args.push(format!("-display {}", shell_escape(&config.display)));
}
if !config.audio.is_empty() {
if config.display == "spice-app" {
args.push("-audiodev spice,id=audio0".to_string());
} else {
args.push("-audiodev pa,id=audio0".to_string());
}
}
if config.display == "spice-app" {
for a in SPICE_AGENT_ARGS {
args.push((*a).to_string());
}
}
for audio in &config.audio {
match audio.as_str() {
"intel-hda" => args.push("-device intel-hda".to_string()),
"hda-duplex" | "hda-output" | "hda-micro" => {
args.push(format!("-device {},audiodev=audio0", shell_escape(audio)));
}
"ac97" => args.push("-device AC97,audiodev=audio0".to_string()),
"sb16" => args.push("-device sb16,audiodev=audio0".to_string()),
"screamer" => {
}
_ => {
args.push(format!("-device {},audiodev=audio0", shell_escape(audio)));
}
}
}
if config.network_model != "none" {
let net_device = match config.network_model.as_str() {
"virtio" => "virtio-net-pci".to_string(),
other => shell_escape(other),
};
let mac_suffix = config
.mac_address
.as_deref()
.filter(|m| crate::vm::mac::is_valid_mac(m))
.map(|m| format!(",mac={}", m))
.unwrap_or_default();
match config.network_backend.as_str() {
"none" => {
}
"passt" => {
args.push("-netdev passt,id=net0".to_string());
args.push(format!("-device {},netdev=net0{}", net_device, mac_suffix));
}
"bridge" => {
let br = config.bridge_name.as_deref().unwrap_or("qemubr0");
args.push(format!("-netdev bridge,id=net0,br={}", shell_escape(br)));
args.push(format!("-device {},netdev=net0{}", net_device, mac_suffix));
}
_ => {
let mut netdev = "-netdev user,id=net0".to_string();
for pf in &config.port_forwards {
let proto = match pf.protocol {
PortProtocol::Tcp => "tcp",
PortProtocol::Udp => "udp",
};
netdev.push_str(&format!(
",hostfwd={}::{}-:{}",
proto, pf.host_port, pf.guest_port
));
}
args.push(netdev);
args.push(format!("-device {},netdev=net0{}", net_device, mac_suffix));
}
}
}
if config.usb_tablet {
args.push("-usb".to_string());
if is_intel_macos_vm {
args.push("-device usb-kbd".to_string());
}
args.push("-device usb-tablet".to_string());
}
if config.rtc_localtime {
args.push("-rtc base=localtime".to_string());
}
if needs_tpm {
args.push("-chardev socket,id=chrtpm,path=\"$TPM_DIR/swtpm-sock\"".to_string());
args.push("-tpmdev emulator,id=tpm0,chardev=chrtpm".to_string());
args.push("-device tpm-tis,tpmdev=tpm0".to_string());
}
for arg in &config.extra_args {
args.push(arg.clone());
}
args.push("-qmp".to_string());
args.push("unix:$VM_DIR/qemu.sock,server=on,wait=off".to_string());
args.join(" \\\n ")
}
pub fn write_launch_script(vm_dir: &Path, content: &str) -> Result<PathBuf> {
use std::os::unix::fs::PermissionsExt;
let script_path = vm_dir.join("launch.sh");
fs::write(&script_path, content)
.with_context(|| format!("Failed to write launch script: {}", script_path.display()))?;
let mut perms = fs::metadata(&script_path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&script_path, perms)
.with_context(|| format!("Failed to set permissions on: {}", script_path.display()))?;
Ok(script_path)
}
pub fn update_network_in_script(
vm_path: &Path,
model: &str,
backend: &str,
bridge_name: Option<&str>,
port_forwards: &[PortForward],
mac_address: Option<&str>,
) -> Result<()> {
let script_path = vm_path.join("launch.sh");
let content = std::fs::read_to_string(&script_path)
.with_context(|| format!("Failed to read launch script: {}", script_path.display()))?;
let new_net_args =
generate_network_args(model, backend, bridge_name, port_forwards, mac_address);
let mut new_lines = Vec::new();
let lines: Vec<&str> = content.lines().collect();
let mut i = 0;
let mut replaced = false;
fn is_network_line(trimmed: &str) -> bool {
let is_netdev = trimmed.contains("-netdev ")
|| trimmed.contains("-net user")
|| trimmed.contains("-net bridge");
let is_net_device = (trimmed.contains("-device ") && trimmed.contains("netdev=net0"))
|| (trimmed.contains("-device ")
&& (trimmed.contains("e1000")
|| trimmed.contains("virtio-net")
|| trimmed.contains("rtl8139")
|| trimmed.contains("ne2k_pci")
|| trimmed.contains("pcnet"))
&& !trimmed.contains("vga")
&& !trimmed.contains("audio"));
is_netdev || is_net_device
}
while i < lines.len() {
let line = lines[i];
let trimmed = line.trim();
if trimmed.starts_with('#') {
new_lines.push(line.to_string());
i += 1;
continue;
}
if is_network_line(trimmed) {
while i < lines.len() && is_network_line(lines[i].trim()) {
i += 1;
}
for arg in &new_net_args {
new_lines.push(arg.clone());
}
replaced = true;
} else {
new_lines.push(line.to_string());
i += 1;
}
}
if !replaced && !new_net_args.is_empty() {
let insert_pos = new_lines.len().saturating_sub(2);
for (j, arg) in new_net_args.iter().enumerate() {
new_lines.insert(insert_pos + j, arg.clone());
}
}
let new_content = new_lines.join("\n");
let new_content = if new_content.ends_with('\n') {
new_content
} else {
format!("{}\n", new_content)
};
std::fs::write(&script_path, new_content)
.with_context(|| format!("Failed to write launch script: {}", script_path.display()))?;
Ok(())
}
fn is_spice_agent_line(line: &str) -> bool {
let mut t = line.trim();
if let Some(stripped) = t.strip_suffix('\\') {
t = stripped.trim_end();
}
SPICE_AGENT_ARGS.contains(&t)
}
pub fn set_spice_agent_args(content: &str, enable: bool) -> String {
let ends_with_newline = content.ends_with('\n');
let stripped: Vec<String> = content
.lines()
.filter(|line| !is_spice_agent_line(line))
.map(|s| s.to_string())
.collect();
let lines: Vec<String> = if enable {
let mut out: Vec<String> = Vec::with_capacity(stripped.len() + SPICE_AGENT_ARGS.len());
for line in stripped {
let t = line.trim_start();
let is_display = !t.starts_with('#') && t.starts_with("-display ");
if !is_display {
out.push(line);
continue;
}
let indent_len = line.len() - line.trim_start().len();
let indent = line[..indent_len].to_string();
if line.trim_end().ends_with('\\') {
out.push(line);
for a in SPICE_AGENT_ARGS {
out.push(format!("{}{} \\", indent, a));
}
} else {
out.push(format!("{} \\", line.trim_end()));
let last = SPICE_AGENT_ARGS.len() - 1;
for (idx, a) in SPICE_AGENT_ARGS.iter().enumerate() {
if idx == last {
out.push(format!("{}{}", indent, a));
} else {
out.push(format!("{}{} \\", indent, a));
}
}
}
}
out
} else {
stripped
};
let mut s = lines.join("\n");
if ends_with_newline {
s.push('\n');
}
s
}
fn generate_network_args(
model: &str,
backend: &str,
bridge_name: Option<&str>,
port_forwards: &[PortForward],
mac_address: Option<&str>,
) -> Vec<String> {
if model == "none" {
return Vec::new();
}
let net_device = match model {
"virtio" => "virtio-net-pci".to_string(),
other => shell_escape(other),
};
let mac_suffix = mac_address
.filter(|m| crate::vm::mac::is_valid_mac(m))
.map(|m| format!(",mac={}", m))
.unwrap_or_default();
let mut args = Vec::new();
match backend {
"none" => {
}
"passt" => {
args.push(" -netdev passt,id=net0 \\".to_string());
args.push(format!(
" -device {},netdev=net0{} \\",
net_device, mac_suffix
));
}
"bridge" => {
let br = bridge_name.unwrap_or("qemubr0");
args.push(format!(
" -netdev bridge,id=net0,br={} \\",
shell_escape(br)
));
args.push(format!(
" -device {},netdev=net0{} \\",
net_device, mac_suffix
));
}
_ => {
let mut netdev = " -netdev user,id=net0".to_string();
for pf in port_forwards {
let proto = match pf.protocol {
PortProtocol::Tcp => "tcp",
PortProtocol::Udp => "udp",
};
netdev.push_str(&format!(
",hostfwd={}::{}-:{}",
proto, pf.host_port, pf.guest_port
));
}
netdev.push_str(" \\");
args.push(netdev);
args.push(format!(
" -device {},netdev=net0{} \\",
net_device, mac_suffix
));
}
}
args
}
#[cfg(test)]
#[path = "tests/create.rs"]
mod tests;