nazrin/nzrd/src/cmd/vm.rs
snow flurry da51722c54 nzrd: don't store ci-metadata
This will be handled entirely in omyacid.
2024-08-11 23:48:34 -07:00

207 lines
7.1 KiB
Rust

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<RwLock<Progress>>,
args: &args::NewInstance,
) -> Result<(Instance, dom::Domain), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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(())
}