diff --git a/xc-bin/src/image/patch.rs b/xc-bin/src/image/patch.rs index 172cc1b..4fbc46a 100644 --- a/xc-bin/src/image/patch.rs +++ b/xc-bin/src/image/patch.rs @@ -192,7 +192,9 @@ impl PatchActions { destination: mount_point.to_path_buf(), required: *required, }; - config.mounts.insert(key.to_string_lossy().to_string(), mountspec); + config + .mounts + .insert(key.to_string_lossy().to_string(), mountspec); } PatchActions::ModAllow { allows } => { for allow in allows.iter() { diff --git a/xc-bin/src/jailfile/directives/mod.rs b/xc-bin/src/jailfile/directives/mod.rs index acdbfe9..834d2ea 100644 --- a/xc-bin/src/jailfile/directives/mod.rs +++ b/xc-bin/src/jailfile/directives/mod.rs @@ -114,7 +114,9 @@ impl ConfigMod { } } Self::Volume(name, mount_spec) => { - config.mounts.insert(name.to_string_lossy().to_string(), mount_spec.clone()); + config + .mounts + .insert(name.to_string_lossy().to_string(), mount_spec.clone()); } Self::WorkDir(entry_point, dir) => { let work_dir = dir.to_string(); diff --git a/xc-bin/src/redirect.rs b/xc-bin/src/redirect.rs index 47cfda5..021db78 100644 --- a/xc-bin/src/redirect.rs +++ b/xc-bin/src/redirect.rs @@ -39,6 +39,7 @@ pub(crate) enum RdrAction { #[arg(short = 'H', action)] without_header: bool, }, + ShowRules, } pub(crate) fn use_rdr_action( @@ -73,6 +74,14 @@ pub(crate) fn use_rdr_action( } } } + RdrAction::ShowRules => { + let response = do_list_rdr_rules(conn, ())?; + if let Ok(response) = response { + for rule in response.iter() { + println!("{}", rule.to_pf_rule()); + } + } + } } Ok(()) } diff --git a/xc-bin/src/volume.rs b/xc-bin/src/volume.rs index 0e53231..3e05ddd 100644 --- a/xc-bin/src/volume.rs +++ b/xc-bin/src/volume.rs @@ -30,7 +30,7 @@ use oci_util::image_reference::ImageReference; use std::collections::HashMap; use std::ffi::OsString; use xcd::ipc::*; -use xcd::volume::VolumeDriverKind; +use xcd::resources::volume::VolumeDriverKind; #[derive(Parser, Debug)] pub(crate) enum VolumeAction { diff --git a/xc/src/container/error.rs b/xc/src/container/error.rs index e2bc614..7df6d39 100644 --- a/xc/src/container/error.rs +++ b/xc/src/container/error.rs @@ -1,4 +1,4 @@ -// Copyright (c) 2023 Yan Ka, Chiu. +// Copyright (c), , Yan Ka, Chiu. // All rights reserved. // // Redistribution and use in source and binary forms, with or without @@ -21,11 +21,20 @@ // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF // SUCH DAMAGE. +use freebsd::libc::*; +use std::io::ErrorKind; use thiserror::Error; + #[macro_export] -macro_rules! precondition_failure { +macro_rules! errx { ($errno:expr, $($t:tt)*) => { - return Err(xc::container::error::PreconditionFailure::new($errno, anyhow::anyhow!($($t)*)).into()) + return Err(xc::container::error::Error::new($errno, anyhow::anyhow!($($t)*)).into()) + } +} +#[macro_export] +macro_rules! err { + ($errno:expr, $($t:tt)*) => { + xc::container::error::Error::new($errno, anyhow::anyhow!($($t)*)) } } @@ -50,27 +59,344 @@ pub enum ExecError { } #[derive(Error, Debug)] -pub struct PreconditionFailure { +pub struct Error { errno: i32, source: anyhow::Error, } -impl std::fmt::Display for PreconditionFailure { +impl std::fmt::Display for Error { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(fmt, "{}", self.source) } } -impl PreconditionFailure { - pub fn new(errno: i32, source: anyhow::Error) -> PreconditionFailure { - PreconditionFailure { errno, source } +impl Error { + pub fn new(errno: i32, source: anyhow::Error) -> Error { + Error { errno, source } } pub fn errno(&self) -> i32 { self.errno } + pub fn with_errno(self, errno: i32) -> Error { + Self { errno, ..self } + } + pub fn error_message(&self) -> String { format!("{:#}", self.source) } } + +pub trait WithErrno { + fn with_errno(self, errno: i32) -> Error; +} + +impl WithErrno for E { + fn with_errno(self, errno: i32) -> Error { + Error { + errno, + source: anyhow::Error::from(self), + } + } +} + +impl From for Error { + fn from(error: anyhow::Error) -> Error { + Error { + errno: 128, + source: error, + } + } +} + +impl From for Error { + fn from(error: std::io::Error) -> Error { + let errno = match error.kind() { + ErrorKind::NotFound => ENOENT, + ErrorKind::OutOfMemory => ENOMEM, + ErrorKind::Interrupted => EINTR, + ErrorKind::Other => ETIMEDOUT, + #[cfg(feature = "io_error_more")] + ErrorKind::Deadlock => EDEADLK, + ErrorKind::AddrInUse => EADDRINUSE, + ErrorKind::BrokenPipe => EPIPE, + ErrorKind::WouldBlock => EWOULDBLOCK, + #[cfg(feature = "io_error_more")] + ErrorKind::NetworkDown => ENETDOWN, + #[cfg(feature = "io_error_more")] + ErrorKind::InvalidData => EINVAL, + #[cfg(feature = "io_error_more")] + ErrorKind::StorageFull => ENOSPC, + #[cfg(feature = "io_error_more")] + ErrorKind::NotSeekable => ENXIO, + #[cfg(feature = "io_error_more")] + ErrorKind::Unsupported => ENOTSUP, + ErrorKind::NotConnected => ENOTCONN, + #[cfg(feature = "io_error_more")] + ErrorKind::IsADirectory => EISDIR, + #[cfg(feature = "io_error_more")] + ErrorKind::InvalidInput => EINVAL, + #[cfg(feature = "io_error_more")] + ErrorKind::FileTooLarge => EFBIG, + #[cfg(feature = "io_error_more")] + ErrorKind::ResourceBusy => EBUSY, + #[cfg(feature = "io_error_more")] + ErrorKind::TooManyLinks => ELOOP, + ErrorKind::AlreadyExists => EEXIST, + ErrorKind::TimedOut => ETIMEDOUT, + #[cfg(feature = "io_error_more")] + ErrorKind::NotADirectory => ENOTDIR, + #[cfg(feature = "io_error_more")] + ErrorKind::FilesystemLoop => ELOOP, + #[cfg(feature = "io_error_more")] + ErrorKind::CrossesDevices => ENOTSUP, + ErrorKind::ConnectionReset => ECONNRESET, + #[cfg(feature = "io_error_more")] + ErrorKind::HostUnreachable => EHOSTUNREACH, + #[cfg(feature = "io_error_more")] + ErrorKind::InvalidFilename => ENAMETOOLONG, + ErrorKind::PermissionDenied => EPERM, + ErrorKind::AddrNotAvailable => EADDRNOTAVAIL, + ErrorKind::ConnectionRefused => ECONNREFUSED, + ErrorKind::ConnectionAborted => ECONNABORTED, + #[cfg(feature = "io_error_more")] + ErrorKind::DirectoryNotEmpty => ENOTEMPTY, + #[cfg(feature = "io_error_more")] + ErrorKind::NetworkUnreachable => ENETUNREACH, + #[cfg(feature = "io_error_more")] + ErrorKind::ReadOnlyFilesystem => EROFS, + #[cfg(feature = "io_error_more")] + ErrorKind::ExecutableFileBusy => ETXTBSY, + #[cfg(feature = "io_error_more")] + ErrorKind::ArgumentListTooLong => E2BIG, + #[cfg(feature = "io_error_more")] + ErrorKind::StaleNetworkFileHandle => ESTALE, + #[cfg(feature = "io_error_more")] + ErrorKind::FilesystemQuotaExceeded => EDQUOT, + _ => 128, + }; + + Error { + errno, + source: error.into(), + } + } +} + +pub mod errno { + + macro_rules! export { + ($($name:ident, $desc:expr),*) => { + $(pub const $name: i32 = freebsd::libc::$name;)* + + pub fn errno_desc(v: i32) -> &'static str { + #[allow(unreachable_patterns)] + match v { + $($name => $desc,)* + _ => "unknown" + } + } + } + } + export!( + EPERM, + "Operation not permitted", + ENOENT, + "No such file or directory", + ESRCH, + "No such process", + EINTR, + "Interrupted system call", + EIO, + "Input/output error", + ENXIO, + "Device not configured", + E2BIG, + "Argument list too long", + ENOEXEC, + "Exec format error", + EBADF, + "Bad file descriptor", + ECHILD, + "No child processes", + EDEADLK, + "Resource deadlock avoided", + ENOMEM, + "Cannot allocate memory", + EACCES, + "Permission denied", + EFAULT, + "Bad address", + ENOTBLK, + "Block device required", + EBUSY, + "Device busy", + EEXIST, + "File exists", + EXDEV, + "Cross-device link", + ENODEV, + "Operation not supported by device", + ENOTDIR, + "Not a directory", + EISDIR, + "Is a directory", + EINVAL, + "Invalid argument", + ENFILE, + "Too many open files in system", + EMFILE, + "Too many open files", + ENOTTY, + "Inappropriate ioctl for device", + ETXTBSY, + "Text file busy", + EFBIG, + "File too large", + ENOSPC, + "No space left on device", + ESPIPE, + "Illegal seek", + EROFS, + "Read-only filesystem", + EMLINK, + "Too many links", + EPIPE, + "Broken pipe", + EDOM, + "Numerical argument out of domain", + ERANGE, + "Result too large", + EAGAIN, + "Resource temporarily unavailable", + EWOULDBLOCK, + "Operation would block", + EINPROGRESS, + "Operation now in progress", + EALREADY, + "Operation already in progress", + ENOTSOCK, + "Socket operation on non-socket", + EDESTADDRREQ, + "Destination address required", + EMSGSIZE, + "Message too long", + EPROTOTYPE, + "Protocol wrong type for socket", + ENOPROTOOPT, + "Protocol not available", + EPROTONOSUPPORT, + "Protocol not supported", + ESOCKTNOSUPPORT, + "Socket type not supported", + EOPNOTSUPP, + "Operation not supported", + ENOTSUP, + "Operation not supported", + EPFNOSUPPORT, + "Protocol family not supported", + EAFNOSUPPORT, + "Address family not supported by protocol family", + EADDRINUSE, + "Address already in use", + EADDRNOTAVAIL, + "Can't assign requested address", + ENETDOWN, + "Network is down", + ENETUNREACH, + "Network is unreachable", + ENETRESET, + "Network dropped connection on reset", + ECONNABORTED, + "Software caused connection abort", + ECONNRESET, + "Connection reset by peer", + ENOBUFS, + "No buffer space available", + EISCONN, + "Socket is already connected", + ENOTCONN, + "Socket is not connected", + ESHUTDOWN, + "Can't send after socket shutdown", + ETOOMANYREFS, + "Too many references: can't splice", + ETIMEDOUT, + "Operation timed out", + ECONNREFUSED, + "Connection refused", + ELOOP, + "Too many levels of symbolic links", + ENAMETOOLONG, + "File name too long", + EHOSTDOWN, + "Host is down", + EHOSTUNREACH, + "No route to host", + ENOTEMPTY, + "Directory not empty", + EPROCLIM, + "Too many processes", + EUSERS, + "Too many users", + EDQUOT, + "Disc quota exceeded", + ESTALE, + "Stale NFS file handle", + EREMOTE, + "Too many levels of remote in path", + EBADRPC, + "RPC struct is bad", + ERPCMISMATCH, + "RPC version wrong", + EPROGUNAVAIL, + "RPC prog. not avail", + EPROGMISMATCH, + "Program version wrong", + EPROCUNAVAIL, + "Bad procedure for program", + ENOLCK, + "No locks available", + ENOSYS, + "Function not implemented", + EFTYPE, + "Inappropriate file type or format", + EAUTH, + "Authentication error", + ENEEDAUTH, + "Need authenticator", + EIDRM, + "Identifier removed", + ENOMSG, + "No message of desired type", + EOVERFLOW, + "Value too large to be stored in data type", + ECANCELED, + "Operation canceled", + EILSEQ, + "Illegal byte sequence", + ENOATTR, + "Attribute not found", + EDOOFUS, + "Programming error", + EBADMSG, + "Bad message", + EMULTIHOP, + "Multihop attempted", + ENOLINK, + "Link has been severed", + EPROTO, + "Protocol error", + ENOTCAPABLE, + "Capabilities insufficient", + ECAPMODE, + "Not permitted in capability mode", + ENOTRECOVERABLE, + "State not recoverable", + EOWNERDEAD, + "Previous owner died", + EINTEGRITY, + "Integrity check failed" + ); +} diff --git a/xc/src/models/exec.rs b/xc/src/models/exec.rs index c47b708..0d1af79 100644 --- a/xc/src/models/exec.rs +++ b/xc/src/models/exec.rs @@ -105,7 +105,7 @@ impl Exec { &self, envs: &HashMap, args: &[String], - ) -> Result { + ) -> Result { let mut argv = Vec::new(); let mut resolved_envs = if self.clear_env { HashMap::new() @@ -117,7 +117,7 @@ impl Exec { for env in self.required_envs.iter() { if !resolved_envs.contains_key(env.as_str()) { - return Err(crate::container::error::PreconditionFailure::new( + return Err(crate::container::error::Error::new( freebsd::libc::ENOENT, anyhow::anyhow!("missing required environment variable {env}"), )); diff --git a/xc/src/util.rs b/xc/src/util.rs index 50846a8..10f8295 100644 --- a/xc/src/util.rs +++ b/xc/src/util.rs @@ -291,7 +291,8 @@ impl CompressionFormatExt for std::fs::File { pub fn elf_abi_fallback_brand() -> String { Ctl::new("kern.elf64.fallback_brand") - .expect("cannot sysctl").value_string() + .expect("cannot sysctl") + .value_string() .expect("cannot get sysctl output as string") } diff --git a/xcd/src/config/config_manager.rs b/xcd/src/config/config_manager.rs deleted file mode 100644 index fdbde48..0000000 --- a/xcd/src/config/config_manager.rs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2023 Yan Ka, Chiu. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions -// are met: -// 1. Redistributions of source code must retain the above copyright -// notice, this list of conditions, and the following disclaimer, -// without modification, immediately at the beginning of the file. -// 2. The name of the author may not be used to endorse or promote products -// derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND -// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR -// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -// SUCH DAMAGE. -use anyhow::Context; -use std::path::{Path, PathBuf}; -use tracing::error; - -use crate::config::inventory::Inventory; - -pub(crate) struct InventoryManager { - path: PathBuf, - cached: Inventory, -} - -impl InventoryManager { - pub(crate) fn load_from_path( - path: impl AsRef, - ) -> Result { - let path = path.as_ref().to_path_buf(); - let data = std::fs::read_to_string(&path).context("Cannot read config file")?; - let cached: Inventory = match serde_json::from_str(&data).context("Cannot parse config") { - Ok(inventory) => inventory, - Err(_) => { - let default = Inventory::default(); - std::fs::write(&path, serde_json::to_vec_pretty(&default).unwrap()) - .context("cannot write default inventory")?; - default - } - }; - Ok(InventoryManager { path, cached }) - } - - pub(crate) fn borrow(&self) -> &Inventory { - &self.cached - } - - /// Modify the underlying config, and synchronize the changes to underlying config file - pub(crate) fn modify(&mut self, f: F) - where - F: FnOnce(&mut Inventory), - { - let mut config = self.cached.clone(); - let old_config = config.clone(); - - f(&mut config); - - if old_config != config { - if let Ok(serialized) = serde_json::to_vec_pretty(&config) { - if std::fs::write(&self.path, serialized).is_err() { - error!( - "failed to write new config to config file at {:?}", - self.path - ); - } - } else { - error!("failed to serialize new config to bytes"); - } - self.cached = config - } - } -} diff --git a/xcd/src/config/inventory.rs b/xcd/src/config/inventory.rs index b985fc4..5171f90 100644 --- a/xcd/src/config/inventory.rs +++ b/xcd/src/config/inventory.rs @@ -21,11 +21,14 @@ // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF // SUCH DAMAGE. +use anyhow::Context; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use tracing::error; -use crate::network::Network; -use crate::volume::Volume; +use crate::resources::network::Network; +use crate::resources::volume::Volume; #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] pub(crate) struct Inventory { @@ -34,3 +37,61 @@ pub(crate) struct Inventory { #[serde(default)] pub volumes: HashMap, } + +/// Handle to access an on disk inventory storage. +/// +/// This structure act as a middleman between the real, on disk inventory manifest and logics want +/// to access and modify the inventories. On modification to the inventory via this structure, it +/// automatically serialize the updated inventory and store on disk +pub(crate) struct InventoryManager { + path: PathBuf, + cached: Inventory, +} + +impl InventoryManager { + pub(crate) fn load_from_path( + path: impl AsRef, + ) -> Result { + let path = path.as_ref().to_path_buf(); + let data = std::fs::read_to_string(&path).context("Cannot read config file")?; + let cached: Inventory = match serde_json::from_str(&data).context("Cannot parse config") { + Ok(inventory) => inventory, + Err(_) => { + let default = Inventory::default(); + std::fs::write(&path, serde_json::to_vec_pretty(&default).unwrap()) + .context("cannot write default inventory")?; + default + } + }; + Ok(InventoryManager { path, cached }) + } + + pub(crate) fn borrow(&self) -> &Inventory { + &self.cached + } + + /// Modify the underlying config, and synchronize the changes to underlying config file + pub(crate) fn modify(&mut self, f: F) + where + F: FnOnce(&mut Inventory), + { + let mut config = self.cached.clone(); + let old_config = config.clone(); + + f(&mut config); + + if old_config != config { + if let Ok(serialized) = serde_json::to_vec_pretty(&config) { + if std::fs::write(&self.path, serialized).is_err() { + error!( + "failed to write new config to config file at {:?}", + self.path + ); + } + } else { + error!("failed to serialize new config to bytes"); + } + self.cached = config + } + } +} diff --git a/xcd/src/config/mod.rs b/xcd/src/config/mod.rs index 1eade34..db00ab7 100644 --- a/xcd/src/config/mod.rs +++ b/xcd/src/config/mod.rs @@ -21,7 +21,6 @@ // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF // SUCH DAMAGE. -pub mod config_manager; pub mod inventory; use anyhow::{bail, Context}; diff --git a/xcd/src/context.rs b/xcd/src/context.rs index a7fda66..33cb211 100644 --- a/xcd/src/context.rs +++ b/xcd/src/context.rs @@ -23,25 +23,23 @@ // SUCH DAMAGE. use crate::auth::Credential; -use crate::config::config_manager::InventoryManager; -use crate::config::inventory::Inventory; use crate::config::XcConfig; use crate::database::Database; -use crate::dataset::JailedDatasetTracker; use crate::devfs_store::DevfsRulesetStore; use crate::image::pull::PullImageError; use crate::image::ImageManager; use crate::instantiate::{AppliedInstantiateRequest, InstantiateBlueprint}; use crate::ipc::InstantiateRequest; -use crate::network_manager::NetworkManager; use crate::port::PortForwardTable; use crate::registry::JsonRegistryProvider; +use crate::resources::volume::{Volume, VolumeDriverKind}; +use crate::resources::Resources; use crate::site::Site; use crate::util::TwoWayMap; -use crate::volume::{Volume, VolumeDriverKind, VolumeManager}; use anyhow::Context; use freebsd::fs::zfs::ZfsHandle; +use freebsd::libc::EIO; use freebsd::net::pf; use oci_util::digest::OciDigest; use oci_util::image_reference::{ImageReference, ImageTag}; @@ -52,8 +50,9 @@ use std::str::FromStr; use std::sync::Arc; use tokio::sync::{Mutex, RwLock}; use tracing::{debug, error, info, warn}; -use xc::container::error::PreconditionFailure; +use xc::container::error::Error; use xc::container::ContainerManifest; +use xc::errx; use xc::image_store::sqlite::SqliteImageStore; use xc::image_store::ImageRecord; use xc::models::jail_image::{JailConfig, JailImage}; @@ -66,7 +65,6 @@ mod context_provider { } pub struct ServerContext { - pub(crate) network_manager: Arc>, pub(crate) sites: HashMap>>, pub(crate) terminated_sites: Vec<(String, Arc>)>, @@ -82,10 +80,7 @@ pub struct ServerContext { pub(crate) ng2jails: HashMap>, pub(crate) jail2ngs: HashMap>, - pub(crate) inventory: Arc>, - - pub(crate) volume_manager: Arc>, - pub(crate) dataset_tracker: Arc>, + pub(crate) resources: Arc>, } impl ServerContext { @@ -105,14 +100,9 @@ impl ServerContext { db.perform(xc::res::create_tables) .expect("cannot create tables"); - let inventory = Arc::new(std::sync::Mutex::new( - InventoryManager::load_from_path(&config.inventory).expect("cannot write inventory"), - )); - - let network_manager = Arc::new(Mutex::new(NetworkManager::new(db, inventory.clone()))); - let is = Arc::new(Mutex::new(image_store)); let provider = JsonRegistryProvider::from_path(&config.registries).unwrap(); + let resources = Arc::new(RwLock::new(Resources::new(db, &config))); let image_manager = ImageManager::new( is, @@ -121,14 +111,7 @@ impl ServerContext { Arc::new(Mutex::new(Box::new(provider))), ); - let volume_manager = Arc::new(Mutex::new(VolumeManager::new( - inventory.clone(), - config.default_volume_dataset.clone(), - None, - ))); - ServerContext { - network_manager, alias_map: TwoWayMap::new(), devfs_store: DevfsRulesetStore::new(config.devfs_id_offset, config.force_devfs_ruleset), image_manager: Arc::new(RwLock::new(image_manager)), @@ -138,9 +121,7 @@ impl ServerContext { port_forward_table: PortForwardTable::new(), ng2jails: HashMap::new(), jail2ngs: HashMap::new(), - inventory, - volume_manager, - dataset_tracker: Arc::new(RwLock::new(JailedDatasetTracker::default())), + resources, } } @@ -148,8 +129,13 @@ impl ServerContext { self.config.clone() } - pub(crate) fn inventory(&self) -> Inventory { - self.inventory.lock().unwrap().borrow().clone() + pub(crate) async fn inventory(&self) -> crate::config::inventory::Inventory { + self.resources + .read() + .await + .inventory_manager + .borrow() + .clone() } pub(crate) fn create_channel( @@ -380,17 +366,21 @@ impl ServerContext { } /// Reload the entry pf anchor "xc-rdr" - pub fn reload_pf_rdr_anchor(&self) -> Result<(), std::io::Error> { + pub fn reload_pf_rdr_anchor(&self) -> Result<(), Error> { let rules = self.port_forward_table.generate_rdr_rules(); info!(rules, "reloading xc-rdr anchor"); - pf::is_pf_enabled().and_then(|enabled| { + if let Err(error) = pf::is_pf_enabled().and_then(|enabled| { if enabled { pf::set_rules_unchecked(Some("xc-rdr".to_string()), &rules) } else { warn!("pf is not enabled"); Ok(()) } - }) + }) { + errx!(EIO, "pfctl error: {error:?}") + } else { + Ok(()) + } } pub(crate) async fn terminate(&mut self, id: &str) -> Result<(), anyhow::Error> { @@ -409,9 +399,10 @@ impl ServerContext { return Err(err); } - let mut nm = self.network_manager.lock().await; + let mut resources = self.resources.write().await; + // let mut nm = self.network_manager.lock().await; - let addresses = nm + let addresses = resources .release_addresses(id) .context("sqlite failure on ip address release")?; @@ -429,7 +420,9 @@ impl ServerContext { self.port_forward_table.remove_rules(id); self.reload_pf_rdr_anchor()?; self.alias_map.remove_all_referenced(id); - self.dataset_tracker.write().await.release_container(id); + + resources.dataset_tracker.release_container(id); + // self.dataset_tracker.write().await.release_container(id); if let Some(networks) = self.jail2ngs.get(id) { for network in networks.iter() { @@ -559,47 +552,25 @@ impl ServerContext { site.stage(image)?; let name = request.name.clone(); - let applied = { - let network_manager = this.network_manager.clone(); - let network_manager = network_manager.lock().await; - let volume_manager = this.volume_manager.clone(); - let volume_manager = volume_manager.lock().await; - AppliedInstantiateRequest::new( - request, - image, - &cred, - &network_manager, - &volume_manager, - )? - }; + let resources_ref = this.resources.clone(); + let mut resources = resources_ref.write().await; + + let applied = + { AppliedInstantiateRequest::new(request, image, &cred, &mut resources)? }; let blueprint = { - let network_manager = this.network_manager.clone(); - let mut network_manager = network_manager.lock().await; - let volume_manager = this.volume_manager.clone(); - let volume_manager = volume_manager.lock().await; - let dataset_tracker = this.dataset_tracker.clone(); - let mut dataset_tracker = dataset_tracker.write().await; - InstantiateBlueprint::new( id, image, applied, &mut this.devfs_store, &cred, - &mut network_manager, - &volume_manager, - &mut dataset_tracker, + &mut resources, )? }; if pf::is_pf_enabled().unwrap_or_default() { - if let Some(map) = this - .network_manager - .lock() - .await - .get_allocated_addresses(id) - { + if let Some(map) = resources.get_allocated_addresses(id) { for (network, addresses) in map.iter() { let table = format!("xc:network:{network}"); let result = pf::table_add_addresses(None, &table, addresses); @@ -688,7 +659,7 @@ impl ServerContext { } pub(crate) async fn list_volumes(&self) -> HashMap { - self.volume_manager.lock().await.list_volumes() + self.resources.read().await.list_volumes() } pub(crate) async fn create_volume( @@ -698,9 +669,9 @@ impl ServerContext { kind: VolumeDriverKind, source: Option, zfs_props: HashMap, - ) -> Result<(), PreconditionFailure> { - self.volume_manager - .lock() + ) -> Result<(), Error> { + self.resources + .write() .await .create_volume(kind, name, template, source, zfs_props) } diff --git a/xcd/src/database.rs b/xcd/src/database.rs index af95aa8..b4d6b56 100644 --- a/xcd/src/database.rs +++ b/xcd/src/database.rs @@ -25,7 +25,7 @@ use rusqlite::{Connection, OptionalExtension, Params, Row}; use std::net::IpAddr; use std::sync::Mutex; -use crate::network::AddressStore; +use crate::resources::network::AddressStore; pub struct Database { db: Mutex, diff --git a/xcd/src/instantiate.rs b/xcd/src/instantiate.rs index 21d38c0..8cc931a 100644 --- a/xcd/src/instantiate.rs +++ b/xcd/src/instantiate.rs @@ -23,11 +23,10 @@ // SUCH DAMAGE. use crate::auth::Credential; -use crate::dataset::JailedDatasetTracker; use crate::devfs_store::DevfsRulesetStore; use crate::ipc::InstantiateRequest; -use crate::network_manager::NetworkManager; -use crate::volume::{Volume, VolumeManager}; +use crate::resources::volume::Volume; +use crate::resources::Resources; use anyhow::Context; use freebsd::event::EventFdNotify; @@ -39,12 +38,12 @@ use std::net::IpAddr; use std::os::fd::{AsRawFd, RawFd}; use varutil::string_interpolation::InterpolatedString; use xc::container::request::{CopyFileReq, Mount, NetworkAllocRequest}; +use xc::errx; use xc::format::devfs_rules::DevfsRule; use xc::models::exec::{Jexec, StdioMode}; use xc::models::jail_image::JailImage; use xc::models::network::{DnsSetting, IpAssign}; use xc::models::EnforceStatfs; -use xc::precondition_failure; pub struct AppliedInstantiateRequest { base: InstantiateRequest, @@ -63,8 +62,7 @@ impl AppliedInstantiateRequest { mut request: InstantiateRequest, oci_config: &JailImage, cred: &Credential, - network_manager: &NetworkManager, - volume_manager: &VolumeManager, + resources: &mut Resources, ) -> anyhow::Result { let existing_ifaces = freebsd::net::ifconfig::interfaces()?; let available_allows = xc::util::jail_allowables(); @@ -82,7 +80,7 @@ impl AppliedInstantiateRequest { .as_ref() .map(|d| format!(" - {d}")) .unwrap_or_default(); - precondition_failure!( + errx!( ENOENT, "missing required environment variable: {key}{extra_info}" ); @@ -142,7 +140,7 @@ impl AppliedInstantiateRequest { for assign in request.ips.iter() { let iface = &assign.interface; if !existing_ifaces.contains(iface) { - precondition_failure!(ENOENT, "missing network interface {iface}"); + errx!(ENOENT, "missing network interface {iface}"); } } @@ -152,7 +150,7 @@ impl AppliedInstantiateRequest { if available_allows.contains(allow) { allows.push(allow.to_string()); } else { - precondition_failure!(EIO, "allow.{allow} is not available on this system"); + errx!(EIO, "allow.{allow} is not available on this system"); } } allows @@ -186,16 +184,13 @@ impl AppliedInstantiateRequest { if !source_path.is_absolute() { let name = source_path.to_string_lossy().to_string(); - match volume_manager.query_volume(&name) { + match resources.query_volume(&name) { None => { - precondition_failure!(ENOENT, "no such volume {name}") + errx!(ENOENT, "no such volume {name}") } Some(volume) => { if !volume.can_mount(cred.uid()) { - precondition_failure!( - EPERM, - "this user is not allowed to mount the volume" - ) + errx!(EPERM, "this user is not allowed to mount the volume") } } } @@ -206,14 +201,14 @@ impl AppliedInstantiateRequest { for (key, spec) in mount_specs.iter() { if spec.required { - precondition_failure!(ENOENT, "Required volume {key:?} is not mounted"); + errx!(ENOENT, "Required volume {key:?} is not mounted"); } } for req in request.ipreq.iter() { let network = req.network(); - if !network_manager.has_network(&network) { - precondition_failure!(ENOENT, "no such network: {network}"); + if !resources.has_network(&network) { + errx!(ENOENT, "no such network: {network}"); } } @@ -236,7 +231,7 @@ impl AppliedInstantiateRequest { let applied = rule.apply(&envs); match applied.parse::() { Err(error) => { - precondition_failure!(EINVAL, "invaild devfs rule: [{applied}], {error}") + errx!(EINVAL, "invaild devfs rule: [{applied}], {error}") } Ok(rule) => devfs_rules.push(rule), } @@ -302,9 +297,12 @@ impl InstantiateBlueprint { request: AppliedInstantiateRequest, devfs_store: &mut DevfsRulesetStore, cred: &Credential, + /* network_manager: &mut NetworkManager, volume_manager: &VolumeManager, dataset_tracker: &mut JailedDatasetTracker, + */ + resources: &mut Resources, ) -> anyhow::Result { let existing_ifaces = freebsd::net::ifconfig::interfaces()?; let config = oci_config.jail_config(); @@ -312,9 +310,9 @@ impl InstantiateBlueprint { None => format!("xc-{id}"), Some(name) => { if name.parse::().is_ok() { - precondition_failure!(EINVAL, "Jail name cannot be numeric") + errx!(EINVAL, "Jail name cannot be numeric") } else if name.contains('.') { - precondition_failure!(EINVAL, "Jail name cannot contain dot (.)") + errx!(EINVAL, "Jail name cannot contain dot (.)") } else { name } @@ -326,15 +324,12 @@ impl InstantiateBlueprint { if config.linux { if !freebsd::exists_kld("linux64") { - precondition_failure!( + errx!( EIO, "Linux image require linux64 kmod but it is missing from the system" ); } else if xc::util::elf_abi_fallback_brand() != "3" { - precondition_failure!( - EIO, - "kern.elf64.fallback_brand did not set to 3 (Linux)" - ); + errx!(EIO, "kern.elf64.fallback_brand did not set to 3 (Linux)"); } } @@ -348,14 +343,10 @@ impl InstantiateBlueprint { let mut default_router = None; for req in request.base.ipreq.iter() { - match network_manager.allocate(vnet, req, id) { + match resources.allocate(vnet, req, id) { Ok((alloc, router)) => { if !existing_ifaces.contains(&alloc.interface) { - precondition_failure!( - ENOENT, - "missing network interface {}", - &alloc.interface - ); + errx!(ENOENT, "missing network interface {}", &alloc.interface); } if let Some(router) = router { if default_router.is_none() { @@ -365,28 +356,22 @@ impl InstantiateBlueprint { ip_alloc.push(alloc); } Err(error) => match error { - crate::network_manager::Error::Sqlite(error) => { + crate::resources::network::Error::Sqlite(error) => { Err(error).context("sqlite error on address allocation")?; } - crate::network_manager::Error::AllocationFailure(network) => { - precondition_failure!( - ENOENT, - "cannot allocate address from network {network}" - ) + crate::resources::network::Error::AllocationFailure(network) => { + errx!(ENOENT, "cannot allocate address from network {network}") } - crate::network_manager::Error::AddressUsed(addr) => { - precondition_failure!(ENOENT, "address {addr} already consumed") + crate::resources::network::Error::AddressUsed(addr) => { + errx!(ENOENT, "address {addr} already consumed") } - crate::network_manager::Error::InvalidAddress(addr, network) => { - precondition_failure!(EINVAL, "{addr} is not in the subnet of {network}") + crate::resources::network::Error::InvalidAddress(addr, network) => { + errx!(EINVAL, "{addr} is not in the subnet of {network}") } - crate::network_manager::Error::NoSuchNetwork(network) => { - precondition_failure!( - ENOENT, - "network {network} is missing from config file" - ) + crate::resources::network::Error::NoSuchNetwork(network) => { + errx!(ENOENT, "network {network} is missing from config file") } - crate::network_manager::Error::Other(error) => { + crate::resources::network::Error::Other(error) => { Err(error).context("error occured during address allocation")?; } }, @@ -411,16 +396,13 @@ impl InstantiateBlueprint { let volume = if !source_path.is_absolute() { let name = source_path.to_string_lossy().to_string(); - match volume_manager.query_volume(&name) { + match resources.query_volume(&name) { None => { - precondition_failure!(ENOENT, "no such volume {name}") + errx!(ENOENT, "no such volume {name}") } Some(volume) => { if !volume.can_mount(cred.uid()) { - precondition_failure!( - EPERM, - "this user is not allowed to mount the volume" - ) + errx!(EPERM, "this user is not allowed to mount the volume") } else { volume } @@ -428,18 +410,18 @@ impl InstantiateBlueprint { } } else { match &req.evid { - Maybe::None => precondition_failure!(ENOENT, "missing evidence"), + Maybe::None => errx!(ENOENT, "missing evidence"), Maybe::Some(fd) => { println!("process to check evidence"); let Ok(stat) = freebsd::nix::sys::stat::fstat(fd.as_raw_fd()) else { println!("cannot stat evidence"); - precondition_failure!(ENOENT, "cannot stat evidence") + errx!(ENOENT, "cannot stat evidence") }; let check_stat = freebsd::nix::sys::stat::stat(source_path).unwrap(); println!("c: {}", stat.st_ino); println!("n: {}", check_stat.st_ino); if stat.st_ino != check_stat.st_ino { - precondition_failure!(ENOENT, "evidence inode mismatch") + errx!(ENOENT, "evidence inode mismatch") } freebsd::nix::unistd::close(fd.as_raw_fd()); @@ -455,18 +437,18 @@ impl InstantiateBlueprint { added_mount_specs.insert(&req.dest, mount_spec.clone().unwrap()); } - let mount = volume_manager.mount(id, cred, req, mount_spec.as_ref(), &volume)?; + let mount = resources.mount(id, cred, req, mount_spec.as_ref(), &volume)?; mount_req.push(mount); } for dataset in request.base.jail_datasets.iter() { - if dataset_tracker.is_jailed(dataset) { - precondition_failure!( + if resources.dataset_tracker.is_jailed(dataset) { + errx!( EPERM, "another container is using this dataset: {dataset:?}" ) } else { - dataset_tracker.set_jailed(id, dataset) + resources.dataset_tracker.set_jailed(id, dataset) } } diff --git a/xcd/src/ipc.rs b/xcd/src/ipc.rs index 275d888..07e12cf 100644 --- a/xcd/src/ipc.rs +++ b/xcd/src/ipc.rs @@ -26,8 +26,8 @@ use crate::auth::Credential; use crate::context::ServerContext; use crate::image::pull::PullImageError; use crate::image::push::{PushImageError, PushImageStatusDesc}; -use crate::network::Network; -use crate::volume::{Volume, VolumeDriverKind}; +use crate::resources::network::Network; +use crate::resources::volume::{Volume, VolumeDriverKind}; use freebsd::event::EventFdNotify; use freebsd::libc::{EINVAL, EIO}; @@ -333,8 +333,7 @@ async fn instantiate( .await; if let Err(error) = instantiate_result { tracing::error!("instantiate error: {error:#?}"); - if let Some(err) = error.downcast_ref::() - { + if let Some(err) = error.downcast_ref::() { ipc_err(err.errno(), &err.error_message()) } else { enoent(error.to_string().as_str()) @@ -525,7 +524,7 @@ pub struct ListNetworkRequest {} #[derive(Serialize, Deserialize, Debug)] pub struct ListNetworkResponse { - pub network_info: Vec, + pub network_info: Vec, } #[ipc_method(method = "list_networks")] @@ -535,9 +534,8 @@ async fn list_networks( request: ListNetworkRequest, ) -> GenericResult { let context = context.read().await; - let network_manager = context.network_manager.clone(); - let network_manager = network_manager.lock().await; - match network_manager.get_network_info() { + let resources = context.resources.read().await; + match resources.get_network_info() { Ok(network_info) => Ok(ListNetworkResponse { network_info }), Err(e) => ipc_err(EIO, e.to_string().as_str()), } @@ -546,7 +544,6 @@ async fn list_networks( #[derive(Serialize, Deserialize, Debug)] pub struct CreateNetworkRequest { pub name: String, - // pub ext_if: Option, pub alias_iface: String, pub bridge_iface: String, pub subnet: ipcidr::IpCidr, @@ -562,13 +559,13 @@ async fn create_network( request: CreateNetworkRequest, ) -> GenericResult<()> { let context = context.write().await; - let config = context.inventory(); + let config = context.inventory().await; let existing_ifaces = freebsd::net::ifconfig::interfaces().unwrap(); if config.networks.contains_key(&request.name) { ipc_err(EINVAL, "Network with such name already exists") } else { - let nm = context.network_manager.clone(); - let nm = nm.lock().await; + let nm = context.resources.clone(); + let mut nm = nm.write().await; if !existing_ifaces.contains(&request.alias_iface) { return enoent(format!("interface {} not found", request.alias_iface).as_str()); @@ -722,6 +719,17 @@ async fn list_site_rdr( } } +#[ipc_method(method = "list_rdr_rules")] +async fn list_rdr_rules( + context: Arc>, + local_context: &mut ConnectionContext, + request: (), +) -> GenericResult> { + let context = context.read().await; + let rdr_rules = context.port_forward_table.all_rules(); + Ok(rdr_rules) +} + #[derive(FromPacket)] pub struct FdImport { pub fd: Fd, @@ -1239,6 +1247,7 @@ pub(crate) async fn register_to_service( service: &mut Service, Variables>, ) { service.register_event_delegate(on_channel_closed).await; + service.register(list_rdr_rules).await; service.register(create_volume).await; service.register(list_volumes).await; service.register(commit_netgroup).await; diff --git a/xcd/src/lib.rs b/xcd/src/lib.rs index aaa5ce1..129bab4 100644 --- a/xcd/src/lib.rs +++ b/xcd/src/lib.rs @@ -29,16 +29,13 @@ mod dataset; mod devfs_store; mod image; mod instantiate; -mod network; -mod network_manager; +pub mod ipc; mod port; mod registry; +pub mod resources; mod site; mod task; mod util; -pub mod volume; - -pub mod ipc; use crate::config::XcConfig; diff --git a/xcd/src/network_manager.rs b/xcd/src/network_manager.rs deleted file mode 100644 index fde47e1..0000000 --- a/xcd/src/network_manager.rs +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright (c) 2023 Yan Ka, Chiu. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions -// are met: -// 1. Redistributions of source code must retain the above copyright -// notice, this list of conditions, and the following disclaimer, -// without modification, immediately at the beginning of the file. -// 2. The name of the author may not be used to endorse or promote products -// derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND -// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR -// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -// SUCH DAMAGE. - -use anyhow::Context; -use ipcidr::IpCidr; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::net::IpAddr; -use std::sync::{Arc, Mutex}; -use thiserror::Error; -use xc::container::request::NetworkAllocRequest; -use xc::models::network::IpAssign; - -use crate::config::config_manager::InventoryManager; -use crate::database::Database; -use crate::network::{AddressStore, Network}; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct NetworkInfo { - pub name: String, - pub subnet: IpCidr, - pub start_addr: IpAddr, - pub end_addr: IpAddr, - pub last_addr: Option, - pub alias_iface: String, - pub bridge_iface: String, -} - -impl NetworkInfo { - fn new(db: &impl AddressStore, name: String, network: &Network) -> NetworkInfo { - let netpool = network.parameterize(&name, db); - NetworkInfo { - name, - subnet: netpool.network.subnet.clone(), - start_addr: netpool.start_addr, - end_addr: netpool.end_addr, - last_addr: netpool.last_addr, - alias_iface: network.alias_iface.clone(), - bridge_iface: network.bridge_iface.clone(), - } - } -} - -pub(crate) struct NetworkManager { - db: Arc, - inventory_manager: Arc>, - table_cache: HashMap>>, -} - -#[derive(Error, Debug)] -pub(crate) enum Error { - #[error("sqlite error: {0}")] - Sqlite(rusqlite::Error), - #[error("cannot allocate address from network {0}")] - AllocationFailure(String), - #[error("address {0} is being used")] - AddressUsed(std::net::IpAddr), - #[error("address {0} and network {1} in different subnet")] - InvalidAddress(IpAddr, String), - #[error("no such network {0}")] - NoSuchNetwork(String), - #[error("{0}")] - Other(anyhow::Error), -} - -impl From for Error { - fn from(error: rusqlite::Error) -> Error { - Error::Sqlite(error) - } -} - -impl From for Error { - fn from(error: anyhow::Error) -> Error { - Error::Other(error) - } -} - -impl NetworkManager { - pub(crate) fn new( - db: Arc, - inventory_manager: Arc>, - ) -> NetworkManager { - NetworkManager { - db, - inventory_manager, - table_cache: HashMap::new(), - } - } - - pub(crate) fn has_network(&self, name: &str) -> bool { - self.inventory_manager - .lock() - .unwrap() - .borrow() - .networks - .contains_key(name) - } - - pub(crate) fn get_network_info(&self) -> Result, anyhow::Error> { - let config = self.inventory_manager.lock().unwrap().borrow().clone(); - let mut info = Vec::new(); - for (name, network) in config.networks.iter() { - let db: &Database = &self.db; - info.push(NetworkInfo::new(db, name.to_string(), network)); - } - Ok(info) - } - - pub(crate) fn create_network( - &self, - name: &str, - network: &Network, - ) -> Result<(), rusqlite::Error> { - self.inventory_manager.lock().unwrap().modify(|inventory| { - inventory.networks.insert(name.to_string(), network.clone()); - }); - Ok(()) - } - - pub(crate) fn release_addresses( - &mut self, - token: &str, - ) -> anyhow::Result>> { - self.db - .release_addresses(token) - .context("fail to release addresses")?; - let networks = self.table_cache.remove(token).unwrap_or_default(); - Ok(networks) - } - - pub(crate) fn get_allocated_addresses( - &self, - token: &str, - ) -> Option<&HashMap>> { - self.table_cache.get(token) - } - - /// Request allociation using `req`, and return (an_assignment, default_router) - pub(crate) fn allocate( - &mut self, - vnet: bool, - req: &NetworkAllocRequest, - token: &str, - ) -> Result<(IpAssign, Option), Error> { - let network_name = req.network(); - let config = self.inventory_manager.lock().unwrap().borrow().clone(); - let network = config - .networks - .get(&network_name) - .ok_or_else(|| Error::NoSuchNetwork(network_name.to_string()))? - .clone(); - - let interface = if vnet { - network.bridge_iface.to_string() - } else { - network.alias_iface.to_string() - }; - - let db: &Database = &self.db; - - let mut netpool = network.parameterize(&network_name, db); - - match req { - NetworkAllocRequest::Any { .. } => { - let Some(address) = netpool.next_cidr(token)? else { - return Err(Error::AllocationFailure(network_name)); - }; - - self.insert_to_cache(token, &network_name, &address.addr()); - - Ok(( - IpAssign { - network: Some(network_name), - interface, - addresses: vec![address], - }, - network.default_router, - )) - } - NetworkAllocRequest::Explicit { ip, .. } => { - let address = IpCidr::from_addr(*ip, netpool.network.subnet.mask()).unwrap(); - if netpool.network.subnet.network_addr() == address.network_addr() { - if netpool.is_address_consumed(ip)? { - return Err(Error::AddressUsed(*ip)); - } - netpool.register_address(ip, token)?; - self.insert_to_cache(token, &network_name, ip); - - Ok(( - IpAssign { - network: Some(network_name), - interface, - addresses: vec![address], - }, - network.default_router, - )) - } else { - Err(Error::InvalidAddress(*ip, network_name)) - } - } - } - } - - #[inline] - fn insert_to_cache(&mut self, token: &str, network: &str, address: &IpAddr) { - if let Some(network_address) = self.table_cache.get_mut(token) { - if let Some(addresses) = network_address.get_mut(network) { - addresses.push(*address); - } else { - network_address.insert(network.to_string(), vec![*address]); - } - } else { - let mut hmap = HashMap::new(); - hmap.insert(network.to_string(), vec![*address]); - self.table_cache.insert(token.to_string(), hmap); - } - } -} diff --git a/xcd/src/resources/mod.rs b/xcd/src/resources/mod.rs new file mode 100644 index 0000000..65a201f --- /dev/null +++ b/xcd/src/resources/mod.rs @@ -0,0 +1,65 @@ +// Copyright (c) 2023 Yan Ka, Chiu. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions, and the following disclaimer, +// without modification, immediately at the beginning of the file. +// 2. The name of the author may not be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR +// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +// SUCH DAMAGE. +use crate::config::inventory::InventoryManager; +use crate::config::XcConfig; +use crate::database::Database; +use crate::dataset::JailedDatasetTracker; +use crate::resources::volume::VolumeShareMode; +use std::collections::HashMap; +use std::net::IpAddr; +use std::path::PathBuf; +use std::sync::Arc; + +pub(crate) mod network; +pub mod volume; + +pub(crate) struct Resources { + pub(crate) db: Arc, + pub(crate) inventory_manager: InventoryManager, + // network: { container: [address] } + pub(crate) net_addr_alloc_cache: HashMap>>, + + pub(crate) default_volume_dataset: Option, + pub(crate) default_volume_dir: Option, + + pub(crate) constrained_shares: HashMap, + + pub(crate) dataset_tracker: JailedDatasetTracker, +} + +impl Resources { + pub(crate) fn new(database: Arc, config: &XcConfig) -> Resources { + let inventory_manager = + InventoryManager::load_from_path(&config.inventory).expect("cannot read inventory"); + Resources { + db: database, + inventory_manager, + default_volume_dataset: config.default_volume_dataset.clone(), + default_volume_dir: None, + net_addr_alloc_cache: HashMap::new(), + constrained_shares: HashMap::new(), + dataset_tracker: JailedDatasetTracker::default(), + } + } +} diff --git a/xcd/src/network/mod.rs b/xcd/src/resources/network.rs similarity index 54% rename from xcd/src/network/mod.rs rename to xcd/src/resources/network.rs index 62b5518..37e9ab4 100644 --- a/xcd/src/network/mod.rs +++ b/xcd/src/resources/network.rs @@ -22,9 +22,45 @@ // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF // SUCH DAMAGE. +use anyhow::Context; use ipcidr::IpCidr; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::net::IpAddr; +use thiserror::Error; +use xc::container::request::NetworkAllocRequest; +use xc::models::network::IpAssign; + +use crate::database::Database; +use crate::resources::Resources; + +#[derive(Error, Debug)] +pub(crate) enum Error { + #[error("sqlite error: {0}")] + Sqlite(rusqlite::Error), + #[error("cannot allocate address from network {0}")] + AllocationFailure(String), + #[error("address {0} is being used")] + AddressUsed(std::net::IpAddr), + #[error("address {0} and network {1} in different subnet")] + InvalidAddress(IpAddr, String), + #[error("no such network {0}")] + NoSuchNetwork(String), + #[error("{0}")] + Other(anyhow::Error), +} + +impl From for Error { + fn from(error: rusqlite::Error) -> Error { + Error::Sqlite(error) + } +} + +impl From for Error { + fn from(error: anyhow::Error) -> Error { + Error::Other(error) + } +} #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Network { @@ -145,6 +181,158 @@ impl<'a, 'b, A: AddressStore> Netpool<'a, 'b, A> { } } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NetworkInfo { + pub name: String, + pub subnet: IpCidr, + pub start_addr: IpAddr, + pub end_addr: IpAddr, + pub last_addr: Option, + pub alias_iface: String, + pub bridge_iface: String, +} + +impl NetworkInfo { + fn new(db: &impl AddressStore, name: String, network: &Network) -> NetworkInfo { + let netpool = network.parameterize(&name, db); + NetworkInfo { + name, + subnet: netpool.network.subnet.clone(), + start_addr: netpool.start_addr, + end_addr: netpool.end_addr, + last_addr: netpool.last_addr, + alias_iface: network.alias_iface.clone(), + bridge_iface: network.bridge_iface.clone(), + } + } +} + +impl Resources { + pub(crate) fn has_network(&self, name: &str) -> bool { + self.inventory_manager.borrow().networks.contains_key(name) + } + + pub(crate) fn get_network_info(&self) -> Result, anyhow::Error> { + let config = self.inventory_manager.borrow().clone(); + let mut info = Vec::new(); + for (name, network) in config.networks.iter() { + let db: &Database = &self.db; + info.push(NetworkInfo::new(db, name.to_string(), network)); + } + Ok(info) + } + + pub(crate) fn create_network( + &mut self, + name: &str, + network: &Network, + ) -> Result<(), rusqlite::Error> { + self.inventory_manager.modify(|inventory| { + inventory.networks.insert(name.to_string(), network.clone()); + }); + Ok(()) + } + + pub(crate) fn release_addresses( + &mut self, + token: &str, + ) -> anyhow::Result>> { + self.db + .release_addresses(token) + .context("fail to release addresses")?; + let networks = self.net_addr_alloc_cache.remove(token).unwrap_or_default(); + Ok(networks) + } + + pub(crate) fn get_allocated_addresses( + &self, + token: &str, + ) -> Option<&HashMap>> { + self.net_addr_alloc_cache.get(token) + } + + /// Request allociation using `req`, and return (an_assignment, default_router) + pub(crate) fn allocate( + &mut self, + vnet: bool, + req: &NetworkAllocRequest, + token: &str, + ) -> Result<(IpAssign, Option), Error> { + let network_name = req.network(); + let config = self.inventory_manager.borrow().clone(); + let network = config + .networks + .get(&network_name) + .ok_or_else(|| Error::NoSuchNetwork(network_name.to_string()))? + .clone(); + + let interface = if vnet { + network.bridge_iface.to_string() + } else { + network.alias_iface.to_string() + }; + + let db: &Database = &self.db; + + let mut netpool = network.parameterize(&network_name, db); + + match req { + NetworkAllocRequest::Any { .. } => { + let Some(address) = netpool.next_cidr(token)? else { + return Err(Error::AllocationFailure(network_name)); + }; + + self.insert_to_cache(token, &network_name, &address.addr()); + + Ok(( + IpAssign { + network: Some(network_name), + interface, + addresses: vec![address], + }, + network.default_router, + )) + } + NetworkAllocRequest::Explicit { ip, .. } => { + let address = IpCidr::from_addr(*ip, netpool.network.subnet.mask()).unwrap(); + if netpool.network.subnet.network_addr() == address.network_addr() { + if netpool.is_address_consumed(ip)? { + return Err(Error::AddressUsed(*ip)); + } + netpool.register_address(ip, token)?; + self.insert_to_cache(token, &network_name, ip); + + Ok(( + IpAssign { + network: Some(network_name), + interface, + addresses: vec![address], + }, + network.default_router, + )) + } else { + Err(Error::InvalidAddress(*ip, network_name)) + } + } + } + } + + #[inline] + fn insert_to_cache(&mut self, token: &str, network: &str, address: &IpAddr) { + if let Some(network_address) = self.net_addr_alloc_cache.get_mut(token) { + if let Some(addresses) = network_address.get_mut(network) { + addresses.push(*address); + } else { + network_address.insert(network.to_string(), vec![*address]); + } + } else { + let mut hmap = HashMap::new(); + hmap.insert(network.to_string(), vec![*address]); + self.net_addr_alloc_cache.insert(token.to_string(), hmap); + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/xcd/src/volume/drivers/local.rs b/xcd/src/resources/volume/drivers/local.rs similarity index 80% rename from xcd/src/volume/drivers/local.rs rename to xcd/src/resources/volume/drivers/local.rs index 6b7c6bc..33cc8cb 100644 --- a/xcd/src/volume/drivers/local.rs +++ b/xcd/src/resources/volume/drivers/local.rs @@ -27,13 +27,13 @@ use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use xc::models::MountSpec; use xc::{ - container::{error::PreconditionFailure, request::Mount}, - precondition_failure, + container::{error::Error, request::Mount}, + errx, }; +use crate::auth::Credential; use crate::ipc::MountReq; -use crate::volume::VolumeDriverKind; -use crate::{auth::Credential, volume::Volume}; +use crate::resources::volume::{Volume, VolumeDriverKind}; use super::VolumeDriver; @@ -48,23 +48,23 @@ impl VolumeDriver for LocalDriver { _template: Option, source: Option, _props: HashMap, - ) -> Result { + ) -> Result { let path = match source { None => { let Some(mut parent) = self.default_subdir.clone() else { - precondition_failure!(ENOENT, "Default volume directory not found") + errx!(ENOENT, "Default volume directory not found") }; parent.push(name); if parent.exists() { - precondition_failure!(ENOENT, "Target directory already exists") + errx!(ENOENT, "Target directory already exists") } parent } Some(path) => { if !path.exists() { - precondition_failure!(ENOENT, "No such directory") + errx!(ENOENT, "No such directory") } else if !path.is_dir() { - precondition_failure!(ENOTDIR, "Source path is not a directory") + errx!(ENOTDIR, "Source path is not a directory") } path } @@ -86,23 +86,23 @@ impl VolumeDriver for LocalDriver { mount_req: &MountReq, mount_spec: Option<&MountSpec>, volume: &Volume, - ) -> Result { + ) -> Result { let source_path = &volume.device; if !&source_path.exists() { - precondition_failure!(ENOENT, "source mount point does not exist: {source_path:?}"); + errx!(ENOENT, "source mount point does not exist: {source_path:?}"); } if !&source_path.is_dir() && !source_path.is_file() { - precondition_failure!( + errx!( ENOTDIR, "mount point source is not a file nor directory: {source_path:?}" ) } let Ok(meta) = std::fs::metadata(&volume.device) else { - precondition_failure!(ENOENT, "invalid nullfs mount source") + errx!(ENOENT, "invalid nullfs mount source") }; if !cred.can_mount(&meta, false) { - precondition_failure!(EPERM, "permission denied: {source_path:?}") + errx!(EPERM, "permission denied: {source_path:?}") } let mut mount_options = HashSet::new(); diff --git a/xcd/src/volume/drivers/mod.rs b/xcd/src/resources/volume/drivers/mod.rs similarity index 92% rename from xcd/src/volume/drivers/mod.rs rename to xcd/src/resources/volume/drivers/mod.rs index 6226916..2ce0b84 100644 --- a/xcd/src/volume/drivers/mod.rs +++ b/xcd/src/resources/volume/drivers/mod.rs @@ -28,7 +28,7 @@ use super::Volume; use crate::auth::Credential; use crate::ipc::MountReq; use std::collections::HashMap; -use xc::container::{error::PreconditionFailure, request::Mount}; +use xc::container::{error::Error, request::Mount}; use xc::models::MountSpec; pub trait VolumeDriver { @@ -38,7 +38,7 @@ pub trait VolumeDriver { mount_req: &MountReq, mount_spec: Option<&MountSpec>, volume: &Volume, - ) -> Result; + ) -> Result; fn create( &self, @@ -46,5 +46,5 @@ pub trait VolumeDriver { template: Option, source: Option, props: HashMap, - ) -> Result; + ) -> Result; } diff --git a/xcd/src/volume/drivers/zfs.rs b/xcd/src/resources/volume/drivers/zfs.rs similarity index 83% rename from xcd/src/volume/drivers/zfs.rs rename to xcd/src/resources/volume/drivers/zfs.rs index 70ea146..e2787e8 100644 --- a/xcd/src/volume/drivers/zfs.rs +++ b/xcd/src/resources/volume/drivers/zfs.rs @@ -22,17 +22,17 @@ // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF // SUCH DAMAGE. +use crate::auth::Credential; use crate::ipc::MountReq; -use crate::volume::VolumeDriverKind; -use crate::{auth::Credential, volume::Volume}; +use crate::resources::volume::{Volume, VolumeDriverKind}; use freebsd::fs::zfs::{ZfsCreate, ZfsHandle}; use freebsd::libc::{EEXIST, EIO, ENOENT, EPERM}; use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use xc::models::MountSpec; use xc::{ - container::{error::PreconditionFailure, request::Mount}, - precondition_failure, + container::{error::Error, request::Mount}, + errx, }; use super::VolumeDriver; @@ -50,7 +50,7 @@ impl VolumeDriver for ZfsDriver { template: Option, source: Option, props: HashMap, - ) -> Result { + ) -> Result { let mut zfs_props = props; if let Some(template) = template { @@ -66,26 +66,26 @@ impl VolumeDriver for ZfsDriver { let dataset = match source { None => { let Some(mut dataset) = self.default_dataset.clone() else { - precondition_failure!(ENOENT, "Default volume dataset not set") + errx!(ENOENT, "Default volume dataset not set") }; dataset.push(name); if self.handle.exists(&dataset) { - precondition_failure!(EEXIST, "Dataset already exist") + errx!(EEXIST, "Dataset already exist") } let mut zfs_create = ZfsCreate::new(&dataset, true, false); zfs_create.set_props(zfs_props.clone()); if let Err(error) = self.handle.create(zfs_create) { - precondition_failure!(EIO, "Cannot create zfs dataset: {error:?}") + errx!(EIO, "Cannot create zfs dataset: {error:?}") } dataset } Some(dataset) => { if !self.handle.exists(&dataset) { - precondition_failure!(ENOENT, "Requested dataset does not exist") + errx!(ENOENT, "Requested dataset does not exist") } dataset } @@ -108,22 +108,22 @@ impl VolumeDriver for ZfsDriver { mount_req: &MountReq, mount_spec: Option<&MountSpec>, volume: &Volume, - ) -> Result { + ) -> Result { if !self.handle.exists(&volume.device) { - precondition_failure!(ENOENT, "No such dataset: {:?}", volume.device); + errx!(ENOENT, "No such dataset: {:?}", volume.device); } let Ok(Some(mount_point)) = self.handle.mount_point(&volume.device) else { - precondition_failure!( + errx!( ENOENT, "Dataset {:?} does not have a mount point", volume.device ); }; let Ok(meta) = std::fs::metadata(&mount_point) else { - precondition_failure!(EIO, "cannot get metadata of {mount_point:?}"); + errx!(EIO, "cannot get metadata of {mount_point:?}"); }; if !cred.can_mount(&meta, false) { - precondition_failure!(EPERM, "permission denied: {mount_point:?}"); + errx!(EPERM, "permission denied: {mount_point:?}"); } let mut mount_options = HashSet::new(); diff --git a/xcd/src/volume/mod.rs b/xcd/src/resources/volume/mod.rs similarity index 87% rename from xcd/src/volume/mod.rs rename to xcd/src/resources/volume/mod.rs index 11db86a..59e1e54 100644 --- a/xcd/src/volume/mod.rs +++ b/xcd/src/resources/volume/mod.rs @@ -24,22 +24,20 @@ pub mod drivers; use crate::auth::Credential; -use crate::config::config_manager::InventoryManager; use crate::ipc::MountReq; -use crate::volume::drivers::local::LocalDriver; -use crate::volume::drivers::zfs::ZfsDriver; -use crate::volume::drivers::VolumeDriver; +use crate::resources::volume::drivers::local::LocalDriver; +use crate::resources::volume::drivers::zfs::ZfsDriver; +use crate::resources::volume::drivers::VolumeDriver; use freebsd::libc::EPERM; use serde::de::Deserializer; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; -use xc::container::error::PreconditionFailure; +use xc::container::error::Error; use xc::container::request::Mount; +use xc::errx; use xc::models::MountSpec; -use xc::precondition_failure; #[derive(Default, PartialEq, Eq, Debug, Clone)] pub enum VolumeDriverKind { @@ -188,37 +186,17 @@ pub enum VolumeShareMode { SingleWriter, } -pub(crate) struct VolumeManager { - inventory: Arc>, - default_volume_dataset: Option, - default_volume_dir: Option, - constrained_shares: HashMap, -} - -impl VolumeManager { - pub(crate) fn new( - inventory: Arc>, - default_volume_dataset: Option, - default_volume_dir: Option, - ) -> VolumeManager { - VolumeManager { - inventory, - default_volume_dataset, - default_volume_dir, - constrained_shares: HashMap::new(), - } - } - +impl crate::resources::Resources { // insert or override a volume pub(crate) fn add_volume(&mut self, name: &str, volume: &Volume) { - self.inventory.lock().unwrap().modify(|inventory| { + self.inventory_manager.modify(|inventory| { inventory.volumes.insert(name.to_string(), volume.clone()); }); } pub(crate) fn list_volumes(&self) -> HashMap { let mut hm = HashMap::new(); - for (name, volume) in self.inventory.lock().unwrap().borrow().volumes.iter() { + for (name, volume) in self.inventory_manager.borrow().volumes.iter() { let mut vol = volume.clone(); vol.name = Some(name.to_string()); hm.insert(name.to_string(), vol); @@ -227,9 +205,7 @@ impl VolumeManager { } pub(crate) fn query_volume(&self, name: &str) -> Option { - self.inventory - .lock() - .unwrap() + self.inventory_manager .borrow() .volumes .get(name) @@ -247,7 +223,7 @@ impl VolumeManager { template: Option, source: Option, props: HashMap, - ) -> Result<(), PreconditionFailure> { + ) -> Result<(), Error> { let volume = match kind { VolumeDriverKind::Directory => { let local_driver = LocalDriver { @@ -276,16 +252,16 @@ impl VolumeManager { mount_req: &MountReq, mount_spec: Option<&MountSpec>, volume: &Volume, - ) -> Result { + ) -> Result { for (name, share) in self.constrained_shares.iter() { if volume.name.as_ref().unwrap() == name { if share == &VolumeShareMode::Exclusive { - precondition_failure!( + errx!( EPERM, "The volume has been mounted exclusively by other container" ) } else if share == &VolumeShareMode::SingleWriter && !mount_req.read_only { - precondition_failure!( + errx!( EPERM, "The volume has been mounted for exclusively write by other container" )