diff --git a/proxmox-api/Cargo.toml b/proxmox-api/Cargo.toml
index 59160b17..7bd79b01 100644
--- a/proxmox-api/Cargo.toml
+++ b/proxmox-api/Cargo.toml
@@ -9,9 +9,13 @@ bytes = "0.4"
failure = "0.1"
futures-preview = "0.3.0-alpha"
http = "0.1"
+hyper = { version = "0.13.0-alpha.1" }
+proxmox-tools = { version = "0.1", path = "../proxmox-tools" }
+regex = "1.0"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
+url = "1.7"
[dev-dependencies]
lazy_static = "1.3"
diff --git a/proxmox-api/src/lib.rs b/proxmox-api/src/lib.rs
index d2796a36..b84912a1 100644
--- a/proxmox-api/src/lib.rs
+++ b/proxmox-api/src/lib.rs
@@ -1,13 +1,44 @@
//! Proxmox API module. This provides utilities for HTTP and command line APIs.
use std::future::Future;
-use std::pin::Pin;
use failure::Error;
-use http::Response;
+use hyper::http::request::Parts;
+use hyper::{Body, Response};
+use serde_json::Value;
-/// Return type of an API method.
-pub type ApiOutput
= Result, Error>;
+pub mod router;
+pub mod rpc_environment;
+pub mod schema;
-/// Future type of an API method. In order to support `async fn` this is a pinned box.
-pub type ApiFuture = Pin> + Send>>;
+#[doc(inline)]
+pub use rpc_environment::{RpcEnvironment, RpcEnvironmentType};
+
+#[doc(inline)]
+pub use router::ApiMethod;
+
+/// A synchronous API handler gets a json Value as input and returns a json Value as output.
+pub type ApiHandlerFn = &'static (dyn Fn(Value, &ApiMethod, &mut dyn RpcEnvironment) -> Result
+ + Send
+ + Sync
+ + 'static);
+
+/// Asynchronous API handlers get more lower level access to request data.
+pub type ApiAsyncHandlerFn = &'static (dyn Fn(
+ Parts,
+ Body,
+ Value,
+ &'static ApiMethod,
+ Box,
+) -> Result
+ + Send
+ + Sync
+ + 'static);
+
+/// The output of an asynchronous API handler is a futrue yielding a `Response`.
+pub type ApiFuture = Box, failure::Error>> + Send>;
+
+pub enum ApiHandler {
+ Sync(ApiHandlerFn),
+ Async(ApiAsyncHandlerFn),
+}
diff --git a/proxmox-api/src/router.rs b/proxmox-api/src/router.rs
new file mode 100644
index 00000000..13681375
--- /dev/null
+++ b/proxmox-api/src/router.rs
@@ -0,0 +1,264 @@
+use std::collections::HashMap;
+use std::fmt;
+
+use failure::{Error, Fail};
+use hyper::{Method, StatusCode};
+use serde_json::Value;
+
+use crate::schema::{ObjectSchema, Schema};
+use crate::ApiHandler;
+use crate::RpcEnvironment;
+
+#[derive(Debug, Fail)]
+pub struct HttpError {
+ pub code: StatusCode,
+ pub message: String,
+}
+
+impl HttpError {
+ pub fn new(code: StatusCode, message: String) -> Self {
+ HttpError { code, message }
+ }
+}
+
+impl fmt::Display for HttpError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}", self.message)
+ }
+}
+
+#[macro_export]
+macro_rules! http_err {
+ ($status:ident, $msg:expr) => {{
+ Error::from(HttpError::new(StatusCode::$status, $msg))
+ }};
+}
+
+/// This struct defines synchronous API call which returns the restulkt as json `Value`
+pub struct ApiMethod {
+ /// The protected flag indicates that the provides function should be forwarded
+ /// to the deaemon running in priviledged mode.
+ pub protected: bool,
+ /// This flag indicates that the provided method may change the local timezone, so the server
+ /// should do a tzset afterwards
+ pub reload_timezone: bool,
+ /// Parameter type Schema
+ pub parameters: &'static ObjectSchema,
+ /// Return type Schema
+ pub returns: &'static Schema,
+ /// Handler function
+ pub handler: &'static ApiHandler,
+}
+
+impl std::fmt::Debug for ApiMethod {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "ApiMethod {{ ")?;
+ write!(f, " parameters: {:?}", self.parameters)?;
+ write!(f, " returns: {:?}", self.returns)?;
+ write!(f, " handler: {:p}", &self.handler)?;
+ write!(f, "}}")
+ }
+}
+
+const NULL_SCHEMA: Schema = Schema::Null;
+
+fn dummy_handler_fn(
+ _arg: Value,
+ _method: &ApiMethod,
+ _env: &mut dyn RpcEnvironment,
+) -> Result {
+ // do nothing
+ Ok(Value::Null)
+}
+
+const DUMMY_HANDLER: ApiHandler = ApiHandler::Sync(&dummy_handler_fn);
+
+impl ApiMethod {
+ pub const fn new(handler: &'static ApiHandler, parameters: &'static ObjectSchema) -> Self {
+ Self {
+ parameters,
+ handler,
+ returns: &NULL_SCHEMA,
+ protected: false,
+ reload_timezone: false,
+ }
+ }
+
+ pub const fn new_dummy(parameters: &'static ObjectSchema) -> Self {
+ Self {
+ parameters,
+ handler: &DUMMY_HANDLER,
+ returns: &NULL_SCHEMA,
+ protected: false,
+ reload_timezone: false,
+ }
+ }
+
+ pub const fn returns(mut self, schema: &'static Schema) -> Self {
+ self.returns = schema;
+
+ self
+ }
+
+ pub const fn protected(mut self, protected: bool) -> Self {
+ self.protected = protected;
+
+ self
+ }
+
+ pub const fn reload_timezone(mut self, reload_timezone: bool) -> Self {
+ self.reload_timezone = reload_timezone;
+
+ self
+ }
+}
+
+pub type SubdirMap = &'static [(&'static str, &'static Router)];
+
+pub enum SubRoute {
+ //Hash(HashMap),
+ Map(SubdirMap),
+ MatchAll {
+ router: &'static Router,
+ param_name: &'static str,
+ },
+}
+
+/// Macro to create an ApiMethod to list entries from SubdirMap
+#[macro_export]
+macro_rules! list_subdirs_api_method {
+ ($map:expr) => {
+ ApiMethod::new(
+ &ApiHandler::Sync( & |_, _, _| {
+ let index = serde_json::json!(
+ $map.iter().map(|s| serde_json::json!({ "subdir": s.0}))
+ .collect::>()
+ );
+ Ok(index)
+ }),
+ &crate::api_schema::ObjectSchema::new("Directory index.", &[]).additional_properties(true)
+ )
+ }
+}
+
+pub struct Router {
+ pub get: Option<&'static ApiMethod>,
+ pub put: Option<&'static ApiMethod>,
+ pub post: Option<&'static ApiMethod>,
+ pub delete: Option<&'static ApiMethod>,
+ pub subroute: Option,
+}
+
+impl Router {
+ pub const fn new() -> Self {
+ Self {
+ get: None,
+ put: None,
+ post: None,
+ delete: None,
+ subroute: None,
+ }
+ }
+
+ pub const fn subdirs(mut self, map: SubdirMap) -> Self {
+ self.subroute = Some(SubRoute::Map(map));
+ self
+ }
+
+ pub const fn match_all(mut self, param_name: &'static str, router: &'static Router) -> Self {
+ self.subroute = Some(SubRoute::MatchAll { router, param_name });
+ self
+ }
+
+ pub const fn get(mut self, m: &'static ApiMethod) -> Self {
+ self.get = Some(m);
+ self
+ }
+
+ pub const fn put(mut self, m: &'static ApiMethod) -> Self {
+ self.put = Some(m);
+ self
+ }
+
+ pub const fn post(mut self, m: &'static ApiMethod) -> Self {
+ self.post = Some(m);
+ self
+ }
+
+ /// Same as post, buth async (fixme: expect Async)
+ pub const fn upload(mut self, m: &'static ApiMethod) -> Self {
+ self.post = Some(m);
+ self
+ }
+
+ /// Same as get, but async (fixme: expect Async)
+ pub const fn download(mut self, m: &'static ApiMethod) -> Self {
+ self.get = Some(m);
+ self
+ }
+
+ /// Same as get, but async (fixme: expect Async)
+ pub const fn upgrade(mut self, m: &'static ApiMethod) -> Self {
+ self.get = Some(m);
+ self
+ }
+
+ pub const fn delete(mut self, m: &'static ApiMethod) -> Self {
+ self.delete = Some(m);
+ self
+ }
+
+ pub fn find_route(
+ &self,
+ components: &[&str],
+ uri_param: &mut HashMap,
+ ) -> Option<&Router> {
+ if components.is_empty() {
+ return Some(self);
+ };
+
+ let (dir, rest) = (components[0], &components[1..]);
+
+ match self.subroute {
+ None => {}
+ Some(SubRoute::Map(dirmap)) => {
+ if let Ok(ind) = dirmap.binary_search_by_key(&dir, |(name, _)| name) {
+ let (_name, router) = dirmap[ind];
+ //println!("FOUND SUBDIR {}", dir);
+ return router.find_route(rest, uri_param);
+ }
+ }
+ Some(SubRoute::MatchAll { router, param_name }) => {
+ //println!("URI PARAM {} = {}", param_name, dir); // fixme: store somewhere
+ uri_param.insert(param_name.to_owned(), dir.into());
+ return router.find_route(rest, uri_param);
+ }
+ }
+
+ None
+ }
+
+ pub fn find_method(
+ &self,
+ components: &[&str],
+ method: Method,
+ uri_param: &mut HashMap,
+ ) -> Option<&ApiMethod> {
+ if let Some(info) = self.find_route(components, uri_param) {
+ return match method {
+ Method::GET => info.get,
+ Method::PUT => info.put,
+ Method::POST => info.post,
+ Method::DELETE => info.delete,
+ _ => None,
+ };
+ }
+ None
+ }
+}
+
+impl Default for Router {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/proxmox-api/src/rpc_environment.rs b/proxmox-api/src/rpc_environment.rs
new file mode 100644
index 00000000..2587e334
--- /dev/null
+++ b/proxmox-api/src/rpc_environment.rs
@@ -0,0 +1,37 @@
+use proxmox_tools::AsAny;
+
+use serde_json::Value;
+
+/// Abstract Interface for API methods to interact with the environment
+pub trait RpcEnvironment: std::any::Any + AsAny + Send {
+ /// Use this to pass additional result data. It is up to the environment
+ /// how the data is used.
+ fn set_result_attrib(&mut self, name: &str, value: Value);
+
+ /// Query additional result data.
+ fn get_result_attrib(&self, name: &str) -> Option<&Value>;
+
+ /// The environment type
+ fn env_type(&self) -> RpcEnvironmentType;
+
+ /// Set user name
+ fn set_user(&mut self, user: Option);
+
+ /// Get user name
+ fn get_user(&self) -> Option;
+}
+
+/// Environment Type
+///
+/// We use this to enumerate the different environment types. Some methods
+/// needs to do different things when started from the command line interface,
+/// or when executed from a privileged server running as root.
+#[derive(PartialEq, Copy, Clone)]
+pub enum RpcEnvironmentType {
+ /// Command started from command line
+ CLI,
+ /// Access from public accessible server
+ PUBLIC,
+ /// Access from privileged server (run as root)
+ PRIVILEGED,
+}
diff --git a/proxmox-api/src/schema.rs b/proxmox-api/src/schema.rs
new file mode 100644
index 00000000..890bcd57
--- /dev/null
+++ b/proxmox-api/src/schema.rs
@@ -0,0 +1,1051 @@
+use std::fmt;
+
+use failure::*;
+use serde_json::{json, Value};
+use url::form_urlencoded;
+
+#[derive(Default, Debug, Fail)]
+pub struct ParameterError {
+ error_list: Vec,
+}
+
+/// Error type for schema validation
+///
+/// The validation functions may produce several error message,
+/// i.e. when validation objects, it can produce one message for each
+/// erroneous object property.
+
+// fixme: record parameter names, to make it usefull to display errord
+// on HTML forms.
+impl ParameterError {
+ pub fn new() -> Self {
+ Self {
+ error_list: Vec::new(),
+ }
+ }
+
+ pub fn push(&mut self, value: Error) {
+ self.error_list.push(value);
+ }
+
+ pub fn len(&self) -> usize {
+ self.error_list.len()
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+}
+
+impl fmt::Display for ParameterError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let mut msg = String::new();
+
+ if !self.is_empty() {
+ msg.push_str("parameter verification errors\n\n");
+ }
+
+ msg.push_str(
+ &self
+ .error_list
+ .iter()
+ .fold(String::from(""), |acc, item| acc + &item.to_string() + "\n"),
+ );
+
+ write!(f, "{}", msg)
+ }
+}
+
+#[derive(Debug)]
+pub struct BooleanSchema {
+ pub description: &'static str,
+ pub default: Option,
+}
+
+impl BooleanSchema {
+ pub const fn new(description: &'static str) -> Self {
+ BooleanSchema {
+ description,
+ default: None,
+ }
+ }
+
+ pub const fn default(mut self, default: bool) -> Self {
+ self.default = Some(default);
+ self
+ }
+
+ pub const fn schema(self) -> Schema {
+ Schema::Boolean(self)
+ }
+}
+
+#[derive(Debug)]
+pub struct IntegerSchema {
+ pub description: &'static str,
+ pub minimum: Option,
+ pub maximum: Option,
+ pub default: Option,
+}
+
+impl IntegerSchema {
+ pub const fn new(description: &'static str) -> Self {
+ IntegerSchema {
+ description,
+ default: None,
+ minimum: None,
+ maximum: None,
+ }
+ }
+
+ pub const fn default(mut self, default: isize) -> Self {
+ self.default = Some(default);
+ self
+ }
+
+ pub const fn minimum(mut self, minimum: isize) -> Self {
+ self.minimum = Some(minimum);
+ self
+ }
+
+ pub const fn maximum(mut self, maximium: isize) -> Self {
+ self.maximum = Some(maximium);
+ self
+ }
+
+ pub const fn schema(self) -> Schema {
+ Schema::Integer(self)
+ }
+
+ fn check_constraints(&self, value: isize) -> Result<(), Error> {
+ if let Some(minimum) = self.minimum {
+ if value < minimum {
+ bail!(
+ "value must have a minimum value of {} (got {})",
+ minimum,
+ value
+ );
+ }
+ }
+
+ if let Some(maximum) = self.maximum {
+ if value > maximum {
+ bail!(
+ "value must have a maximum value of {} (got {})",
+ maximum,
+ value
+ );
+ }
+ }
+
+ Ok(())
+ }
+}
+
+/// Helper to represent const regular expressions
+///
+/// This is mostly a workaround, unless we can create const_fn Regex.
+pub struct ConstRegexPattern {
+ pub regex_string: &'static str,
+ pub regex_obj: fn() -> &'static regex::Regex,
+}
+
+impl std::fmt::Debug for ConstRegexPattern {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{:?}", self.regex_string)
+ }
+}
+
+/// Macro to generate a ConstRegexPattern
+#[macro_export]
+macro_rules! const_regex {
+ () => {};
+ ($(#[$attr:meta])* pub ($($vis:tt)+) $name:ident = $regex:expr; $($rest:tt)*) => {
+ const_regex! { (pub ($($vis)+)) $(#[$attr])* $name = $regex; $($rest)* }
+ };
+ ($(#[$attr:meta])* pub $name:ident = $regex:expr; $($rest:tt)*) => {
+ const_regex! { (pub) $(#[$attr])* $name = $regex; $($rest)* }
+ };
+ ($(#[$attr:meta])* $name:ident = $regex:expr; $($rest:tt)*) => {
+ const_regex! { () $(#[$attr])* $name = $regex; $($rest)* }
+ };
+ (
+ ($($pub:tt)*) $(#[$attr:meta])* $name:ident = $regex:expr;
+ $($rest:tt)*
+ ) => {
+ $(#[$attr])* $($pub)* const $name: ConstRegexPattern = ConstRegexPattern {
+ regex_string: $regex,
+ regex_obj: (|| -> &'static regex::Regex {
+ lazy_static::lazy_static! {
+ static ref SCHEMA: regex::Regex = regex::Regex::new($regex).unwrap();
+ }
+ &SCHEMA
+ })
+ };
+
+ const_regex! { $($rest)* }
+ };
+}
+
+#[derive(Debug)]
+pub struct StringSchema {
+ pub description: &'static str,
+ pub default: Option<&'static str>,
+ pub min_length: Option,
+ pub max_length: Option,
+ pub format: Option<&'static ApiStringFormat>,
+}
+
+impl StringSchema {
+ pub const fn new(description: &'static str) -> Self {
+ StringSchema {
+ description,
+ default: None,
+ min_length: None,
+ max_length: None,
+ format: None,
+ }
+ }
+
+ pub const fn default(mut self, text: &'static str) -> Self {
+ self.default = Some(text);
+ self
+ }
+
+ pub const fn format(mut self, format: &'static ApiStringFormat) -> Self {
+ self.format = Some(format);
+ self
+ }
+
+ pub const fn min_length(mut self, min_length: usize) -> Self {
+ self.min_length = Some(min_length);
+ self
+ }
+
+ pub const fn max_length(mut self, max_length: usize) -> Self {
+ self.max_length = Some(max_length);
+ self
+ }
+
+ pub const fn schema(self) -> Schema {
+ Schema::String(self)
+ }
+
+ fn check_length(&self, length: usize) -> Result<(), Error> {
+ if let Some(min_length) = self.min_length {
+ if length < min_length {
+ bail!("value must be at least {} characters long", min_length);
+ }
+ }
+
+ if let Some(max_length) = self.max_length {
+ if length > max_length {
+ bail!("value may only be {} characters long", max_length);
+ }
+ }
+
+ Ok(())
+ }
+
+ pub fn check_constraints(&self, value: &str) -> Result<(), Error> {
+ self.check_length(value.chars().count())?;
+
+ if let Some(ref format) = self.format {
+ match format {
+ ApiStringFormat::Pattern(regex) => {
+ if !(regex.regex_obj)().is_match(value) {
+ bail!("value does not match the regex pattern");
+ }
+ }
+ ApiStringFormat::Enum(stringvec) => {
+ if stringvec.iter().find(|&e| *e == value) == None {
+ bail!("value '{}' is not defined in the enumeration.", value);
+ }
+ }
+ ApiStringFormat::Complex(subschema) => {
+ parse_property_string(value, subschema)?;
+ }
+ ApiStringFormat::VerifyFn(verify_fn) => {
+ verify_fn(value)?;
+ }
+ }
+ }
+
+ Ok(())
+ }
+}
+
+#[derive(Debug)]
+pub struct ArraySchema {
+ pub description: &'static str,
+ pub items: &'static Schema,
+ pub min_length: Option,
+ pub max_length: Option,
+}
+
+impl ArraySchema {
+ pub const fn new(description: &'static str, item_schema: &'static Schema) -> Self {
+ ArraySchema {
+ description,
+ items: item_schema,
+ min_length: None,
+ max_length: None,
+ }
+ }
+
+ pub const fn min_length(mut self, min_length: usize) -> Self {
+ self.min_length = Some(min_length);
+ self
+ }
+
+ pub const fn max_length(mut self, max_length: usize) -> Self {
+ self.max_length = Some(max_length);
+ self
+ }
+
+ pub const fn schema(self) -> Schema {
+ Schema::Array(self)
+ }
+
+ fn check_length(&self, length: usize) -> Result<(), Error> {
+ if let Some(min_length) = self.min_length {
+ if length < min_length {
+ bail!("array must contain at least {} elements", min_length);
+ }
+ }
+
+ if let Some(max_length) = self.max_length {
+ if length > max_length {
+ bail!("array may only contain {} elements", max_length);
+ }
+ }
+
+ Ok(())
+ }
+}
+
+/// Lookup table to Schema properties
+///
+/// Stores a sorted list of (name, optional, schema) tuples:
+///
+/// name: The name of the property
+/// optional: Set when the property is optional
+/// schema: Property type schema
+///
+/// NOTE: The list has to be storted by name, because we use
+/// a binary search to find items.
+///
+/// This is a workaround unless RUST can const_fn Hash::new()
+pub type SchemaPropertyMap = &'static [(&'static str, bool, &'static Schema)];
+
+#[derive(Debug)]
+pub struct ObjectSchema {
+ pub description: &'static str,
+ pub additional_properties: bool,
+ pub properties: SchemaPropertyMap,
+ pub default_key: Option<&'static str>,
+}
+
+impl ObjectSchema {
+ pub const fn new(description: &'static str, properties: SchemaPropertyMap) -> Self {
+ ObjectSchema {
+ description,
+ properties,
+ additional_properties: false,
+ default_key: None,
+ }
+ }
+
+ pub const fn additional_properties(mut self, additional_properties: bool) -> Self {
+ self.additional_properties = additional_properties;
+ self
+ }
+
+ pub const fn default_key(mut self, key: &'static str) -> Self {
+ self.default_key = Some(key);
+ self
+ }
+
+ pub const fn schema(self) -> Schema {
+ Schema::Object(self)
+ }
+
+ pub fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
+ if let Ok(ind) = self
+ .properties
+ .binary_search_by_key(&key, |(name, _, _)| name)
+ {
+ let (_name, optional, prop_schema) = self.properties[ind];
+ Some((optional, prop_schema))
+ } else {
+ None
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum Schema {
+ Null,
+ Boolean(BooleanSchema),
+ Integer(IntegerSchema),
+ String(StringSchema),
+ Object(ObjectSchema),
+ Array(ArraySchema),
+}
+
+pub enum ApiStringFormat {
+ Enum(&'static [&'static str]),
+ Pattern(&'static ConstRegexPattern),
+ Complex(&'static Schema),
+ VerifyFn(fn(&str) -> Result<(), Error>),
+}
+
+impl std::fmt::Debug for ApiStringFormat {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ ApiStringFormat::VerifyFn(fnptr) => write!(f, "VerifyFn({:p}", fnptr),
+ ApiStringFormat::Enum(strvec) => write!(f, "Enum({:?}", strvec),
+ ApiStringFormat::Pattern(regex) => write!(f, "Pattern({:?}", regex),
+ ApiStringFormat::Complex(schema) => write!(f, "Complex({:?}", schema),
+ }
+ }
+}
+
+pub fn parse_boolean(value_str: &str) -> Result {
+ match value_str.to_lowercase().as_str() {
+ "1" | "on" | "yes" | "true" => Ok(true),
+ "0" | "off" | "no" | "false" => Ok(false),
+ _ => bail!("Unable to parse boolean option."),
+ }
+}
+
+fn parse_property_string(value_str: &str, schema: &Schema) -> Result {
+ println!("Parse property string: {}", value_str);
+
+ let mut param_list: Vec<(String, String)> = vec![];
+
+ match schema {
+ Schema::Object(object_schema) => {
+ for key_val in value_str.split(',').filter(|s| !s.is_empty()) {
+ let kv: Vec<&str> = key_val.splitn(2, '=').collect();
+ if kv.len() == 2 {
+ param_list.push((kv[0].into(), kv[1].into()));
+ } else if let Some(key) = object_schema.default_key {
+ param_list.push((key.into(), kv[0].into()));
+ } else {
+ bail!("Value without key, but schema does not define a default key.");
+ }
+ }
+
+ parse_parameter_strings(¶m_list, &object_schema, true).map_err(Error::from)
+ }
+ Schema::Array(array_schema) => {
+ let mut array: Vec = vec![];
+ for value in value_str.split(',').filter(|s| !s.is_empty()) {
+ match parse_simple_value(value, &array_schema.items) {
+ Ok(res) => array.push(res),
+ Err(err) => bail!("unable to parse array element: {}", err),
+ }
+ }
+ array_schema.check_length(array.len())?;
+
+ Ok(array.into())
+ }
+ _ => bail!("Got unexpetec schema type."),
+ }
+}
+
+pub fn parse_simple_value(value_str: &str, schema: &Schema) -> Result {
+ let value = match schema {
+ Schema::Null => {
+ bail!("internal error - found Null schema.");
+ }
+ Schema::Boolean(_boolean_schema) => {
+ let res = parse_boolean(value_str)?;
+ Value::Bool(res)
+ }
+ Schema::Integer(integer_schema) => {
+ let res: isize = value_str.parse()?;
+ integer_schema.check_constraints(res)?;
+ Value::Number(res.into())
+ }
+ Schema::String(string_schema) => {
+ string_schema.check_constraints(value_str)?;
+ Value::String(value_str.into())
+ }
+ _ => bail!("unable to parse complex (sub) objects."),
+ };
+ Ok(value)
+}
+
+pub fn parse_parameter_strings(
+ data: &[(String, String)],
+ schema: &ObjectSchema,
+ test_required: bool,
+) -> Result {
+ let mut params = json!({});
+
+ let mut errors = ParameterError::new();
+
+ let additional_properties = schema.additional_properties;
+
+ for (key, value) in data {
+ if let Some((_optional, prop_schema)) = schema.lookup(&key) {
+ match prop_schema {
+ Schema::Array(array_schema) => {
+ if params[key] == Value::Null {
+ params[key] = json!([]);
+ }
+ match params[key] {
+ Value::Array(ref mut array) => {
+ match parse_simple_value(value, &array_schema.items) {
+ Ok(res) => array.push(res), // fixme: check_length??
+ Err(err) => {
+ errors.push(format_err!("parameter '{}': {}", key, err))
+ }
+ }
+ }
+ _ => errors.push(format_err!(
+ "parameter '{}': expected array - type missmatch",
+ key
+ )),
+ }
+ }
+ _ => match parse_simple_value(value, prop_schema) {
+ Ok(res) => {
+ if params[key] == Value::Null {
+ params[key] = res;
+ } else {
+ errors.push(format_err!("parameter '{}': duplicate parameter.", key));
+ }
+ }
+ Err(err) => errors.push(format_err!("parameter '{}': {}", key, err)),
+ },
+ }
+ } else if additional_properties {
+ match params[key] {
+ Value::Null => {
+ params[key] = Value::String(value.to_owned());
+ }
+ Value::String(ref old) => {
+ params[key] = Value::Array(vec![
+ Value::String(old.to_owned()),
+ Value::String(value.to_owned()),
+ ]);
+ }
+ Value::Array(ref mut array) => {
+ array.push(Value::String(value.to_string()));
+ }
+ _ => errors.push(format_err!(
+ "parameter '{}': expected array - type missmatch",
+ key
+ )),
+ }
+ } else {
+ errors.push(format_err!(
+ "parameter '{}': schema does not allow additional properties.",
+ key
+ ));
+ }
+ }
+
+ if test_required && errors.len() == 0 {
+ for (name, optional, _prop_schema) in schema.properties {
+ if !(*optional) && params[name] == Value::Null {
+ errors.push(format_err!(
+ "parameter '{}': parameter is missing and it is not optional.",
+ name
+ ));
+ }
+ }
+ }
+
+ if !errors.is_empty() {
+ Err(errors)
+ } else {
+ Ok(params)
+ }
+}
+
+pub fn parse_query_string(
+ query: &str,
+ schema: &ObjectSchema,
+ test_required: bool,
+) -> Result {
+ let param_list: Vec<(String, String)> = form_urlencoded::parse(query.as_bytes())
+ .into_owned()
+ .collect();
+
+ parse_parameter_strings(¶m_list, schema, test_required)
+}
+
+pub fn verify_json(data: &Value, schema: &Schema) -> Result<(), Error> {
+ match schema {
+ Schema::Object(object_schema) => {
+ verify_json_object(data, &object_schema)?;
+ }
+ Schema::Array(array_schema) => {
+ verify_json_array(data, &array_schema)?;
+ }
+ Schema::Null => {
+ if !data.is_null() {
+ bail!("Expected Null, but value is not Null.");
+ }
+ }
+ Schema::Boolean(boolean_schema) => verify_json_boolean(data, &boolean_schema)?,
+ Schema::Integer(integer_schema) => verify_json_integer(data, &integer_schema)?,
+ Schema::String(string_schema) => verify_json_string(data, &string_schema)?,
+ }
+ Ok(())
+}
+
+pub fn verify_json_string(data: &Value, schema: &StringSchema) -> Result<(), Error> {
+ if let Some(value) = data.as_str() {
+ schema.check_constraints(value)
+ } else {
+ bail!("Expected string value.");
+ }
+}
+
+pub fn verify_json_boolean(data: &Value, _schema: &BooleanSchema) -> Result<(), Error> {
+ if !data.is_boolean() {
+ bail!("Expected boolean value.");
+ }
+ Ok(())
+}
+
+pub fn verify_json_integer(data: &Value, schema: &IntegerSchema) -> Result<(), Error> {
+ if let Some(value) = data.as_i64() {
+ schema.check_constraints(value as isize)
+ } else {
+ bail!("Expected integer value.");
+ }
+}
+
+pub fn verify_json_array(data: &Value, schema: &ArraySchema) -> Result<(), Error> {
+ let list = match data {
+ Value::Array(ref list) => list,
+ Value::Object(_) => bail!("Expected array - got object."),
+ _ => bail!("Expected array - got scalar value."),
+ };
+
+ schema.check_length(list.len())?;
+
+ for item in list {
+ verify_json(item, &schema.items)?;
+ }
+
+ Ok(())
+}
+
+pub fn verify_json_object(data: &Value, schema: &ObjectSchema) -> Result<(), Error> {
+ let map = match data {
+ Value::Object(ref map) => map,
+ Value::Array(_) => bail!("Expected object - got array."),
+ _ => bail!("Expected object - got scalar value."),
+ };
+
+ let additional_properties = schema.additional_properties;
+
+ for (key, value) in map {
+ if let Some((_optional, prop_schema)) = schema.lookup(&key) {
+ match prop_schema {
+ Schema::Object(object_schema) => {
+ verify_json_object(value, object_schema)?;
+ }
+ Schema::Array(array_schema) => {
+ verify_json_array(value, array_schema)?;
+ }
+ _ => verify_json(value, prop_schema)?,
+ }
+ } else if !additional_properties {
+ bail!(
+ "property '{}': schema does not allow additional properties.",
+ key
+ );
+ }
+ }
+
+ for (name, optional, _prop_schema) in schema.properties {
+ if !(*optional) && data[name] == Value::Null {
+ bail!(
+ "property '{}': property is missing and it is not optional.",
+ name
+ );
+ }
+ }
+
+ Ok(())
+}
+
+#[test]
+fn test_schema1() {
+ let schema = Schema::Object(ObjectSchema {
+ description: "TEST",
+ additional_properties: false,
+ properties: &[],
+ default_key: None,
+ });
+
+ println!("TEST Schema: {:?}", schema);
+}
+
+#[test]
+fn test_query_string() {
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[("name", false, &StringSchema::new("Name.").schema())],
+ );
+
+ let res = parse_query_string("", &SCHEMA, true);
+ assert!(res.is_err());
+ }
+
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[("name", true, &StringSchema::new("Name.").schema())],
+ );
+
+ let res = parse_query_string("", &SCHEMA, true);
+ assert!(res.is_ok());
+ }
+
+ // TEST min_length and max_length
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "name",
+ true,
+ &StringSchema::new("Name.")
+ .min_length(5)
+ .max_length(10)
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("name=abcd", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("name=abcde", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("name=abcdefghijk", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("name=abcdefghij", &SCHEMA, true);
+ assert!(res.is_ok());
+ }
+
+ // TEST regex pattern
+ const_regex! {
+ TEST_REGEX = "test";
+ TEST2_REGEX = "^test$";
+ }
+
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "name",
+ false,
+ &StringSchema::new("Name.")
+ .format(&ApiStringFormat::Pattern(&TEST_REGEX))
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("name=abcd", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("name=ateststring", &SCHEMA, true);
+ assert!(res.is_ok());
+ }
+
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "name",
+ false,
+ &StringSchema::new("Name.")
+ .format(&ApiStringFormat::Pattern(&TEST2_REGEX))
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("name=ateststring", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("name=test", &SCHEMA, true);
+ assert!(res.is_ok());
+ }
+
+ // TEST string enums
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "name",
+ false,
+ &StringSchema::new("Name.")
+ .format(&ApiStringFormat::Enum(&["ev1", "ev2"]))
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("name=noenum", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("name=ev1", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("name=ev2", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("name=ev3", &SCHEMA, true);
+ assert!(res.is_err());
+ }
+}
+
+#[test]
+fn test_query_integer() {
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[("count", false, &IntegerSchema::new("Count.").schema())],
+ );
+
+ let res = parse_query_string("", &SCHEMA, true);
+ assert!(res.is_err());
+ }
+
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "count",
+ true,
+ &IntegerSchema::new("Count.")
+ .minimum(-3)
+ .maximum(50)
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("count=abc", &SCHEMA, false);
+ assert!(res.is_err());
+
+ let res = parse_query_string("count=30", &SCHEMA, false);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("count=-1", &SCHEMA, false);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("count=300", &SCHEMA, false);
+ assert!(res.is_err());
+
+ let res = parse_query_string("count=-30", &SCHEMA, false);
+ assert!(res.is_err());
+
+ let res = parse_query_string("count=50", &SCHEMA, false);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("count=-3", &SCHEMA, false);
+ assert!(res.is_ok());
+ }
+}
+
+#[test]
+fn test_query_boolean() {
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[("force", false, &BooleanSchema::new("Force.").schema())],
+ );
+
+ let res = parse_query_string("", &SCHEMA, true);
+ assert!(res.is_err());
+ }
+
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[("force", true, &BooleanSchema::new("Force.").schema())],
+ );
+
+ let res = parse_query_string("", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("a=b", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("force", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("force=yes", &SCHEMA, true);
+ assert!(res.is_ok());
+ let res = parse_query_string("force=1", &SCHEMA, true);
+ assert!(res.is_ok());
+ let res = parse_query_string("force=On", &SCHEMA, true);
+ assert!(res.is_ok());
+ let res = parse_query_string("force=TRUE", &SCHEMA, true);
+ assert!(res.is_ok());
+ let res = parse_query_string("force=TREU", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("force=NO", &SCHEMA, true);
+ assert!(res.is_ok());
+ let res = parse_query_string("force=0", &SCHEMA, true);
+ assert!(res.is_ok());
+ let res = parse_query_string("force=off", &SCHEMA, true);
+ assert!(res.is_ok());
+ let res = parse_query_string("force=False", &SCHEMA, true);
+ assert!(res.is_ok());
+ }
+}
+
+#[test]
+fn test_verify_function() {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "p1",
+ false,
+ &StringSchema::new("P1")
+ .format(&ApiStringFormat::VerifyFn(|value| {
+ if value == "test" {
+ return Ok(());
+ };
+ bail!("format error");
+ }))
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("p1=tes", &SCHEMA, true);
+ assert!(res.is_err());
+ let res = parse_query_string("p1=test", &SCHEMA, true);
+ assert!(res.is_ok());
+}
+
+#[test]
+fn test_verify_complex_object() {
+ const NIC_MODELS: ApiStringFormat = ApiStringFormat::Enum(&["e1000", "virtio"]);
+
+ const PARAM_SCHEMA: Schema = ObjectSchema::new(
+ "Properties.",
+ &[
+ (
+ "enable",
+ true,
+ &BooleanSchema::new("Enable device.").schema(),
+ ),
+ (
+ "model",
+ false,
+ &StringSchema::new("Ethernet device Model.")
+ .format(&NIC_MODELS)
+ .schema(),
+ ),
+ ],
+ )
+ .default_key("model")
+ .schema();
+
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "net0",
+ false,
+ &StringSchema::new("First Network device.")
+ .format(&ApiStringFormat::Complex(&PARAM_SCHEMA))
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("test=abc", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("net0=model=abc", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("net0=model=virtio", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("net0=model=virtio,enable=1", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("net0=virtio,enable=no", &SCHEMA, true);
+ assert!(res.is_ok());
+}
+
+#[test]
+fn test_verify_complex_array() {
+ {
+ const PARAM_SCHEMA: Schema =
+ ArraySchema::new("Integer List.", &IntegerSchema::new("Soemething").schema()).schema();
+
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "list",
+ false,
+ &StringSchema::new("A list on integers, comma separated.")
+ .format(&ApiStringFormat::Complex(&PARAM_SCHEMA))
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("list=", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("list=abc", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("list=1", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("list=2,3,4,5", &SCHEMA, true);
+ assert!(res.is_ok());
+ }
+
+ {
+ const PARAM_SCHEMA: Schema =
+ ArraySchema::new("Integer List.", &IntegerSchema::new("Soemething").schema())
+ .min_length(1)
+ .max_length(3)
+ .schema();
+
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "list",
+ false,
+ &StringSchema::new("A list on integers, comma separated.")
+ .format(&ApiStringFormat::Complex(&PARAM_SCHEMA))
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("list=", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("list=1,2,3", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("list=2,3,4,5", &SCHEMA, true);
+ assert!(res.is_err());
+ }
+}
diff --git a/proxmox-tools/src/as_any.rs b/proxmox-tools/src/as_any.rs
new file mode 100644
index 00000000..39eaedc8
--- /dev/null
+++ b/proxmox-tools/src/as_any.rs
@@ -0,0 +1,14 @@
+use std::any::Any;
+
+/// An easy way to convert types to Any
+///
+/// Mostly useful to downcast trait objects (see RpcEnvironment).
+pub trait AsAny {
+ fn as_any(&self) -> &dyn Any;
+}
+
+impl AsAny for T {
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
diff --git a/proxmox-tools/src/lib.rs b/proxmox-tools/src/lib.rs
index 76688db0..96309c6a 100644
--- a/proxmox-tools/src/lib.rs
+++ b/proxmox-tools/src/lib.rs
@@ -3,6 +3,7 @@
use failure::*;
use lazy_static::lazy_static;
+pub mod as_any;
pub mod borrow;
pub mod common_regex;
pub mod fd;
@@ -16,6 +17,9 @@ pub mod vec;
#[doc(inline)]
pub use uuid::Uuid;
+#[doc(inline)]
+pub use as_any::AsAny;
+
/// An identity (nop) macro. Used by the `#[sortable]` proc macro.
#[macro_export]
macro_rules! identity {