mirror of
https://git.proxmox.com/git/proxmox
synced 2025-07-09 13:57:39 +00:00
api-test: import struct tests
This is a bigger set of tests for the type-side (mostly for `struct`s) of the #[api] macro, tasting serialization and verifiers in various forms. Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
75e90ebb25
commit
e985dc8f84
@ -13,6 +13,10 @@ endian_trait = { version = "0.6", features = [ "arrays" ] }
|
|||||||
failure = "0.1"
|
failure = "0.1"
|
||||||
http = "0.1"
|
http = "0.1"
|
||||||
hyper = { version = "0.13.0-a.0", git = "https://github.com/hyperium/hyper" }
|
hyper = { version = "0.13.0-a.0", git = "https://github.com/hyperium/hyper" }
|
||||||
|
lazy_static = "1.3"
|
||||||
proxmox = { path = "../proxmox" }
|
proxmox = { path = "../proxmox" }
|
||||||
|
regex = "1.1"
|
||||||
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
serde_plain = "0.3"
|
||||||
tokio = { version = "0.2", git = "https://github.com/tokio-rs/tokio" }
|
tokio = { version = "0.2", git = "https://github.com/tokio-rs/tokio" }
|
||||||
|
@ -1,13 +1,79 @@
|
|||||||
|
#![feature(async_await)]
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use failure::{bail, Error};
|
use failure::{bail, format_err, Error};
|
||||||
|
use http::Request;
|
||||||
use http::Response;
|
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 tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
use proxmox::api::{api, router};
|
use proxmox::api::{api, router};
|
||||||
|
|
||||||
|
async fn run_request(request: Request<Body>) -> Result<http::Response<Body>, 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<Body>) -> Result<http::Response<Body>, 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({
|
#[api({
|
||||||
description: "Hello API call",
|
description: "Hello API call",
|
||||||
})]
|
})]
|
4
api-test/src/lib.rs
Normal file
4
api-test/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
//! PVE base library
|
||||||
|
|
||||||
|
pub mod lxc;
|
||||||
|
pub mod schema;
|
226
api-test/src/lxc/mod.rs
Normal file
226
api-test/src/lxc/mod.rs
Normal file
@ -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/<ostype>.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::<crate::schema::memory::Mb>
|
||||||
|
},
|
||||||
|
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<schema::ConfigLock>,
|
||||||
|
pub onboot: Option<bool>,
|
||||||
|
pub startup: Option<crate::schema::StartupOrder>,
|
||||||
|
pub template: Option<bool>,
|
||||||
|
pub arch: Option<Architecture>,
|
||||||
|
pub ostype: Option<schema::OsType>,
|
||||||
|
pub console: Option<bool>,
|
||||||
|
pub tty: Option<usize>,
|
||||||
|
pub cores: Option<usize>,
|
||||||
|
pub cpulimit: Option<usize>,
|
||||||
|
pub cpuunits: Option<usize>,
|
||||||
|
pub memory: Option<Memory>,
|
||||||
|
pub swap: Option<Memory>,
|
||||||
|
pub hostname: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub searchdomain: Option<Vec<String>>,
|
||||||
|
pub nameserver: Option<Vec<String>>,
|
||||||
|
pub rootfs: Option<Rootfs>,
|
||||||
|
// pub parent: Option<String>,
|
||||||
|
// pub snaptime: Option<usize>,
|
||||||
|
pub cmode: Option<schema::ConsoleMode>,
|
||||||
|
pub protection: Option<bool>,
|
||||||
|
pub unprivileged: Option<bool>,
|
||||||
|
// pub features: Option<schema::Features>,
|
||||||
|
pub hookscript: Option<VolumeId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Memory>,
|
||||||
|
pub acl: Option<bool>,
|
||||||
|
pub ro: Option<bool>,
|
||||||
|
pub mountoptions: Option<HashSet<String>>,
|
||||||
|
pub quota: Option<bool>,
|
||||||
|
pub replicate: Option<bool>,
|
||||||
|
pub shared: Option<bool>,
|
||||||
|
}
|
55
api-test/src/lxc/schema/mod.rs
Normal file
55
api-test/src/lxc/schema/mod.rs
Normal file
@ -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<T: crate::schema::tools::StringContainer>(value: &T) -> bool {
|
||||||
|
value.all(|s| VALID_MOUNT_OPTIONS.contains(&s))
|
||||||
|
}
|
||||||
|
}
|
@ -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<Body>) -> Result<http::Response<Body>, 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<Body>) -> Result<http::Response<Body>, 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));
|
|
||||||
}
|
|
110
api-test/src/schema/memory.rs
Normal file
110
api-test/src/schema/memory.rs
Normal file
@ -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<U: Unit>(PhantomData<U>);
|
||||||
|
impl<'de, U: Unit> serde::de::Visitor<'de> for MemoryVisitor<U> {
|
||||||
|
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<E: serde::de::Error>(self, v: u8) -> Result<Self::Value, E> {
|
||||||
|
Ok(Memory::from_bytes(v as u64 * U::FACTOR))
|
||||||
|
}
|
||||||
|
fn visit_u16<E: serde::de::Error>(self, v: u16) -> Result<Self::Value, E> {
|
||||||
|
Ok(Memory::from_bytes(v as u64 * U::FACTOR))
|
||||||
|
}
|
||||||
|
fn visit_u32<E: serde::de::Error>(self, v: u32) -> Result<Self::Value, E> {
|
||||||
|
Ok(Memory::from_bytes(v as u64 * U::FACTOR))
|
||||||
|
}
|
||||||
|
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
|
||||||
|
Ok(Memory::from_bytes(v as u64 * U::FACTOR))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
|
||||||
|
match v.parse::<u64>() {
|
||||||
|
Ok(v) => Ok(Memory::from_bytes(v * U::FACTOR)),
|
||||||
|
Err(_) => v.parse().map_err(serde::de::Error::custom),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Parser<U: Unit>(PhantomData<U>);
|
||||||
|
|
||||||
|
impl<U: Unit> Parser<U> {
|
||||||
|
pub fn serialize<S>(value: &Memory, ser: S) -> Result<S::Ok, S::Error>
|
||||||
|
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<Memory, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
de.deserialize_any(MemoryVisitor::<U>(PhantomData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod optional {
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
use super::Unit;
|
||||||
|
use crate::schema::types::Memory;
|
||||||
|
|
||||||
|
pub struct Parser<U: Unit>(PhantomData<U>);
|
||||||
|
|
||||||
|
impl<U: Unit> Parser<U> {
|
||||||
|
pub fn serialize<S>(value: &Option<Memory>, ser: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
super::Parser::<U>::serialize::<S>(&value.unwrap(), ser)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(de: D) -> Result<Option<Memory>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
super::Parser::<U>::deserialize::<'de, D>(de).map(Some)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
82
api-test/src/schema/mod.rs
Normal file
82
api-test/src/schema/mod.rs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
//! Common schema definitions.
|
||||||
|
|
||||||
|
use proxmox::api::api;
|
||||||
|
|
||||||
|
pub mod memory;
|
||||||
|
pub mod string_list;
|
||||||
|
pub mod string_set;
|
||||||
|
pub mod tools;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
#[api({
|
||||||
|
cli: false,
|
||||||
|
description:
|
||||||
|
r"Startup and shutdown behavior. \
|
||||||
|
Order is a non-negative number defining the general startup order. \
|
||||||
|
Shutdown in done with reverse ordering. \
|
||||||
|
Additionally you can set the 'up' or 'down' delay in seconds, which specifies a delay \
|
||||||
|
to wait before the next VM is started or stopped.",
|
||||||
|
fields: {
|
||||||
|
order: "Absolute ordering",
|
||||||
|
up: "Delay to wait before moving on to the next VM during startup.",
|
||||||
|
down: "Delay to wait before moving on to the next VM during shutdown.",
|
||||||
|
},
|
||||||
|
})]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct StartupOrder {
|
||||||
|
pub order: Option<usize>,
|
||||||
|
pub up: Option<usize>,
|
||||||
|
pub down: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[api({description: "Architecture."})]
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum Architecture {
|
||||||
|
// FIXME: suppport: #[api(alternatives = ["x86_64"])]
|
||||||
|
Amd64,
|
||||||
|
I386,
|
||||||
|
Arm64,
|
||||||
|
Armhf,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod dns_name {
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
pub const NAME: &'static str = "DNS name";
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
//static ref DNS_BASE_RE: Regex =
|
||||||
|
// Regex::new(r#"(?:[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)"#).unwrap();
|
||||||
|
static ref REGEX: Regex =
|
||||||
|
Regex::new(r#"^(?x)
|
||||||
|
(?:[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)
|
||||||
|
(?:\.(?:[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?))*
|
||||||
|
$"#).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify<T: crate::schema::tools::StringContainer>(value: &T) -> bool {
|
||||||
|
value.all(|s| REGEX.is_match(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod ip_address {
|
||||||
|
pub const NAME: &'static str = "IP Address";
|
||||||
|
|
||||||
|
pub fn verify<T: crate::schema::tools::StringContainer>(value: &T) -> bool {
|
||||||
|
value.all(|s| proxmox::tools::common_regex::IP_REGEX.is_match(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod safe_path {
|
||||||
|
pub const NAME: &'static str = "A canonical, absolute file system path";
|
||||||
|
|
||||||
|
pub fn verify<T: crate::schema::tools::StringContainer>(value: &T) -> bool {
|
||||||
|
value.all(|s| {
|
||||||
|
s != ".."
|
||||||
|
&& !s.starts_with("../")
|
||||||
|
&& !s.ends_with("/..")
|
||||||
|
&& !s.contains("/../")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
100
api-test/src/schema/string_list.rs
Normal file
100
api-test/src/schema/string_list.rs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
//! Comma separated string list.
|
||||||
|
//!
|
||||||
|
//! Used as a proxy type for when a struct should contain a `Vec<String>` which should be
|
||||||
|
//! serialized as a single comma separated list.
|
||||||
|
|
||||||
|
use failure::{bail, Error};
|
||||||
|
|
||||||
|
pub trait ForEachStr {
|
||||||
|
fn for_each_str<F>(&self, func: F) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
F: FnMut(&str) -> Result<(), Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ForEachStr for Vec<String> {
|
||||||
|
fn for_each_str<F>(&self, mut func: F) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
F: FnMut(&str) -> Result<(), Error>,
|
||||||
|
{
|
||||||
|
for i in self.iter() {
|
||||||
|
func(i.as_str())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize<S, T: ForEachStr>(value: &T, ser: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
let mut data = String::new();
|
||||||
|
value
|
||||||
|
.for_each_str(|s| {
|
||||||
|
if s.contains(',') {
|
||||||
|
bail!("cannot include value \"{}\" in a comma separated list", s);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !data.is_empty() {
|
||||||
|
data.push_str(", ");
|
||||||
|
}
|
||||||
|
data.push_str(s);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.map_err(serde::ser::Error::custom)?;
|
||||||
|
ser.serialize_str(&data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybe a custom visitor can also decode arrays by implementing visit_seq?
|
||||||
|
struct StringListVisitor;
|
||||||
|
|
||||||
|
impl<'de> serde::de::Visitor<'de> for StringListVisitor {
|
||||||
|
type Value = Vec<String>;
|
||||||
|
|
||||||
|
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"a comma separated list as a string, or an array of strings"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
|
||||||
|
Ok(v.split(',').map(|i| i.trim().to_string()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
|
||||||
|
let mut out = seq.size_hint().map_or_else(Vec::new, |size| Vec::with_capacity(size));
|
||||||
|
loop {
|
||||||
|
match seq.next_element::<String>()? {
|
||||||
|
Some(el) => out.push(el),
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(de: D) -> Result<Vec<String>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
de.deserialize_any(StringListVisitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod optional {
|
||||||
|
pub fn serialize<S, T: super::ForEachStr>(value: &Option<T>, ser: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
match value {
|
||||||
|
Some(value) => super::serialize(value, ser),
|
||||||
|
None => ser.serialize_none(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(de: D) -> Result<Option<Vec<String>>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
super::deserialize(de).map(Some)
|
||||||
|
}
|
||||||
|
}
|
106
api-test/src/schema/string_set.rs
Normal file
106
api-test/src/schema/string_set.rs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
//! A "set" of strings, semicolon separated, loaded into a `HashSet`.
|
||||||
|
//!
|
||||||
|
//! Used as a proxy type for when a struct should contain a `HashSet<String>` which should be
|
||||||
|
//! serialized as a single comma separated list.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use failure::{bail, Error};
|
||||||
|
|
||||||
|
pub trait ForEachStr {
|
||||||
|
fn for_each_str<F>(&self, func: F) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
F: FnMut(&str) -> Result<(), Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ForEachStr for HashSet<String> {
|
||||||
|
fn for_each_str<F>(&self, mut func: F) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
F: FnMut(&str) -> Result<(), Error>,
|
||||||
|
{
|
||||||
|
for i in self.iter() {
|
||||||
|
func(i.as_str())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize<S, T: ForEachStr>(value: &T, ser: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
let mut data = String::new();
|
||||||
|
value
|
||||||
|
.for_each_str(|s| {
|
||||||
|
if s.contains(';') {
|
||||||
|
bail!("cannot include value \"{}\" in a semicolon separated list", s);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !data.is_empty() {
|
||||||
|
data.push_str(";");
|
||||||
|
}
|
||||||
|
data.push_str(s);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.map_err(serde::ser::Error::custom)?;
|
||||||
|
ser.serialize_str(&data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybe a custom visitor can also decode arrays by implementing visit_seq?
|
||||||
|
struct StringSetVisitor;
|
||||||
|
|
||||||
|
impl<'de> serde::de::Visitor<'de> for StringSetVisitor {
|
||||||
|
type Value = HashSet<String>;
|
||||||
|
|
||||||
|
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"a string containing semicolon separated elements, or an array of strings"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
|
||||||
|
Ok(v.split(';').map(|i| i.trim().to_string()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
|
||||||
|
let mut out = seq
|
||||||
|
.size_hint()
|
||||||
|
.map_or_else(HashSet::new, |size| HashSet::with_capacity(size));
|
||||||
|
loop {
|
||||||
|
match seq.next_element::<String>()? {
|
||||||
|
Some(el) => out.insert(el),
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(de: D) -> Result<HashSet<String>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
de.deserialize_any(StringSetVisitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod optional {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
pub fn serialize<S, T: super::ForEachStr>(value: &Option<T>, ser: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
match value {
|
||||||
|
Some(value) => super::serialize(value, ser),
|
||||||
|
None => ser.serialize_none(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(de: D) -> Result<Option<HashSet<String>>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
super::deserialize(de).map(Some)
|
||||||
|
}
|
||||||
|
}
|
50
api-test/src/schema/tools.rs
Normal file
50
api-test/src/schema/tools.rs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
//! Helper module to perform the same format checks on various string types.
|
||||||
|
//!
|
||||||
|
//! This is used for formats which should be checked on strings, arrays of strings, and optional
|
||||||
|
//! variants of both.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
/// Allows testing predicates on all the contained strings of a type.
|
||||||
|
pub trait StringContainer {
|
||||||
|
fn all<F: Fn(&str) -> bool>(&self, pred: F) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StringContainer for String {
|
||||||
|
fn all<F: Fn(&str) -> bool>(&self, pred: F) -> bool {
|
||||||
|
pred(&self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StringContainer for Option<String> {
|
||||||
|
fn all<F: Fn(&str) -> bool>(&self, pred: F) -> bool {
|
||||||
|
match self {
|
||||||
|
Some(ref v) => pred(v),
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StringContainer for Vec<String> {
|
||||||
|
fn all<F: Fn(&str) -> bool>(&self, pred: F) -> bool {
|
||||||
|
self.iter().all(|s| pred(&s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StringContainer for Option<Vec<String>> {
|
||||||
|
fn all<F: Fn(&str) -> bool>(&self, pred: F) -> bool {
|
||||||
|
self.as_ref().map(|c| StringContainer::all(c, pred)).unwrap_or(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StringContainer for HashSet<String> {
|
||||||
|
fn all<F: Fn(&str) -> bool>(&self, pred: F) -> bool {
|
||||||
|
self.iter().all(|s| pred(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StringContainer for Option<HashSet<String>> {
|
||||||
|
fn all<F: Fn(&str) -> bool>(&self, pred: F) -> bool {
|
||||||
|
self.as_ref().map(|c| StringContainer::all(c, pred)).unwrap_or(true)
|
||||||
|
}
|
||||||
|
}
|
191
api-test/src/schema/types/memory.rs
Normal file
191
api-test/src/schema/types/memory.rs
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
//! 'Memory' type, represents an amount of memory.
|
||||||
|
|
||||||
|
use failure::Error;
|
||||||
|
|
||||||
|
use proxmox::api::api;
|
||||||
|
|
||||||
|
// TODO: manually implement Serialize/Deserialize to support both numeric and string
|
||||||
|
// representations. Numeric always being bytes, string having suffixes.
|
||||||
|
#[api({
|
||||||
|
description: "Represents an amount of memory and can be expressed with suffixes such as MiB.",
|
||||||
|
})]
|
||||||
|
#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct Memory(pub u64);
|
||||||
|
|
||||||
|
impl std::str::FromStr for Memory {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if s.ends_with("KiB") {
|
||||||
|
Ok(Self::from_kibibytes(s[..s.len() - 3].parse()?))
|
||||||
|
} else if s.ends_with("MiB") {
|
||||||
|
Ok(Self::from_mebibytes(s[..s.len() - 3].parse()?))
|
||||||
|
} else if s.ends_with("GiB") {
|
||||||
|
Ok(Self::from_gibibytes(s[..s.len() - 3].parse()?))
|
||||||
|
} else if s.ends_with("TiB") {
|
||||||
|
Ok(Self::from_tebibytes(s[..s.len() - 3].parse()?))
|
||||||
|
} else if s.ends_with("K") {
|
||||||
|
Ok(Self::from_kibibytes(s[..s.len() - 1].parse()?))
|
||||||
|
} else if s.ends_with("M") {
|
||||||
|
Ok(Self::from_mebibytes(s[..s.len() - 1].parse()?))
|
||||||
|
} else if s.ends_with("G") {
|
||||||
|
Ok(Self::from_gibibytes(s[..s.len() - 1].parse()?))
|
||||||
|
} else if s.ends_with("T") {
|
||||||
|
Ok(Self::from_tebibytes(s[..s.len() - 1].parse()?))
|
||||||
|
} else if s.ends_with("b") || s.ends_with("B") {
|
||||||
|
Ok(Self::from_bytes(s[..s.len() - 1].parse()?))
|
||||||
|
} else {
|
||||||
|
Ok(Self::from_bytes(s[..s.len() - 1].parse()?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serde_plain::derive_deserialize_from_str!(Memory, "valid memory amount description");
|
||||||
|
proxmox::api::derive_parse_cli_from_str!(Memory);
|
||||||
|
|
||||||
|
impl std::fmt::Display for Memory {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
const SUFFIXES: &'static [&'static str] = &["b", "KiB", "MiB", "GiB", "TiB"];
|
||||||
|
let mut n = self.0;
|
||||||
|
let mut i = 0;
|
||||||
|
while i < SUFFIXES.len() && (n & 0x3ff) == 0 {
|
||||||
|
n >>= 10;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
write!(f, "{}{}", n, SUFFIXES[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serde_plain::derive_serialize_from_display!(Memory);
|
||||||
|
|
||||||
|
impl Memory {
|
||||||
|
pub const fn from_bytes(v: u64) -> Self {
|
||||||
|
Self(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn as_bytes(&self) -> u64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_kibibytes(v: u64) -> Self {
|
||||||
|
Self(v * 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn as_kibibytes(&self) -> u64 {
|
||||||
|
self.0 / 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_si_kilobytes(v: u64) -> Self {
|
||||||
|
Self(v * 1_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn as_si_kilobytes(&self) -> u64 {
|
||||||
|
self.0 / 1_000
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_mebibytes(v: u64) -> Self {
|
||||||
|
Self(v * 1024 * 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn as_mebibytes(&self) -> u64 {
|
||||||
|
self.0 / 1024 / 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_si_megabytes(v: u64) -> Self {
|
||||||
|
Self(v * 1_000_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn as_si_megabytes(&self) -> u64 {
|
||||||
|
self.0 / 1_000_000
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_gibibytes(v: u64) -> Self {
|
||||||
|
Self(v * 1024 * 1024 * 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn as_gibibytes(&self) -> u64 {
|
||||||
|
self.0 / 1024 / 1024 / 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_si_gigabytes(v: u64) -> Self {
|
||||||
|
Self(v * 1_000_000_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn as_si_gigabytes(&self) -> u64 {
|
||||||
|
self.0 / 1_000_000_000
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_tebibytes(v: u64) -> Self {
|
||||||
|
Self(v * 1024 * 1024 * 1024 * 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn as_tebibytes(&self) -> u64 {
|
||||||
|
self.0 / 1024 / 1024 / 1024 / 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_si_terabytes(v: u64) -> Self {
|
||||||
|
Self(v * 1_000_000_000_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn as_si_terabytes(&self) -> u64 {
|
||||||
|
self.0 / 1_000_000_000_000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Add<Memory> for Memory {
|
||||||
|
type Output = Memory;
|
||||||
|
|
||||||
|
fn add(self, rhs: Memory) -> Memory {
|
||||||
|
Self(self.0 + rhs.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::AddAssign<Memory> for Memory {
|
||||||
|
fn add_assign(&mut self, rhs: Memory) {
|
||||||
|
self.0 += rhs.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Sub<Memory> for Memory {
|
||||||
|
type Output = Memory;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Memory) -> Memory {
|
||||||
|
Self(self.0 - rhs.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::SubAssign<Memory> for Memory {
|
||||||
|
fn sub_assign(&mut self, rhs: Memory) {
|
||||||
|
self.0 -= rhs.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Mul<u64> for Memory {
|
||||||
|
type Output = Memory;
|
||||||
|
|
||||||
|
fn mul(self, rhs: u64) -> Memory {
|
||||||
|
Self(self.0 * rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::MulAssign<u64> for Memory {
|
||||||
|
fn mul_assign(&mut self, rhs: u64) {
|
||||||
|
self.0 *= rhs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn memory() {
|
||||||
|
assert_eq!(Memory::from_mebibytes(1).as_kibibytes(), 1024);
|
||||||
|
assert_eq!(Memory::from_mebibytes(1).as_bytes(), 1024 * 1024);
|
||||||
|
assert_eq!(Memory::from_si_megabytes(1).as_bytes(), 1_000_000);
|
||||||
|
assert_eq!(Memory::from_tebibytes(1), Memory::from_gibibytes(1024));
|
||||||
|
assert_eq!(Memory::from_gibibytes(1), Memory::from_mebibytes(1024));
|
||||||
|
assert_eq!(Memory::from_mebibytes(1), Memory::from_kibibytes(1024));
|
||||||
|
assert_eq!(Memory::from_kibibytes(1), Memory::from_bytes(1024));
|
||||||
|
assert_eq!(
|
||||||
|
Memory::from_kibibytes(1) + Memory::from_bytes(6),
|
||||||
|
Memory::from_bytes(1030)
|
||||||
|
);
|
||||||
|
assert_eq!("1M".parse::<Memory>().unwrap(), Memory::from_mebibytes(1));
|
||||||
|
assert_eq!("1MiB".parse::<Memory>().unwrap(), Memory::from_mebibytes(1));
|
||||||
|
}
|
7
api-test/src/schema/types/mod.rs
Normal file
7
api-test/src/schema/types/mod.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
//! Commonly used basic types, such as a type safe `Memory` type.
|
||||||
|
|
||||||
|
mod memory;
|
||||||
|
pub use memory::Memory;
|
||||||
|
|
||||||
|
mod volume_id;
|
||||||
|
pub use volume_id::VolumeId;
|
53
api-test/src/schema/types/volume_id.rs
Normal file
53
api-test/src/schema/types/volume_id.rs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
//! A 'VolumeId' is a storage + volume combination.
|
||||||
|
|
||||||
|
use failure::{format_err, Error};
|
||||||
|
|
||||||
|
use proxmox::api::api;
|
||||||
|
|
||||||
|
#[api({
|
||||||
|
serialize_as_string: true,
|
||||||
|
cli: FromStr,
|
||||||
|
description: "A volume ID consisting of a storage name and a volume name",
|
||||||
|
fields: {
|
||||||
|
storage: {
|
||||||
|
description: "A storage name",
|
||||||
|
pattern: r#"^[a-z][a-z0-9\-_.]*[a-z0-9]$"#,
|
||||||
|
},
|
||||||
|
volume: "A volume name",
|
||||||
|
},
|
||||||
|
})]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct VolumeId {
|
||||||
|
storage: String,
|
||||||
|
volume: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for VolumeId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(f, "{}:{}", self.storage, self.volume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for VolumeId {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut parts = s.splitn(2, ':');
|
||||||
|
|
||||||
|
let this = Self {
|
||||||
|
storage: parts
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| format_err!("not a volume id: {}", s))?
|
||||||
|
.to_string(),
|
||||||
|
volume: parts
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| format_err!("not a volume id: {}", s))?
|
||||||
|
.to_string(),
|
||||||
|
};
|
||||||
|
assert!(parts.next().is_none());
|
||||||
|
|
||||||
|
proxmox::api::ApiType::verify(&this)?;
|
||||||
|
|
||||||
|
Ok(this)
|
||||||
|
}
|
||||||
|
}
|
190
api-test/tests/lxc-config.rs
Normal file
190
api-test/tests/lxc-config.rs
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
use failure::Error;
|
||||||
|
|
||||||
|
use proxmox::api::ApiType;
|
||||||
|
|
||||||
|
use api_test::lxc;
|
||||||
|
|
||||||
|
/// This just checks the string in order to avoid `T: Eq` as requirement.
|
||||||
|
/// in other words:
|
||||||
|
/// assert that serialize(value) == serialize(deserialize(serialize(value)))
|
||||||
|
/// We assume that serialize(value) has already been checked before entering this function.
|
||||||
|
fn check_ser_de<T>(value: &T) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
T: ApiType + serde::Serialize + serde::de::DeserializeOwned,
|
||||||
|
{
|
||||||
|
assert!(value.verify().is_ok());
|
||||||
|
let s1 = serde_json::to_string(value)?;
|
||||||
|
let v2: T = serde_json::from_str(&s1)?;
|
||||||
|
assert!(v2.verify().is_ok());
|
||||||
|
let s2 = serde_json::to_string(&v2)?;
|
||||||
|
assert_eq!(s1, s2);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lxc_config() -> Result<(), Error> {
|
||||||
|
let mut config = lxc::Config::default();
|
||||||
|
assert!(config.verify().is_ok());
|
||||||
|
assert_eq!(serde_json::to_string(&config)?, "{}");
|
||||||
|
check_ser_de(&config)?;
|
||||||
|
assert_eq!(*config.onboot(), false);
|
||||||
|
assert_eq!(*config.template(), false);
|
||||||
|
assert_eq!(*config.arch(), api_test::schema::Architecture::Amd64);
|
||||||
|
assert_eq!(*config.console(), true);
|
||||||
|
assert_eq!(*config.tty(), 2);
|
||||||
|
assert_eq!(*config.cmode(), api_test::lxc::schema::ConsoleMode::Tty);
|
||||||
|
assert_eq!(config.memory().as_bytes(), 512 << 20);
|
||||||
|
|
||||||
|
config.lock = Some(lxc::schema::ConfigLock::Backup);
|
||||||
|
check_ser_de(&config)?;
|
||||||
|
assert_eq!(serde_json::to_string(&config)?, r#"{"lock":"backup"}"#);
|
||||||
|
|
||||||
|
// test the renamed one:
|
||||||
|
config.lock = Some(lxc::schema::ConfigLock::SnapshotDelete);
|
||||||
|
check_ser_de(&config)?;
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&config)?,
|
||||||
|
r#"{"lock":"snapshot-delete"}"#
|
||||||
|
);
|
||||||
|
|
||||||
|
config.onboot = Some(true);
|
||||||
|
check_ser_de(&config)?;
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&config)?,
|
||||||
|
r#"{"lock":"snapshot-delete","onboot":true}"#
|
||||||
|
);
|
||||||
|
assert_eq!(*config.onboot(), true);
|
||||||
|
|
||||||
|
config.lock = None;
|
||||||
|
config.onboot = Some(false);
|
||||||
|
check_ser_de(&config)?;
|
||||||
|
assert_eq!(serde_json::to_string(&config)?, r#"{"onboot":false}"#);
|
||||||
|
assert_eq!(*config.onboot(), false);
|
||||||
|
|
||||||
|
config.onboot = None;
|
||||||
|
check_ser_de(&config)?;
|
||||||
|
assert_eq!(*config.onboot(), false);
|
||||||
|
|
||||||
|
config.set_onboot(true);
|
||||||
|
check_ser_de(&config)?;
|
||||||
|
assert_eq!(*config.onboot(), true);
|
||||||
|
assert_eq!(serde_json::to_string(&config)?, r#"{"onboot":true}"#);
|
||||||
|
|
||||||
|
config.set_onboot(false);
|
||||||
|
check_ser_de(&config)?;
|
||||||
|
assert_eq!(*config.onboot(), false);
|
||||||
|
assert_eq!(serde_json::to_string(&config)?, r#"{"onboot":false}"#);
|
||||||
|
|
||||||
|
config.set_template(true);
|
||||||
|
check_ser_de(&config)?;
|
||||||
|
assert_eq!(*config.template(), true);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&config)?,
|
||||||
|
r#"{"onboot":false,"template":true}"#
|
||||||
|
);
|
||||||
|
|
||||||
|
config.onboot = None;
|
||||||
|
config.template = None;
|
||||||
|
|
||||||
|
config.startup = Some(api_test::schema::StartupOrder {
|
||||||
|
order: Some(5),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
check_ser_de(&config)?;
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&config)?,
|
||||||
|
r#"{"startup":{"order":5}}"#
|
||||||
|
);
|
||||||
|
|
||||||
|
config = serde_json::from_str(r#"{"memory":"123MiB"}"#)?;
|
||||||
|
assert!(config.verify().is_ok());
|
||||||
|
assert_eq!(serde_json::to_string(&config)?, r#"{"memory":123}"#);
|
||||||
|
|
||||||
|
config = serde_json::from_str(r#"{"memory":"1024MiB"}"#)?;
|
||||||
|
assert!(config.verify().is_ok());
|
||||||
|
assert_eq!(serde_json::to_string(&config)?, r#"{"memory":1024}"#);
|
||||||
|
|
||||||
|
config = serde_json::from_str(r#"{"memory":"1300001KiB"}"#)?;
|
||||||
|
assert!(config.verify().is_ok());
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&config)?,
|
||||||
|
r#"{"memory":"1300001KiB"}"#
|
||||||
|
);
|
||||||
|
|
||||||
|
// test numeric values
|
||||||
|
config = serde_json::from_str(r#"{"tty":3}"#)?;
|
||||||
|
assert!(config.verify().is_ok());
|
||||||
|
assert_eq!(serde_json::to_string(&config)?, r#"{"tty":3}"#);
|
||||||
|
assert!(serde_json::from_str::<lxc::Config>(r#"{"tty":"3"}"#).is_err()); // string as int
|
||||||
|
|
||||||
|
config = serde_json::from_str(r#"{"tty":9}"#)?;
|
||||||
|
assert_eq!(
|
||||||
|
config.verify().map_err(|e| e.to_string()),
|
||||||
|
Err("field tty out of range, must be <= 6".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
config = serde_json::from_str(r#"{"hostname":"xx"}"#)?;
|
||||||
|
assert_eq!(
|
||||||
|
config.verify().map_err(|e| e.to_string()),
|
||||||
|
Err("field hostname too short, must be >= 3 characters".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
config = serde_json::from_str(r#"{"hostname":"foo.bar.com"}"#)?;
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&config)?,
|
||||||
|
r#"{"hostname":"foo.bar.com"}"#
|
||||||
|
);
|
||||||
|
assert!(config.verify().is_ok());
|
||||||
|
|
||||||
|
config = serde_json::from_str(r#"{"hostname":"foo"}"#)?;
|
||||||
|
assert!(config.verify().is_ok());
|
||||||
|
|
||||||
|
config = serde_json::from_str(r#"{"hostname":"..."}"#)?;
|
||||||
|
assert_eq!(
|
||||||
|
config.verify().map_err(|e| e.to_string()),
|
||||||
|
Err("field hostname does not match format DNS name".to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
config = serde_json::from_str(r#"{"searchdomain":"foo.bar"}"#)?;
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&config)?,
|
||||||
|
r#"{"searchdomain":"foo.bar"}"#
|
||||||
|
);
|
||||||
|
|
||||||
|
config = serde_json::from_str(r#"{"searchdomain":"foo.."}"#)?;
|
||||||
|
assert_eq!(
|
||||||
|
config.verify().map_err(|e| e.to_string()),
|
||||||
|
Err("field searchdomain does not match format DNS name".to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
config = serde_json::from_str(r#"{"searchdomain":"foo.com, bar.com"}"#)?;
|
||||||
|
assert!(config.verify().is_ok());
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&config)?,
|
||||||
|
r#"{"searchdomain":"foo.com, bar.com"}"#
|
||||||
|
);
|
||||||
|
config = serde_json::from_str(r#"{"searchdomain":["foo.com", "bar.com"]}"#)?;
|
||||||
|
assert!(config.verify().is_ok());
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&config)?,
|
||||||
|
r#"{"searchdomain":"foo.com, bar.com"}"#
|
||||||
|
);
|
||||||
|
|
||||||
|
config = serde_json::from_str(r#"{"nameserver":["127.0.0.1", "::1"]}"#)?;
|
||||||
|
check_ser_de(&config)?;
|
||||||
|
|
||||||
|
config = serde_json::from_str(r#"{"nameserver":"127.0.0.1, foo"}"#)?;
|
||||||
|
assert_eq!(
|
||||||
|
config.verify().map_err(|e| e.to_string()),
|
||||||
|
Err("field nameserver does not match format IP Address".to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
config = serde_json::from_str(r#"{"cmode":"tty"}"#)?;
|
||||||
|
check_ser_de(&config)?;
|
||||||
|
config = serde_json::from_str(r#"{"cmode":"shell"}"#)?;
|
||||||
|
check_ser_de(&config)?;
|
||||||
|
config = serde_json::from_str(r#"{"hookscript":"local:snippets/foo.sh"}"#)?;
|
||||||
|
check_ser_de(&config)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user