diff options
author | Toby Vincent <tobyv13@gmail.com> | 2022-01-28 19:06:17 -0600 |
---|---|---|
committer | Toby Vincent <tobyv13@gmail.com> | 2022-01-28 19:06:17 -0600 |
commit | eee73902ab0b0b3ae2671ef1109ab2e44e7883be (patch) | |
tree | 05d82146c0e1e047f02ce46a7dd4daff8976a5d6 | |
parent | 7f782ad73127ebc2e78b794d3eb999a6efb26f9f (diff) |
refactor: move zfs and nspawn to seperate lib crates
Co-authored-by: Neil Kollack <nkollack@gmail.com>"
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | .vscode/settings.json | 4 | ||||
-rw-r--r-- | Cargo.lock | 74 | ||||
-rw-r--r-- | Cargo.toml | 4 | ||||
-rw-r--r-- | core/src/lib.rs | 38 | ||||
-rw-r--r-- | daemon/src/lib.rs | 250 | ||||
-rw-r--r-- | daemon/src/main.rs | 9 | ||||
-rw-r--r-- | init.sh | 143 | ||||
-rw-r--r-- | zone/Cargo.toml (renamed from cli/Cargo.toml) | 8 | ||||
-rw-r--r-- | zone/src/lib.rs (renamed from cli/src/lib.rs) | 30 | ||||
-rw-r--r-- | zone/src/main.rs (renamed from cli/src/main.rs) | 0 | ||||
-rw-r--r-- | zone_core/Cargo.toml (renamed from core/Cargo.toml) | 8 | ||||
-rw-r--r-- | zone_core/src/lib.rs | 63 | ||||
-rw-r--r-- | zone_nspawn/Cargo.toml | 8 | ||||
-rw-r--r-- | zone_nspawn/src/lib.rs | 14 | ||||
-rw-r--r-- | zone_zfs/Cargo.toml | 11 | ||||
-rw-r--r-- | zone_zfs/src/file_system.rs | 157 | ||||
-rw-r--r-- | zone_zfs/src/lib.rs | 42 | ||||
-rw-r--r-- | zone_zfs/src/snapshot.rs | 64 | ||||
-rw-r--r-- | zoned/Cargo.toml (renamed from daemon/Cargo.toml) | 17 | ||||
-rw-r--r-- | zoned/src/lib.rs | 71 | ||||
-rw-r--r-- | zoned/src/main.rs | 32 |
22 files changed, 710 insertions, 342 deletions
@@ -1,2 +1,5 @@ /target -**/*.rs.bk
\ No newline at end of file +**/*.rs.bk +.vscode/* +!.vscode/tasks.json +!.vscode/launch.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index a7c1615..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "lldb.showDisassembly": "never", - "lldb.dereferencePointers": true -}
\ No newline at end of file @@ -240,7 +240,7 @@ version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41a0645a430ec9136d2d701e54a95d557de12649a9dd7109ced3187e648ac824" dependencies = [ - "heck", + "heck 0.4.0", "proc-macro-error", "proc-macro2", "quote", @@ -635,6 +635,15 @@ checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" @@ -1773,6 +1782,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] +name = "strum" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" + +[[package]] +name = "strum_macros" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] name = "subtle" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1849,15 +1877,6 @@ dependencies = [ ] [[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - -[[package]] name = "time" version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2107,6 +2126,12 @@ dependencies = [ ] [[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] name = "unicode-width" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2299,7 +2324,6 @@ dependencies = [ "clap_complete", "log", "reqwest", - "serde", "simplelog", "tabled", "zone_core", @@ -2309,22 +2333,42 @@ dependencies = [ name = "zone_core" version = "0.1.0" dependencies = [ + "clap", "rocket", "rocket_okapi", "serde", + "strum", + "strum_macros", "tabled", ] [[package]] -name = "zoned" +name = "zone_nspawn" +version = "0.1.0" +dependencies = [ + "zone_core", +] + +[[package]] +name = "zone_zfs" version = "0.1.0" dependencies = [ "anyhow", "chrono", + "tracing", + "zone_core", +] + +[[package]] +name = "zoned" +version = "0.1.0" +dependencies = [ + "figment", + "lazy_static", "rocket", "rocket_okapi", - "threadpool", - "tracing", - "tracing-subscriber", + "serde", "zone_core", + "zone_nspawn", + "zone_zfs", ] @@ -1,3 +1,3 @@ [workspace] -members = ["cli", "core", "daemon"] -default-members = ["cli"] +members = ["zone", "zone_core", "zone_nspawn", "zone_zfs", "zoned"] +default-members = ["zone"] diff --git a/core/src/lib.rs b/core/src/lib.rs deleted file mode 100644 index f7824ee..0000000 --- a/core/src/lib.rs +++ /dev/null @@ -1,38 +0,0 @@ -use rocket_okapi::okapi::schemars::{self, JsonSchema}; -use serde::{Deserialize, Serialize}; -use std::fmt; -use tabled::Tabled; - -pub static DEFAULT_ENDPOINT: &str = "127.0.0.1:8000"; - -#[derive(Debug, Serialize, Deserialize, JsonSchema, Tabled)] -#[serde(rename_all = "camelCase")] -pub enum ContainerStatus { - Running, - Stopped, -} - -impl fmt::Display for ContainerStatus { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -#[derive(Debug, Serialize, Deserialize, JsonSchema, Tabled)] -#[serde(rename_all = "camelCase")] -pub struct Container { - #[header("ID")] - pub id: u64, - - #[header("Name")] - pub name: String, - - #[header("Template")] - pub template: String, - - #[header("User")] - pub user: String, - - #[header("Status")] - pub status: ContainerStatus, -} diff --git a/daemon/src/lib.rs b/daemon/src/lib.rs deleted file mode 100644 index d5a28e4..0000000 --- a/daemon/src/lib.rs +++ /dev/null @@ -1,250 +0,0 @@ -pub mod api { - use rocket::{get, post, serde::json::Json, Build, Config, Rocket}; - use rocket_okapi::{ - openapi, openapi_get_routes, - rapidoc::{make_rapidoc, GeneralConfig, HideShowConfig, RapiDocConfig}, - settings::UrlObject, - swagger_ui::{make_swagger_ui, SwaggerUIConfig}, - }; - use std::net::Ipv4Addr; - use zone_core::Container; - - /// # Get all containers - /// - /// Returns all containers. - #[openapi(tag = "Containers")] - #[get("/containers/list")] - fn get_all_containers() -> Json<Vec<Container>> { - let containers = vec![]; - Json(containers) - } - - /// # Get a user's containers - /// - /// Returns all containers belonging to a single user. - #[openapi(tag = "Containers")] - #[get("/containers/list/<user>")] - fn get_containers_by_user(user: String) -> Json<Vec<Container>> { - let containers = vec![]; - let _user = user; - Json(containers) - } - - /// # Create container - #[openapi(tag = "Containers")] - #[post("/container", data = "<container>")] - fn create_container(container: Json<Container>) -> Json<Container> { - container - } - - pub fn build_rocket() -> Rocket<Build> { - let config = Config { - address: Ipv4Addr::new(127, 0, 0, 1).into(), - port: 8000, - ..Config::debug_default() - }; - - rocket::custom(config) - .mount( - "/", - openapi_get_routes![get_all_containers, get_containers_by_user, create_container,], - ) - .mount( - "/swagger-ui/", - make_swagger_ui(&SwaggerUIConfig { - url: "../openapi.json".to_owned(), - ..Default::default() - }), - ) - .mount( - "/rapidoc/", - make_rapidoc(&RapiDocConfig { - general: GeneralConfig { - spec_urls: vec![UrlObject::new("General", "../openapi.json")], - ..Default::default() - }, - hide_show: HideShowConfig { - allow_spec_url_load: false, - allow_spec_file_load: false, - ..Default::default() - }, - ..Default::default() - }), - ) - } -} - -pub mod zfs { - use anyhow::{anyhow, Result}; - use chrono::{DateTime, Utc}; - use std::{ - ffi::{OsStr, OsString}, - fmt::Display, - path::PathBuf, - process::{Command, Output}, - }; - - #[derive(Debug)] - pub struct FileSystem { - value: OsString, - mountpoint: Option<PathBuf>, - } - - impl Display for FileSystem { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.value.to_string_lossy()) - } - } - - // pool/000.0/nkollack-1 - impl TryFrom<OsString> for FileSystem { - type Error = anyhow::Error; - - fn try_from(value: OsString) -> Result<Self, Self::Error> { - Ok(FileSystem { - value, - mountpoint: None, - }) - } - } - - impl TryFrom<&str> for FileSystem { - type Error = anyhow::Error; - - fn try_from(value: &str) -> Result<Self, Self::Error> { - value.try_into() - } - } - - impl AsRef<OsStr> for FileSystem { - fn as_ref(&self) -> &OsStr { - self.value.as_ref() - } - } - - impl From<FileSystem> for PathBuf { - fn from(val: FileSystem) -> Self { - PathBuf::from(val.value) - } - } - - impl From<FileSystem> for String { - fn from(val: FileSystem) -> Self { - val.value.to_string_lossy().to_string() - } - } - - impl TryFrom<Output> for FileSystem { - type Error = anyhow::Error; - - fn try_from(value: Output) -> Result<Self, Self::Error> { - std::str::from_utf8(&value.stdout)?.try_into() - } - } - - impl FileSystem { - pub fn get_snapshots(&self) -> Result<Vec<Snapshot>> { - let output = Command::new("zfs") - .arg("list") - .arg("-H") - .arg("-o") - .arg("name") - .arg("-t") - .arg("snapshot") - .arg(self) - .output()? - .stdout; - - String::from_utf8(output)? - .split_whitespace() - .map(|s| s.try_into()) - .collect() - } - - pub fn get_latest_snapshot(&self) -> Result<Option<Snapshot>> { - // pool/447.0@210119221709 - Ok(self - .get_snapshots()? - .into_iter() - .max_by_key(|s| s.timestamp)) - } - } - - #[derive()] - pub struct Snapshot { - // pool/000.0@00000000000 - value: OsString, - filesystem: FileSystem, - timestamp: DateTime<Utc>, - } - - impl TryFrom<&str> for Snapshot { - // filesystem, '@', timestamp - type Error = anyhow::Error; - - fn try_from(value: &str) -> Result<Self, Self::Error> { - match value.split('@').collect::<Vec<&str>>()[..] { - [filesystem, name] => Ok(Snapshot { - filesystem: FileSystem::try_from(filesystem)?, - value: filesystem.into(), - timestamp: name.parse().unwrap_or(chrono::MIN_DATETIME), - }), - _ => Err(anyhow!("Failed to parse")), - } - } - } - - impl Snapshot { - pub fn clone_into_fs( - &self, - fs_name: String, - mountpoint: Option<PathBuf>, - ) -> Result<FileSystem> { - let new_fs = FileSystem { - value: PathBuf::from(&self.filesystem.value).join(fs_name).into(), - mountpoint, - }; - - let mut command = Command::new("zfs"); - - command.arg("clone"); - - if let Some(mp) = &new_fs.mountpoint { - command - .arg("-o") - .arg(format!("mountpoint={}", mp.to_string_lossy())); - }; - - match command.arg(&self.value).arg(&new_fs).status()?.success() { - true => Ok(new_fs), - false => Err(anyhow!("Failed to clone snapshot")), - } - } - } - - pub fn get_filesystems() -> Result<Vec<FileSystem>> { - let output = Command::new("zfs") - .arg("list") - .arg("-H") - .arg("-o") - .arg("name") - .output()? - .stdout; - - std::str::from_utf8(&output)? - .split_whitespace() - .map(|fs| fs.try_into()) - .collect() - } - - #[cfg(test)] - mod tests { - #[test] - fn zfs_list() { - use super::*; - assert!(get_filesystems().is_ok()); - } - } -} - -pub mod nspawn {} diff --git a/daemon/src/main.rs b/daemon/src/main.rs deleted file mode 100644 index 2f2bcf1..0000000 --- a/daemon/src/main.rs +++ /dev/null @@ -1,9 +0,0 @@ -use zoned::api; - -#[rocket::main] -async fn main() { - match api::build_rocket().launch().await { - Ok(()) => println!("Rocket shut down gracefully."), - Err(err) => eprintln!("Rocket had an error: {}", err), - }; -} @@ -0,0 +1,143 @@ +#!/usr/bin/sh +# vim:set ts=3: + +set -e +shopt -s extglob + +POOL="pool" +QUOTA="16G" + +[[ -n ${SUDO_UID} ]] || exec /usr/bin/sudo $0 + +if [[ -n ${SIUEUSER} ]] && id -Gn ${SUDO_UID} | grep -q -w professors; then + MECH_UID=$(id -u ${SIUEUSER}) + if ! [[ -n ${MECH_UID} ]]; then + echo -e "\nERROR: Invalid value specified for SIUEUSER" 1>&2 + exit 1 + fi +else + MECH_UID=${SUDO_UID} +fi + +if [[ -z ${INSTANCE} ]]; then + INSTANCE=0 +else + if ! [[ ${INSTANCE} =~ ^[0-9]$ ]]; then + echo -e "\nERROR: Invalid value specified for INSTANCE" 1>&2 + exit 1 + fi +fi + +ID="${MECH_UID#14}${INSTANCE}" + +USERNAME="$(getent passwd ${MECH_UID} | cut -d ':' -f 1)" +SRVRNAME="${USERNAME}-${INSTANCE}" +SRVRPATH="/srv/${USERNAME}/${SRVRNAME}" + +if [[ -n ${STARTNAT} ]] && id -Gn ${SUDO_UID} | grep -q -w professors; then + INTERNET="--network-veth-extra=vn-${SRVRNAME}:host9" +fi + +if [[ -n ${TEMPLATE} ]]; then + if zfs list -o name | grep -q "^${POOL}/${SRVRNAME}$"; then + echo -e "\nERROR: TEMPLATE specified for existing instance ${SRVRNAME}. Remove TEMPLATE and retry." 1>&2 + exit 1 + fi + + if ! [[ ${TEMPLATE} =~ ^[0-9]{3}\.[0-9](@[0-9]{12})?$ ]]; then + echo -e "\nERROR: Invalid value specified for TEMPLATE" 1>&2 + exit 1 + fi + + if ! [[ ${TEMPLATE} =~ @ ]]; then + TEMPLATE=$(grep -m 1 "^${POOL}/${TEMPLATE}@" ${0%/*}/default) + if [[ -z ${TEMPLATE} ]]; then + echo -e "\nERROR: Invalid value specified for TEMPLATE" 1>&2 + exit 1 + fi + fi +fi + +#if ! ip link list ${SRVRNAME} &> /dev/null; then +# ip tuntap add dev ${SRVRNAME} mod tap +# ip link set up ${SRVRNAME} +#fi +# +#if ! ip link list ns-${SRVRNAME} &> /dev/null; then +# ip link add link ${SRVRNAME} name ns-${SRVRNAME} type macvlan mode passthru +#fi + +mkdir -p ${SRVRPATH%/*} +if ! mountpoint -q ${SRVRPATH}; then + if ! zfs list -o name | grep -q "^${POOL}/${SRVRNAME}$"; then + if ! [[ -n ${TEMPLATE} ]]; then + echo -e "\nERROR: TEMPLATE not specified for new instance ${SRVRNAME}. Specify TEMPLATE and retry." 1>&2 + exit 1 + fi + + zfs clone -o mountpoint=${SRVRPATH} ${TEMPLATE} ${POOL}/${SRVRNAME} + zfs set quota=${QUOTA} ${POOL}/${SRVRNAME} + + TEMP=${ID##*(0)} + for i in {2..0}; do + ADDRESS[$i]=$(($TEMP%256)) + TEMP=$(($TEMP/256)) + done + + IFS='.' command eval 'NAT="10.${ADDRESS[*]}/8"' + + mkdir -p ${SRVRPATH}/etc/systemd/resolved.conf.d + cat <<- END > ${SRVRPATH}/etc/systemd/resolved.conf.d/llmnr.conf + [Resolve] + LLMNR=true + END + + cat <<- END > ${SRVRPATH}/etc/systemd/network/00-host0.network + [Match] + Virtualization=container + Name=host0 + + [Network] + LinkLocalAddressing=0 + ConfigureWithoutCarrier=1 + Address=192.168.0.$((10+${INSTANCE}))/24 + END + + cat <<- END > ${SRVRPATH}/etc/systemd/network/00-host9.network + [Match] + Name=host9 + + [Network] + LinkLocalAddressing=0 + ConfigureWithoutCarrier=1 + Address=$NAT + Gateway=10.255.255.254 + LLMNR=false + DNS=146.163.252.126 + DNS=146.163.252.127 + END + else + zfs mount ${POOL}/${SRVRNAME} + fi +fi + +if machinectl show ${SRVRNAME} &> /dev/null; then + OPTIONS="--quiet --wait --machine=${SRVRNAME} --collect --service-type=exec" + if [[ -n ${SSH_ORIGINAL_COMMAND} ]]; then + exec systemd-run ${OPTIONS} --pipe /usr/bin/bash -c "${SSH_ORIGINAL_COMMAND}" + else + exec systemd-run ${OPTIONS} --pty --send-sighup /usr/bin/login -H -f root + fi +fi + +if [[ -z ${SSH_ORIGINAL_COMMAND} ]]; then + exec systemd-nspawn --settings=trusted --quiet --console=interactive \ + --link-journal=no --resolv-conf=off --timezone=off --capability=all --boot \ + --directory=${SRVRPATH} --private-users=false --bind-ro=/sys/module \ + --bind-ro=/lib/modules --network-zone=${USERNAME} ${INTERNET} + # ip link del ns-${SRVRNAME} + # ip link del ${SRVRNAME} +else + echo -e "\nERROR: ${SRVRNAME} is not running" 1>&2 + exit 1 +fi diff --git a/cli/Cargo.toml b/zone/Cargo.toml index 78bed07..014a14e 100644 --- a/cli/Cargo.toml +++ b/zone/Cargo.toml @@ -9,16 +9,14 @@ authors = [ "Anthony Schneider <tonyschneider3@gmail.com>", ] description = "Manages containers using systemd-nspawn and ZFS" - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -zone_core = { path = "../core" } +anyhow = "1.0.53" clap = { version = "3.0.13", features = ["derive", "env"] } clap_complete = "3.0.5" -reqwest = { version = "0.11.9", features = ["blocking", "json"] } -serde = "1.0.136" -anyhow = "1.0.53" log = "0.4.14" +reqwest = { version = "0.11.9", features = ["blocking", "json"] } simplelog = "0.11.2" tabled = "0.4.2" +zone_core = { version = "0.1.0", path = "../zone_core" } diff --git a/cli/src/lib.rs b/zone/src/lib.rs index a087b87..9bd5e2a 100644 --- a/cli/src/lib.rs +++ b/zone/src/lib.rs @@ -1,11 +1,11 @@ +use zone_core::{Container, DEFAULT_ENDPOINT}; use anyhow::{Context, Result}; -use clap::{ArgEnum, ErrorKind, IntoApp, Parser, Subcommand, ValueHint}; +use clap::{ArgEnum, Args, ErrorKind, IntoApp, Parser, Subcommand, ValueHint}; use clap_complete::{generate, Shell}; use log::LevelFilter; use reqwest::Url; use std::{ffi::OsString, io, process::Command}; use tabled::{Style, Table}; -use zone_core::{Container, DEFAULT_ENDPOINT}; #[derive(Debug, Parser)] #[clap(about, version)] @@ -47,21 +47,37 @@ pub enum Commands { }, /// List existing containers - List { - /// Filter the list of containers - filter: Option<String>, - }, + List(List), + /// Create a container + /// + /// Create a new container from an existing template. + Create(Create), #[clap(external_subcommand)] External(Vec<OsString>), } +#[derive(Debug, Args)] +pub struct List { + /// Filter the list of containers + pub filter: Option<String>, +} + +impl List {} + +#[derive(Debug, Args)] +pub struct Create { + // /// Filter the list of containers +// pub filter: Container, +} + impl Cli { pub fn run(self) -> Result<Option<String>> { match self.command { Commands::Completion { shell } => self.completion(shell), - Commands::List { ref filter } => self.list(filter), + Commands::List(ref list) => self.list(&list.filter), Commands::External(args) => Cli::external(args), + Commands::Create(_) => todo!(), } } diff --git a/cli/src/main.rs b/zone/src/main.rs index 054633a..054633a 100644 --- a/cli/src/main.rs +++ b/zone/src/main.rs diff --git a/core/Cargo.toml b/zone_core/Cargo.toml index 6c7c9b8..b0bce4a 100644 --- a/core/Cargo.toml +++ b/zone_core/Cargo.toml @@ -8,12 +8,14 @@ authors = [ "Toby Vincent <tobyv13@gmail.com>", "Anthony Schneider <tonyschneider3@gmail.com>", ] -description = "Core zone traits and tools for implementation." - +description = "Manages containers using systemd-nspawn and ZFS" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +clap = { version = "3.0.13", default-features = false, features = ["derive"] } rocket = { version = "0.5.0-rc.1", default-features = false, features = ["json"] } -rocket_okapi = { version = "0.8.0-rc.1", features = ["rapidoc", "swagger", "secrets"] } +rocket_okapi = { version = "0.8.0-rc.1" } serde = "1.0.136" +strum = "0.23.0" +strum_macros = "0.23.1" tabled = "0.4.2" diff --git a/zone_core/src/lib.rs b/zone_core/src/lib.rs new file mode 100644 index 0000000..ca204fd --- /dev/null +++ b/zone_core/src/lib.rs @@ -0,0 +1,63 @@ +use clap::Args; +use rocket::{FromForm, FromFormField}; +use rocket_okapi::okapi::schemars::{self, JsonSchema}; +use serde::{Deserialize, Serialize}; +use strum_macros::{Display, EnumString}; +use tabled::Tabled; + +pub static DEFAULT_ENDPOINT: &str = "127.0.0.1:8000"; + +pub trait PartialEqOrDefault { + fn eq_or_default(&self, other: &Self) -> bool; +} + +#[derive( + Debug, + Serialize, + Deserialize, + JsonSchema, + Tabled, + FromFormField, + PartialEq, + Clone, + EnumString, + Display, +)] +#[serde(rename_all = "camelCase")] +#[strum(ascii_case_insensitive)] +pub enum ContainerStatus { + Running, + Stopped, + Unknown, +} + +impl Default for ContainerStatus { + fn default() -> Self { + ContainerStatus::Unknown + } +} + +#[derive(Debug, Default, Serialize, Deserialize, JsonSchema, Tabled, FromForm, Clone, Args)] +#[serde(rename_all = "camelCase")] +pub struct Container { + #[header("ID")] + pub id: u64, + + #[header("Template")] + pub template: String, + + #[header("User")] + pub user: String, + + #[header("Status")] + pub status: ContainerStatus, +} + +impl PartialEqOrDefault for Container { + fn eq_or_default(&self, other: &Self) -> bool { + (self.id == other.id || self.id == Self::default().id) + && (self.template == other.template || self.template == Self::default().template) + && (self.user == other.user || self.user == Self::default().user) + && (self.status == other.status || self.status == Self::default().status) + } +} diff --git a/zone_nspawn/Cargo.toml b/zone_nspawn/Cargo.toml new file mode 100644 index 0000000..b318223 --- /dev/null +++ b/zone_nspawn/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "zone_nspawn" +version = "0.1.0" +edition = "2021" +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +zone_core = { version = "0.1.0", path = "../zone_core" } diff --git a/zone_nspawn/src/lib.rs b/zone_nspawn/src/lib.rs new file mode 100644 index 0000000..85ec1d5 --- /dev/null +++ b/zone_nspawn/src/lib.rs @@ -0,0 +1,14 @@ +use zone_core::Container; + +pub fn get_containers() -> Vec<Container> { + vec![] +} + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} diff --git a/zone_zfs/Cargo.toml b/zone_zfs/Cargo.toml new file mode 100644 index 0000000..1f1a851 --- /dev/null +++ b/zone_zfs/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "zone_zfs" +version = "0.1.0" +edition = "2021" +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.53" +chrono = "0.4.19" +tracing = "0.1.29" +zone_core = { version = "0.1.0", path = "../zone_core" } diff --git a/zone_zfs/src/file_system.rs b/zone_zfs/src/file_system.rs new file mode 100644 index 0000000..390c00d --- /dev/null +++ b/zone_zfs/src/file_system.rs @@ -0,0 +1,157 @@ +use anyhow::{anyhow, Context, Result}; +use zone_core::{Container, ContainerStatus}; +use std::{ + ffi::{OsStr, OsString}, + fmt::Display, + path::PathBuf, + process::{Command, Output}, +}; + +use super::snapshot::Snapshot; + +#[derive(Debug)] +pub struct FileSystem { + pub(crate) value: OsString, + pub(crate) mountpoint: Option<PathBuf>, +} + +impl Display for FileSystem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.value.to_string_lossy()) + } +} + +// pool/000.0/nkollack-1 +impl TryFrom<OsString> for FileSystem { + type Error = anyhow::Error; + + fn try_from(value: OsString) -> Result<Self, Self::Error> { + Ok(FileSystem { + value, + mountpoint: None, + }) + } +} + +impl TryFrom<&str> for FileSystem { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + value.try_into() + } +} + +impl AsRef<OsStr> for FileSystem { + fn as_ref(&self) -> &OsStr { + self.value.as_ref() + } +} + +impl From<FileSystem> for PathBuf { + fn from(val: FileSystem) -> Self { + PathBuf::from(val.value) + } +} + +impl From<FileSystem> for String { + fn from(val: FileSystem) -> Self { + val.value.to_string_lossy().to_string() + } +} + +impl From<FileSystem> for Container { + fn from(file_system: FileSystem) -> Self { + let path_buf = PathBuf::from(&file_system) + .file_name() + .expect("Invalid FileSystem path") + .to_string_lossy() + .into_owned(); + + let (user, id) = path_buf.rsplit_once("-").expect("Invalid FileSystem name!"); + + Container { + id: id + .parse() + .expect("Failed to parse ID from FileSystem name!"), + template: PathBuf::from(file_system) + .parent() + .expect("Base path has no parent!") + .to_string_lossy() + .into_owned(), + user: user.to_string(), + status: ContainerStatus::default(), + } + } +} + +impl TryFrom<Output> for FileSystem { + type Error = anyhow::Error; + + fn try_from(value: Output) -> Result<Self, Self::Error> { + std::str::from_utf8(&value.stdout)?.try_into() + } +} + +impl FileSystem { + pub(super) fn get_name(&self) -> Result<String> { + Ok(PathBuf::from(self.value.clone()) + .file_name() + .context("Invalid path for filesystem")? + .to_string_lossy() + .into_owned()) + } + + fn set_quota(&self) -> Result<()> { + match Command::new("zfs") + .arg("set") + .arg(format!("quota={}", "")) + .arg(&self.value) + .status()? + .success() + { + true => Ok(()), + false => Err(anyhow!("Failed to set a quota: {:?}", self)), + } + } + + pub(super) fn get_snapshots(&self) -> Result<Vec<Snapshot>> { + let stdout = Command::new("zfs") + .arg("list") + .arg("-H") + .arg("-o") + .arg("name") + .arg("-t") + .arg("snapshot") + .arg(self) + .output()? + .stdout; + + String::from_utf8(stdout)? + .split_whitespace() + .map(|s| s.try_into()) + .collect() + } + + pub(super) fn get_latest_snapshot(&self) -> Result<Option<Snapshot>> { + // pool/447.0@210119221709 + Ok(self + .get_snapshots()? + .into_iter() + .max_by_key(|s| s.timestamp)) + } + + pub(super) fn get_file_systems() -> Result<Vec<FileSystem>> { + let output = Command::new("zfs") + .arg("list") + .arg("-H") + .arg("-o") + .arg("name") + .output()? + .stdout; + + std::str::from_utf8(&output)? + .split_whitespace() + .map(|fs| fs.try_into()) + .collect() + } +} diff --git a/zone_zfs/src/lib.rs b/zone_zfs/src/lib.rs new file mode 100644 index 0000000..5d22a5f --- /dev/null +++ b/zone_zfs/src/lib.rs @@ -0,0 +1,42 @@ +use anyhow::Result; +use std::path::PathBuf; +use self::file_system::FileSystem; + +pub mod file_system; + +pub mod snapshot; + +pub fn create_file_system(base_fs_name: String, name: String) -> Result<FileSystem> { + let fs = FileSystem::get_file_systems() + .unwrap() + .into_iter() + .find(|fs| match fs.get_name() { + Ok(name) => name == base_fs_name, + Err(_) => false, + }) + .unwrap_or_else(|| todo!("Handle!")); + + let snapshot = fs + .get_latest_snapshot() + .expect("No snapshot found") + .unwrap(); + + let mut mountpoint = fs.value; + mountpoint.push(name); + + snapshot.clone_into_file_system( + snapshot.file_system.get_name()?, + Some(PathBuf::from(mountpoint)), + ) + + //call set_quota +} + +#[cfg(test)] +mod tests { + #[test] + fn zfs_list() { + use super::*; + assert!(FileSystem::get_file_systems().is_ok()); + } +} diff --git a/zone_zfs/src/snapshot.rs b/zone_zfs/src/snapshot.rs new file mode 100644 index 0000000..48faa68 --- /dev/null +++ b/zone_zfs/src/snapshot.rs @@ -0,0 +1,64 @@ +use anyhow::{anyhow, Result}; +use chrono::{DateTime, Utc}; +use std::{ffi::OsString, path::PathBuf, process::Command}; +use tracing::warn; + +use super::file_system::FileSystem; + +#[derive()] +pub struct Snapshot { + // pool/000.0@00000000000 + pub value: OsString, + pub file_system: FileSystem, + pub timestamp: DateTime<Utc>, +} + +impl TryFrom<&str> for Snapshot { + // <file_system>@<timestamp> + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + match value.split('@').collect::<Vec<&str>>()[..] { + [file_system, name] => Ok(Snapshot { + file_system: FileSystem::try_from(file_system)?, + value: file_system.into(), + timestamp: name.parse().unwrap_or_else(|_err| { + warn!( + "Failed to parse timestamp from `{}`, using default value.", + value + ); + chrono::MIN_DATETIME + }), + }), + _ => Err(anyhow!("Failed to parse snapshot: {}", value)), + } + } +} + +impl Snapshot { + pub fn clone_into_file_system( + &self, + name: String, + mountpoint: Option<PathBuf>, + ) -> Result<FileSystem> { + let new_fs = FileSystem { + value: PathBuf::from(&self.file_system.value).join(name).into(), + mountpoint, + }; + + let mut command = Command::new("zfs"); + + command.arg("clone"); + + if let Some(mp) = &new_fs.mountpoint { + command + .arg("-o") + .arg(format!("mountpoint={}", mp.to_string_lossy())); + }; + + match command.arg(&self.value).arg(&new_fs).status()?.success() { + true => Ok(new_fs), + false => Err(anyhow!("Failed to clone snapshot: {:?}", new_fs)), + } + } +} diff --git a/daemon/Cargo.toml b/zoned/Cargo.toml index b295e15..cb25530 100644 --- a/daemon/Cargo.toml +++ b/zoned/Cargo.toml @@ -8,16 +8,17 @@ authors = [ "Toby Vincent <tobyv13@gmail.com>", "Anthony Schneider <tonyschneider3@gmail.com>", ] -description = "A simple daemon for managing containers using systemd-nspawn and ZFS" - +description = "Manages containers using systemd-nspawn and ZFS" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -zone_core = { path = "../core" } -threadpool = "1.8.1" -tracing = "0.1.29" -tracing-subscriber = "0.3.7" +figment = "0.10.6" +lazy_static = "1.4.0" rocket = { version = "0.5.0-rc.1", default-features = false, features = ["json"] } rocket_okapi = { version = "0.8.0-rc.1", features = ["rapidoc", "swagger", "secrets"] } -anyhow = "1.0.53" -chrono = "0.4.19" +serde = "1.0.136" +zone_core = { version = "0.1.0", path = "../zone_core" } +zone_nspawn = { version = "0.1.0", path = "../zone_nspawn" } +zone_zfs = { version = "0.1.0", path = "../zone_zfs" } + +[features] diff --git a/zoned/src/lib.rs b/zoned/src/lib.rs new file mode 100644 index 0000000..baa567d --- /dev/null +++ b/zoned/src/lib.rs @@ -0,0 +1,71 @@ +use rocket::{get, post, serde::json::Json, Build, Config, Rocket}; +use rocket_okapi::{ + openapi, openapi_get_routes, + rapidoc::{make_rapidoc, GeneralConfig, HideShowConfig, RapiDocConfig}, + settings::UrlObject, + swagger_ui::{make_swagger_ui, SwaggerUIConfig}, +}; +use std::net::Ipv4Addr; +use zone_core::{Container, PartialEqOrDefault}; + +/// # List containers +/// +/// Returns a list of containers based on the query. +#[openapi(tag = "Container")] +#[get("/container/list?<container..>")] +pub fn container_list(container: Container) -> Json<Vec<Container>> { + zone_nspawn::get_containers() + .iter() + .filter(|c| container.eq_or_default(c)) + .cloned() + .collect::<Vec<Container>>() + .into() +} + +/// # Create container +#[openapi(tag = "Container")] +#[post("/container", data = "<container>")] +fn create_container(container: Json<Container>) -> Json<Container> { + let container = zone_zfs::create_file_system( + container.template.clone(), + format!("{}-{}", container.user, container.id), + ); + + match container { + Ok(c) => Json(c.into()), + Err(_err) => todo!("Respond with error message"), + } +} + +pub fn build_rocket() -> Rocket<Build> { + let config = Config { + address: Ipv4Addr::new(127, 0, 0, 1).into(), + port: 8000, + ..Config::debug_default() + }; + + rocket::custom(config) + .mount("/", openapi_get_routes![container_list, create_container,]) + .mount( + "/swagger-ui/", + make_swagger_ui(&SwaggerUIConfig { + url: "../openapi.json".to_owned(), + ..Default::default() + }), + ) + .mount( + "/rapidoc/", + make_rapidoc(&RapiDocConfig { + general: GeneralConfig { + spec_urls: vec![UrlObject::new("General", "../openapi.json")], + ..Default::default() + }, + hide_show: HideShowConfig { + allow_spec_url_load: false, + allow_spec_file_load: false, + ..Default::default() + }, + ..Default::default() + }), + ) +} diff --git a/zoned/src/main.rs b/zoned/src/main.rs new file mode 100644 index 0000000..71ae05e --- /dev/null +++ b/zoned/src/main.rs @@ -0,0 +1,32 @@ +use figment::providers::{Format, Serialized}; +use figment::{providers::Toml, Figment}; +use serde::{Deserialize, Serialize}; + +#[macro_use] +extern crate lazy_static; + +#[derive(Deserialize, Serialize)] +struct Config { + quota: String, +} + +impl Default for Config { + fn default() -> Self { + Config { + quota: "16G".to_string(), + } + } +} + +lazy_static! { + static ref CONFIG: Figment = Figment::from(Serialized::defaults(Config::default())) + .merge(Toml::file("/etc/zoned/Config.toml")); +} + +#[rocket::main] +async fn main() { + match zoned::build_rocket().launch().await { + Ok(()) => println!("Rocket shut down gracefully."), + Err(err) => eprintln!("Rocket had an error: {}", err), + }; +} |