diff options
Diffstat (limited to 'zone_zfs')
-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 |
7 files changed, 196 insertions, 153 deletions
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()); + } +} |