diff --git a/api-test/Cargo.toml b/api-test/Cargo.toml
index a328638c..67a5e4fa 100644
--- a/api-test/Cargo.toml
+++ b/api-test/Cargo.toml
@@ -13,6 +13,10 @@ endian_trait = { version = "0.6", features = [ "arrays" ] }
failure = "0.1"
http = "0.1"
hyper = { version = "0.13.0-a.0", git = "https://github.com/hyperium/hyper" }
+lazy_static = "1.3"
proxmox = { path = "../proxmox" }
+regex = "1.1"
+serde = "1.0"
serde_json = "1.0"
+serde_plain = "0.3"
tokio = { version = "0.2", git = "https://github.com/tokio-rs/tokio" }
diff --git a/api-test/src/api.rs b/api-test/src/bin/api-test.rs
similarity index 51%
rename from api-test/src/api.rs
rename to api-test/src/bin/api-test.rs
index 84b84822..522c4d21 100644
--- a/api-test/src/api.rs
+++ b/api-test/src/bin/api-test.rs
@@ -1,13 +1,79 @@
+#![feature(async_await)]
+
use std::io;
use std::path::Path;
-use failure::{bail, Error};
+use failure::{bail, format_err, Error};
+use http::Request;
use http::Response;
-use hyper::Body;
+use hyper::service::{make_service_fn, service_fn};
+use hyper::{Body, Server};
+use serde_json::Value;
use tokio::io::AsyncReadExt;
use proxmox::api::{api, router};
+async fn run_request(request: Request
) -> Result, hyper::Error> {
+ match route_request(request).await {
+ Ok(r) => Ok(r),
+ Err(err) => Ok(Response::builder()
+ .status(400)
+ .body(Body::from(format!("ERROR: {}", err)))
+ .expect("building an error response...")),
+ }
+}
+
+async fn route_request(request: Request) -> Result, Error> {
+ let path = request.uri().path();
+
+ let (target, params) = ROUTER
+ .lookup(path)
+ .ok_or_else(|| format_err!("missing path: {}", path))?;
+
+ target
+ .get
+ .as_ref()
+ .ok_or_else(|| format_err!("no GET method for: {}", path))?
+ .call(params.unwrap_or(Value::Null))
+ .await
+}
+
+async fn main_do(www_dir: String) {
+ // Construct our SocketAddr to listen on...
+ let addr = ([0, 0, 0, 0], 3000).into();
+
+ // And a MakeService to handle each connection...
+ let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(run_request)) });
+
+ // Then bind and serve...
+ let server = Server::bind(&addr).serve(service);
+
+ println!("Serving {} under http://localhost:3000/www/", www_dir);
+
+ if let Err(e) = server.await {
+ eprintln!("server error: {}", e);
+ }
+}
+
+fn main() {
+ // We expect a path, where to find our files we expose via the www/ dir:
+ let mut args = std::env::args();
+
+ // real code should have better error handling
+ let _program_name = args.next();
+ let www_dir = args.next().expect("expected a www/ subdirectory");
+ set_www_dir(www_dir.to_string());
+
+ // show our api info:
+ println!(
+ "{}",
+ serde_json::to_string_pretty(&ROUTER.api_dump()).unwrap()
+ );
+
+ let rt = tokio::runtime::Runtime::new().unwrap();
+ rt.block_on(main_do(www_dir));
+}
+
#[api({
description: "Hello API call",
})]
diff --git a/api-test/src/lib.rs b/api-test/src/lib.rs
new file mode 100644
index 00000000..35d66fbd
--- /dev/null
+++ b/api-test/src/lib.rs
@@ -0,0 +1,4 @@
+//! PVE base library
+
+pub mod lxc;
+pub mod schema;
diff --git a/api-test/src/lxc/mod.rs b/api-test/src/lxc/mod.rs
new file mode 100644
index 00000000..cb4e2ace
--- /dev/null
+++ b/api-test/src/lxc/mod.rs
@@ -0,0 +1,226 @@
+//! PVE LXC module
+
+use std::collections::HashSet;
+
+use proxmox::api::api;
+
+use crate::schema::{
+ types::{Memory, VolumeId},
+ Architecture,
+};
+
+pub mod schema;
+
+#[api({
+ description: "The PVE side of an lxc container configuration.",
+ cli: false,
+ fields: {
+ lock: "The current long-term lock held on this container by another operation.",
+ onboot: {
+ description: "Specifies whether a VM will be started during system bootup.",
+ default: false,
+ },
+ startup: "The container's startup order.",
+ template: {
+ description: "Whether this is a template.",
+ default: false,
+ },
+ arch: {
+ description: "The container's architecture type.",
+ default: Architecture::Amd64,
+ },
+ ostype: {
+ description:
+ "OS type. This is used to setup configuration inside the container, \
+ and corresponds to lxc setup scripts in \
+ /usr/share/lxc/config/.common.conf. \
+ Value 'unmanaged' can be used to skip and OS specific setup.",
+ },
+ console: {
+ description: "Attach a console device (/dev/console) to the container.",
+ default: true,
+ },
+ tty: {
+ description: "Number of ttys available to the container",
+ minimum: 0,
+ maximum: 6,
+ default: 2,
+ },
+ cores: {
+ description:
+ "The number of cores assigned to the container. \
+ A container can use all available cores by default.",
+ minimum: 1,
+ maximum: 128,
+ },
+ cpulimit: {
+ description:
+ "Limit of CPU usage.\
+ \n\n\
+ NOTE: If the computer has 2 CPUs, it has a total of '2' CPU time. \
+ Value '0' indicates no CPU limit.",
+ minimum: 0,
+ maximum: 128,
+ default: 0,
+ },
+ cpuunits: {
+ description:
+ "CPU weight for a VM. Argument is used in the kernel fair scheduler. \
+ The larger the number is, the more CPU time this VM gets. \
+ Number is relative to the weights of all the other running VMs.\
+ \n\n\
+ NOTE: You can disable fair-scheduler configuration by setting this to 0.",
+ minimum: 0,
+ maximum: 500000,
+ default: 1024,
+ },
+ memory: {
+ description: "Amount of RAM for the VM.",
+ minimum: Memory::from_mebibytes(16),
+ default: Memory::from_mebibytes(512),
+ serialization: crate::schema::memory::optional::Parser::
+ },
+ swap: {
+ description: "Amount of SWAP for the VM.",
+ minimum: Memory::from_bytes(0),
+ default: Memory::from_mebibytes(512),
+ },
+ hostname: {
+ description: "Set a host name for the container.",
+ maximum_length: 255,
+ minimum_length: 3,
+ format: crate::schema::dns_name,
+ },
+ description: "Container description. Only used on the configuration web interface.",
+ searchdomain: {
+ description:
+ "Sets DNS search domains for a container. Create will automatically use the \
+ setting from the host if you neither set searchdomain nor nameserver.",
+ format: crate::schema::dns_name,
+ serialization: crate::schema::string_list::optional,
+ },
+ nameserver: {
+ description:
+ "Sets DNS server IP address for a container. Create will automatically use the \
+ setting from the host if you neither set searchdomain nor nameserver.",
+ format: crate::schema::ip_address,
+ serialization: crate::schema::string_list::optional,
+ },
+ rootfs: "Container root volume",
+ cmode: {
+ description:
+ "Console mode. By default, the console command tries to open a connection to one \
+ of the available tty devices. By setting cmode to 'console' it tries to attach \
+ to /dev/console instead. \
+ If you set cmode to 'shell', it simply invokes a shell inside the container \
+ (no login).",
+ default: schema::ConsoleMode::Tty,
+ },
+ protection: {
+ description:
+ "Sets the protection flag of the container. \
+ This will prevent the CT or CT's disk remove/update operation.",
+ default: false,
+ },
+ unprivileged: {
+ description:
+ "Makes the container run as unprivileged user. (Should not be modified manually.)",
+ default: false,
+ },
+ hookscript: {
+ description:
+ "Script that will be exectued during various steps in the containers lifetime.",
+ },
+ },
+})]
+#[derive(Default)]
+pub struct Config {
+ // FIXME: short form? Since all the type info is literally factored out into the ConfigLock
+ // type already...
+ //#[api("The current long-term lock held on this container by another operation.")]
+ pub lock: Option,
+ pub onboot: Option,
+ pub startup: Option,
+ pub template: Option,
+ pub arch: Option,
+ pub ostype: Option,
+ pub console: Option,
+ pub tty: Option,
+ pub cores: Option,
+ pub cpulimit: Option,
+ pub cpuunits: Option,
+ pub memory: Option,
+ pub swap: Option,
+ pub hostname: Option,
+ pub description: Option,
+ pub searchdomain: Option>,
+ pub nameserver: Option>,
+ pub rootfs: Option,
+ // pub parent: Option,
+ // pub snaptime: Option,
+ pub cmode: Option,
+ pub protection: Option,
+ pub unprivileged: Option,
+ // pub features: Option,
+ pub hookscript: Option,
+}
+
+#[api({
+ description: "Container's rootfs definition",
+ cli: false,
+ fields: {
+ volume: {
+ description: "Volume, device or directory to mount into the container.",
+ format: crate::schema::safe_path,
+ // format_description: 'volume',
+ // default_key: 1,
+ },
+ size: {
+ description: "Volume size (read only value).",
+ // format_description: 'DiskSize',
+ },
+ acl: {
+ description: "Explicitly enable or disable ACL support.",
+ default: false,
+ },
+ ro: {
+ description: "Read-only mount point.",
+ default: false,
+ },
+ mountoptions: {
+ description: "Extra mount options for rootfs/mps.",
+ //format_description: "opt[;opt...]",
+ format: schema::mount_options,
+ serialization: crate::schema::string_set::optional,
+ },
+ quota: {
+ description:
+ "Enable user quotas inside the container (not supported with zfs subvolumes)",
+ default: false,
+ },
+ replicate: {
+ description: "Will include this volume to a storage replica job.",
+ default: true,
+ },
+ shared: {
+ description:
+ "Mark this non-volume mount point as available on multiple nodes (see 'nodes')",
+ //verbose_description:
+ // "Mark this non-volume mount point as available on all nodes.\n\
+ // \n\
+ // WARNING: This option does not share the mount point automatically, it assumes \
+ // it is shared already!",
+ default: false,
+ },
+ },
+})]
+pub struct Rootfs {
+ pub volume: String,
+ pub size: Option,
+ pub acl: Option,
+ pub ro: Option,
+ pub mountoptions: Option>,
+ pub quota: Option,
+ pub replicate: Option,
+ pub shared: Option,
+}
diff --git a/api-test/src/lxc/schema/mod.rs b/api-test/src/lxc/schema/mod.rs
new file mode 100644
index 00000000..c5cf9e66
--- /dev/null
+++ b/api-test/src/lxc/schema/mod.rs
@@ -0,0 +1,55 @@
+//! PVE LXC related schema module.
+
+use proxmox::api::api;
+
+#[api({
+ description: "A long-term lock on a container",
+})]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum ConfigLock {
+ Backup,
+ Create,
+ Disk,
+ Fstrim,
+ Migrate,
+ Mounted,
+ Rollback,
+ Snapshot,
+ #[api(rename = "snapshot-delete")]
+ SnapshotDelete,
+}
+
+#[api({
+ description: "Operating System Type.",
+})]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum OsType {
+ Unmanaged,
+ Debian,
+ //...
+}
+
+#[api({
+ description: "Console mode.",
+})]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum ConsoleMode {
+ Tty,
+ Console,
+ Shell,
+}
+
+pub mod mount_options {
+ pub const NAME: &'static str = "mount options";
+
+ const VALID_MOUNT_OPTIONS: &[&'static str] = &[
+ "noatime",
+ "nodev",
+ "noexec",
+ "nosuid",
+ ];
+
+ pub fn verify(value: &T) -> bool {
+ value.all(|s| VALID_MOUNT_OPTIONS.contains(&s))
+ }
+}
diff --git a/api-test/src/main.rs b/api-test/src/main.rs
deleted file mode 100644
index 4d8a7d35..00000000
--- a/api-test/src/main.rs
+++ /dev/null
@@ -1,71 +0,0 @@
-#![feature(async_await)]
-
-use failure::{format_err, Error};
-use http::Request;
-use http::Response;
-use hyper::service::{make_service_fn, service_fn};
-use hyper::{Body, Server};
-use serde_json::Value;
-
-mod api;
-
-async fn run_request(request: Request) -> Result, hyper::Error> {
- match route_request(request).await {
- Ok(r) => Ok(r),
- Err(err) => Ok(Response::builder()
- .status(400)
- .body(Body::from(format!("ERROR: {}", err)))
- .expect("building an error response...")),
- }
-}
-
-async fn route_request(request: Request) -> Result, Error> {
- let path = request.uri().path();
-
- let (target, params) = api::ROUTER
- .lookup(path)
- .ok_or_else(|| format_err!("missing path: {}", path))?;
-
- target
- .get
- .as_ref()
- .ok_or_else(|| format_err!("no GET method for: {}", path))?
- .call(params.unwrap_or(Value::Null))
- .await
-}
-
-async fn main_do(www_dir: String) {
- // Construct our SocketAddr to listen on...
- let addr = ([0, 0, 0, 0], 3000).into();
-
- // And a MakeService to handle each connection...
- let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(run_request)) });
-
- // Then bind and serve...
- let server = Server::bind(&addr).serve(service);
-
- println!("Serving {} under http://localhost:3000/www/", www_dir);
-
- if let Err(e) = server.await {
- eprintln!("server error: {}", e);
- }
-}
-
-fn main() {
- // We expect a path, where to find our files we expose via the www/ dir:
- let mut args = std::env::args();
-
- // real code should have better error handling
- let _program_name = args.next();
- let www_dir = args.next().expect("expected a www/ subdirectory");
- api::set_www_dir(www_dir.to_string());
-
- // show our api info:
- println!(
- "{}",
- serde_json::to_string_pretty(&api::ROUTER.api_dump()).unwrap()
- );
-
- let rt = tokio::runtime::Runtime::new().unwrap();
- rt.block_on(main_do(www_dir));
-}
diff --git a/api-test/src/schema/memory.rs b/api-test/src/schema/memory.rs
new file mode 100644
index 00000000..a214cb13
--- /dev/null
+++ b/api-test/src/schema/memory.rs
@@ -0,0 +1,110 @@
+//! Serialization/deserialization for memory values with specific units.
+
+use std::marker::PhantomData;
+
+use super::types::Memory;
+
+pub trait Unit {
+ const FACTOR: u64;
+ const NAME: &'static str;
+}
+
+pub struct B;
+impl Unit for B {
+ const FACTOR: u64 = 1;
+ const NAME: &'static str = "bytes";
+}
+
+pub struct Kb;
+impl Unit for Kb {
+ const FACTOR: u64 = 1024;
+ const NAME: &'static str = "kilobytes";
+}
+
+pub struct Mb;
+impl Unit for Mb {
+ const FACTOR: u64 = 1024 * 1024;
+ const NAME: &'static str = "megabytes";
+}
+
+pub struct Gb;
+impl Unit for Gb {
+ const FACTOR: u64 = 1024 * 1024 * 1024;
+ const NAME: &'static str = "gigabytes";
+}
+
+struct MemoryVisitor(PhantomData);
+impl<'de, U: Unit> serde::de::Visitor<'de> for MemoryVisitor {
+ type Value = Memory;
+
+ fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "amount of memory in {}", U::NAME)
+ }
+
+ fn visit_u8(self, v: u8) -> Result {
+ Ok(Memory::from_bytes(v as u64 * U::FACTOR))
+ }
+ fn visit_u16(self, v: u16) -> Result {
+ Ok(Memory::from_bytes(v as u64 * U::FACTOR))
+ }
+ fn visit_u32(self, v: u32) -> Result {
+ Ok(Memory::from_bytes(v as u64 * U::FACTOR))
+ }
+ fn visit_u64(self, v: u64) -> Result {
+ Ok(Memory::from_bytes(v as u64 * U::FACTOR))
+ }
+
+ fn visit_str(self, v: &str) -> Result {
+ match v.parse::() {
+ Ok(v) => Ok(Memory::from_bytes(v * U::FACTOR)),
+ Err(_) => v.parse().map_err(serde::de::Error::custom),
+ }
+ }
+}
+
+pub struct Parser(PhantomData);
+
+impl Parser {
+ pub fn serialize(value: &Memory, ser: S) -> Result
+ where
+ S: serde::Serializer,
+ {
+ if (value.as_bytes() % U::FACTOR) == 0 {
+ ser.serialize_u64(value.as_bytes() / U::FACTOR)
+ } else {
+ ser.serialize_str(&value.to_string())
+ }
+ }
+
+ pub fn deserialize<'de, D>(de: D) -> Result
+ where
+ D: serde::de::Deserializer<'de>,
+ {
+ de.deserialize_any(MemoryVisitor::(PhantomData))
+ }
+}
+
+pub mod optional {
+ use std::marker::PhantomData;
+
+ use super::Unit;
+ use crate::schema::types::Memory;
+
+ pub struct Parser(PhantomData);
+
+ impl Parser {
+ pub fn serialize(value: &Option, ser: S) -> Result
+ where
+ S: serde::Serializer,
+ {
+ super::Parser::::serialize::(&value.unwrap(), ser)
+ }
+
+ pub fn deserialize<'de, D>(de: D) -> Result