This replaces all the API functions that returned Result<T, String>. Additionally, ToApiResult<T> and Simplify<T> make converting errors to ApiError easier than with String.
282 lines
9.5 KiB
Rust
282 lines
9.5 KiB
Rust
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<RwLock<Progress>>,
|
|
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::<ApiError>(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<Option<model::Instance>, 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<dyn std::error::Error>> {
|
|
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(())
|
|
}
|