use nzr_api::net::cidr::CidrV4; use nzr_virt::error::DomainError; use nzr_virt::xml::build::DomainBuilder; use nzr_virt::xml::{self, SerialType}; use nzr_virt::{datasize, dom, vol}; use tokio::sync::RwLock; use super::*; use crate::ctrl::vm::Progress; use crate::ctx::Context; use crate::model::{Instance, Subnet}; use log::{debug, info, warn}; use nzr_api::args; use nzr_api::net::mac::MacAddr; use std::sync::Arc; 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), Box> { progress!(prog_task, 0.0, "Starting..."); // find the subnet corresponding to the interface let subnet = Subnet::get_by_name(&ctx, &args.subnet) .await .map_err(|er| cmd_error!("Unable to get interface: {}", er))? .ok_or(cmd_error!( "Subnet {} wasn't found in database", &args.subnet ))?; // bail if a domain already exists if let Ok(dom) = ctx.virt.conn.get_instance(&args.name).await { Err(cmd_error!( "Domain with name already exists (uuid {})", dom.xml().await.uuid, )) } else { // make sure the base image exists let mut base_image = ctx .virt .pools .baseimg .volume(&args.base_image) .await .map_err(|er| cmd_error!("Couldn't find base image: {}", er))?; 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) } .map_err(|er| cmd_error!("Unable to create a new MAC address: {}", er))?; // Get highest host addr + 1 for our new addr let addr = { let addr_num = Instance::all_in_subnet(&ctx, &subnet) .await? .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 { Err(cmd_error!("Got invalid lease address for instance"))?; } let addr = subnet.network.make_ip(addr_num as u32)?; 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 = Instance::insert(&ctx, &args.name, &subnet, lease.clone(), None).await?; 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 .map_err(|er| cmd_error!("Failed to clone base image: {}", er))?; // 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?) } 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 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) }) .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?; // not a fatal error, we can set autostart afterward if let Err(er) = virt_dom.autostart(true).await { warn!("Couldn't set autostart for domain: {}", er); } if let Err(er) = virt_dom.start().await { warn!("Domain defined, but couldn't be started! Error: {}", er); } // 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, virt_dom)) } } pub async fn delete_instance(ctx: Context, name: String) -> Result<(), Box> { let Some(inst_db) = Instance::get_by_name(&ctx, &name).await? else { return Err(cmd_error!("Instance {name} not found")); }; let mut inst = ctx.virt.conn.get_instance(name.clone()).await?; inst.undefine(true).await?; inst_db.delete(&ctx).await?; Ok(()) } pub async fn prune_instances(ctx: &Context) -> Result<(), Box> { for entity in Instance::all(ctx).await? { if let Err(err) = ctx.virt.conn.get_instance(&entity.name).await { if err == DomainError::DomainNotFound { info!("Invalid domain {}, deleting", &entity.name); let name = entity.name.clone(); if let Err(err) = entity.delete(ctx).await { warn!("Couldn't delete {}: {}", name, err); } } } } Ok(()) }