hardpass-vm 0.3.1

A small, reliable Ubuntu cloud-image VM manager built on QEMU.
Documentation
use std::path::{Path, PathBuf};
use std::sync::Arc;

use anyhow::Result;

use crate::cli::CreateArgs;
use crate::instance::{InstanceManager, VmInfo};
use crate::ssh::ExecOutput;
use crate::state::{
    AccelMode, GuestArch, HardpassState, InstanceStatus, PortForward, validate_name,
};

pub struct Hardpass {
    manager: Arc<InstanceManager>,
}

impl Hardpass {
    pub async fn load() -> Result<Self> {
        let state = HardpassState::load().await?;
        Self::from_state(state).await
    }

    pub async fn with_root(root: impl AsRef<Path>) -> Result<Self> {
        let state = HardpassState::load_with_root(root.as_ref().to_path_buf()).await?;
        Self::from_state(state).await
    }

    pub async fn doctor(&self) -> Result<()> {
        self.manager.doctor().await
    }

    pub async fn create(&self, spec: VmSpec) -> Result<Vm> {
        let name = spec.name.clone();
        self.manager.create_silent(spec.into_create_args()).await?;
        self.vm(name)
    }

    pub fn vm(&self, name: impl Into<String>) -> Result<Vm> {
        let name = name.into();
        validate_name(&name)?;
        Ok(Vm {
            manager: Arc::clone(&self.manager),
            name,
        })
    }

    async fn from_state(state: HardpassState) -> Result<Self> {
        let instance = Self {
            manager: Arc::new(InstanceManager::new(state)),
        };
        instance.manager.auto_configure_ssh_if_enabled().await;
        Ok(instance)
    }
}

#[derive(Debug, Clone, Default)]
pub struct VmSpec {
    pub name: String,
    pub release: Option<String>,
    pub arch: Option<GuestArch>,
    pub accel: Option<AccelMode>,
    pub cpus: Option<u8>,
    pub memory_mib: Option<u32>,
    pub disk_gib: Option<u32>,
    pub ssh_key: Option<PathBuf>,
    pub forwards: Vec<PortForward>,
    pub timeout_secs: Option<u64>,
    pub cloud_init_user_data: Option<PathBuf>,
    pub cloud_init_network_config: Option<PathBuf>,
}

impl VmSpec {
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            ..Self::default()
        }
    }

    pub fn release(mut self, release: impl Into<String>) -> Self {
        self.release = Some(release.into());
        self
    }

    pub fn arch(mut self, arch: GuestArch) -> Self {
        self.arch = Some(arch);
        self
    }

    pub fn accel(mut self, accel: AccelMode) -> Self {
        self.accel = Some(accel);
        self
    }

    pub fn cpus(mut self, cpus: u8) -> Self {
        self.cpus = Some(cpus);
        self
    }

    pub fn memory_mib(mut self, memory_mib: u32) -> Self {
        self.memory_mib = Some(memory_mib);
        self
    }

    pub fn disk_gib(mut self, disk_gib: u32) -> Self {
        self.disk_gib = Some(disk_gib);
        self
    }

    pub fn ssh_key(mut self, ssh_key: impl AsRef<Path>) -> Self {
        self.ssh_key = Some(ssh_key.as_ref().to_path_buf());
        self
    }

    pub fn forward(mut self, host: u16, guest: u16) -> Self {
        self.forwards.push(PortForward { host, guest });
        self
    }

    pub fn timeout_secs(mut self, timeout_secs: u64) -> Self {
        self.timeout_secs = Some(timeout_secs);
        self
    }

    pub fn cloud_init_user_data(mut self, path: impl AsRef<Path>) -> Self {
        self.cloud_init_user_data = Some(path.as_ref().to_path_buf());
        self
    }

    pub fn cloud_init_network_config(mut self, path: impl AsRef<Path>) -> Self {
        self.cloud_init_network_config = Some(path.as_ref().to_path_buf());
        self
    }

    fn into_create_args(self) -> CreateArgs {
        CreateArgs {
            name: self.name,
            release: self.release,
            arch: self.arch,
            accel: self.accel,
            cpus: self.cpus,
            memory_mib: self.memory_mib,
            disk_gib: self.disk_gib,
            ssh_key: self.ssh_key.map(|path| path.display().to_string()),
            forwards: self
                .forwards
                .into_iter()
                .map(|forward| (forward.host, forward.guest))
                .collect(),
            timeout_secs: self.timeout_secs,
            cloud_init_user_data: self
                .cloud_init_user_data
                .map(|path| path.display().to_string()),
            cloud_init_network_config: self
                .cloud_init_network_config
                .map(|path| path.display().to_string()),
        }
    }
}

pub struct Vm {
    manager: Arc<InstanceManager>,
    name: String,
}

impl Vm {
    pub fn name(&self) -> &str {
        &self.name
    }

    pub async fn start(&self) -> Result<()> {
        self.manager.start_silent(&self.name).await?;
        Ok(())
    }

    pub async fn info(&self) -> Result<VmInfo> {
        self.manager.vm_info(&self.name).await
    }

    pub async fn status(&self) -> Result<InstanceStatus> {
        self.manager.status(&self.name).await
    }

    pub async fn wait_for_ssh(&self) -> Result<VmInfo> {
        self.manager.wait_for_ssh_ready(&self.name).await
    }

    pub async fn exec<I, S>(&self, command: I) -> Result<ExecOutput>
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        let command = command.into_iter().map(Into::into).collect::<Vec<_>>();
        self.manager.exec_capture(&self.name, &command).await
    }

    pub async fn exec_checked<I, S>(&self, command: I) -> Result<ExecOutput>
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        let command = command.into_iter().map(Into::into).collect::<Vec<_>>();
        self.manager.exec_checked(&self.name, &command).await
    }

    pub async fn stop(&self) -> Result<()> {
        self.manager.stop_silent(&self.name).await
    }

    pub async fn delete(&self) -> Result<()> {
        self.manager.delete_silent(&self.name).await
    }
}