use nzr_api::error::{ApiError, ErrorType, ToApiResult}; use nzr_api::net::cidr::CidrV4; use nzr_virt::error::DomainError; use nzr_virt::xml::build::DomainBuilder; use nzr_virt::xml::{self, InfoMap, SerialType, Sysinfo}; use nzr_virt::{datasize, dom, vol}; use tokio::sync::RwLock; use crate::ctrl::vm::Progress; use crate::ctx::Context; use crate::model::tx::Transaction; use crate::model::{Instance, Subnet}; use nzr_api::net::mac::MacAddr; use nzr_api::{args, model, nzr_event}; use std::sync::Arc; use tracing::{debug, info, warn}; const VIRT_MAC_OUI: &[u8] = &[0x02, 0xf1, 0x0f]; macro_rules! progress { ($task:ident, $pct:literal, $($arg:tt)*) => {{ let mut pt = $task.write().await; pt.status_text = format!($($arg)*); pt.percentage = $pct; }}; } /// Creates a new instance pub async fn new_instance( ctx: Context, prog_task: Arc>, args: &args::NewInstance, ) -> Result<(Instance, dom::Domain), ApiError> { progress!(prog_task, 0.0, "Starting..."); // find the subnet corresponding to the interface let subnet = Subnet::get_by_name(&ctx, &args.subnet) .await .to_api_with("Unable to get interface")? .ok_or::(format!("Subnet {} wasn't found in database", &args.subnet).into())?; // bail if a domain already exists if let Ok(dom) = ctx.virt.conn.get_instance(&args.name).await { Err(format!( "Domain with name already exists (uuid {})", dom.xml().await.uuid, ) .into()) } else { // make sure the base image exists let mut base_image = ctx .virt .pools .baseimg .volume(&args.base_image) .await .to_api_with("Couldn't find base image")?; progress!(prog_task, 10.0, "Generating metadata..."); // generate a new lease with a new MAC addr let mac_addr = { let bytes = [VIRT_MAC_OUI, rand::random::<[u8; 3]>().as_ref()].concat(); MacAddr::from_bytes(bytes) } .to_api_with("Unable to create a new MAC address")?; // Get highest host addr + 1 for our new addr let addr = { let addr_num = Instance::all_in_subnet(&ctx, &subnet) .await .to_api_with("Couldn't get instances in subnet")? .into_iter() .max_by(|a, b| a.host_num.cmp(&b.host_num)) .map_or(subnet.start_host, |i| i.host_num + 1); if addr_num > subnet.end_host || addr_num < subnet.start_host { return Err("Got invalid lease address for instance".into()); } let addr = subnet .network .make_ip(addr_num as u32) .to_api_with("Unable to generate instance IP")?; CidrV4::new(addr, subnet.network.cidr()) }; let lease = nzr_api::model::Lease { subnet: subnet.name.clone(), addr, mac_addr, }; // generate cloud-init data let db_inst = { let inst = Instance::insert(&ctx, &args.name, &subnet, lease.clone(), None) .await .to_api_type(ErrorType::Database)?; Transaction::begin(&ctx, inst) }; progress!(prog_task, 30.0, "Creating instance images..."); // create primary volume from base image let mut pri_vol = base_image .clone_vol( &ctx.virt.pools.primary, &args.name, datasize!((args.disk_sizes.0) GiB), ) .await .to_api_with("Failed to clone base image")?; // and, if it exists: the second volume let sec_vol = match args.disk_sizes.1 { Some(sec_size) => { let voldata = // TODO: Fix VolType xml::Volume::new(&args.name, xml::VolType::Qcow2, datasize!(sec_size GiB)); Some( vol::Volume::create(&ctx.virt.pools.secondary, voldata, 0) .await .to_api_with("Couldn't create secondary volume")?, ) } None => None, }; // build domain xml let ifname = subnet.ifname.clone(); let devname = format!( "veth-{:02x}{:02x}{:02x}", mac_addr[3], mac_addr[4], mac_addr[5] ); progress!(prog_task, 60.0, "Initializing instance..."); let dom_xml = { let pri_name = &ctx.config.storage.primary_pool; let sec_name = &ctx.config.storage.secondary_pool; let smbios_info = { let mut sysinfo = Sysinfo::new(); let mut system_map = InfoMap::new(); system_map.push( "serial", format!("ds=nocloud-net;s={}", ctx.config.cloud.http_addr()), ); sysinfo.system(system_map); sysinfo }; let mut instdata = DomainBuilder::default() .name(&args.name) .memory(datasize!((args.memory) MiB)) .cpu_topology(1, 1, args.cores, 1) .net_device(|nd| { nd.mac_addr(mac_addr) .with_bridge(&ifname) .target_dev(&devname) }) .disk_device(|dsk| { dsk.volume_source(pri_name, &pri_vol.name) .target("vda", "virtio") .qcow2() .boot_order(1) }) .smbios(smbios_info) .serial_device(SerialType::Pty); // add desription, if provided instdata = match &args.description { Some(desc) => instdata.description(desc), None => instdata, }; // add second volume, if provided match &sec_vol { Some(vol) => instdata.disk_device(|dsk| { dsk.volume_source(sec_name, &vol.name) .target("vdb", "virtio") .qcow2() }), None => instdata, } .build() }; let mut virt_dom = ctx .virt .conn .define_instance(dom_xml) .await .to_api_with("Couldn't define libvirt instance")?; // not a fatal error, we can set autostart afterward if let Err(err) = virt_dom.autostart(true).await { warn!("Couldn't set autostart for domain: {err}"); } if let Err(err) = virt_dom.start().await { warn!("Domain defined, but couldn't be started! Error: {err}"); } // set all volumes to persistent to avoid deletion pri_vol.persist = true; if let Some(mut sec_vol) = sec_vol { sec_vol.persist = true; } virt_dom.persist().await; progress!(prog_task, 80.0, "Domain created!"); debug!("Domain {} created!", virt_dom.xml().await.name.as_str()); Ok((db_inst.take(), virt_dom)) } } pub async fn delete_instance( ctx: Context, name: String, ) -> Result, ApiError> { let Some(inst_db) = Instance::get_by_name(&ctx, &name) .await .to_api_with_type(ErrorType::Database, "Couldn't find instance")? else { return Err(ErrorType::NotFound.into()); }; let api_model = match inst_db.api_model(&ctx).await { Ok(model) => Some(model), Err(err) => { warn!("Couldn't get API model to notify clients: {err}"); None } }; // First, destroy the instance match ctx.virt.conn.get_instance(name.clone()).await { Ok(mut inst) => { inst.stop().await.to_api_with("Couldn't stop instance")?; inst.undefine(true) .await .to_api_with("Couldn't undefine instance")?; } Err(DomainError::DomainNotFound) => { warn!("Deleting instance that exists in DB but not libvirt"); } Err(err) => Err(ApiError::new( nzr_api::error::ErrorType::VirtError, "Couldn't get instance from libvirt", err, ))?, } // Then, delete the DB entity inst_db .delete(&ctx) .await .to_api_with("Couldn't delete from database")?; Ok(api_model) } /// Delete all instances that don't have a matching libvirt domain pub async fn prune_instances(ctx: &Context) -> Result<(), Box> { for entity in Instance::all(ctx).await? { if let Err(DomainError::DomainNotFound) = ctx.virt.conn.get_instance(&entity.name).await { info!("Invalid domain {}, deleting", &entity.name); // First, get the API model to notify clients with let api_model = match entity.api_model(ctx).await { Ok(ent) => Some(ent), Err(err) => { warn!("Couldn't get api model to notify clients: {err}"); None } }; // then, delete by name let name = entity.name.clone(); if let Err(err) = entity.delete(ctx).await { warn!("Couldn't delete {}: {}", name, err); } // and assuming all goes well, notify clients if let Some(ent) = api_model { nzr_event!(ctx.events, Deleted, ent); } } } Ok(()) }