use std::path::{Path, PathBuf};
use arcbox_constants::cmdline::MACHINE_NAME_KEY;
use chrono::Utc;
use super::{MachineInfo, MachineManager, MachineState, reserve_hostname, vm_shared_dirs};
use crate::error::{EngineError, Result};
use crate::vm::{BlockDeviceConfig, VmConfig};
pub(super) const DATA_DISK: &str = "data.img";
impl MachineManager {
pub fn clone_machine(&self, source: &str, name: &str) -> Result<String> {
let mut machines = self
.machines
.write()
.map_err(|_| EngineError::LockPoisoned)?;
let hostname = reserve_hostname(&machines, name)?;
let src = machines
.get(source)
.ok_or_else(|| EngineError::not_found(source.to_owned()))?;
let src_disk = stopped_data_disk(src)?;
let machine_dir = self.machines_dir.join(name);
std::fs::create_dir_all(&machine_dir)?;
let disk = machine_dir.join(DATA_DISK);
let built = clone_file(&src_disk, &disk).and_then(|()| {
let info = cloned_record(src, name, &hostname, &src_disk, &disk);
let vm_id = self.vm_manager.create(VmConfig {
cpus: info.cpus,
memory_mb: info.memory_mb,
kernel: info.kernel.clone(),
cmdline: info.cmdline.clone(),
shared_dirs: vm_shared_dirs(&self.data_dir, &info.mounts),
block_devices: info.block_devices.clone(),
backend: info.backend,
nested_virt: info.nested_virt,
..Default::default()
})?;
Ok(MachineInfo { vm_id, ..info })
});
let info = match built {
Ok(info) => info,
Err(e) => {
if let Err(cleanup) = std::fs::remove_dir_all(&machine_dir) {
tracing::warn!(
machine = name,
error = %cleanup,
"could not remove the directory of a failed clone"
);
}
return Err(e);
}
};
tracing::info!(machine = name, source, "cloned machine");
self.register(&mut machines, info)
}
}
pub(super) fn stopped_data_disk(machine: &MachineInfo) -> Result<PathBuf> {
let Some(disk) = machine
.disk_path
.clone()
.filter(|_| machine.distro.is_some())
else {
return Err(EngineError::invalid_state(format!(
"machine '{}' is not a distro machine with its own data disk",
machine.name
)));
};
if !matches!(machine.state, MachineState::Created | MachineState::Stopped) {
let state = format!("{:?}", machine.state).to_lowercase();
return Err(EngineError::invalid_state(format!(
"machine '{}' is {state}; stop it first",
machine.name
)));
}
Ok(disk)
}
fn cloned_record(
src: &MachineInfo,
name: &str,
hostname: &str,
src_disk: &Path,
disk: &Path,
) -> MachineInfo {
let block_devices = src
.block_devices
.iter()
.map(|device| {
if Path::new(&device.path) == src_disk {
BlockDeviceConfig {
path: disk.to_string_lossy().into_owned(),
read_only: device.read_only,
}
} else {
device.clone()
}
})
.collect();
MachineInfo {
name: name.to_owned(),
state: MachineState::Created,
vm_id: src.vm_id.clone(),
cid: None,
cpus: src.cpus,
memory_mb: src.memory_mb,
disk_gb: src.disk_gb,
kernel: src.kernel.clone(),
cmdline: src
.cmdline
.as_deref()
.map(|cmdline| cmdline_with_hostname(cmdline, hostname)),
block_devices,
distro: src.distro.clone(),
distro_version: src.distro_version.clone(),
disk_path: Some(disk.to_path_buf()),
ssh_key_path: None,
ip_address: None,
bridge_ip_address: None,
backend: src.backend,
nested_virt: src.nested_virt,
created_at: Utc::now(),
started_at: None,
mounts: src.mounts.clone(),
}
}
pub(super) fn cmdline_with_hostname(cmdline: &str, hostname: &str) -> String {
let token = format!("{MACHINE_NAME_KEY}{hostname}");
let mut replaced = false;
let mut tokens: Vec<&str> = cmdline
.split_whitespace()
.map(|t| {
if t.starts_with(MACHINE_NAME_KEY) {
replaced = true;
token.as_str()
} else {
t
}
})
.collect();
if !replaced {
tokens.push(&token);
}
tokens.join(" ")
}
#[cfg(target_os = "macos")]
pub fn clone_file(src: &Path, dst: &Path) -> Result<()> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt as _;
let c_path = |path: &Path| {
CString::new(path.as_os_str().as_bytes())
.map_err(|_| EngineError::config(format!("path {} contains NUL", path.display())))
};
let (c_src, c_dst) = (c_path(src)?, c_path(dst)?);
if unsafe { libc::clonefile(c_src.as_ptr(), c_dst.as_ptr(), 0) } == 0 {
return Ok(());
}
let err = std::io::Error::last_os_error();
Err(match err.raw_os_error() {
Some(libc::ENOTSUP | libc::EXDEV) => EngineError::config(format!(
"cannot clone {}: the data directory must be on an APFS volume ({err})",
src.display()
)),
_ => EngineError::from(std::io::Error::new(
err.kind(),
format!("clonefile {} -> {}: {err}", src.display(), dst.display()),
)),
})
}
#[cfg(not(target_os = "macos"))]
pub fn clone_file(src: &Path, dst: &Path) -> Result<()> {
if dst.exists() {
return Err(EngineError::already_exists(dst.display().to_string()));
}
std::fs::copy(src, dst)?;
Ok(())
}