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"
|
||||
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" }
|
||||
|
@ -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<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({
|
||||
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…
x
Reference in New Issue
Block a user