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