diff options
author | Toby Vincent <tobyv13@gmail.com> | 2022-04-30 19:46:55 -0500 |
---|---|---|
committer | Toby Vincent <tobyv13@gmail.com> | 2022-04-30 19:46:55 -0500 |
commit | c83a62dbf5bb9afc55dbef85d6a1d9932c99a1e8 (patch) | |
tree | 8761629e8576a41d7109ffbb5344c2aed3ab3571 | |
parent | b627ec4e0d9c8686d5d193b61e7e2496a1116f60 (diff) |
refactor: write runtime into trait impl nspawn
-rw-r--r-- | Cargo.lock | 10 | ||||
-rw-r--r-- | zone/src/lib.rs | 4 | ||||
-rw-r--r-- | zone_core/Cargo.toml | 4 | ||||
-rw-r--r-- | zone_core/src/container.rs | 74 | ||||
-rw-r--r-- | zone_core/src/error.rs | 6 | ||||
-rw-r--r-- | zone_core/src/lib.rs | 5 | ||||
-rw-r--r-- | zone_core/src/runtime.rs | 38 | ||||
-rw-r--r-- | zone_nspawn/Cargo.toml | 2 | ||||
-rw-r--r-- | zone_nspawn/src/container.rs | 17 | ||||
-rw-r--r-- | zone_nspawn/src/error.rs | 40 | ||||
-rw-r--r-- | zone_nspawn/src/lib.rs | 4 | ||||
-rw-r--r-- | zone_nspawn/src/machine.rs | 44 | ||||
-rw-r--r-- | zone_nspawn/src/nspawn.rs | 86 | ||||
-rw-r--r-- | zoned/src/http.rs | 33 | ||||
-rw-r--r-- | zoned/src/ws.rs | 4 |
15 files changed, 229 insertions, 142 deletions
@@ -28,9 +28,9 @@ checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" [[package]] name = "async-trait" -version = "0.1.52" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" dependencies = [ "proc-macro2", "quote", @@ -2221,6 +2221,7 @@ dependencies = [ name = "zone_core" version = "0.1.0" dependencies = [ + "async-trait", "clap", "derive_builder", "serde", @@ -2228,7 +2229,8 @@ dependencies = [ "strum_macros", "tabled", "thiserror", - "zone_nspawn", + "tokio", + "wspty", "zone_zfs", ] @@ -2236,12 +2238,14 @@ dependencies = [ name = "zone_nspawn" version = "0.1.0" dependencies = [ + "async-trait", "serde", "serde_ini", "serde_json", "thiserror", "tokio", "wspty", + "zone_core", ] [[package]] diff --git a/zone/src/lib.rs b/zone/src/lib.rs index 1f0b758..b3f140e 100644 --- a/zone/src/lib.rs +++ b/zone/src/lib.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use clap::{Args, ErrorKind, IntoApp, Parser, Subcommand, ValueHint}; use clap_complete::{generate, Shell}; -use futures::{SinkExt, StreamExt}; +use futures::SinkExt; use log::{debug, LevelFilter}; use reqwest::Url; use std::{ffi::OsString, io, process::Command}; @@ -130,7 +130,7 @@ impl Cli { let mut container_options = Container::builder(); container_options - .template(create.template.to_owned()) + .parent(create.template.to_owned()) .user(create.user.to_owned()); let container = client diff --git a/zone_core/Cargo.toml b/zone_core/Cargo.toml index a4e2072..d617946 100644 --- a/zone_core/Cargo.toml +++ b/zone_core/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["zone", "zoned"] workspace = ".." [dependencies] +async-trait = "0.1.53" clap = { version = "3.1.9", default-features = false, features = ["std", "derive"] } derive_builder = "0.10.2" serde = "1.0.136" @@ -19,7 +20,8 @@ strum = "0.23.0" strum_macros = "0.23.1" tabled = "0.6.1" thiserror = "1.0.30" -zone_nspawn = { version = "0.1.0", path = "../zone_nspawn" } +tokio = "1.17.0" +wspty = "0.1.1" zone_zfs = { version = "0.1.0", path = "../zone_zfs" } [features] diff --git a/zone_core/src/container.rs b/zone_core/src/container.rs index 330705e..71dfb73 100644 --- a/zone_core/src/container.rs +++ b/zone_core/src/container.rs @@ -1,7 +1,7 @@ use clap::Args; use derive_builder::Builder; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::{fmt::Display, path::PathBuf}; use tabled::Tabled; use crate::FilterContainer; @@ -22,7 +22,7 @@ pub struct Container { pub id: u64, #[tabled("Template")] - pub template: String, + pub parent: String, #[tabled("User")] pub user: String, @@ -34,35 +34,42 @@ impl Container { } } +impl Display for Container { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}-{}-{}", self.user, self.parent, self.id) + } +} + impl From<Container> for String { - fn from(val: Container) -> Self { - format!("{}-{}-{}", val.user, val.template, val.id) + fn from(c: Container) -> Self { + format!("{}-{}-{}", c.user, c.parent, c.id) } } impl From<Container> for ContainerOptions { - fn from(val: Container) -> Self { + fn from(c: Container) -> Self { Self { - id: Some(val.id), - template: Some(val.template), - user: Some(val.user), + id: Some(c.id), + parent: Some(c.parent), + user: Some(c.user), } } } impl From<&Container> for &ContainerOptions { - fn from(val: &Container) -> Self { + fn from(c: &Container) -> Self { ContainerOptions { - id: Some(val.id.to_owned()), - template: Some(val.template.to_owned()), - user: Some(val.user.to_owned()), - }.into() + id: Some(c.id.to_owned()), + parent: Some(c.parent.to_owned()), + user: Some(c.user.to_owned()), + } + .into() } } impl From<ContainerOptions> for &ContainerOptions { - fn from(val: ContainerOptions) -> Self { - val.into() + fn from(o: ContainerOptions) -> Self { + o.into() } } @@ -81,7 +88,7 @@ where self.filter_map(|c| -> Option<Container> { c.try_into().ok() }) .filter(|c| { pred.id.map_or(false, |p| p == c.id) - && pred.template.as_ref().map_or(false, |p| p == &c.template) + && pred.parent.as_ref().map_or(false, |p| p == &c.parent) && pred.user.as_ref().map_or(false, |p| p == &c.user) }) .collect() @@ -91,56 +98,35 @@ where impl TryFrom<zone_zfs::FileSystem> for Container { type Error = zone_zfs::Error; - fn try_from(file_system: zone_zfs::FileSystem) -> zone_zfs::Result<Self> { - let path_buf = PathBuf::from(file_system.dataset()) + fn try_from(fs: zone_zfs::FileSystem) -> zone_zfs::Result<Self> { + let path_buf = PathBuf::from(fs.dataset()) .file_name() .ok_or_else(|| { - zone_zfs::Error::FileSystem(format!("Invalid FileSystem path: {:?}", file_system)) + zone_zfs::Error::FileSystem(format!("Invalid FileSystem path: {:?}", fs)) })? .to_string_lossy() .into_owned(); let (user, id) = path_buf.rsplit_once('-').ok_or_else(|| { - zone_zfs::Error::FileSystem(format!("Invalid FileSystem name: {:?}", file_system)) + zone_zfs::Error::FileSystem(format!("Invalid FileSystem name: {:?}", fs)) })?; let id = id.parse::<u64>().map_err(|err| { zone_zfs::Error::FileSystem(format!("Failed to parse container ID: {:?}", err)) })?; - let template = PathBuf::from(file_system.dataset()) + let template = PathBuf::from(fs.dataset()) .parent() .ok_or_else(|| { - zone_zfs::Error::FileSystem(format!( - "Invalid path for filesystem: {:?}", - &file_system - )) + zone_zfs::Error::FileSystem(format!("Invalid path for filesystem: {:?}", &fs)) })? .to_string_lossy() .into_owned(); Ok(Container { id, - template, + parent: template, user: user.to_string(), }) } } - -impl TryFrom<zone_nspawn::Container> for Container { - type Error = zone_nspawn::Error; - - fn try_from(value: zone_nspawn::Container) -> zone_nspawn::Result<Self> { - // user, template, id - let machine = value.machine.to_string_lossy(); - let v: Vec<&str> = machine.split('-').collect(); - - Ok(Container { - id: v[2].parse().map_err(|err| { - zone_nspawn::Error::Parsing(format!("Failed to parse container id: {:?}", err)) - })?, - template: v[1].to_owned(), - user: v[0].to_owned(), - }) - } -} diff --git a/zone_core/src/error.rs b/zone_core/src/error.rs index 9476bdd..d30b5cc 100644 --- a/zone_core/src/error.rs +++ b/zone_core/src/error.rs @@ -6,12 +6,6 @@ pub enum Error { #[error(transparent)] Builder(#[from] crate::container::ContainerOptionsError), - #[error("NSpawn Error: {source:?}")] - Nspawn { - #[from] - source: zone_nspawn::Error, - }, - #[error("ZFS Error: {source:?}")] ZFS { #[from] diff --git a/zone_core/src/lib.rs b/zone_core/src/lib.rs index 17cc173..2711a80 100644 --- a/zone_core/src/lib.rs +++ b/zone_core/src/lib.rs @@ -2,8 +2,11 @@ use std::net::{IpAddr, Ipv4Addr}; pub use crate::container::{CloneOptions, Container, ContainerOptions, ContainerStatus}; pub use crate::error::{Error, Result}; +pub use crate::runtime::Runtime; +mod container; mod error; +mod runtime; pub static DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); pub static DEFAULT_PORT: u16 = 8000; @@ -14,5 +17,3 @@ pub trait FilterContainer { self.filter_container(predicate).into_iter().next() } } - -mod container; diff --git a/zone_core/src/runtime.rs b/zone_core/src/runtime.rs new file mode 100644 index 0000000..d7aded1 --- /dev/null +++ b/zone_core/src/runtime.rs @@ -0,0 +1,38 @@ +use async_trait::async_trait; +use std::{ffi::OsStr, path::PathBuf}; +use tokio::sync::mpsc::UnboundedReceiver; +use wspty::PtyMaster; + +use crate::Container; + +#[async_trait] +pub trait Runtime { + type Error; + + fn list(&self) -> Result<Vec<Container>, Self::Error>; + + fn get(&self, container: Container) -> Result<Option<Container>, Self::Error> { + self.list().map(|v| v.into_iter().find(|c| *c == container)) + } + + fn create(&self, root: PathBuf, container: &Container) -> Result<(), Self::Error>; + + async fn attach( + &self, + container: Container, + rx: UnboundedReceiver<()>, + ) -> Result<PtyMaster, Self::Error>; + + async fn run<C, S>( + &self, + container: Container, + cmd: C, + rx: UnboundedReceiver<()>, + ) -> Result<PtyMaster, Self::Error> + where + C: IntoIterator<Item = S>, + C: std::marker::Send, + S: AsRef<OsStr>; + + fn shutdown(container: Container) -> Result<(), Self::Error>; +} diff --git a/zone_nspawn/Cargo.toml b/zone_nspawn/Cargo.toml index 22b5a92..9e423aa 100644 --- a/zone_nspawn/Cargo.toml +++ b/zone_nspawn/Cargo.toml @@ -13,9 +13,11 @@ keywords = ["zone", "zoned", "systemd", "nspawn", "systemd-nspawn"] workspace = ".." [dependencies] +async-trait = "0.1.53" serde = "1.0.136" serde_ini = "0.2.0" serde_json = "1.0.78" thiserror = "1.0.30" tokio = { version = "1.17.0", default-features = false, features = ["process"] } wspty = "0.1.1" +zone_core = { version = "0.1.0", path = "../zone_core" } diff --git a/zone_nspawn/src/container.rs b/zone_nspawn/src/container.rs deleted file mode 100644 index 43639f7..0000000 --- a/zone_nspawn/src/container.rs +++ /dev/null @@ -1,17 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::ffi::OsString; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Container { - pub machine: OsString, - - pub class: OsString, - - pub service: OsString, - - pub os: OsString, - - pub version: OsString, - - pub addresses: OsString, -} diff --git a/zone_nspawn/src/error.rs b/zone_nspawn/src/error.rs index e5af696..468b9ae 100644 --- a/zone_nspawn/src/error.rs +++ b/zone_nspawn/src/error.rs @@ -1,19 +1,39 @@ +use zone_core::Container; + pub type Result<T> = std::result::Result<T, Error>; #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("NSpawn Error: {0:?}")] - NSpawn(String), + #[error("Machine Error: Failed to initialize container: {0:?}")] + Initialization(Container), + + #[error("Machine Error: Failed to shutdown container: {0:?}")] + Shutdown(Container), + + #[error("Name Error: Invalid machine name: {0:?}")] + Name(String), - #[error("Parsing Error: {0:?}")] - Parsing(String), + #[error("Name Error: Invalid machine ID: {source:?}")] + Id { + #[from] + source: std::num::ParseIntError, + }, - #[error("Failed to run command: {0:?}")] - Io(#[from] std::io::Error), + #[error("Failed to run command: {source:?}")] + Io { + #[from] + source: std::io::Error, + }, - #[error("Failed to deserialize json: {0:?}")] - Json(#[from] serde_json::Error), + #[error("Failed to deserialize json: {source:?}")] + Json { + #[from] + source: serde_json::Error, + }, - #[error("Failed to parse Command output: {0:?}")] - Utf8(#[from] std::string::FromUtf8Error), + #[error("Failed to parse Command output: {source:?}")] + Utf8 { + #[from] + source: std::string::FromUtf8Error, + }, } diff --git a/zone_nspawn/src/lib.rs b/zone_nspawn/src/lib.rs index 8459446..32b8a32 100644 --- a/zone_nspawn/src/lib.rs +++ b/zone_nspawn/src/lib.rs @@ -1,9 +1,9 @@ -pub use crate::container::Container; pub use crate::error::{Error, Result}; +pub use crate::machine::Machine; pub use crate::nspawn::NSpawn; -mod container; mod error; +mod machine; mod nspawn; #[cfg(test)] diff --git a/zone_nspawn/src/machine.rs b/zone_nspawn/src/machine.rs new file mode 100644 index 0000000..9d77242 --- /dev/null +++ b/zone_nspawn/src/machine.rs @@ -0,0 +1,44 @@ +use serde::{Deserialize, Serialize}; + +pub(crate) use crate::Error; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Machine { + pub machine: String, + + pub class: String, + + pub service: String, + + pub os: String, + + pub version: String, + + pub addresses: String, +} + +impl TryFrom<Machine> for zone_core::Container { + type Error = Error; + + fn try_from(machine: Machine) -> Result<Self, Error> { + // let name = machine.machine.to_str().ok_or(Error::Parsing(machine))?; + // let name = machine.machine; + + let mut iter = machine.machine.split('-'); + + let (id, parent, user) = ( + iter.next() + .ok_or_else(|| Error::Name(machine.machine.to_owned()))? + .parse::<u64>()? + .to_owned(), + iter.next() + .ok_or_else(|| Error::Name(machine.machine.to_owned()))? + .to_owned(), + iter.next() + .ok_or_else(|| Error::Name(machine.machine.to_owned()))? + .to_owned(), + ); + + Ok(zone_core::Container { id, parent, user }) + } +} diff --git a/zone_nspawn/src/nspawn.rs b/zone_nspawn/src/nspawn.rs index 017e5a3..89f51ef 100644 --- a/zone_nspawn/src/nspawn.rs +++ b/zone_nspawn/src/nspawn.rs @@ -1,24 +1,53 @@ use std::{ffi::OsStr, path::PathBuf, process::Command}; +use async_trait::async_trait; use tokio::sync::mpsc::UnboundedReceiver; use wspty::{PtyCommand, PtyMaster}; +use zone_core::{Container, Runtime}; -use crate::{Container, Error, Result}; +use crate::{Error, Result}; #[derive(Default, Debug)] pub struct NSpawn; impl NSpawn { - pub fn list(&self) -> Result<Vec<Container>> { + async fn spawn<O, S, C, A>(opts: O, cmd: C, kill_rx: UnboundedReceiver<()>) -> Result<PtyMaster> + where + O: IntoIterator<Item = S>, + S: AsRef<OsStr>, + C: IntoIterator<Item = A>, + A: AsRef<OsStr>, + { + let base_opts = ["--quiet", "--wait", "--collect", "--service-type=exec"]; + + let mut proc = tokio::process::Command::new("systemd-run"); + + proc.args(base_opts) + .args(opts) + .args(cmd) + .env("TERM", "xterm-256color"); + + PtyCommand::from(proc) + .run(kill_rx) + .await + .map_err(Error::from) + } +} + +#[async_trait] +impl Runtime for NSpawn { + type Error = crate::Error; + + fn list(&self) -> Result<Vec<Container>> { Command::new("machinectl") .arg("list") .args(["-o", "json"]) .output() - .map(|o| serde_json::from_slice(o.stdout.as_slice()))? .map_err(Error::from) + .and_then(|o| serde_json::from_slice(o.stdout.as_slice()).map_err(Error::from)) } - pub fn create(&self, root: PathBuf, name: String) -> Result<()> { + fn create(&self, root: PathBuf, container: &Container) -> Result<()> { let opts = [ "--settings=trusted", "--quiet", @@ -34,41 +63,25 @@ impl NSpawn { Command::new("systemd-nspawn") .arg("--machine") - .arg(name) + .arg(container.to_string()) .arg("--directory") .arg(root) .args(opts) - .status()? + .status() + .map_err(Error::from)? .success() .then(|| ()) - .ok_or_else(|| Error::NSpawn(format!("Failed to create container: {:?}", self))) - } - - async fn spawn<O, S, C, A>(opts: O, cmd: C, kill_rx: UnboundedReceiver<()>) -> Result<PtyMaster> - where - O: IntoIterator<Item = S>, - S: AsRef<OsStr>, - C: IntoIterator<Item = A>, - A: AsRef<OsStr>, - { - let base_opts = ["--quiet", "--wait", "--collect", "--service-type=exec"]; - - let mut proc = tokio::process::Command::new("systemd-run"); - - proc.args(base_opts) - .args(opts) - .args(cmd) - .env("TERM", "xterm-256color"); - - PtyCommand::from(proc) - .run(kill_rx) - .await + .ok_or_else(|| Error::Initialization(container.to_owned())) .map_err(Error::from) } - pub async fn attach(&self, name: String, kill_rx: UnboundedReceiver<()>) -> Result<PtyMaster> { + async fn attach( + &self, + container: Container, + kill_rx: UnboundedReceiver<()>, + ) -> Result<PtyMaster> { let opts = [ - &format!("{}={}", "--machine", name), + &format!("--machine={}", container), "--pty", "--send-sighup", ]; @@ -76,18 +89,19 @@ impl NSpawn { Self::spawn(opts, cmd, kill_rx).await } - pub async fn run<C, S>( + async fn run<C, S>( &self, - name: String, + container: Container, cmd: C, kill_rx: UnboundedReceiver<()>, ) -> Result<PtyMaster> where C: IntoIterator<Item = S>, + C: std::marker::Send, S: AsRef<OsStr>, { let opts = [ - &format!("{}={}", "--machine", name), + &format!("--machine={}", container), "--pty", "--send-sighup", ]; @@ -95,13 +109,13 @@ impl NSpawn { Self::spawn(opts, cmd, kill_rx).await } - pub fn shutdown(name: String) -> Result<()> { + fn shutdown(container: Container) -> Result<()> { Command::new("machinectl") .arg("poweroff") - .arg(&name) + .arg(container.to_string()) .status()? .success() .then(|| ()) - .ok_or_else(|| Error::NSpawn(format!("Failed to shutdown container: {:?}", name))) + .ok_or(Error::Shutdown(container)) } } diff --git a/zoned/src/http.rs b/zoned/src/http.rs index 8d388b1..e5dcef6 100644 --- a/zoned/src/http.rs +++ b/zoned/src/http.rs @@ -7,7 +7,9 @@ use axum::{ }; use std::{process::Command, sync::Arc}; use tracing::{info, instrument, warn}; -use zone_core::{CloneOptions, Container, ContainerOptions, ContainerStatus, FilterContainer}; +use zone_core::{ + CloneOptions, Container, ContainerOptions, ContainerStatus, FilterContainer, Runtime, +}; use crate::{ws, Error, Result, State}; @@ -40,11 +42,6 @@ async fn container_list( .nspawn .list()? .into_iter() - .filter_map(|c| { - Container::try_from(c) - .map_err(|err| warn!("Ignoring invalid nspawn container: {:?}", err)) - .ok() - }) .filter_container(¶ms) .into(); @@ -56,12 +53,12 @@ async fn container_list( /// Creates a new container volume from the provided container json data #[instrument(err, ret, skip(state))] async fn clone_container( - Json(container): Json<CloneOptions>, + Json(clone_options): Json<CloneOptions>, Extension(state): Extension<Arc<State>>, ) -> Result<Json<(Container, ContainerStatus)>> { let predicate = Container::builder() - .user(container.user.to_owned()) - .template(container.template.to_owned()) + .user(clone_options.user.to_owned()) + .parent(clone_options.template.to_owned()) .to_owned(); let id = state @@ -73,14 +70,16 @@ async fn clone_container( .max_by_key(|c| c.id) .map_or(0, |c| c.id + 1); - let name = format!("{}-{}", container.user, id); - let root = state.zfs.clone_latest(name, (&container.template).into())?; + let name = format!("{}-{}", clone_options.user, id); + let root = state + .zfs + .clone_latest(name, (&clone_options.template).into())?; if let Some(script) = &state.config.init_script { Command::new(script) .env("DIRECTORY", &root) - .env("USER", &container.user) - .env("TEMPLATE", &container.template) + .env("USER", &clone_options.user) + .env("TEMPLATE", &clone_options.template) .status()? .code() .map_or(Ok(()), |code| { @@ -91,14 +90,14 @@ async fn clone_container( })?; }; - let name = format!("{}-{}-{}", container.template, container.user, id); - state.nspawn.create(root, name)?; let container = Container { id, - template: container.template, - user: container.user, + parent: clone_options.template, + user: clone_options.user, }; + state.nspawn.create(root, &container)?; + Ok(Json::from((container, ContainerStatus::Running))) } diff --git a/zoned/src/ws.rs b/zoned/src/ws.rs index 1f5a6fc..9289284 100644 --- a/zoned/src/ws.rs +++ b/zoned/src/ws.rs @@ -13,7 +13,7 @@ use tokio::{ }; use tracing::{instrument, warn}; use wspty::PtyMaster; -use zone_core::{Container, FilterContainer}; +use zone_core::{Container, FilterContainer, Runtime}; use crate::{Error, Result, State}; @@ -48,7 +48,7 @@ pub async fn handler(mut ws_stream: WebSocket, state: Arc<State>) -> Result<()> let (kill_tx, kill_rx) = mpsc::unbounded_channel(); let (tx, rx) = mpsc::channel(1024); - let pty = state.nspawn.attach(container.into(), kill_rx).await?; + let pty = state.nspawn.attach(container, kill_rx).await?; tokio::select! { res = msg_handler(receiver, pty.clone(), tx.clone(), kill_tx) => res, |