207 lines
7.1 KiB
Rust
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(())
|
|
}
|