Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
[workspace]
resolver = "3"
default-members = ["monitor"]
members = ["monitor", "monitor/cli", "monitor/settings", "monitor/web"]
members = ["monitor", "monitor/cli", "monitor/hypervisor", "monitor/settings", "monitor/shell", "monitor/web"]
exclude = ["analysis", "docker"]

[workspace.dependencies]
bytesize = "2.0.1"
chrono = { version = "0.4.39", features = ["serde"] }
cli = { path = "monitor/cli" }
cmd_lib = "1.9.5"
dotenv = "0.15.0"
hypervisor = { path = "monitor/hypervisor" }
jane-eyre = "0.3.0"
mktemp = "0.5.1"
rocket = { version = "0.5.1", features = ["json"] }
serde = { version = "1.0.204", features = ["derive"] }
settings = { path = "monitor/settings" }
shell = { path = "monitor/shell" }
toml = "0.8.15"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
Expand Down
5 changes: 3 additions & 2 deletions monitor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ bytesize = { workspace = true }
cfg-if = "1.0.1"
chrono = { workspace = true }
cli = { workspace = true }
cmd_lib = "1.9.5"
cmd_lib = { workspace = true }
crossbeam-channel = "0.5.13"
dotenv = { workspace = true }
http = "0.2"
hypervisor = { workspace = true }
itertools = "0.13.0"
jane-eyre = { workspace = true }
mktemp = { workspace = true }
reflink = "0.1.3"
reqwest = { version = "0.12.24", features = ["charset", "http2", "json", "rustls-tls", "system-proxy"], default-features = false }
rocket = { workspace = true }
serde = { workspace = true }
Expand All @@ -31,6 +31,7 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true }
web = { workspace = true }
rand = "0.9.1"
shell.workspace = true

[dev-dependencies]
settings = { workspace = true, features = ["test"] }
11 changes: 11 additions & 0 deletions monitor/hypervisor/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "hypervisor"
version = "0.1.0"
edition = "2024"

[dependencies]
cmd_lib.workspace = true
jane-eyre.workspace = true
settings.workspace = true
shell.workspace = true
tracing.workspace = true
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
use core::str;
use std::{
fs::{create_dir_all, rename},
collections::BTreeSet,
fs::{create_dir_all, read_dir, rename},
net::Ipv4Addr,
path::Path,
time::Duration,
};

use cmd_lib::{run_fun, spawn_with_output};
use jane_eyre::eyre;
use settings::TOML;
use tracing::debug;
use cmd_lib::{run_cmd, run_fun, spawn_with_output};
use jane_eyre::eyre::{self, OptionExt, bail};
use settings::{TOML, profile::Profile};
use shell::log_output_as_trace;
use tracing::{debug, info};

use crate::shell::log_output_as_trace;
use crate::libvirt::{delete_template_or_rebuild_image_file, template_or_rebuild_images_path};

