diff options
-rw-r--r-- | Cargo.lock | 10 | ||||
-rw-r--r-- | zone_core/src/lib.rs | 10 | ||||
-rw-r--r-- | zone_zfs/Cargo.toml | 3 | ||||
-rw-r--r-- | zone_zfs/src/config.rs | 48 | ||||
-rw-r--r-- | zone_zfs/src/error.rs | 20 | ||||
-rw-r--r-- | zone_zfs/src/file_system.rs | 84 | ||||
-rw-r--r-- | zone_zfs/src/lib.rs | 105 | ||||
-rw-r--r-- | zone_zfs/src/snapshot.rs | 29 | ||||
-rw-r--r-- | zone_zfs/src/zfs.rs | 60 | ||||
-rw-r--r-- | zoned/src/api.rs | 35 | ||||
-rw-r--r-- | zoned/src/config.rs | 8 | ||||
-rw-r--r-- | zoned/src/error.rs | 52 | ||||
-rw-r--r-- | zoned/src/lib.rs | 62 | ||||
-rw-r--r-- | zoned/src/main.rs | 11 |
14 files changed, 308 insertions, 229 deletions
@@ -112,6 +112,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] +name = "bytesize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70" +dependencies = [ + "serde", +] + +[[package]] name = "cc" version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2165,6 +2174,7 @@ dependencies = [ name = "zone_zfs" version = "0.1.0" dependencies = [ + "bytesize", "chrono", "figment", "serde", diff --git a/zone_core/src/lib.rs b/zone_core/src/lib.rs index 25fe33a..6801ee8 100644 --- a/zone_core/src/lib.rs +++ b/zone_core/src/lib.rs @@ -76,7 +76,7 @@ impl<'r> Responder<'r, 'static> for Container { } impl TryFrom<FileSystem> for Container { - type Error = zone_zfs::Error; + type Error = zone_zfs::error::Error; fn try_from(file_system: FileSystem) -> Result<Self, Self::Error> { let path_buf = PathBuf::from(&file_system) @@ -116,11 +116,13 @@ impl TryFrom<zone_nspawn::Container> for Container { type Error = zone_nspawn::Error; fn try_from(value: zone_nspawn::Container) -> Result<Self, Self::Error> { - // id, template, user - let v: Vec<&str> = value.machine.split("-").collect(); + // id, template, user + let v: Vec<&str> = value.machine.split('-').collect(); Ok(Container { - id: v[0].parse().map_err(|err| Self::Error::Parsing(format!("Failed to parse container id: {:?}", err)))?, + id: v[0].parse().map_err(|err| { + Self::Error::Parsing(format!("Failed to parse container id: {:?}", err)) + })?, template: v[1].to_owned(), user: v[2].to_owned(), status: ContainerStatus::Running, diff --git a/zone_zfs/Cargo.toml b/zone_zfs/Cargo.toml index 472dda1..41ef655 100644 --- a/zone_zfs/Cargo.toml +++ b/zone_zfs/Cargo.toml @@ -5,8 +5,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bytesize = { version = "1.1.0", features = ["serde"] } chrono = "0.4.19" figment = "0.10.6" serde = "1.0.136" thiserror = "1.0.30" tracing = "0.1.29" + +[features] diff --git a/zone_zfs/src/config.rs b/zone_zfs/src/config.rs new file mode 100644 index 0000000..f3dc390 --- /dev/null +++ b/zone_zfs/src/config.rs @@ -0,0 +1,48 @@ +use bytesize::ByteSize; +use figment::{ + error::Result, + providers::{Env, Format, Serialized, Toml}, + value::{Dict, Map}, + Figment, Metadata, Profile, Provider, +}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Config { + pub quota: ByteSize, + pub pool_name: String, + pub mountpoint: PathBuf, +} + +impl Default for Config { + fn default() -> Self { + Config { + quota: ByteSize::gb(16), + pool_name: String::from("pool"), + mountpoint: PathBuf::from("/svr"), + } + } +} + +impl Config { + pub fn from<T: Provider>(provider: T) -> Result<Config> { + Figment::from(provider).extract() + } + + pub fn figment() -> Figment { + Figment::from(Config::default()) + .merge(Toml::file(Env::var_or("ZFS_CONFIG", "ZFS.toml")).nested()) + .merge(Env::prefixed("ZFS_").global()) + } +} + +impl Provider for Config { + fn metadata(&self) -> Metadata { + Metadata::named("ZFS Config") + } + + fn data(&self) -> Result<Map<Profile, Dict>> { + Serialized::defaults(Config::default()).data() + } +} diff --git a/zone_zfs/src/error.rs b/zone_zfs/src/error.rs new file mode 100644 index 0000000..0275384 --- /dev/null +++ b/zone_zfs/src/error.rs @@ -0,0 +1,20 @@ +pub type Result<T> = std::result::Result<T, Error>; + +#[allow(clippy::large_enum_variant)] +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("ZFS error")] + ZFS(String), + + #[error("Snapshot Error: {0:?}")] + Snapshot(String), + + #[error("File System Error: {0:?}")] + FileSystem(String), + + #[error("IO Error: Failed to run command")] + IO(#[from] std::io::Error), + + #[error("Config Error: {0:?}")] + Config(#[from] figment::Error), +} diff --git a/zone_zfs/src/file_system.rs b/zone_zfs/src/file_system.rs index 83164c9..6bee10b 100644 --- a/zone_zfs/src/file_system.rs +++ b/zone_zfs/src/file_system.rs @@ -1,3 +1,8 @@ +use crate::{ + error::{Error, Result}, + snapshot::Snapshot, +}; +use bytesize::ByteSize; use std::{ ffi::{OsStr, OsString}, fmt::Display, @@ -5,12 +10,21 @@ use std::{ process::Command, }; -use crate::{snapshot::Snapshot, Error, Result}; - -#[derive(Debug)] +macro_rules! opt [ + ($name:expr, $value:expr) => ({ + let mut _temp = OsString::new(); + _temp.push($name); + _temp.push("="); + _temp.push($value); + _temp + }) +]; + +#[derive(Debug, Default, Clone)] pub struct FileSystem { pub(crate) value: OsString, - pub(crate) mountpoint: Option<PathBuf>, + pub(crate) mountpoint: PathBuf, + pub(crate) quota: ByteSize, } impl Display for FileSystem { @@ -19,14 +33,13 @@ impl Display for FileSystem { } } -// pool/000.0/nkollack-1 impl TryFrom<OsString> for FileSystem { type Error = Error; fn try_from(value: OsString) -> Result<Self> { Ok(FileSystem { value, - mountpoint: None, + ..FileSystem::default() }) } } @@ -59,26 +72,43 @@ impl From<FileSystem> for String { impl FileSystem { pub(super) fn get_name(&self) -> Result<String> { - Ok(PathBuf::from(self.value.clone()) + PathBuf::from(&self.value) .file_name() - .ok_or_else(|| Error::FileSystem(format!("Invalid path for filesystem: {:?}", self)))? - .to_string_lossy() - .into_owned()) + .ok_or_else(|| Error::FileSystem(format!("Invalid path for filesystem: {:?}", self))) + .map(|s| s.to_string_lossy().into_owned()) } - pub(super) fn set_quota(&self, quota: &str) -> Result<()> { + pub(super) fn set_opt<T, U>(&self, name: T, value: U) -> Result<&Self> + where + T: AsRef<OsStr>, + U: AsRef<OsStr>, + { Command::new("zfs") .arg("set") - .arg(format!("quota={}", quota)) + .arg(opt!(&name, value)) .arg(&self.value) .status()? .success() - .then(|| ()) - .ok_or_else(|| Error::FileSystem(format!("Failed to set quota: {:?}", self))) + .then(|| self) + .ok_or_else(|| { + Error::FileSystem(format!("Failed to set {:?}: {:?}", name.as_ref(), self)) + }) + } + + pub(super) fn set_quota(&mut self, quota: ByteSize) -> Result<Self> { + self.set_opt("quota", quota.to_string())?; + self.quota = quota; + Ok(self.to_owned()) + } + + pub(super) fn set_mountpoint(&mut self, mountpoint: PathBuf) -> Result<Self> { + self.set_opt("mountpoint", &mountpoint)?; + self.mountpoint = mountpoint; + Ok(self.to_owned()) } pub(super) fn get_snapshots(&self) -> Result<Vec<Snapshot>> { - let stdout = Command::new("zfs") + Command::new("zfs") .arg("list") .arg("-H") .arg("-o") @@ -86,13 +116,11 @@ impl FileSystem { .arg("-t") .arg("snapshot") .arg(self) - .output()? - .stdout; - - String::from_utf8(stdout) + .output() + .map(|o| String::from_utf8(o.stdout))? .map_err(|err| Error::FileSystem(format!("Failed to parse command output: {:?}", err)))? .split_whitespace() - .map(|s| s.try_into()) + .map(Snapshot::try_from) .collect() } @@ -104,22 +132,6 @@ impl FileSystem { .max_by_key(|s| s.timestamp)) } - pub(super) fn get_file_systems() -> Result<Vec<FileSystem>> { - let stdout = Command::new("zfs") - .arg("list") - .arg("-H") - .arg("-o") - .arg("name") - .output()? - .stdout; - - String::from_utf8(stdout) - .map_err(|err| Error::FileSystem(format!("Failed to parse command output: {:?}", err)))? - .split_whitespace() - .map(|s| s.try_into()) - .collect() - } - pub fn mount(&self) -> Result<()> { Command::new("zfs") .arg("mount") diff --git a/zone_zfs/src/lib.rs b/zone_zfs/src/lib.rs index c430cba..7c50861 100644 --- a/zone_zfs/src/lib.rs +++ b/zone_zfs/src/lib.rs @@ -1,104 +1,19 @@ -use self::file_system::FileSystem; -use figment::{ - providers::{Env, Format, Serialized, Toml}, - Figment, Metadata, Profile, Provider, -}; -use serde::{Deserialize, Serialize}; -use std::{io, path::PathBuf, result}; +pub use zfs::ZFS; +pub use error::{Error, Result}; +pub use file_system::FileSystem; +pub use config::Config; +pub mod config; +pub mod error; pub mod file_system; - pub mod snapshot; - -type Result<T> = result::Result<T, Error>; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("ZFS error")] - ZFS(String), - - #[error("Snapshot Error: {0:?}")] - Snapshot(String), - - #[error("File System Error: {0:?}")] - FileSystem(String), - - #[error("IO Error: Failed to run command")] - IO(#[from] io::Error), -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct Config { - pub quota: String, -} - -impl Default for Config { - fn default() -> Self { - Config { - quota: "16G".to_string(), - } - } -} - -impl Config { - pub fn from<T: Provider>(provider: T) -> result::Result<Config, figment::Error> { - Figment::from(provider).extract() - } - - pub fn figment() -> Figment { - Figment::from(Config::default()) - .merge(Toml::file(Env::var_or("ZFS_CONFIG", "ZFS.toml")).nested()) - .merge(Env::prefixed("ZFS_").global()) - } -} - -impl Provider for Config { - fn metadata(&self) -> Metadata { - Metadata::named("ZFS Config") - } - - fn data( - &self, - ) -> result::Result<figment::value::Map<Profile, figment::value::Dict>, figment::Error> { - Serialized::defaults(Config::default()).data() - } -} - -pub fn create_file_system( - base_fs_name: String, - name: String, - config: &Config, -) -> Result<FileSystem> { - let fs = FileSystem::get_file_systems()? - .into_iter() - .find_map(|fs| match fs.get_name() { - Ok(n) if n == base_fs_name => Some(fs), - _ => None, - }) - .ok_or_else(|| Error::FileSystem("No ".to_string()))?; - - let snapshot = fs - .get_latest_snapshot()? - .ok_or_else(|| Error::Snapshot("No snapshot found".to_string()))?; - - let mut mountpoint = fs.value; - mountpoint.push(name); - - let cloned_fs = snapshot.clone_into_file_system( - snapshot.file_system.get_name()?, - Some(PathBuf::from(mountpoint)), - )?; - - cloned_fs.set_quota(&config.quota)?; - - Ok(cloned_fs) -} +pub mod zfs; #[cfg(test)] mod tests { #[test] - fn zfs_list() { - use super::*; - assert!(FileSystem::get_file_systems().is_ok()); + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); } } diff --git a/zone_zfs/src/snapshot.rs b/zone_zfs/src/snapshot.rs index 14fafb4..b0fabb9 100644 --- a/zone_zfs/src/snapshot.rs +++ b/zone_zfs/src/snapshot.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use std::{ffi::OsString, path::PathBuf, process::Command}; use tracing::warn; -use crate::{file_system::FileSystem, Error, Result}; +use crate::{error::Error, error::Result, file_system::FileSystem, Config}; #[derive(Debug)] pub struct Snapshot { @@ -38,32 +38,17 @@ impl TryFrom<&str> for Snapshot { } 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, - }; + pub fn clone_into_file_system(&self, new_fs: PathBuf) -> Result<FileSystem> { + let mut file_system = self.file_system.clone(); + file_system.value.push(&new_fs); - 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())); - }; - - command + Command::new("zfs") + .arg("clone") .arg(&self.value) .arg(&new_fs) .status()? .success() - .then(|| new_fs) + .then(|| file_system) .ok_or_else(|| Error::Snapshot(format!("Failed to clone snapshot: {:?}", self))) } } diff --git a/zone_zfs/src/zfs.rs b/zone_zfs/src/zfs.rs new file mode 100644 index 0000000..7f34951 --- /dev/null +++ b/zone_zfs/src/zfs.rs @@ -0,0 +1,60 @@ +use figment::{Figment, Provider}; +use std::path::PathBuf; +use std::process::Command; + +use crate::{Config, Error, FileSystem, Result}; + +#[derive(Default, Debug)] +pub struct ZFS { + pub config: Config, +} + +impl ZFS { + pub fn new() -> Result<Self> { + Self::custom(Config::figment()) + } + + pub fn custom<T: Provider>(provider: T) -> Result<Self> { + Config::from(Figment::from(provider)) + .map_err(Error::from) + .map(|config| Self { config }) + } + + pub fn clone_from_latest(&self, name: PathBuf, parent: PathBuf) -> Result<FileSystem> { + Self::get_file_system(parent)? + .get_latest_snapshot()? + .ok_or_else(|| Error::Snapshot("No snapshot found".into()))? + .clone_into_file_system(name)? + .set_quota(self.config.quota) + } + + pub(super) fn get_file_systems() -> Result<Vec<FileSystem>> { + Command::new("zfs") + .arg("list") + .arg("-H") + .arg("-o") + .arg("name") + .output() + .map(|o| String::from_utf8(o.stdout))? + .map_err(|err| Error::FileSystem(format!("Failed to parse command output: {:?}", err)))? + .split_whitespace() + .map(FileSystem::try_from) + .collect() + } + + fn get_file_system(name: PathBuf) -> Result<FileSystem> { + Self::get_file_systems()? + .into_iter() + .find(|fs| PathBuf::from(&fs.value).ends_with(&name)) + .ok_or_else(|| Error::FileSystem("No file system found".into())) + } +} + +#[cfg(test)] +mod tests { + #[test] + fn zfs_list() { + use super::*; + assert!(ZFS::get_file_systems().is_ok()); + } +} diff --git a/zoned/src/api.rs b/zoned/src/api.rs index fa98562..a1a892e 100644 --- a/zoned/src/api.rs +++ b/zoned/src/api.rs @@ -6,16 +6,17 @@ use rocket_okapi::{ swagger_ui::{make_swagger_ui, SwaggerUIConfig}, }; use zone_core::{Container, PartialEqOrDefault}; +use zone_zfs::ZFS; -use crate::{Error, Result}; +use crate::{Config, Error, Result}; /// # Test endpoint /// /// Returns a list of containers based on the query. #[openapi(tag = "Testing")] #[get("/test")] -pub fn test_endpoint(zfs_config: &State<zone_zfs::Config>) -> Json<String> { - Json(zfs_config.quota.to_owned()) +pub fn test_endpoint(zfs: &State<zone_zfs::ZFS>) -> Json<String> { + Json(zfs.config.pool_name.to_owned()) } /// List containers @@ -25,10 +26,16 @@ pub fn test_endpoint(zfs_config: &State<zone_zfs::Config>) -> Json<String> { #[get("/container/list?<container..>")] pub fn container_list(container: Container) -> Json<Vec<Container>> { zone_nspawn::get_containers() + .unwrap_or_else(|_err| { + todo!("Log this error"); + Default::default() + }) .into_iter() .map(|c| { - c.try_into() - .unwrap_or_else(|_err| todo!("Log this error and return `Container::default()`")) + c.try_into().unwrap_or_else(|_err| { + todo!("Log this error"); + Default::default() + }) }) .filter(|c| container.eq_or_default(c)) .collect::<Vec<Container>>() @@ -42,20 +49,23 @@ pub fn container_list(container: Container) -> Json<Vec<Container>> { #[post("/container", data = "<container>")] fn create_container( container: Json<Container>, - zfs_config: &State<zone_zfs::Config>, + zfs: &State<zone_zfs::ZFS>, ) -> Result<Json<Container>> { - zone_zfs::create_file_system( - container.template.clone(), - format!("{}-{}", container.user, container.id), - zfs_config, + zfs.clone_from_latest( + format!("{}-{}", container.user, container.id).into(), + container.template.to_owned().into(), )? .try_into() .map_err(Error::from) .map(Container::into) } -pub fn build_rocket(config: crate::Config) -> Rocket<Build> { - rocket::custom(config.rocket_config) +pub fn build_zfs(config: &Config) -> Result<ZFS> { + zone_zfs::ZFS::custom(&config.zfs_config).map_err(Error::from) +} + +pub fn build_rocket(config: &Config) -> Rocket<Build> { + rocket::custom(config.rocket_config.to_owned()) .mount( "/", openapi_get_routes![test_endpoint, container_list, create_container,], @@ -82,5 +92,4 @@ pub fn build_rocket(config: crate::Config) -> Rocket<Build> { ..Default::default() }), ) - .manage(config.zfs_config) } diff --git a/zoned/src/config.rs b/zoned/src/config.rs new file mode 100644 index 0000000..aa16360 --- /dev/null +++ b/zoned/src/config.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Default, Serialize, Deserialize)] +pub struct Config { + pub(crate) rocket_config: rocket::Config, + + pub(crate) zfs_config: zone_zfs::Config, +} diff --git a/zoned/src/error.rs b/zoned/src/error.rs new file mode 100644 index 0000000..14b30db --- /dev/null +++ b/zoned/src/error.rs @@ -0,0 +1,52 @@ +use rocket::{ + http::Status, + response::{self, Responder}, + Request, +}; +use rocket_okapi::{ + gen::OpenApiGenerator, okapi::openapi3::Responses, response::OpenApiResponderInner, + util::ensure_status_code_exists, +}; +use thiserror::Error; +use zone_core::Container; + +pub type Result<T> = std::result::Result<T, Error>; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Container Error {0:?}")] + Container(Container), + + #[error("ZFS Error {source:?}")] + ZFS { + #[from] + source: zone_zfs::Error, + }, + + #[error("NSpawn Error {source:?}")] + Nspawn { + #[from] + source: zone_nspawn::Error, + }, + + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +impl<'r, 'o: 'r> Responder<'r, 'o> for Error { + fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { + // https://stuarth.github.io/rocket-error-handling/ + // match self { + // _ => Status::InternalServerError.respond_to(req), + // } + Status::InternalServerError.respond_to(req) + } +} + +impl OpenApiResponderInner for Error { + fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result<Responses> { + let mut responses = Responses::default(); + ensure_status_code_exists(&mut responses, 500); + Ok(responses) + } +} diff --git a/zoned/src/lib.rs b/zoned/src/lib.rs index 4784ff5..6e300dc 100644 --- a/zoned/src/lib.rs +++ b/zoned/src/lib.rs @@ -1,62 +1,8 @@ -use rocket::{ - http::Status, - response::{self, Responder}, - Request, -}; -use rocket_okapi::{ - gen::OpenApiGenerator, okapi::openapi3::Responses, response::OpenApiResponderInner, - util::ensure_status_code_exists, -}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; -use zone_core::Container; +pub use config::Config; +pub use error::{Error, Result}; -type Result<T> = std::result::Result<T, Error>; +pub mod error; -#[derive(Error, Debug)] -pub enum Error { - #[error("Container Error {0:?}")] - Container(Container), - - #[error("ZFS Error {source:?}")] - ZFS { - #[from] - source: zone_zfs::Error, - }, - - #[error("NSpawn Error {source:?}")] - Nspawn { - #[from] - source: zone_nspawn::Error, - }, - - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -impl<'r, 'o: 'r> Responder<'r, 'o> for Error { - fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { - // https://stuarth.github.io/rocket-error-handling/ - // match self { - // _ => Status::InternalServerError.respond_to(req), - // } - Status::InternalServerError.respond_to(req) - } -} - -impl OpenApiResponderInner for Error { - fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result<Responses> { - let mut responses = Responses::default(); - ensure_status_code_exists(&mut responses, 500); - Ok(responses) - } -} - -#[derive(Default, Serialize, Deserialize)] -pub struct Config { - pub(crate) rocket_config: rocket::Config, - - pub(crate) zfs_config: zone_zfs::Config, -} +pub mod config; pub mod api; diff --git a/zoned/src/main.rs b/zoned/src/main.rs index 5ca2862..f9512a2 100644 --- a/zoned/src/main.rs +++ b/zoned/src/main.rs @@ -2,6 +2,7 @@ use figment::{ providers::{Env, Format, Serialized, Toml}, Figment, }; +use std::process; use zoned::{api, Config}; #[rocket::main] @@ -12,7 +13,15 @@ async fn main() { .extract() .expect("Failed to parse config"); - match api::build_rocket(config).launch().await { + let zfs = match api::build_zfs(&config) { + Ok(zfs) => zfs, + Err(err) => { + eprintln!("ZFS error: {}", err); + process::exit(1) + } + }; + + match api::build_rocket(&config).manage(zfs).launch().await { Ok(()) => println!("Rocket shut down gracefully."), Err(err) => eprintln!("Rocket had an error: {}", err), }; |