pub fn list_template_guests() -> eyre::Result<Vec<String>> {
// Output is not filtered by prefix, so we must filter it ourselves.
Expand Down Expand Up @@ -72,6 +75,66 @@ pub fn get_ipv4_address(guest_name: &str) -> Option<Ipv4Addr> {
.or_else(|| virsh_domifaddr(guest_name, "agent"))
}

pub fn start_guest(guest_name: &str) -> eyre::Result<()> {
info!(?guest_name, "Starting guest");
run_cmd!(virsh start -- $guest_name)?;

Ok(())
}

pub fn wait_for_guest(guest_name: &str, timeout: Duration) -> eyre::Result<()> {
let timeout = timeout.as_secs();
info!("Waiting for guest to shut down (max {timeout} seconds)");
if !run_cmd!(time virsh event --timeout $timeout -- $guest_name lifecycle).is_ok() {
bail!("`virsh event` failed or timed out!");
}
for _ in 0..100 {
if run_fun!(virsh domstate -- $guest_name)?.trim_ascii() == "shut off" {
return Ok(());
}
}

bail!("Guest did not shut down as expected")
}

pub fn rename_guest(old_guest_name: &str, new_guest_name: &str) -> eyre::Result<()> {
run_cmd!(virsh domrename -- $old_guest_name $new_guest_name)?;
Ok(())
}

pub fn delete_guest(guest_name: &str) -> eyre::Result<()> {
if run_cmd!(virsh domstate -- $guest_name).is_ok() {
// FIXME make this idempotent in a less noisy way?
let _ = run_cmd!(virsh destroy -- $guest_name);
run_cmd!(virsh undefine --nvram -- $guest_name)?;
}

Ok(())
}

pub fn prune_base_image_files(
profile: &Profile,
keep_snapshots: BTreeSet<String>,
) -> eyre::Result<()> {
let base_images_path = template_or_rebuild_images_path(profile);
info!(?base_images_path, "Pruning base image files");
create_dir_all(&base_images_path)?;

for entry in read_dir(&base_images_path)? {
let filename = entry?.file_name();
let filename = filename.to_str().ok_or_eyre("Unsupported path")?;
if let Some((_base, snapshot_name)) = filename.split_once("@") {
if !keep_snapshots.contains(snapshot_name) {
delete_template_or_rebuild_image_file(profile, filename);
}
} else {
delete_template_or_rebuild_image_file(profile, filename);
}
}

Ok(())
}

fn virsh_domifaddr(guest_name: &str, source: &str) -> Option<Ipv4Addr> {
let output = run_fun!(virsh domifaddr --source $source $guest_name 2> /dev/null);
match output {
Expand Down
56 changes: 56 additions & 0 deletions monitor/hypervisor/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
pub mod libvirt;

#[cfg_attr(target_os = "linux", path = "impl_libvirt.rs")]
mod platform;

use std::{collections::BTreeSet, net::Ipv4Addr, path::Path, time::Duration};

use jane_eyre::eyre;
use settings::profile::Profile;

pub fn list_template_guests() -> eyre::Result<Vec<String>> {
self::platform::list_template_guests()
}

pub fn list_rebuild_guests() -> eyre::Result<Vec<String>> {
self::platform::list_rebuild_guests()
}

pub fn list_runner_guests() -> eyre::Result<Vec<String>> {
self::platform::list_runner_guests()
}

pub fn update_screenshot(guest_name: &str, output_dir: &Path) -> eyre::Result<()> {
self::platform::update_screenshot(guest_name, output_dir)
}

pub fn take_screenshot(guest_name: &str, output_path: &Path) -> eyre::Result<()> {
self::platform::take_screenshot(guest_name, output_path)
}

pub fn get_ipv4_address(guest_name: &str) -> Option<Ipv4Addr> {
self::platform::get_ipv4_address(guest_name)
}

pub fn start_guest(guest_name: &str) -> eyre::Result<()> {
self::platform::start_guest(guest_name)
}

pub fn wait_for_guest(guest_name: &str, timeout: Duration) -> eyre::Result<()> {
self::platform::wait_for_guest(guest_name, timeout)
}

pub fn rename_guest(old_guest_name: &str, new_guest_name: &str) -> eyre::Result<()> {
self::platform::rename_guest(old_guest_name, new_guest_name)
}

pub fn delete_guest(guest_name: &str) -> eyre::Result<()> {
self::platform::delete_guest(guest_name)
}

pub fn prune_base_image_files(
profile: &Profile,
keep_snapshots: BTreeSet<String>,
) -> eyre::Result<()> {
self::platform::prune_base_image_files(profile, keep_snapshots)
}
79 changes: 79 additions & 0 deletions monitor/hypervisor/src/libvirt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use std::{
ffi::OsStr,
fs::{create_dir_all, remove_file},
path::{Path, PathBuf},
};

use cmd_lib::run_cmd;
use jane_eyre::eyre;
use settings::profile::Profile;
use tracing::{debug, info, warn};

pub fn template_or_rebuild_images_path(profile: &Profile) -> PathBuf {
Path::new("/var/lib/libvirt/images/base").join(&profile.profile_name)
}

pub fn runner_images_path() -> PathBuf {
PathBuf::from("/var/lib/libvirt/images/runner")
}

pub fn delete_template_or_rebuild_image_file(profile: &Profile, filename: &str) {
let base_images_path = template_or_rebuild_images_path(profile);
let path = base_images_path.join(filename);
info!(?path, "Deleting");
if let Err(error) = remove_file(&path) {
warn!(?path, ?error, "Failed to delete");
}
}

pub fn create_template_or_rebuild_images_dir(profile: &Profile) -> eyre::Result<PathBuf> {
let base_images_path = template_or_rebuild_images_path(profile);
debug!(?base_images_path, "Creating base images subdirectory");
create_dir_all(&base_images_path)?;

Ok(base_images_path)
}

pub fn create_runner_images_dir() -> eyre::Result<PathBuf> {
let runner_images_path = runner_images_path();
debug!(?runner_images_path, "Creating runner images directory");
create_dir_all(&runner_images_path)?;

Ok(runner_images_path)
}

pub fn define_libvirt_guest(
profile_name: &str,
guest_name: &str,
guest_xml_path: impl AsRef<Path>,
args: &[&dyn AsRef<OsStr>],
cdrom_images: &[CdromImage],
) -> eyre::Result<()> {
// This dance is needed to randomise the MAC address of the guest.
let guest_xml_path = guest_xml_path.as_ref();
let args = args.iter().map(|x| x.as_ref()).collect::<Vec<_>>();
run_cmd!(virsh define -- $guest_xml_path)?;
run_cmd!(virt-clone --preserve-data --check path_in_use=off -o $profile_name.init -n $guest_name $[args])?;
libvirt_change_media(guest_name, cdrom_images)?;
run_cmd!(virsh undefine -- $profile_name.init)?;

Ok(())
}

pub fn libvirt_change_media(guest_name: &str, cdrom_images: &[CdromImage]) -> eyre::Result<()> {
for CdromImage { target_dev, path } in cdrom_images {
run_cmd!(virsh change-media -- $guest_name $target_dev $path)?;
}

Ok(())
}

pub struct CdromImage<'path> {
pub target_dev: &'static str,
pub path: &'path str,
}
impl<'path> CdromImage<'path> {
pub fn new(target_dev: &'static str, path: &'path str) -> Self {
Self { target_dev, path }
}
}
10 changes: 10 additions & 0 deletions monitor/shell/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "shell"
version = "0.1.0"
edition = "2024"

[dependencies]
jane-eyre.workspace = true
mktemp.workspace = true
reflink = "0.1.3"
tracing.workspace = true
File renamed without changes.
Loading