use const api definitions

This commit is contained in:
Dietmar Maurer 2019-11-21 09:36:41 +01:00
parent e4a5ab8ddb
commit 255f378a1b
40 changed files with 2368 additions and 1974 deletions

View File

@ -10,19 +10,20 @@ mod access;
use crate::api_schema::router::*; use crate::api_schema::router::*;
pub fn router() -> Router { const NODES_ROUTER: Router = Router::new()
.match_all("node", &node::ROUTER);
let nodes = Router::new() pub const SUBDIRS: SubdirMap = &[
.match_all("node", node::router()); ("access", &access::ROUTER),
("admin", &admin::ROUTER),
("backup", &backup::ROUTER),
("config", &config::ROUTER),
("nodes", &NODES_ROUTER),
("reader", &reader::ROUTER),
("subscription", &subscription::ROUTER),
("version", &version::ROUTER),
];
Router::new() pub const ROUTER: Router = Router::new()
.subdir("access", access::router()) .get(&list_subdirs_api_method!(SUBDIRS))
.subdir("admin", admin::router()) .subdirs(SUBDIRS);
.subdir("backup", backup::router())
.subdir("reader", reader::router())
.subdir("config", config::router())
.subdir("nodes", nodes)
.subdir("subscription", subscription::router())
.subdir("version", version::router())
.list_subdirs()
}

View File

@ -66,31 +66,57 @@ fn create_ticket(
} }
} }
pub fn router() -> Router { const SUBDIRS: SubdirMap = &[
Router::new() (
.subdir( "ticket", &Router::new()
"ticket", .post(
Router::new() &ApiMethod::new(
.post( &ApiHandler::Sync(&create_ticket),
ApiMethod::new( &ObjectSchema::new(
create_ticket, "Create or verify authentication ticket.",
ObjectSchema::new("Create or verify authentication ticket.") &[
.required( (
"username", "username",
StringSchema::new("User name.") false,
&StringSchema::new("User name.")
.max_length(64) .max_length(64)
) .schema()
.required( ),
(
"password", "password",
StringSchema::new("The secret password. This can also be a valid ticket.") false,
) &StringSchema::new("The secret password. This can also be a valid ticket.")
).returns( .schema()
ObjectSchema::new("Returns authentication ticket with additional infos.") ),
.required("username", StringSchema::new("User name.")) ],
.required("ticket", StringSchema::new("Auth ticket.")) )
.required("CSRFPreventionToken", StringSchema::new("Cross Site Request Forgery Prevention Token.")) ).returns(
).protected(true) &ObjectSchema::new(
) "Returns authentication ticket with additional infos.",
) &[
.list_subdirs() (
} "username",
false,
&StringSchema::new("User name.").schema()
),
(
"ticket",
false,
&StringSchema::new("Auth ticket.").schema()
),
(
"CSRFPreventionToken",
false,
&StringSchema::new("Cross Site Request Forgery Prevention Token.")
.schema()
),
],
).schema()
).protected(true)
)
)
];
pub const ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SUBDIRS))
.subdirs(SUBDIRS);

View File

@ -2,8 +2,10 @@ use crate::api_schema::router::*;
pub mod datastore; pub mod datastore;
pub fn router() -> Router { const SUBDIRS: SubdirMap = &[
Router::new() ("datastore", &datastore::ROUTER)
.subdir("datastore", datastore::router()) ];
.list_subdirs()
} pub const ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SUBDIRS))
.subdirs(SUBDIRS);

View File

@ -10,7 +10,6 @@ use serde_json::{json, Value};
use std::collections::{HashSet, HashMap}; use std::collections::{HashSet, HashMap};
use chrono::{DateTime, Datelike, TimeZone, Local}; use chrono::{DateTime, Datelike, TimeZone, Local};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
use proxmox::tools::{try_block, fs::file_get_contents, fs::file_set_contents}; use proxmox::tools::{try_block, fs::file_get_contents, fs::file_set_contents};
@ -237,19 +236,61 @@ fn status(
})) }))
} }
fn api_method_status() -> ApiMethod { #[macro_export]
ApiMethod::new( macro_rules! add_common_prune_prameters {
status, ($( $list:tt )*) => {
add_common_prune_prameters( [
ObjectSchema::new("Get datastore status.") (
.required( "keep-last",
"store", true,
StringSchema::new("Datastore name.") &IntegerSchema::new("Number of backups to keep.")
) .minimum(1)
) .schema()
) ),
(
"keep-daily",
true,
&IntegerSchema::new("Number of daily backups to keep.")
.minimum(1)
.schema()
),
(
"keep-weekly",
true,
&IntegerSchema::new("Number of weekly backups to keep.")
.minimum(1)
.schema()
),
(
"keep-monthly",
true,
&IntegerSchema::new("Number of monthly backups to keep.")
.minimum(1)
.schema()
),
(
"keep-yearly",
true,
&IntegerSchema::new("Number of yearly backups to keep.")
.minimum(1)
.schema()
),
$( $list )*
]
}
} }
const API_METHOD_STATUS: ApiMethod = ApiMethod::new(
&ApiHandler::Sync(&status),
&ObjectSchema::new(
"Get datastore status.",
&add_common_prune_prameters!(
("store", false, &StringSchema::new("Datastore name.").schema()),
),
)
);
fn prune( fn prune(
param: Value, param: Value,
_info: &ApiMethod, _info: &ApiMethod,
@ -341,50 +382,17 @@ fn prune(
Ok(json!(null)) Ok(json!(null))
} }
pub fn add_common_prune_prameters(schema: ObjectSchema) -> ObjectSchema { const API_METHOD_PRUNE: ApiMethod = ApiMethod::new(
&ApiHandler::Sync(&prune),
schema &ObjectSchema::new(
.optional( "Prune the datastore.",
"keep-last", &add_common_prune_prameters!(
IntegerSchema::new("Number of backups to keep.") ("store", false, &StringSchema::new("Datastore name.").schema()),
.minimum(1) ("backup-type", false, &BACKUP_TYPE_SCHEMA),
) ("backup-id", false, &BACKUP_ID_SCHEMA),
.optional(
"keep-daily",
IntegerSchema::new("Number of daily backups to keep.")
.minimum(1)
)
.optional(
"keep-weekly",
IntegerSchema::new("Number of weekly backups to keep.")
.minimum(1)
)
.optional(
"keep-monthly",
IntegerSchema::new("Number of monthly backups to keep.")
.minimum(1)
)
.optional(
"keep-yearly",
IntegerSchema::new("Number of yearly backups to keep.")
.minimum(1)
)
}
fn api_method_prune() -> ApiMethod {
ApiMethod::new(
prune,
add_common_prune_prameters(
ObjectSchema::new("Prune the datastore.")
.required(
"store",
StringSchema::new("Datastore name.")
)
.required("backup-type", BACKUP_TYPE_SCHEMA.clone())
.required("backup-id", BACKUP_ID_SCHEMA.clone())
) )
) )
} );
fn start_garbage_collection( fn start_garbage_collection(
param: Value, param: Value,
@ -410,13 +418,13 @@ fn start_garbage_collection(
Ok(json!(upid_str)) Ok(json!(upid_str))
} }
pub fn api_method_start_garbage_collection() -> ApiMethod { pub const API_METHOD_START_GARBAGE_COLLECTION: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&start_garbage_collection),
start_garbage_collection, &ObjectSchema::new(
ObjectSchema::new("Start garbage collection.") "Start garbage collection.",
.required("store", StringSchema::new("Datastore name.")) &[ ("store", false, &StringSchema::new("Datastore name.").schema()) ]
) )
} );
fn garbage_collection_status( fn garbage_collection_status(
param: Value, param: Value,
@ -435,13 +443,13 @@ fn garbage_collection_status(
Ok(serde_json::to_value(&status)?) Ok(serde_json::to_value(&status)?)
} }
pub fn api_method_garbage_collection_status() -> ApiMethod { pub const API_METHOD_GARBAGE_COLLECTION_STATUS: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&garbage_collection_status),
garbage_collection_status, &ObjectSchema::new(
ObjectSchema::new("Garbage collection status.") "Garbage collection status.",
.required("store", StringSchema::new("Datastore name.")) &[ ("store", false, &StringSchema::new("Datastore name.").schema()) ]
) )
} );
fn get_datastore_list( fn get_datastore_list(
_param: Value, _param: Value,
@ -459,7 +467,7 @@ fn download_file(
_parts: Parts, _parts: Parts,
_req_body: Body, _req_body: Body,
param: Value, param: Value,
_info: &ApiAsyncMethod, _info: &ApiMethod,
_rpcenv: Box<dyn RpcEnvironment>, _rpcenv: Box<dyn RpcEnvironment>,
) -> Result<BoxFut, Error> { ) -> Result<BoxFut, Error> {
@ -500,23 +508,28 @@ fn download_file(
Ok(Box::new(response_future)) Ok(Box::new(response_future))
} }
pub fn api_method_download_file() -> ApiAsyncMethod { pub const API_METHOD_DOWNLOAD_FILE: ApiMethod = ApiMethod::new(
ApiAsyncMethod::new( &ApiHandler::Async(&download_file),
download_file, &ObjectSchema::new(
ObjectSchema::new("Download single raw file from backup snapshot.") "Download single raw file from backup snapshot.",
.required("store", StringSchema::new("Datastore name.")) &[
.required("backup-type", BACKUP_TYPE_SCHEMA.clone()) ("store", false, &StringSchema::new("Datastore name.").schema()),
.required("backup-id", BACKUP_ID_SCHEMA.clone()) ("backup-type", false, &BACKUP_TYPE_SCHEMA),
.required("backup-time", BACKUP_TIME_SCHEMA.clone()) ("backup-id", false, &BACKUP_ID_SCHEMA),
.required("file-name", StringSchema::new("Raw file name.").format(FILENAME_FORMAT.clone())) ("backup-time", false, &BACKUP_TIME_SCHEMA),
("file-name", false, &StringSchema::new("Raw file name.")
.format(&FILENAME_FORMAT)
.schema()
),
],
) )
} );
fn upload_backup_log( fn upload_backup_log(
_parts: Parts, _parts: Parts,
req_body: Body, req_body: Body,
param: Value, param: Value,
_info: &ApiAsyncMethod, _info: &ApiMethod,
_rpcenv: Box<dyn RpcEnvironment>, _rpcenv: Box<dyn RpcEnvironment>,
) -> Result<BoxFut, Error> { ) -> Result<BoxFut, Error> {
@ -565,98 +578,122 @@ fn upload_backup_log(
Ok(Box::new(resp)) Ok(Box::new(resp))
} }
pub fn api_method_upload_backup_log() -> ApiAsyncMethod { pub const API_METHOD_UPLOAD_BACKUP_LOG: ApiMethod = ApiMethod::new(
ApiAsyncMethod::new( &ApiHandler::Async(&upload_backup_log),
upload_backup_log, &ObjectSchema::new(
ObjectSchema::new("Download single raw file from backup snapshot.") "Download single raw file from backup snapshot.",
.required("store", StringSchema::new("Datastore name.")) &[
.required("backup-type", BACKUP_TYPE_SCHEMA.clone()) ("store", false, &StringSchema::new("Datastore name.").schema()),
.required("backup-id", BACKUP_ID_SCHEMA.clone()) ("backup-type", false, &BACKUP_TYPE_SCHEMA),
.required("backup-time", BACKUP_TIME_SCHEMA.clone()) ("backup-id", false, &BACKUP_ID_SCHEMA),
("backup-time", false, &BACKUP_TIME_SCHEMA),
],
) )
} );
pub fn router() -> Router { const STORE_SCHEMA: Schema = StringSchema::new("Datastore name.").schema();
let store_schema: Arc<Schema> = Arc::new( const DATASTORE_INFO_SUBDIRS: SubdirMap = &[
StringSchema::new("Datastore name.").into() (
); "download",
&Router::new()
let datastore_info = Router::new() .download(&API_METHOD_DOWNLOAD_FILE)
.subdir( ),
"download", (
Router::new() "files",
.download(api_method_download_file()) &Router::new()
) .get(
.subdir( &ApiMethod::new(
"upload-backup-log", &ApiHandler::Sync(&list_snapshot_files),
Router::new() &ObjectSchema::new(
.upload(api_method_upload_backup_log()) "List snapshot files.",
) &[
.subdir( ("store", false, &STORE_SCHEMA),
"gc", ("backup-type", false, &BACKUP_TYPE_SCHEMA),
Router::new() ("backup-id", false, &BACKUP_ID_SCHEMA),
.get(api_method_garbage_collection_status()) ("backup-time", false, &BACKUP_TIME_SCHEMA),
.post(api_method_start_garbage_collection())) ],
.subdir(
"files",
Router::new()
.get(
ApiMethod::new(
list_snapshot_files,
ObjectSchema::new("List snapshot files.")
.required("store", store_schema.clone())
.required("backup-type", BACKUP_TYPE_SCHEMA.clone())
.required("backup-id", BACKUP_ID_SCHEMA.clone())
.required("backup-time", BACKUP_TIME_SCHEMA.clone())
) )
) )
) )
.subdir( ),
"groups", (
Router::new() "gc",
.get(ApiMethod::new( &Router::new()
list_groups, .get(&API_METHOD_GARBAGE_COLLECTION_STATUS)
ObjectSchema::new("List backup groups.") .post(&API_METHOD_START_GARBAGE_COLLECTION)
.required("store", store_schema.clone())))) ),
.subdir( (
"snapshots", "groups",
Router::new() &Router::new()
.get( .get(
ApiMethod::new( &ApiMethod::new(
list_snapshots, &ApiHandler::Sync(&list_groups),
ObjectSchema::new("List backup groups.") &ObjectSchema::new(
.required("store", store_schema.clone()) "List backup groups.",
.optional("backup-type", BACKUP_TYPE_SCHEMA.clone()) &[ ("store", false, &STORE_SCHEMA) ],
.optional("backup-id", BACKUP_ID_SCHEMA.clone())
) )
) )
.delete( )
ApiMethod::new( ),
delete_snapshots, (
ObjectSchema::new("Delete backup snapshot.") "prune",
.required("store", store_schema.clone()) &Router::new()
.required("backup-type", BACKUP_TYPE_SCHEMA.clone()) .post(&API_METHOD_PRUNE)
.required("backup-id", BACKUP_ID_SCHEMA.clone()) ),
.required("backup-time", BACKUP_TIME_SCHEMA.clone()) (
) "snapshots",
&Router::new()
.get(
&ApiMethod::new(
&ApiHandler::Sync(&list_snapshots),
&ObjectSchema::new(
"List backup groups.",
&[
("store", false, &STORE_SCHEMA),
("backup-type", true, &BACKUP_TYPE_SCHEMA),
("backup-id", true, &BACKUP_ID_SCHEMA),
],
)
) )
) )
.subdir( .delete(
"prune", &ApiMethod::new(
Router::new() &ApiHandler::Sync(&delete_snapshots),
.post(api_method_prune()) &ObjectSchema::new(
) "Delete backup snapshot.",
.subdir( &[
"status", ("store", false, &STORE_SCHEMA),
Router::new() ("backup-type", false, &BACKUP_TYPE_SCHEMA),
.get(api_method_status()) ("backup-id", false, &BACKUP_ID_SCHEMA),
) ("backup-time", false, &BACKUP_TIME_SCHEMA),
.list_subdirs(); ],
)
)
)
),
(
"status",
&Router::new()
.get(&API_METHOD_STATUS)
),
(
"upload-backup-log",
&Router::new()
.upload(&API_METHOD_UPLOAD_BACKUP_LOG)
),
];
Router::new() const DATASTORE_INFO_ROUTER: Router = Router::new()
.get(ApiMethod::new( .get(&list_subdirs_api_method!(DATASTORE_INFO_SUBDIRS))
get_datastore_list, .subdirs(DATASTORE_INFO_SUBDIRS);
ObjectSchema::new("Directory index.")))
.match_all("store", datastore_info)
} pub const ROUTER: Router = Router::new()
.get(
&ApiMethod::new(
&ApiHandler::Sync(&get_datastore_list),
&ObjectSchema::new("Directory index.", &[])
)
)
.match_all("store", &DATASTORE_INFO_ROUTER);

View File

@ -1,7 +1,4 @@
use failure::*; use failure::*;
use lazy_static::lazy_static;
//use std::sync::Arc;
use futures::*; use futures::*;
use hyper::header::{HeaderValue, UPGRADE}; use hyper::header::{HeaderValue, UPGRADE};
@ -24,28 +21,28 @@ use environment::*;
mod upload_chunk; mod upload_chunk;
use upload_chunk::*; use upload_chunk::*;
pub fn router() -> Router { pub const ROUTER: Router = Router::new()
Router::new() .upgrade(&API_METHOD_UPGRADE_BACKUP);
.upgrade(api_method_upgrade_backup())
}
pub fn api_method_upgrade_backup() -> ApiAsyncMethod { pub const API_METHOD_UPGRADE_BACKUP: ApiMethod = ApiMethod::new(
ApiAsyncMethod::new( &ApiHandler::Async(&upgrade_to_backup_protocol),
upgrade_to_backup_protocol, &ObjectSchema::new(
ObjectSchema::new(concat!("Upgraded to backup protocol ('", PROXMOX_BACKUP_PROTOCOL_ID_V1!(), "').")) concat!("Upgraded to backup protocol ('", PROXMOX_BACKUP_PROTOCOL_ID_V1!(), "')."),
.required("store", StringSchema::new("Datastore name.")) &[
.required("backup-type", BACKUP_TYPE_SCHEMA.clone()) ("store", false, &StringSchema::new("Datastore name.").schema()),
.required("backup-id", BACKUP_ID_SCHEMA.clone()) ("backup-type", false, &BACKUP_TYPE_SCHEMA),
.required("backup-time", BACKUP_TIME_SCHEMA.clone()) ("backup-id", false, &BACKUP_ID_SCHEMA),
.optional("debug", BooleanSchema::new("Enable verbose debug logging.")) ("backup-time", false, &BACKUP_TIME_SCHEMA),
("debug", true, &BooleanSchema::new("Enable verbose debug logging.").schema()),
],
) )
} );
fn upgrade_to_backup_protocol( fn upgrade_to_backup_protocol(
parts: Parts, parts: Parts,
req_body: Body, req_body: Body,
param: Value, param: Value,
_info: &ApiAsyncMethod, _info: &ApiMethod,
rpcenv: Box<dyn RpcEnvironment>, rpcenv: Box<dyn RpcEnvironment>,
) -> Result<BoxFut, Error> { ) -> Result<BoxFut, Error> {
@ -99,7 +96,7 @@ fn upgrade_to_backup_protocol(
env.log(format!("starting new backup on datastore '{}': {:?}", store, path)); env.log(format!("starting new backup on datastore '{}': {:?}", store, path));
let service = H2Service::new(env.clone(), worker.clone(), &BACKUP_ROUTER, debug); let service = H2Service::new(env.clone(), worker.clone(), &BACKUP_API_ROUTER, debug);
let abort_future = worker.abort_future(); let abort_future = worker.abort_future();
@ -162,67 +159,67 @@ fn upgrade_to_backup_protocol(
Ok(Box::new(futures::future::ok(response))) Ok(Box::new(futures::future::ok(response)))
} }
lazy_static!{ pub const BACKUP_API_SUBDIRS: SubdirMap = &[
static ref BACKUP_ROUTER: Router = backup_api(); (
} "blob", &Router::new()
.upload(&API_METHOD_UPLOAD_BLOB)
pub fn backup_api() -> Router { ),
Router::new() (
.subdir( "dynamic_chunk", &Router::new()
"blob", Router::new() .upload(&API_METHOD_UPLOAD_DYNAMIC_CHUNK)
.upload(api_method_upload_blob()) ),
) (
.subdir( "dynamic_close", &Router::new()
"dynamic_chunk", Router::new() .post(&API_METHOD_CLOSE_DYNAMIC_INDEX)
.upload(api_method_upload_dynamic_chunk()) ),
) (
.subdir( "dynamic_index", &Router::new()
"dynamic_index", Router::new() .download(&API_METHOD_DYNAMIC_CHUNK_INDEX)
.download(api_method_dynamic_chunk_index()) .post(&API_METHOD_CREATE_DYNAMIC_INDEX)
.post(api_method_create_dynamic_index()) .put(&API_METHOD_DYNAMIC_APPEND)
.put(api_method_dynamic_append()) ),
) (
.subdir( "finish", &Router::new()
"dynamic_close", Router::new() .post(
.post(api_method_close_dynamic_index()) &ApiMethod::new(
) &ApiHandler::Sync(&finish_backup),
.subdir( &ObjectSchema::new("Mark backup as finished.", &[])
"fixed_chunk", Router::new()
.upload(api_method_upload_fixed_chunk())
)
.subdir(
"fixed_index", Router::new()
.download(api_method_fixed_chunk_index())
.post(api_method_create_fixed_index())
.put(api_method_fixed_append())
)
.subdir(
"fixed_close", Router::new()
.post(api_method_close_fixed_index())
)
.subdir(
"finish", Router::new()
.post(
ApiMethod::new(
finish_backup,
ObjectSchema::new("Mark backup as finished.")
)
) )
) )
.subdir( ),
"speedtest", Router::new() (
.upload(api_method_upload_speedtest()) "fixed_chunk", &Router::new()
) .upload(&API_METHOD_UPLOAD_FIXED_CHUNK)
.list_subdirs() ),
} (
"fixed_close", &Router::new()
.post(&API_METHOD_CLOSE_FIXED_INDEX)
),
(
"fixed_index", &Router::new()
.download(&API_METHOD_FIXED_CHUNK_INDEX)
.post(&API_METHOD_CREATE_FIXED_INDEX)
.put(&API_METHOD_FIXED_APPEND)
),
(
"speedtest", &Router::new()
.upload(&API_METHOD_UPLOAD_SPEEDTEST)
),
];
pub fn api_method_create_dynamic_index() -> ApiMethod { pub const BACKUP_API_ROUTER: Router = Router::new()
ApiMethod::new( .get(&list_subdirs_api_method!(BACKUP_API_SUBDIRS))
create_dynamic_index, .subdirs(BACKUP_API_SUBDIRS);
ObjectSchema::new("Create dynamic chunk index file.")
.required("archive-name", crate::api2::types::BACKUP_ARCHIVE_NAME_SCHEMA.clone()) pub const API_METHOD_CREATE_DYNAMIC_INDEX: ApiMethod = ApiMethod::new(
&ApiHandler::Sync(&create_dynamic_index),
&ObjectSchema::new(
"Create dynamic chunk index file.",
&[
("archive-name", false, &crate::api2::types::BACKUP_ARCHIVE_NAME_SCHEMA),
],
) )
} );
fn create_dynamic_index( fn create_dynamic_index(
param: Value, param: Value,
@ -250,16 +247,19 @@ fn create_dynamic_index(
Ok(json!(wid)) Ok(json!(wid))
} }
pub fn api_method_create_fixed_index() -> ApiMethod { pub const API_METHOD_CREATE_FIXED_INDEX: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&create_fixed_index),
create_fixed_index, &ObjectSchema::new(
ObjectSchema::new("Create fixed chunk index file.") "Create fixed chunk index file.",
.required("archive-name", crate::api2::types::BACKUP_ARCHIVE_NAME_SCHEMA.clone()) &[
.required("size", IntegerSchema::new("File size.") ("archive-name", false, &crate::api2::types::BACKUP_ARCHIVE_NAME_SCHEMA),
.minimum(1) ("size", false, &IntegerSchema::new("File size.")
) .minimum(1)
.schema()
),
],
) )
} );
fn create_fixed_index( fn create_fixed_index(
param: Value, param: Value,
@ -292,25 +292,37 @@ fn create_fixed_index(
Ok(json!(wid)) Ok(json!(wid))
} }
pub fn api_method_dynamic_append() -> ApiMethod { pub const API_METHOD_DYNAMIC_APPEND: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&dynamic_append),
dynamic_append, &ObjectSchema::new(
ObjectSchema::new("Append chunk to dynamic index writer.") "Append chunk to dynamic index writer.",
.required("wid", IntegerSchema::new("Dynamic writer ID.") &[
.minimum(1) (
.maximum(256) "wid",
) false,
.required("digest-list", ArraySchema::new( &IntegerSchema::new("Dynamic writer ID.")
"Chunk digest list.", CHUNK_DIGEST_SCHEMA.clone()) .minimum(1)
) .maximum(256)
.required("offset-list", ArraySchema::new( .schema()
"Chunk offset list.", ),
IntegerSchema::new("Corresponding chunk offsets.") (
.minimum(0) "digest-list",
.into()) false,
) &ArraySchema::new("Chunk digest list.", &CHUNK_DIGEST_SCHEMA).schema()
),
(
"offset-list",
false,
&ArraySchema::new(
"Chunk offset list.",
&IntegerSchema::new("Corresponding chunk offsets.")
.minimum(0)
.schema()
).schema()
),
],
) )
} );
fn dynamic_append ( fn dynamic_append (
param: Value, param: Value,
@ -344,25 +356,37 @@ fn dynamic_append (
Ok(Value::Null) Ok(Value::Null)
} }
pub fn api_method_fixed_append() -> ApiMethod { pub const API_METHOD_FIXED_APPEND: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&fixed_append),
fixed_append, &ObjectSchema::new(
ObjectSchema::new("Append chunk to fixed index writer.") "Append chunk to fixed index writer.",
.required("wid", IntegerSchema::new("Fixed writer ID.") &[
.minimum(1) (
.maximum(256) "wid",
) false,
.required("digest-list", ArraySchema::new( &IntegerSchema::new("Fixed writer ID.")
"Chunk digest list.", CHUNK_DIGEST_SCHEMA.clone()) .minimum(1)
) .maximum(256)
.required("offset-list", ArraySchema::new( .schema()
"Chunk offset list.", ),
IntegerSchema::new("Corresponding chunk offsets.") (
.minimum(0) "digest-list",
.into()) false,
&ArraySchema::new("Chunk digest list.", &CHUNK_DIGEST_SCHEMA).schema()
),
(
"offset-list",
false,
&ArraySchema::new(
"Chunk offset list.",
&IntegerSchema::new("Corresponding chunk offsets.")
.minimum(0)
.schema()
).schema()
) )
],
) )
} );
fn fixed_append ( fn fixed_append (
param: Value, param: Value,
@ -396,23 +420,37 @@ fn fixed_append (
Ok(Value::Null) Ok(Value::Null)
} }
pub fn api_method_close_dynamic_index() -> ApiMethod { pub const API_METHOD_CLOSE_DYNAMIC_INDEX: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&close_dynamic_index),
close_dynamic_index, &ObjectSchema::new(
ObjectSchema::new("Close dynamic index writer.") "Close dynamic index writer.",
.required("wid", IntegerSchema::new("Dynamic writer ID.") &[
.minimum(1) (
.maximum(256) "wid",
) false,
.required("chunk-count", IntegerSchema::new("Chunk count. This is used to verify that the server got all chunks.") &IntegerSchema::new("Dynamic writer ID.")
.minimum(1) .minimum(1)
) .maximum(256)
.required("size", IntegerSchema::new("File size. This is used to verify that the server got all data.") .schema()
.minimum(1) ),
) (
.required("csum", StringSchema::new("Digest list checksum.")) "chunk-count",
false,
&IntegerSchema::new("Chunk count. This is used to verify that the server got all chunks.")
.minimum(1)
.schema()
),
(
"size",
false,
&IntegerSchema::new("File size. This is used to verify that the server got all data.")
.minimum(1)
.schema()
),
("csum", false, &StringSchema::new("Digest list checksum.").schema()),
],
) )
} );
fn close_dynamic_index ( fn close_dynamic_index (
param: Value, param: Value,
@ -435,23 +473,37 @@ fn close_dynamic_index (
Ok(Value::Null) Ok(Value::Null)
} }
pub fn api_method_close_fixed_index() -> ApiMethod { pub const API_METHOD_CLOSE_FIXED_INDEX: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&close_fixed_index),
close_fixed_index, &ObjectSchema::new(
ObjectSchema::new("Close fixed index writer.") "Close fixed index writer.",
.required("wid", IntegerSchema::new("Fixed writer ID.") &[
.minimum(1) (
.maximum(256) "wid",
) false,
.required("chunk-count", IntegerSchema::new("Chunk count. This is used to verify that the server got all chunks.") &IntegerSchema::new("Fixed writer ID.")
.minimum(1) .minimum(1)
) .maximum(256)
.required("size", IntegerSchema::new("File size. This is used to verify that the server got all data.") .schema()
.minimum(1) ),
) (
.required("csum", StringSchema::new("Digest list checksum.")) "chunk-count",
false,
&IntegerSchema::new("Chunk count. This is used to verify that the server got all chunks.")
.minimum(1)
.schema()
),
(
"size",
false,
&IntegerSchema::new("File size. This is used to verify that the server got all data.")
.minimum(1)
.schema()
),
("csum", false, &StringSchema::new("Digest list checksum.").schema()),
],
) )
} );
fn close_fixed_index ( fn close_fixed_index (
param: Value, param: Value,
@ -488,23 +540,22 @@ fn finish_backup (
Ok(Value::Null) Ok(Value::Null)
} }
pub fn api_method_dynamic_chunk_index() -> ApiAsyncMethod { pub const API_METHOD_DYNAMIC_CHUNK_INDEX: ApiMethod = ApiMethod::new(
ApiAsyncMethod::new( &ApiHandler::Async(&dynamic_chunk_index),
dynamic_chunk_index, &ObjectSchema::new(
ObjectSchema::new(r###" r###"
Download the dynamic chunk index from the previous backup. Download the dynamic chunk index from the previous backup.
Simply returns an empty list if this is the first backup. Simply returns an empty list if this is the first backup.
"### "### ,
) &[ ("archive-name", false, &crate::api2::types::BACKUP_ARCHIVE_NAME_SCHEMA) ],
.required("archive-name", crate::api2::types::BACKUP_ARCHIVE_NAME_SCHEMA.clone())
) )
} );
fn dynamic_chunk_index( fn dynamic_chunk_index(
_parts: Parts, _parts: Parts,
_req_body: Body, _req_body: Body,
param: Value, param: Value,
_info: &ApiAsyncMethod, _info: &ApiMethod,
rpcenv: Box<dyn RpcEnvironment>, rpcenv: Box<dyn RpcEnvironment>,
) -> Result<BoxFut, Error> { ) -> Result<BoxFut, Error> {
@ -559,23 +610,22 @@ fn dynamic_chunk_index(
Ok(Box::new(future::ok(response))) Ok(Box::new(future::ok(response)))
} }
pub fn api_method_fixed_chunk_index() -> ApiAsyncMethod { pub const API_METHOD_FIXED_CHUNK_INDEX: ApiMethod = ApiMethod::new(
ApiAsyncMethod::new( &ApiHandler::Async(&fixed_chunk_index),
fixed_chunk_index, &ObjectSchema::new(
ObjectSchema::new(r###" r###"
Download the fixed chunk index from the previous backup. Download the fixed chunk index from the previous backup.
Simply returns an empty list if this is the first backup. Simply returns an empty list if this is the first backup.
"### "### ,
) &[ ("archive-name", false, &crate::api2::types::BACKUP_ARCHIVE_NAME_SCHEMA) ],
.required("archive-name", crate::api2::types::BACKUP_ARCHIVE_NAME_SCHEMA.clone())
) )
} );
fn fixed_chunk_index( fn fixed_chunk_index(
_parts: Parts, _parts: Parts,
_req_body: Body, _req_body: Body,
param: Value, param: Value,
_info: &ApiAsyncMethod, _info: &ApiMethod,
rpcenv: Box<dyn RpcEnvironment>, rpcenv: Box<dyn RpcEnvironment>,
) -> Result<BoxFut, Error> { ) -> Result<BoxFut, Error> {

View File

@ -81,31 +81,36 @@ impl Future for UploadChunk {
} }
} }
pub fn api_method_upload_fixed_chunk() -> ApiAsyncMethod { pub const API_METHOD_UPLOAD_FIXED_CHUNK: ApiMethod = ApiMethod::new(
ApiAsyncMethod::new( &ApiHandler::Async(&upload_fixed_chunk),
upload_fixed_chunk, &ObjectSchema::new(
ObjectSchema::new("Upload a new chunk.") "Upload a new chunk.",
.required("wid", IntegerSchema::new("Fixed writer ID.") &[
.minimum(1) ("wid", false, &IntegerSchema::new("Fixed writer ID.")
.maximum(256) .minimum(1)
) .maximum(256)
.required("digest", CHUNK_DIGEST_SCHEMA.clone()) .schema()
.required("size", IntegerSchema::new("Chunk size.") ),
.minimum(1) ("digest", false, &CHUNK_DIGEST_SCHEMA),
.maximum(1024*1024*16) ("size", false, &IntegerSchema::new("Chunk size.")
) .minimum(1)
.required("encoded-size", IntegerSchema::new("Encoded chunk size.") .maximum(1024*1024*16)
.minimum((std::mem::size_of::<DataBlobHeader>() as isize)+1) .schema()
.maximum(1024*1024*16+(std::mem::size_of::<EncryptedDataBlobHeader>() as isize)) ),
) ("encoded-size", false, &IntegerSchema::new("Encoded chunk size.")
.minimum((std::mem::size_of::<DataBlobHeader>() as isize)+1)
.maximum(1024*1024*16+(std::mem::size_of::<EncryptedDataBlobHeader>() as isize))
.schema()
),
],
) )
} );
fn upload_fixed_chunk( fn upload_fixed_chunk(
_parts: Parts, _parts: Parts,
req_body: Body, req_body: Body,
param: Value, param: Value,
_info: &ApiAsyncMethod, _info: &ApiMethod,
rpcenv: Box<dyn RpcEnvironment>, rpcenv: Box<dyn RpcEnvironment>,
) -> Result<BoxFut, Error> { ) -> Result<BoxFut, Error> {
@ -137,31 +142,36 @@ fn upload_fixed_chunk(
Ok(Box::new(resp)) Ok(Box::new(resp))
} }
pub fn api_method_upload_dynamic_chunk() -> ApiAsyncMethod { pub const API_METHOD_UPLOAD_DYNAMIC_CHUNK: ApiMethod = ApiMethod::new(
ApiAsyncMethod::new( &ApiHandler::Async(&upload_dynamic_chunk),
upload_dynamic_chunk, &ObjectSchema::new(
ObjectSchema::new("Upload a new chunk.") "Upload a new chunk.",
.required("wid", IntegerSchema::new("Dynamic writer ID.") &[
.minimum(1) ("wid", false, &IntegerSchema::new("Dynamic writer ID.")
.maximum(256) .minimum(1)
) .maximum(256)
.required("digest", CHUNK_DIGEST_SCHEMA.clone()) .schema()
.required("size", IntegerSchema::new("Chunk size.") ),
.minimum(1) ("digest", false, &CHUNK_DIGEST_SCHEMA),
.maximum(1024*1024*16) ("size", false, &IntegerSchema::new("Chunk size.")
) .minimum(1)
.required("encoded-size", IntegerSchema::new("Encoded chunk size.") .maximum(1024*1024*16)
.minimum((std::mem::size_of::<DataBlobHeader>() as isize) +1) .schema()
.maximum(1024*1024*16+(std::mem::size_of::<EncryptedDataBlobHeader>() as isize)) ),
) ("encoded-size", false, &IntegerSchema::new("Encoded chunk size.")
.minimum((std::mem::size_of::<DataBlobHeader>() as isize) +1)
.maximum(1024*1024*16+(std::mem::size_of::<EncryptedDataBlobHeader>() as isize))
.schema()
),
],
) )
} );
fn upload_dynamic_chunk( fn upload_dynamic_chunk(
_parts: Parts, _parts: Parts,
req_body: Body, req_body: Body,
param: Value, param: Value,
_info: &ApiAsyncMethod, _info: &ApiMethod,
rpcenv: Box<dyn RpcEnvironment>, rpcenv: Box<dyn RpcEnvironment>,
) -> Result<BoxFut, Error> { ) -> Result<BoxFut, Error> {
@ -193,18 +203,16 @@ fn upload_dynamic_chunk(
Ok(Box::new(resp)) Ok(Box::new(resp))
} }
pub fn api_method_upload_speedtest() -> ApiAsyncMethod { pub const API_METHOD_UPLOAD_SPEEDTEST: ApiMethod = ApiMethod::new(
ApiAsyncMethod::new( &ApiHandler::Async(&upload_speedtest),
upload_speedtest, &ObjectSchema::new("Test upload speed.", &[])
ObjectSchema::new("Test uploadf speed.") );
)
}
fn upload_speedtest( fn upload_speedtest(
_parts: Parts, _parts: Parts,
req_body: Body, req_body: Body,
_param: Value, _param: Value,
_info: &ApiAsyncMethod, _info: &ApiMethod,
rpcenv: Box<dyn RpcEnvironment>, rpcenv: Box<dyn RpcEnvironment>,
) -> Result<BoxFut, Error> { ) -> Result<BoxFut, Error> {
@ -231,23 +239,26 @@ fn upload_speedtest(
Ok(Box::new(resp)) Ok(Box::new(resp))
} }
pub fn api_method_upload_blob() -> ApiAsyncMethod { pub const API_METHOD_UPLOAD_BLOB: ApiMethod = ApiMethod::new(
ApiAsyncMethod::new( &ApiHandler::Async(&upload_blob),
upload_blob, &ObjectSchema::new(
ObjectSchema::new("Upload binary blob file.") "Upload binary blob file.",
.required("file-name", crate::api2::types::BACKUP_ARCHIVE_NAME_SCHEMA.clone()) &[
.required("encoded-size", IntegerSchema::new("Encoded blob size.") ("file-name", false, &crate::api2::types::BACKUP_ARCHIVE_NAME_SCHEMA),
.minimum((std::mem::size_of::<DataBlobHeader>() as isize) +1) ("encoded-size", false, &IntegerSchema::new("Encoded blob size.")
.maximum(1024*1024*16+(std::mem::size_of::<EncryptedDataBlobHeader>() as isize)) .minimum((std::mem::size_of::<DataBlobHeader>() as isize) +1)
.maximum(1024*1024*16+(std::mem::size_of::<EncryptedDataBlobHeader>() as isize))
.schema()
) )
],
) )
} );
fn upload_blob( fn upload_blob(
_parts: Parts, _parts: Parts,
req_body: Body, req_body: Body,
param: Value, param: Value,
_info: &ApiAsyncMethod, _info: &ApiMethod,
rpcenv: Box<dyn RpcEnvironment>, rpcenv: Box<dyn RpcEnvironment>,
) -> Result<BoxFut, Error> { ) -> Result<BoxFut, Error> {

View File

@ -1,13 +1,11 @@
//use failure::*;
//use std::collections::HashMap;
//use crate::api_schema;
use crate::api_schema::router::*; use crate::api_schema::router::*;
pub mod datastore; pub mod datastore;
pub fn router() -> Router { const SUBDIRS: SubdirMap = &[
Router::new() ("datastore", &datastore::ROUTER)
.subdir("datastore", datastore::router()) ];
.list_subdirs()
} pub const ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SUBDIRS))
.subdirs(SUBDIRS);

View File

@ -9,11 +9,10 @@ use std::path::PathBuf;
use crate::config::datastore; use crate::config::datastore;
pub fn get() -> ApiMethod { pub const GET: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&get_datastore_list),
get_datastore_list, &ObjectSchema::new("Directory index.", &[])
ObjectSchema::new("Directory index.")) );
}
fn get_datastore_list( fn get_datastore_list(
_param: Value, _param: Value,
@ -26,14 +25,16 @@ fn get_datastore_list(
Ok(config.convert_to_array("name")) Ok(config.convert_to_array("name"))
} }
pub fn post() -> ApiMethod { pub const POST: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&create_datastore),
create_datastore, &ObjectSchema::new(
ObjectSchema::new("Create new datastore.") "Create new datastore.",
.required("name", StringSchema::new("Datastore name.")) &[
.required("path", StringSchema::new("Directory path (must exist).")) ("name", false, &StringSchema::new("Datastore name.").schema()),
) ("path", false, &StringSchema::new("Directory path (must exist).").schema()),
} ],
)
);
fn create_datastore( fn create_datastore(
param: Value, param: Value,
@ -65,12 +66,15 @@ fn create_datastore(
Ok(Value::Null) Ok(Value::Null)
} }
pub fn delete() -> ApiMethod { pub const DELETE: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&delete_datastore),
delete_datastore, &ObjectSchema::new(
ObjectSchema::new("Remove a datastore configuration.") "Remove a datastore configuration.",
.required("name", StringSchema::new("Datastore name."))) &[
} ("name", false, &StringSchema::new("Datastore name.").schema()),
],
)
);
fn delete_datastore( fn delete_datastore(
param: Value, param: Value,
@ -96,9 +100,7 @@ fn delete_datastore(
Ok(Value::Null) Ok(Value::Null)
} }
pub fn router() -> Router { pub const ROUTER: Router = Router::new()
Router::new() .get(&GET)
.get(get()) .post(&POST)
.post(post()) .delete(&DELETE);
.delete(delete())
}

View File

@ -7,13 +7,15 @@ mod dns;
mod syslog; mod syslog;
mod services; mod services;
pub fn router() -> Router { pub const SUBDIRS: SubdirMap = &[
Router::new() ("dns", &dns::ROUTER),
.subdir("dns", dns::router()) ("network", &network::ROUTER),
.subdir("network", network::router()) ("services", &services::ROUTER),
.subdir("services", services::router()) ("syslog", &syslog::ROUTER),
.subdir("syslog", syslog::router()) ("tasks", &tasks::ROUTER),
.subdir("tasks", tasks::router()) ("time", &time::ROUTER),
.subdir("time", time::router()) ];
.list_subdirs()
} pub const ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SUBDIRS))
.subdirs(SUBDIRS);

View File

@ -107,32 +107,40 @@ fn get_dns(
read_etc_resolv_conf() read_etc_resolv_conf()
} }
pub fn router() -> Router { pub const ROUTER: Router = Router::new()
Router::new() .get(
.get( &ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&get_dns),
get_dns, &ObjectSchema::new(
ObjectSchema::new("Read DNS settings.") "Read DNS settings.",
.required("node", NODE_SCHEMA.clone()) &[ ("node", false, &NODE_SCHEMA) ],
).returns(
ObjectSchema::new("Returns DNS server IPs and sreach domain.")
.required("digest", PVE_CONFIG_DIGEST_SCHEMA.clone())
.optional("search", SEARCH_DOMAIN_SCHEMA.clone())
.optional("dns1", FIRST_DNS_SERVER_SCHEMA.clone())
.optional("dns2", SECOND_DNS_SERVER_SCHEMA.clone())
.optional("dns3", THIRD_DNS_SERVER_SCHEMA.clone())
) )
).returns(
&ObjectSchema::new(
"Returns DNS server IPs and sreach domain.",
&[
("digest", false, &PVE_CONFIG_DIGEST_SCHEMA),
("search", true, &SEARCH_DOMAIN_SCHEMA),
("dns1", true, &FIRST_DNS_SERVER_SCHEMA),
("dns2", true, &SECOND_DNS_SERVER_SCHEMA),
("dns3", true, &THIRD_DNS_SERVER_SCHEMA),
],
).schema()
) )
.put( )
ApiMethod::new( .put(
update_dns, &ApiMethod::new(
ObjectSchema::new("Returns DNS server IPs and sreach domain.") &ApiHandler::Sync(&update_dns),
.required("node", NODE_SCHEMA.clone()) &ObjectSchema::new(
.required("search", SEARCH_DOMAIN_SCHEMA.clone()) "Returns DNS server IPs and sreach domain.",
.optional("dns1", FIRST_DNS_SERVER_SCHEMA.clone()) &[
.optional("dns2", SECOND_DNS_SERVER_SCHEMA.clone()) ("node", false, &NODE_SCHEMA),
.optional("dns3", THIRD_DNS_SERVER_SCHEMA.clone()) ("search", false, &SEARCH_DOMAIN_SCHEMA),
.optional("digest", PVE_CONFIG_DIGEST_SCHEMA.clone()) ("dns1", true, &FIRST_DNS_SERVER_SCHEMA),
).protected(true) ("dns2", true, &SECOND_DNS_SERVER_SCHEMA),
) ("dns3", true, &THIRD_DNS_SERVER_SCHEMA),
} ("digest", true, &PVE_CONFIG_DIGEST_SCHEMA),
],
)
).protected(true)
);

View File

@ -16,11 +16,14 @@ fn get_network_config(
Ok(json!({})) Ok(json!({}))
} }
pub fn router() -> Router { pub const ROUTER: Router = Router::new()
Router::new() .get(
.get(ApiMethod::new( &ApiMethod::new(
get_network_config, &ApiHandler::Sync(&get_network_config),
ObjectSchema::new("Read network configuration.") &ObjectSchema::new(
.required("node", NODE_SCHEMA.clone()) "Read network configuration.",
)) &[ ("node", false, &NODE_SCHEMA) ],
} )
)
);

View File

@ -5,7 +5,6 @@ use crate::api_schema::*;
use crate::api_schema::router::*; use crate::api_schema::router::*;
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::sync::Arc;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use crate::api2::types::*; use crate::api2::types::*;
@ -214,92 +213,115 @@ fn reload_service(
run_service_command(service, "reload") run_service_command(service, "reload")
} }
pub fn router() -> Router {
let service_id_schema : Arc<Schema> = Arc::new( const SERVICE_ID_SCHEMA: Schema = StringSchema::new("Service ID.")
StringSchema::new("Service ID.") .max_length(256)
.max_length(256) .schema();
.into()
);
let service_api = Router::new() const SERVICE_SUBDIRS: SubdirMap = &[
.subdir( (
"state", "reload", &Router::new()
Router::new() .post(
.get(ApiMethod::new( &ApiMethod::new(
get_service_state, &ApiHandler::Sync(&reload_service),
ObjectSchema::new("Read service properties.") &ObjectSchema::new(
.required("node", NODE_SCHEMA.clone()) "Reload service.",
.required("service", service_id_schema.clone())) &[
) ("node", false, &NODE_SCHEMA),
) ("service", false, &SERVICE_ID_SCHEMA),
.subdir( ],
"start", )
Router::new() ).protected(true)
.post( )
ApiMethod::new( ),
start_service, (
ObjectSchema::new("Start service.") "restart", &Router::new()
.required("node", NODE_SCHEMA.clone()) .post(
.required("service", service_id_schema.clone()) &ApiMethod::new(
).protected(true) &ApiHandler::Sync(&restart_service),
) &ObjectSchema::new(
) "Restart service.",
.subdir( &[
"stop", ("node", false, &NODE_SCHEMA),
Router::new() ("service", false, &SERVICE_ID_SCHEMA),
.post( ],
ApiMethod::new( )
stop_service, ).protected(true)
ObjectSchema::new("Stop service.") )
.required("node", NODE_SCHEMA.clone()) ),
.required("service", service_id_schema.clone()) (
).protected(true) "start", &Router::new()
) .post(
) &ApiMethod::new(
.subdir( &ApiHandler::Sync(&start_service),
"restart", &ObjectSchema::new(
Router::new() "Start service.",
.post( &[
ApiMethod::new( ("node", false, &NODE_SCHEMA),
restart_service, ("service", false, &SERVICE_ID_SCHEMA),
ObjectSchema::new("Restart service.") ],
.required("node", NODE_SCHEMA.clone()) )
.required("service", service_id_schema.clone()) ).protected(true)
).protected(true) )
) ),
) (
.subdir( "state", &Router::new()
"reload", .get(
Router::new() &ApiMethod::new(
.post( &ApiHandler::Sync(&get_service_state),
ApiMethod::new( &ObjectSchema::new(
reload_service, "Read service properties.",
ObjectSchema::new("Reload service.") &[
.required("node", NODE_SCHEMA.clone()) ("node", false, &NODE_SCHEMA),
.required("service", service_id_schema.clone()) ("service", false, &SERVICE_ID_SCHEMA),
).protected(true) ],
) )
)
.list_subdirs();
Router::new()
.get(
ApiMethod::new(
list_services,
ObjectSchema::new("Service list.")
.required("node", NODE_SCHEMA.clone())
).returns(
ArraySchema::new(
"Returns a list of systemd services.",
ObjectSchema::new("Service details.")
.required("service", service_id_schema.clone())
.required("name", StringSchema::new("systemd service name."))
.required("desc", StringSchema::new("systemd service description."))
.required("state", StringSchema::new("systemd service 'SubState'."))
.into()
) )
) )
),
(
"stop", &Router::new()
.post(
&ApiMethod::new(
&ApiHandler::Sync(&stop_service),
&ObjectSchema::new(
"Stop service.",
&[
("node", false, &NODE_SCHEMA),
("service", false, &SERVICE_ID_SCHEMA),
],
)
).protected(true)
)
),
];
const SERVICE_ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SERVICE_SUBDIRS))
.subdirs(SERVICE_SUBDIRS);
pub const ROUTER: Router = Router::new()
.get(
&ApiMethod::new(
&ApiHandler::Sync(&list_services),
&ObjectSchema::new(
"Service list.",
&[ ("node", false, &NODE_SCHEMA) ],
)
).returns(
&ArraySchema::new(
"Returns a list of systemd services.",
&ObjectSchema::new(
"Service details.",
&[
("service", false, &SERVICE_ID_SCHEMA),
("name", false, &StringSchema::new("systemd service name.").schema()),
("desc", false, &StringSchema::new("systemd service description.").schema()),
("state", false, &StringSchema::new("systemd service 'SubState'.").schema()),
],
).schema()
).schema()
) )
.match_all("service", service_api) )
} .match_all("service", &SERVICE_ROUTER);

View File

@ -5,10 +5,6 @@ use crate::api_schema::router::*;
use crate::api2::types::*; use crate::api2::types::*;
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::sync::Arc;
use lazy_static::lazy_static;
use proxmox::tools::common_regex;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
fn dump_journal( fn dump_journal(
@ -91,47 +87,44 @@ fn get_syslog(
Ok(json!(lines)) Ok(json!(lines))
} }
lazy_static! { pub const ROUTER: Router = Router::new()
pub static ref SYSTEMD_DATETIME_FORMAT: Arc<ApiStringFormat> = .get(
ApiStringFormat::Pattern(&common_regex::SYSTEMD_DATETIME_REGEX).into(); &ApiMethod::new(
} &ApiHandler::Sync(&get_syslog),
&ObjectSchema::new(
"Read server time and time zone settings.",
&[
("node", false, &NODE_SCHEMA),
("start", true, &IntegerSchema::new("Start line number.")
.minimum(0)
.schema()
),
("limit", true, &IntegerSchema::new("Max. number of lines.")
.minimum(0)
.schema()
),
("since", true, &StringSchema::new("Display all log since this date-time string.")
.format(&SYSTEMD_DATETIME_FORMAT)
.schema()
),
("until", true, &StringSchema::new("Display all log until this date-time string.")
.format(&SYSTEMD_DATETIME_FORMAT)
.schema()
),
("service", true, &StringSchema::new("Service ID.")
.max_length(128)
.schema()
),
],
)
).returns(
&ObjectSchema::new(
"Returns a list of syslog entries.",
&[
("n", false, &IntegerSchema::new("Line number.").schema()),
("t", false, &StringSchema::new("Line text.").schema()),
],
).schema()
).protected(true)
);
pub fn router() -> Router {
Router::new()
.get(
ApiMethod::new(
get_syslog,
ObjectSchema::new("Read server time and time zone settings.")
.required("node", NODE_SCHEMA.clone())
.optional(
"start",
IntegerSchema::new("Start line number.")
.minimum(0)
)
.optional(
"limit",
IntegerSchema::new("Max. number of lines.")
.minimum(0)
)
.optional(
"since",
StringSchema::new("Display all log since this date-time string.")
.format(SYSTEMD_DATETIME_FORMAT.clone())
)
.optional(
"until",
StringSchema::new("Display all log until this date-time string.")
.format(SYSTEMD_DATETIME_FORMAT.clone())
)
.optional(
"service",
StringSchema::new("Service ID.")
.max_length(128)
)
).returns(
ObjectSchema::new("Returns a list of syslog entries.")
.required("n", IntegerSchema::new("Line number."))
.required("t", StringSchema::new("Line text."))
).protected(true)
)
}

View File

@ -4,7 +4,6 @@ use crate::tools;
use crate::api_schema::*; use crate::api_schema::*;
use crate::api_schema::router::*; use crate::api_schema::router::*;
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::sync::Arc;
use std::fs::File; use std::fs::File;
use std::io::{BufRead,BufReader}; use std::io::{BufRead,BufReader};
@ -166,84 +165,91 @@ fn list_tasks(
Ok(json!(result)) Ok(json!(result))
} }
pub fn router() -> Router { const UPID_SCHEMA: Schema = StringSchema::new("Unique Process/Task ID.")
.max_length(256)
.schema();
let upid_schema: Arc<Schema> = Arc::new( const UPID_API_SUBDIRS: SubdirMap = &[
StringSchema::new("Unique Process/Task ID.") (
.max_length(256) "log", &Router::new()
.into() .get(
); &ApiMethod::new(
&ApiHandler::Sync(&read_task_log),
let upid_api = Router::new() &ObjectSchema::new(
.delete(ApiMethod::new( "Read task log.",
stop_task, &[
ObjectSchema::new("Try to stop a task.") ("node", false, &NODE_SCHEMA),
.required("node", NODE_SCHEMA.clone()) ("upid", false, &UPID_SCHEMA),
.required("upid", upid_schema.clone())).protected(true) ("start", true, &IntegerSchema::new("Start at this line.")
.minimum(0)
) .default(0)
.subdir( .schema()
"log", Router::new() ),
.get( ("limit", true, &IntegerSchema::new("Only list this amount of lines.")
ApiMethod::new( .minimum(0)
read_task_log, .default(50)
ObjectSchema::new("Read task log.") .schema()
.required("node", NODE_SCHEMA.clone()) ),
.required("upid", upid_schema.clone()) ],
.optional(
"start",
IntegerSchema::new("Start at this line.")
.minimum(0)
.default(0)
)
.optional(
"limit",
IntegerSchema::new("Only list this amount of lines.")
.minimum(0)
.default(50)
)
) )
) )
) )
.subdir( ),
"status", Router::new() (
.get( "status", &Router::new()
ApiMethod::new( .get(
get_task_status, &ApiMethod::new(
ObjectSchema::new("Get task status.") &ApiHandler::Sync(&get_task_status),
.required("node", NODE_SCHEMA.clone()) &ObjectSchema::new(
.required("upid", upid_schema.clone())) "Get task status.",
&[
("node", false, &NODE_SCHEMA),
("upid", false, &UPID_SCHEMA),
],
)
) )
) )
.list_subdirs(); )
];
pub const UPID_API_ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(UPID_API_SUBDIRS))
.delete(
&ApiMethod::new(
&ApiHandler::Sync(&stop_task),
&ObjectSchema::new(
"Try to stop a task.",
&[
("node", false, &NODE_SCHEMA),
("upid", false, &UPID_SCHEMA),
],
)
).protected(true)
)
.subdirs(&UPID_API_SUBDIRS);
Router::new() pub const ROUTER: Router = Router::new()
.get(ApiMethod::new( .get(
list_tasks, &ApiMethod::new(
ObjectSchema::new("List tasks.") &ApiHandler::Sync(&list_tasks),
.required("node", NODE_SCHEMA.clone()) &ObjectSchema::new(
.optional( "List tasks.",
"start", &[
IntegerSchema::new("List tasks beginning from this offset.") ("node", false, &NODE_SCHEMA),
.minimum(0) ("start", true, &IntegerSchema::new("List tasks beginning from this offset.")
.default(0) .minimum(0)
) .default(0)
.optional( .schema()
"limit", ),
IntegerSchema::new("Only list this amount of tasks.") ("limit", true, &IntegerSchema::new("Only list this amount of tasks.")
.minimum(0) .minimum(0)
.default(50) .default(50)
) .schema()
.optional( ),
"errors", ("errors", true, &BooleanSchema::new("Only list erroneous tasks.").schema()),
BooleanSchema::new("Only list erroneous tasks.") ("userfilter", true, &StringSchema::new("Only list tasks from this user.").schema()),
) ],
.optional( )
"userfilter",
StringSchema::new("Only list tasks from this user.")
)
)
) )
.match_all("upid", upid_api) )
} .match_all("upid", &UPID_API_ROUTER);

View File

@ -80,29 +80,44 @@ fn set_timezone(
Ok(Value::Null) Ok(Value::Null)
} }
pub fn router() -> Router { pub const ROUTER: Router = Router::new()
Router::new() .get(
.get( &ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&get_time),
get_time, &ObjectSchema::new(
ObjectSchema::new("Read server time and time zone settings.") "Read server time and time zone settings.",
.required("node", NODE_SCHEMA.clone()) &[ ("node", false, &NODE_SCHEMA) ],
).returns(
ObjectSchema::new("Returns server time and timezone.")
.required("timezone", StringSchema::new("Time zone"))
.required("time", IntegerSchema::new("Seconds since 1970-01-01 00:00:00 UTC.")
.minimum(1_297_163_644))
.required("localtime", IntegerSchema::new("Seconds since 1970-01-01 00:00:00 UTC. (local time)")
.minimum(1_297_163_644))
) )
).returns(
&ObjectSchema::new(
"Returns server time and timezone.",
&[
("timezone", false, &StringSchema::new("Time zone").schema()),
("time", false, &IntegerSchema::new("Seconds since 1970-01-01 00:00:00 UTC.")
.minimum(1_297_163_644)
.schema()
),
("localtime", false, &IntegerSchema::new("Seconds since 1970-01-01 00:00:00 UTC. (local time)")
.minimum(1_297_163_644)
.schema()
),
],
).schema()
) )
.put( )
ApiMethod::new( .put(
set_timezone, &ApiMethod::new(
ObjectSchema::new("Set time zone.") &ApiHandler::Sync(&set_timezone),
.required("node", NODE_SCHEMA.clone()) &ObjectSchema::new(
.required("timezone", StringSchema::new( "Set time zone.",
"Time zone. The file '/usr/share/zoneinfo/zone.tab' contains the list of valid names.")) &[
).protected(true).reload_timezone(true) ("node", false, &NODE_SCHEMA),
) ("timezone", false, &StringSchema::new(
} "Time zone. The file '/usr/share/zoneinfo/zone.tab' contains the list of valid names.")
.schema()
),
],
)
).protected(true).reload_timezone(true)
);

View File

@ -1,7 +1,4 @@
use failure::*; use failure::*;
use lazy_static::lazy_static;
use std::sync::Arc;
use futures::*; use futures::*;
use hyper::header::{self, HeaderValue, UPGRADE}; use hyper::header::{self, HeaderValue, UPGRADE};
@ -21,30 +18,34 @@ use crate::api2::types::*;
mod environment; mod environment;
use environment::*; use environment::*;
pub fn router() -> Router { pub const ROUTER: Router = Router::new()
Router::new() .upgrade(&API_METHOD_UPGRADE_BACKUP);
.upgrade(api_method_upgrade_backup())
}
pub fn api_method_upgrade_backup() -> ApiAsyncMethod { pub const API_METHOD_UPGRADE_BACKUP: ApiMethod = ApiMethod::new(
ApiAsyncMethod::new( &ApiHandler::Async(&upgrade_to_backup_reader_protocol),
upgrade_to_backup_reader_protocol, &ObjectSchema::new(
ObjectSchema::new(concat!("Upgraded to backup protocol ('", PROXMOX_BACKUP_READER_PROTOCOL_ID_V1!(), "').")) concat!("Upgraded to backup protocol ('", PROXMOX_BACKUP_READER_PROTOCOL_ID_V1!(), "')."),
.required("store", StringSchema::new("Datastore name.")) &[
.required("backup-type", StringSchema::new("Backup type.") ("store", false, &StringSchema::new("Datastore name.").schema()),
.format(Arc::new(ApiStringFormat::Enum(&["vm", "ct", "host"])))) ("backup-type", false, &StringSchema::new("Backup type.")
.required("backup-id", StringSchema::new("Backup ID.")) .format(&ApiStringFormat::Enum(&["vm", "ct", "host"]))
.required("backup-time", IntegerSchema::new("Backup time (Unix epoch.)") .schema()
.minimum(1_547_797_308)) ),
.optional("debug", BooleanSchema::new("Enable verbose debug logging.")) ("backup-id", false, &StringSchema::new("Backup ID.").schema()),
("backup-time", false, &IntegerSchema::new("Backup time (Unix epoch.)")
.minimum(1_547_797_308)
.schema()
),
("debug", true, &BooleanSchema::new("Enable verbose debug logging.").schema()),
],
) )
} );
fn upgrade_to_backup_reader_protocol( fn upgrade_to_backup_reader_protocol(
parts: Parts, parts: Parts,
req_body: Body, req_body: Body,
param: Value, param: Value,
_info: &ApiAsyncMethod, _info: &ApiMethod,
rpcenv: Box<dyn RpcEnvironment>, rpcenv: Box<dyn RpcEnvironment>,
) -> Result<BoxFut, Error> { ) -> Result<BoxFut, Error> {
@ -89,7 +90,7 @@ fn upgrade_to_backup_reader_protocol(
env.log(format!("starting new backup reader datastore '{}': {:?}", store, path)); env.log(format!("starting new backup reader datastore '{}': {:?}", store, path));
let service = H2Service::new(env.clone(), worker.clone(), &READER_ROUTER, debug); let service = H2Service::new(env.clone(), worker.clone(), &READER_API_ROUTER, debug);
let abort_future = worker.abort_future(); let abort_future = worker.abort_future();
@ -134,39 +135,35 @@ fn upgrade_to_backup_reader_protocol(
Ok(Box::new(futures::future::ok(response))) Ok(Box::new(futures::future::ok(response)))
} }
lazy_static!{ pub const READER_API_ROUTER: Router = Router::new()
static ref READER_ROUTER: Router = reader_api(); .subdirs(&[
} (
"chunk", &Router::new()
.download(&API_METHOD_DOWNLOAD_CHUNK)
),
(
"download", &Router::new()
.download(&API_METHOD_DOWNLOAD_FILE)
),
(
"speedtest", &Router::new()
.download(&API_METHOD_SPEEDTEST)
),
]);
pub fn reader_api() -> Router { pub const API_METHOD_DOWNLOAD_FILE: ApiMethod = ApiMethod::new(
Router::new() &ApiHandler::Async(&download_file),
.subdir( &ObjectSchema::new(
"chunk", Router::new() "Download specified file.",
.download(api_method_download_chunk()) &[ ("file-name", false, &crate::api2::types::BACKUP_ARCHIVE_NAME_SCHEMA) ],
)
.subdir(
"download", Router::new()
.download(api_method_download_file())
)
.subdir(
"speedtest", Router::new()
.download(api_method_speedtest())
)
}
pub fn api_method_download_file() -> ApiAsyncMethod {
ApiAsyncMethod::new(
download_file,
ObjectSchema::new("Download specified file.")
.required("file-name", crate::api2::types::BACKUP_ARCHIVE_NAME_SCHEMA.clone())
) )
} );
fn download_file( fn download_file(
_parts: Parts, _parts: Parts,
_req_body: Body, _req_body: Body,
param: Value, param: Value,
_info: &ApiAsyncMethod, _info: &ApiMethod,
rpcenv: Box<dyn RpcEnvironment>, rpcenv: Box<dyn RpcEnvironment>,
) -> Result<BoxFut, Error> { ) -> Result<BoxFut, Error> {
@ -202,19 +199,19 @@ fn download_file(
Ok(Box::new(response_future)) Ok(Box::new(response_future))
} }
pub fn api_method_download_chunk() -> ApiAsyncMethod { pub const API_METHOD_DOWNLOAD_CHUNK: ApiMethod = ApiMethod::new(
ApiAsyncMethod::new( &ApiHandler::Async(&download_chunk),
download_chunk, &ObjectSchema::new(
ObjectSchema::new("Download specified chunk.") "Download specified chunk.",
.required("digest", CHUNK_DIGEST_SCHEMA.clone()) &[ ("digest", false, &CHUNK_DIGEST_SCHEMA) ],
) )
} );
fn download_chunk( fn download_chunk(
_parts: Parts, _parts: Parts,
_req_body: Body, _req_body: Body,
param: Value, param: Value,
_info: &ApiAsyncMethod, _info: &ApiMethod,
rpcenv: Box<dyn RpcEnvironment>, rpcenv: Box<dyn RpcEnvironment>,
) -> Result<BoxFut, Error> { ) -> Result<BoxFut, Error> {
@ -250,7 +247,7 @@ fn download_chunk_old(
_parts: Parts, _parts: Parts,
_req_body: Body, _req_body: Body,
param: Value, param: Value,
_info: &ApiAsyncMethod, _info: &ApiMethod,
rpcenv: Box<dyn RpcEnvironment>, rpcenv: Box<dyn RpcEnvironment>,
) -> Result<BoxFut, Error> { ) -> Result<BoxFut, Error> {
@ -286,18 +283,16 @@ fn download_chunk_old(
} }
*/ */
pub fn api_method_speedtest() -> ApiAsyncMethod { pub const API_METHOD_SPEEDTEST: ApiMethod = ApiMethod::new(
ApiAsyncMethod::new( &ApiHandler::Async(&speedtest),
speedtest, &ObjectSchema::new("Test 4M block download speed.", &[])
ObjectSchema::new("Test 4M block download speed.") );
)
}
fn speedtest( fn speedtest(
_parts: Parts, _parts: Parts,
_req_body: Body, _req_body: Body,
_param: Value, _param: Value,
_info: &ApiAsyncMethod, _info: &ApiMethod,
_rpcenv: Box<dyn RpcEnvironment>, _rpcenv: Box<dyn RpcEnvironment>,
) -> Result<BoxFut, Error> { ) -> Result<BoxFut, Error> {

View File

@ -21,9 +21,10 @@ fn get_subscription(
})) }))
} }
pub fn router() -> Router { pub const ROUTER: Router = Router::new()
Router::new() .get(
.get(ApiMethod::new( &ApiMethod::new(
get_subscription, &ApiHandler::Sync(&get_subscription),
ObjectSchema::new("Read subscription info."))) &ObjectSchema::new("Read subscription info.", &[])
} )
);

View File

@ -1,85 +1,97 @@
use failure::*; use failure::*;
use lazy_static::lazy_static; //use lazy_static::lazy_static;
use std::sync::Arc; //use std::sync::Arc;
use crate::api_schema::*; use crate::api_schema::*;
use proxmox::tools::common_regex; use proxmox::tools::*; // required to use IPRE!() macro ???
lazy_static!{ // File names: may not contain slashes, may not start with "."
pub const FILENAME_FORMAT: ApiStringFormat = ApiStringFormat::VerifyFn(|name| {
if name.starts_with('.') {
bail!("file names may not start with '.'");
}
if name.contains('/') {
bail!("file names may not contain slashes");
}
Ok(())
});
// File names: may not contain slashes, may not start with "."
pub static ref FILENAME_FORMAT: Arc<ApiStringFormat> = Arc::new(ApiStringFormat::VerifyFn(|name| {
if name.starts_with('.') {
bail!("file names may not start with '.'");
}
if name.contains('/') {
bail!("file names may not contain slashes");
}
Ok(())
})).into();
pub static ref IP_FORMAT: Arc<ApiStringFormat> = ApiStringFormat::Pattern(&common_regex::IP_REGEX).into();
pub static ref PVE_CONFIG_DIGEST_FORMAT: Arc<ApiStringFormat> =
ApiStringFormat::Pattern(&common_regex::SHA256_HEX_REGEX).into();
pub static ref PVE_CONFIG_DIGEST_SCHEMA: Arc<Schema> =
StringSchema::new("Prevent changes if current configuration file has different SHA256 digest. This can be used to prevent concurrent modifications.")
.format(PVE_CONFIG_DIGEST_FORMAT.clone()).into();
pub static ref CHUNK_DIGEST_FORMAT: Arc<ApiStringFormat> =
ApiStringFormat::Pattern(&common_regex::SHA256_HEX_REGEX).into();
pub static ref CHUNK_DIGEST_SCHEMA: Arc<Schema> =
StringSchema::new("Chunk digest (SHA256).")
.format(CHUNK_DIGEST_FORMAT.clone()).into();
pub static ref NODE_SCHEMA: Arc<Schema> = Arc::new(
StringSchema::new("Node name (or 'localhost')")
.format(
Arc::new(ApiStringFormat::VerifyFn(|node| {
if node == "localhost" || node == proxmox::tools::nodename() {
Ok(())
} else {
bail!("no such node '{}'", node);
}
}))
)
.into()
);
pub static ref SEARCH_DOMAIN_SCHEMA: Arc<Schema> =
StringSchema::new("Search domain for host-name lookup.").into();
pub static ref FIRST_DNS_SERVER_SCHEMA: Arc<Schema> =
StringSchema::new("First name server IP address.")
.format(IP_FORMAT.clone()).into();
pub static ref SECOND_DNS_SERVER_SCHEMA: Arc<Schema> =
StringSchema::new("Second name server IP address.")
.format(IP_FORMAT.clone()).into();
pub static ref THIRD_DNS_SERVER_SCHEMA: Arc<Schema> =
StringSchema::new("Third name server IP address.")
.format(IP_FORMAT.clone()).into();
pub static ref BACKUP_ARCHIVE_NAME_SCHEMA: Arc<Schema> =
StringSchema::new("Backup archive name.")
.format(FILENAME_FORMAT.clone()).into();
pub static ref BACKUP_TYPE_SCHEMA: Arc<Schema> =
StringSchema::new("Backup type.")
.format(Arc::new(ApiStringFormat::Enum(&["vm", "ct", "host"])))
.into();
pub static ref BACKUP_ID_SCHEMA: Arc<Schema> =
StringSchema::new("Backup ID.")
.format(FILENAME_FORMAT.clone())
.into();
pub static ref BACKUP_TIME_SCHEMA: Arc<Schema> =
IntegerSchema::new("Backup time (Unix epoch.)")
.minimum(1_547_797_308)
.into();
const_regex!{
pub IP_FORMAT_REGEX = IPRE!();
pub SHA256_HEX_REGEX = r"^[a-f0-9]{64}$"; // fixme: define in common_regex ?
pub SYSTEMD_DATETIME_REGEX = r"^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$"; // fixme: define in common_regex ?
} }
pub const SYSTEMD_DATETIME_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&SYSTEMD_DATETIME_REGEX);
pub const IP_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&IP_FORMAT_REGEX);
pub const PVE_CONFIG_DIGEST_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&SHA256_HEX_REGEX);
pub const PVE_CONFIG_DIGEST_SCHEMA: Schema = StringSchema::new(r#"\
Prevent changes if current configuration file has different SHA256 digest.
This can be used to prevent concurrent modifications.
"#
)
.format(&PVE_CONFIG_DIGEST_FORMAT)
.schema();
pub const CHUNK_DIGEST_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&SHA256_HEX_REGEX);
pub const CHUNK_DIGEST_SCHEMA: Schema = StringSchema::new("Chunk digest (SHA256).")
.format(&CHUNK_DIGEST_FORMAT)
.schema();
pub const NODE_SCHEMA: Schema = StringSchema::new("Node name (or 'localhost')")
.format(&ApiStringFormat::VerifyFn(|node| {
if node == "localhost" || node == proxmox::tools::nodename() {
Ok(())
} else {
bail!("no such node '{}'", node);
}
}))
.schema();
pub const SEARCH_DOMAIN_SCHEMA: Schema =
StringSchema::new("Search domain for host-name lookup.").schema();
pub const FIRST_DNS_SERVER_SCHEMA: Schema =
StringSchema::new("First name server IP address.")
.format(&IP_FORMAT)
.schema();
pub const SECOND_DNS_SERVER_SCHEMA: Schema =
StringSchema::new("Second name server IP address.")
.format(&IP_FORMAT)
.schema();
pub const THIRD_DNS_SERVER_SCHEMA: Schema =
StringSchema::new("Third name server IP address.")
.format(&IP_FORMAT)
.schema();
pub const BACKUP_ARCHIVE_NAME_SCHEMA: Schema =
StringSchema::new("Backup archive name.")
.format(&FILENAME_FORMAT)
.schema();
pub const BACKUP_TYPE_SCHEMA: Schema =
StringSchema::new("Backup type.")
.format(&ApiStringFormat::Enum(&["vm", "ct", "host"]))
.schema();
pub const BACKUP_ID_SCHEMA: Schema =
StringSchema::new("Backup ID.")
.format(&FILENAME_FORMAT)
.schema();
pub const BACKUP_TIME_SCHEMA: Schema =
IntegerSchema::new("Backup time (Unix epoch.)")
.minimum(1_547_797_308)
.schema();

View File

@ -26,9 +26,11 @@ fn get_version(
})) }))
} }
pub fn router() -> Router { pub const ROUTER: Router = Router::new()
Router::new() .get(
.get(ApiMethod::new( &ApiMethod::new(
get_version, &ApiHandler::Sync(&get_version),
ObjectSchema::new("Proxmox Backup Server API version."))) &ObjectSchema::new("Proxmox Backup Server API version.", &[])
} )
);

View File

@ -12,9 +12,12 @@
mod schema; mod schema;
pub use schema::*; pub use schema::*;
pub mod rpc_environment;
pub mod api_handler; pub mod api_handler;
pub mod registry;
#[macro_use] #[macro_use]
pub mod router; pub mod router;
//pub mod registry;
pub mod config; pub mod config;
pub mod format; pub mod format;

View File

@ -1,135 +1,26 @@
use failure::Error; use failure::Error;
use serde_json::Value; use serde_json::Value;
use super::router::{ApiMethod, RpcEnvironment}; use hyper::{Body, Response};
use hyper::rt::Future;
use hyper::http::request::Parts;
pub type ApiHandlerFn = Box< use super::rpc_environment::RpcEnvironment;
use super::router::ApiMethod;
pub type BoxFut = Box<dyn Future<Output = Result<Response<Body>, failure::Error>> + Send>;
pub type ApiHandlerFn = &'static (
dyn Fn(Value, &ApiMethod, &mut dyn RpcEnvironment) -> Result<Value, Error> dyn Fn(Value, &ApiMethod, &mut dyn RpcEnvironment) -> Result<Value, Error>
+ Send + Sync + 'static + Send + Sync + 'static
>; );
pub trait WrapApiHandler<Args, R, MetaArgs> { pub type ApiAsyncHandlerFn = &'static (
fn wrap(self) -> ApiHandlerFn; dyn Fn(Parts, Body, Value, &'static ApiMethod, Box<dyn RpcEnvironment>) -> Result<BoxFut, Error>
+ Send + Sync + 'static
);
pub enum ApiHandler {
Sync(ApiHandlerFn),
Async(ApiAsyncHandlerFn),
} }
// fn()
impl<F, R> WrapApiHandler<(), R, ()> for F
where
F: Fn() -> Result<R, Error> + Send + Sync + 'static,
R: serde::Serialize,
{
fn wrap(self) -> ApiHandlerFn {
Box::new(move |_value, _method, _rpc_env| {
Ok(serde_json::to_value((self)()?)?)
})
}
}
// fn(Arg)
impl<F, A, R> WrapApiHandler<(A,), R, ()> for F
where
F: Fn(A) -> Result<R, Error> + Send + Sync + 'static,
A: serde::de::DeserializeOwned,
R: serde::Serialize,
{
fn wrap(self) -> ApiHandlerFn {
Box::new(move |value, _method, _rpc_env| {
Ok(serde_json::to_value((self)(serde_json::from_value(value)?)?)?)
})
}
}
// fn(&ApiMethod)
impl<F, R> WrapApiHandler<(), R, (ApiMethod,)> for F
where
F: Fn(&ApiMethod) -> Result<R, Error> + Send + Sync + 'static,
R: serde::Serialize,
{
fn wrap(self) -> ApiHandlerFn {
Box::new(move |_value, method, _rpc_env| {
Ok(serde_json::to_value((self)(method)?)?)
})
}
}
// fn(Arg, &ApiMethod)
impl<F, A, R> WrapApiHandler<(A,), R, (ApiMethod,)> for F
where
F: Fn(A, &ApiMethod) -> Result<R, Error> + Send + Sync + 'static,
A: serde::de::DeserializeOwned,
R: serde::Serialize,
{
fn wrap(self) -> ApiHandlerFn {
Box::new(move |value, method, _rpc_env| {
Ok(serde_json::to_value((self)(
serde_json::from_value(value)?,
method,
)?)?)
})
}
}
// RpcEnvironment is a trait, so use a "marker" type for it instead:
pub struct RpcEnvArg();
// fn(&mut dyn RpcEnvironment)
impl<F, R> WrapApiHandler<(), R, (RpcEnvArg,)> for F
where
F: Fn(&mut dyn RpcEnvironment) -> Result<R, Error> + Send + Sync + 'static,
R: serde::Serialize,
{
fn wrap(self) -> ApiHandlerFn {
Box::new(move |_value, _method, rpc_env| {
Ok(serde_json::to_value((self)(rpc_env)?)?)
})
}
}
// fn(Arg, &mut dyn RpcEnvironment)
impl<F, A, R> WrapApiHandler<(A,), R, (RpcEnvArg,)> for F
where
F: Fn(A, &mut dyn RpcEnvironment) -> Result<R, Error> + Send + Sync + 'static,
A: serde::de::DeserializeOwned,
R: serde::Serialize,
{
fn wrap(self) -> ApiHandlerFn {
Box::new(move |value, _method, rpc_env| {
Ok(serde_json::to_value((self)(
serde_json::from_value(value)?,
rpc_env,
)?)?)
})
}
}
// fn(&ApiMethod, &mut dyn RpcEnvironment)
impl<F, R> WrapApiHandler<(), R, (ApiMethod, RpcEnvArg,)> for F
where
F: Fn(&ApiMethod, &mut dyn RpcEnvironment) -> Result<R, Error> + Send + Sync + 'static,
R: serde::Serialize,
{
fn wrap(self) -> ApiHandlerFn {
Box::new(move |_value, method, rpc_env| {
Ok(serde_json::to_value((self)(method, rpc_env)?)?)
})
}
}
// fn(Arg, &ApiMethod, &mut dyn RpcEnvironment)
impl<F, A, R> WrapApiHandler<(A,), R, (ApiMethod, RpcEnvArg,)> for F
where
F: Fn(A, &ApiMethod, &mut dyn RpcEnvironment) -> Result<R, Error> + Send + Sync + 'static,
A: serde::de::DeserializeOwned,
R: serde::Serialize,
{
fn wrap(self) -> ApiHandlerFn {
Box::new(move |value, method, rpc_env| {
Ok(serde_json::to_value((self)(
serde_json::from_value(value)?,
method,
rpc_env,
)?)?)
})
}
}

View File

@ -23,7 +23,12 @@ impl ApiConfig {
} }
} }
pub fn find_method(&self, components: &[&str], method: Method, uri_param: &mut HashMap<String, String>) -> &'static MethodDefinition { pub fn find_method(
&self,
components: &[&str],
method: Method,
uri_param: &mut HashMap<String, String>,
) -> Option<&'static ApiMethod> {
self.router.find_method(components, method, uri_param) self.router.find_method(components, method, uri_param)
} }

View File

@ -1,8 +1,11 @@
use failure::*; use failure::*;
use std::io::Write; use std::io::Write;
use crate::api_schema::*; //use super::*;
use crate::api_schema::router::*; use super::router::*;
use super::schema::*;
//use super::api_handler::*;
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub enum ParameterDisplayStyle { pub enum ParameterDisplayStyle {
@ -140,17 +143,10 @@ fn dump_api_parameters(param: &ObjectSchema) -> String {
let mut res = wrap_text("", "", param.description, 80); let mut res = wrap_text("", "", param.description, 80);
let properties = &param.properties;
let mut prop_names: Vec<&str> = properties.keys().copied().collect();
prop_names.sort();
let mut required_list: Vec<String> = Vec::new(); let mut required_list: Vec<String> = Vec::new();
let mut optional_list: Vec<String> = Vec::new(); let mut optional_list: Vec<String> = Vec::new();
for prop in prop_names { for (prop, optional, schema) in param.properties {
let (optional, schema) = properties.get(prop).unwrap();
let param_descr = get_property_description( let param_descr = get_property_description(
prop, &schema, ParameterDisplayStyle::Config, DocumentationFormat::ReST); prop, &schema, ParameterDisplayStyle::Config, DocumentationFormat::ReST);
@ -223,26 +219,22 @@ fn dump_api_return_schema(schema: &Schema) -> String {
res res
} }
fn dump_method_definition(method: &str, path: &str, def: &MethodDefinition) -> Option<String> { fn dump_method_definition(method: &str, path: &str, def: Option<&ApiMethod>) -> Option<String> {
match def { match def {
MethodDefinition::None => None, None => None,
MethodDefinition::Simple(simple_method) => { Some(api_method) => {
let param_descr = dump_api_parameters(&simple_method.parameters); let param_descr = dump_api_parameters(api_method.parameters);
let return_descr = dump_api_return_schema(&simple_method.returns); let return_descr = dump_api_return_schema(api_method.returns);
let res = format!("**{} {}**\n\n{}\n\n{}", method, path, param_descr, return_descr); let mut method = method;
Some(res)
}
MethodDefinition::Async(async_method) => {
let method = if method == "POST" { "UPLOAD" } else { method };
let method = if method == "GET" { "DOWNLOAD" } else { method };
let param_descr = dump_api_parameters(&async_method.parameters);
let return_descr = dump_api_return_schema(&async_method.returns);
if let ApiHandler::Async(_) = api_method.handler {
method = if method == "POST" { "UPLOAD" } else { method };
method = if method == "GET" { "DOWNLOAD" } else { method };
}
let res = format!("**{} {}**\n\n{}\n\n{}", method, path, param_descr, return_descr); let res = format!("**{} {}**\n\n{}\n\n{}", method, path, param_descr, return_descr);
Some(res) Some(res)
} }
@ -262,14 +254,14 @@ pub fn dump_api(output: &mut dyn Write, router: &Router, path: &str, mut pos: us
Ok(()) Ok(())
}; };
cond_print(dump_method_definition("GET", path, &router.get))?; cond_print(dump_method_definition("GET", path, router.get))?;
cond_print(dump_method_definition("POST", path, &router.post))?; cond_print(dump_method_definition("POST", path, router.post))?;
cond_print(dump_method_definition("PUT", path, &router.put))?; cond_print(dump_method_definition("PUT", path, router.put))?;
cond_print(dump_method_definition("DELETE", path, &router.delete))?; cond_print(dump_method_definition("DELETE", path, router.delete))?;
match &router.subroute { match &router.subroute {
SubRoute::None => return Ok(()), None => return Ok(()),
SubRoute::MatchAll { router, param_name } => { Some(SubRoute::MatchAll { router, param_name }) => {
let sub_path = if path == "." { let sub_path = if path == "." {
format!("<{}>", param_name) format!("<{}>", param_name)
} else { } else {
@ -277,12 +269,11 @@ pub fn dump_api(output: &mut dyn Write, router: &Router, path: &str, mut pos: us
}; };
dump_api(output, router, &sub_path, pos)?; dump_api(output, router, &sub_path, pos)?;
} }
SubRoute::Hash(map) => { Some(SubRoute::Map(dirmap)) => {
let mut keys: Vec<&String> = map.keys().collect(); //let mut keys: Vec<&String> = map.keys().collect();
keys.sort_unstable_by(|a, b| a.cmp(b)); //keys.sort_unstable_by(|a, b| a.cmp(b));
for key in keys { for (key, sub_router) in dirmap.iter() {
let sub_router = &map[key]; let sub_path = if path == "." { key.to_string() } else { format!("{}/{}", path, key) };
let sub_path = if path == "." { key.to_owned() } else { format!("{}/{}", path, key) };
dump_api(output, sub_router, &sub_path, pos)?; dump_api(output, sub_router, &sub_path, pos)?;
} }
} }

View File

@ -1,54 +1,16 @@
use failure::*; use failure::*;
use crate::api_schema::*; use serde_json::Value;
use serde_json::{json, Value};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc;
use std::fmt; use std::fmt;
use hyper::{Body, Method, Response, StatusCode}; use hyper::{Method, StatusCode};
use hyper::rt::Future; //use hyper::http::request::Parts;
use hyper::http::request::Parts;
use super::api_handler::*; use super::schema::*;
pub use super::rpc_environment::*;
pub use super::api_handler::*;
pub type BoxFut = Box<dyn Future<Output = Result<Response<Body>, failure::Error>> + Send>;
/// Abstract Interface for API methods to interact with the environment
pub trait RpcEnvironment: std::any::Any + crate::tools::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<String>);
/// Get user name
fn get_user(&self) -> Option<String>;
}
/// 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,
}
#[derive(Debug, Fail)] #[derive(Debug, Fail)]
pub struct HttpError { pub struct HttpError {
@ -68,17 +30,13 @@ impl fmt::Display for HttpError {
} }
} }
#[macro_export]
macro_rules! http_err { macro_rules! http_err {
($status:ident, $msg:expr) => {{ ($status:ident, $msg:expr) => {{
Error::from(HttpError::new(StatusCode::$status, $msg)) Error::from(HttpError::new(StatusCode::$status, $msg))
}} }}
} }
type ApiAsyncHandlerFn = Box<
dyn Fn(Parts, Body, Value, &ApiAsyncMethod, Box<dyn RpcEnvironment>) -> Result<BoxFut, Error>
+ Send + Sync + 'static
>;
/// This struct defines synchronous API call which returns the restulkt as json `Value` /// This struct defines synchronous API call which returns the restulkt as json `Value`
pub struct ApiMethod { pub struct ApiMethod {
/// The protected flag indicates that the provides function should be forwarded /// The protected flag indicates that the provides function should be forwarded
@ -88,53 +46,69 @@ pub struct ApiMethod {
/// should do a tzset afterwards /// should do a tzset afterwards
pub reload_timezone: bool, pub reload_timezone: bool,
/// Parameter type Schema /// Parameter type Schema
pub parameters: ObjectSchema, pub parameters: &'static ObjectSchema,
/// Return type Schema /// Return type Schema
pub returns: Arc<Schema>, pub returns: &'static Schema,
/// Handler function /// Handler function
pub handler: Option<ApiHandlerFn>, 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<Value, Error> {
// do nothing
Ok(Value::Null)
}
const DUMMY_HANDLER: ApiHandler = ApiHandler::Sync(&dummy_handler_fn);
impl ApiMethod { impl ApiMethod {
pub fn new<F, Args, R, MetaArgs>(func: F, parameters: ObjectSchema) -> Self pub const fn new(handler: &'static ApiHandler, parameters: &'static ObjectSchema) -> Self {
where
F: WrapApiHandler<Args, R, MetaArgs>,
{
Self { Self {
parameters, parameters,
handler: Some(func.wrap()), handler,
returns: Arc::new(Schema::Null), returns: &NULL_SCHEMA,
protected: false, protected: false,
reload_timezone: false, reload_timezone: false,
} }
} }
pub fn new_dummy(parameters: ObjectSchema) -> Self { pub const fn new_dummy(parameters: &'static ObjectSchema) -> Self {
Self { Self {
parameters, parameters,
handler: None, handler: &DUMMY_HANDLER,
returns: Arc::new(Schema::Null), returns: &NULL_SCHEMA,
protected: false, protected: false,
reload_timezone: false, reload_timezone: false,
} }
} }
pub fn returns<S: Into<Arc<Schema>>>(mut self, schema: S) -> Self { pub const fn returns(mut self, schema: &'static Schema) -> Self {
self.returns = schema.into(); self.returns = schema;
self self
} }
pub fn protected(mut self, protected: bool) -> Self { pub const fn protected(mut self, protected: bool) -> Self {
self.protected = protected; self.protected = protected;
self self
} }
pub fn reload_timezone(mut self, reload_timezone: bool) -> Self { pub const fn reload_timezone(mut self, reload_timezone: bool) -> Self {
self.reload_timezone = reload_timezone; self.reload_timezone = reload_timezone;
@ -142,143 +116,96 @@ impl ApiMethod {
} }
} }
pub struct ApiAsyncMethod { pub type SubdirMap = &'static [(&'static str, &'static Router)];
pub parameters: ObjectSchema,
pub returns: Arc<Schema>,
pub handler: ApiAsyncHandlerFn,
}
impl ApiAsyncMethod {
pub fn new<F>(handler: F, parameters: ObjectSchema) -> Self
where
F: Fn(Parts, Body, Value, &ApiAsyncMethod, Box<dyn RpcEnvironment>) -> Result<BoxFut, Error>
+ Send + Sync + 'static,
{
Self {
parameters,
handler: Box::new(handler),
returns: Arc::new(Schema::Null),
}
}
pub fn returns<S: Into<Arc<Schema>>>(mut self, schema: S) -> Self {
self.returns = schema.into();
self
}
}
pub enum SubRoute { pub enum SubRoute {
None, //Hash(HashMap<String, Router>),
Hash(HashMap<String, Router>), Map(SubdirMap),
MatchAll { router: Box<Router>, param_name: String }, MatchAll { router: &'static Router, param_name: &'static str },
} }
pub enum MethodDefinition { /// Macro to create an ApiMethod to list entries from SubdirMap
None, #[macro_export]
Simple(ApiMethod), macro_rules! list_subdirs_api_method {
Async(ApiAsyncMethod), ($map:expr) => {
ApiMethod::new(
&ApiHandler::Sync( & |_, _, _| {
let index = serde_json::json!(
$map.iter().map(|s| serde_json::json!({ "subdir": s.0}))
.collect::<Vec<serde_json::Value>>()
);
Ok(index)
}),
&crate::api_schema::ObjectSchema::new("Directory index.", &[]).additional_properties(true)
)
}
} }
pub struct Router { pub struct Router {
pub get: MethodDefinition, pub get: Option<&'static ApiMethod>,
pub put: MethodDefinition, pub put: Option<&'static ApiMethod>,
pub post: MethodDefinition, pub post: Option<&'static ApiMethod>,
pub delete: MethodDefinition, pub delete: Option<&'static ApiMethod>,
pub subroute: SubRoute, pub subroute: Option<SubRoute>,
} }
impl Router { impl Router {
pub fn new() -> Self { pub const fn new() -> Self {
Self { Self {
get: MethodDefinition::None, get: None,
put: MethodDefinition::None, put: None,
post: MethodDefinition::None, post: None,
delete: MethodDefinition::None, delete: None,
subroute: SubRoute::None subroute: None,
} }
} }
pub fn subdir<S: Into<String>>(mut self, subdir: S, router: Router) -> Self { pub const fn subdirs(mut self, map: SubdirMap) -> Self {
if let SubRoute::None = self.subroute { self.subroute = Some(SubRoute::Map(map));
self.subroute = SubRoute::Hash(HashMap::new());
}
match self.subroute {
SubRoute::Hash(ref mut map) => {
map.insert(subdir.into(), router);
}
_ => panic!("unexpected subroute type"),
}
self self
} }
pub fn subdirs(mut self, map: HashMap<String, Router>) -> Self { pub const fn match_all(mut self, param_name: &'static str, router: &'static Router) -> Self {
self.subroute = SubRoute::Hash(map); self.subroute = Some(SubRoute::MatchAll { router, param_name });
self
}
pub const fn get(mut self, m: &'static ApiMethod) -> Self {
self.get = Some(m);
self self
} }
pub fn match_all<S: Into<String>>(mut self, param_name: S, router: Router) -> Self { pub const fn put(mut self, m: &'static ApiMethod) -> Self {
if let SubRoute::None = self.subroute { self.put = Some(m);
self.subroute = SubRoute::MatchAll { router: Box::new(router), param_name: param_name.into() };
} else {
panic!("unexpected subroute type");
}
self self
} }
pub fn list_subdirs(self) -> Self { pub const fn post(mut self, m: &'static ApiMethod) -> Self {
match self.get { self.post = Some(m);
MethodDefinition::None => {},
_ => panic!("cannot create directory index - method get already in use"),
}
match self.subroute {
SubRoute::Hash(ref map) => {
let index = json!(map.keys().map(|s| json!({ "subdir": s}))
.collect::<Vec<Value>>());
self.get(ApiMethod::new(
move || { Ok(index.clone()) },
ObjectSchema::new("Directory index.").additional_properties(true))
)
}
_ => panic!("cannot create directory index (no SubRoute::Hash)"),
}
}
pub fn get(mut self, m: ApiMethod) -> Self {
self.get = MethodDefinition::Simple(m);
self self
} }
pub fn put(mut self, m: ApiMethod) -> Self { /// Same as post, buth async (fixme: expect Async)
self.put = MethodDefinition::Simple(m); pub const fn upload(mut self, m: &'static ApiMethod) -> Self {
self.post = Some(m);
self self
} }
pub fn post(mut self, m: ApiMethod) -> Self { /// Same as get, but async (fixme: expect Async)
self.post = MethodDefinition::Simple(m); pub const fn download(mut self, m: &'static ApiMethod) -> Self {
self.get = Some(m);
self self
} }
pub fn upload(mut self, m: ApiAsyncMethod) -> Self { /// Same as get, but async (fixme: expect Async)
self.post = MethodDefinition::Async(m); pub const fn upgrade(mut self, m: &'static ApiMethod) -> Self {
self.get = Some(m);
self self
} }
pub fn download(mut self, m: ApiAsyncMethod) -> Self { pub const fn delete(mut self, m: &'static ApiMethod) -> Self {
self.get = MethodDefinition::Async(m); self.delete = Some(m);
self
}
pub fn upgrade(mut self, m: ApiAsyncMethod) -> Self {
self.get = MethodDefinition::Async(m);
self
}
pub fn delete(mut self, m: ApiMethod) -> Self {
self.delete = MethodDefinition::Simple(m);
self self
} }
@ -289,16 +216,17 @@ impl Router {
let (dir, rest) = (components[0], &components[1..]); let (dir, rest) = (components[0], &components[1..]);
match self.subroute { match self.subroute {
SubRoute::None => {}, None => {},
SubRoute::Hash(ref dirmap) => { Some(SubRoute::Map(dirmap)) => {
if let Some(ref router) = dirmap.get(dir) { if let Ok(ind) = dirmap.binary_search_by_key(&dir, |(name, _)| name) {
let (_name, router) = dirmap[ind];
//println!("FOUND SUBDIR {}", dir); //println!("FOUND SUBDIR {}", dir);
return router.find_route(rest, uri_param); return router.find_route(rest, uri_param);
} }
} }
SubRoute::MatchAll { ref router, ref param_name } => { Some(SubRoute::MatchAll { router, param_name }) => {
//println!("URI PARAM {} = {}", param_name, dir); // fixme: store somewhere //println!("URI PARAM {} = {}", param_name, dir); // fixme: store somewhere
uri_param.insert(param_name.clone(), dir.into()); uri_param.insert(param_name.to_owned(), dir.into());
return router.find_route(rest, uri_param); return router.find_route(rest, uri_param);
}, },
} }
@ -311,18 +239,18 @@ impl Router {
components: &[&str], components: &[&str],
method: Method, method: Method,
uri_param: &mut HashMap<String, String> uri_param: &mut HashMap<String, String>
) -> &MethodDefinition { ) -> Option<&ApiMethod> {
if let Some(info) = self.find_route(components, uri_param) { if let Some(info) = self.find_route(components, uri_param) {
return match method { return match method {
Method::GET => &info.get, Method::GET => info.get,
Method::PUT => &info.put, Method::PUT => info.put,
Method::POST => &info.post, Method::POST => info.post,
Method::DELETE => &info.delete, Method::DELETE => info.delete,
_ => &MethodDefinition::None, _ => None,
}; };
} }
&MethodDefinition::None None
} }
} }

View File

@ -0,0 +1,37 @@
use serde_json::Value;
/// Abstract Interface for API methods to interact with the environment
pub trait RpcEnvironment: std::any::Any + crate::tools::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<String>);
/// Get user name
fn get_user(&self) -> Option<String>;
}
/// 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,
}

View File

@ -1,10 +1,7 @@
use failure::*; use failure::*;
use std::collections::HashMap;
use serde_json::{json, Value}; use serde_json::{json, Value};
use url::form_urlencoded; use url::form_urlencoded;
use regex::Regex;
use std::fmt; use std::fmt;
use std::sync::Arc;
#[derive(Default, Debug, Fail)] #[derive(Default, Debug, Fail)]
pub struct ParameterError { pub struct ParameterError {
@ -63,17 +60,21 @@ pub struct BooleanSchema {
impl BooleanSchema { impl BooleanSchema {
pub fn new(description: &'static str) -> Self { pub const fn new(description: &'static str) -> Self {
BooleanSchema { BooleanSchema {
description, description,
default: None, default: None,
} }
} }
pub fn default(mut self, default: bool) -> Self { pub const fn default(mut self, default: bool) -> Self {
self.default = Some(default); self.default = Some(default);
self self
} }
pub const fn schema(self) -> Schema {
Schema::Boolean(self)
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -86,7 +87,7 @@ pub struct IntegerSchema {
impl IntegerSchema { impl IntegerSchema {
pub fn new(description: &'static str) -> Self { pub const fn new(description: &'static str) -> Self {
IntegerSchema { IntegerSchema {
description, description,
default: None, default: None,
@ -95,21 +96,25 @@ impl IntegerSchema {
} }
} }
pub fn default(mut self, default: isize) -> Self { pub const fn default(mut self, default: isize) -> Self {
self.default = Some(default); self.default = Some(default);
self self
} }
pub fn minimum(mut self, minimum: isize) -> Self { pub const fn minimum(mut self, minimum: isize) -> Self {
self.minimum = Some(minimum); self.minimum = Some(minimum);
self self
} }
pub fn maximum(mut self, maximium: isize) -> Self { pub const fn maximum(mut self, maximium: isize) -> Self {
self.maximum = Some(maximium); self.maximum = Some(maximium);
self self
} }
pub const fn schema(self) -> Schema {
Schema::Integer(self)
}
fn check_constraints(&self, value: isize) -> Result<(), Error> { fn check_constraints(&self, value: isize) -> Result<(), Error> {
if let Some(minimum) = self.minimum { if let Some(minimum) = self.minimum {
@ -128,6 +133,50 @@ impl IntegerSchema {
} }
} }
/// 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)] #[derive(Debug)]
pub struct StringSchema { pub struct StringSchema {
@ -135,12 +184,12 @@ pub struct StringSchema {
pub default: Option<&'static str>, pub default: Option<&'static str>,
pub min_length: Option<usize>, pub min_length: Option<usize>,
pub max_length: Option<usize>, pub max_length: Option<usize>,
pub format: Option<Arc<ApiStringFormat>>, pub format: Option<&'static ApiStringFormat>,
} }
impl StringSchema { impl StringSchema {
pub fn new(description: &'static str) -> Self { pub const fn new(description: &'static str) -> Self {
StringSchema { StringSchema {
description, description,
default: None, default: None,
@ -150,26 +199,30 @@ impl StringSchema {
} }
} }
pub fn default(mut self, text: &'static str) -> Self { pub const fn default(mut self, text: &'static str) -> Self {
self.default = Some(text); self.default = Some(text);
self self
} }
pub fn format(mut self, format: Arc<ApiStringFormat>) -> Self { pub const fn format(mut self, format: &'static ApiStringFormat) -> Self {
self.format = Some(format); self.format = Some(format);
self self
} }
pub fn min_length(mut self, min_length: usize) -> Self { pub const fn min_length(mut self, min_length: usize) -> Self {
self.min_length = Some(min_length); self.min_length = Some(min_length);
self self
} }
pub fn max_length(mut self, max_length: usize) -> Self { pub const fn max_length(mut self, max_length: usize) -> Self {
self.max_length = Some(max_length); self.max_length = Some(max_length);
self self
} }
pub const fn schema(self) -> Schema {
Schema::String(self)
}
fn check_length(&self, length: usize) -> Result<(), Error> { fn check_length(&self, length: usize) -> Result<(), Error> {
if let Some(min_length) = self.min_length { if let Some(min_length) = self.min_length {
@ -192,18 +245,18 @@ impl StringSchema {
self.check_length(value.chars().count())?; self.check_length(value.chars().count())?;
if let Some(ref format) = self.format { if let Some(ref format) = self.format {
match format.as_ref() { match format {
ApiStringFormat::Pattern(ref regex) => { ApiStringFormat::Pattern(regex) => {
if !regex.is_match(value) { if !(regex.regex_obj)().is_match(value) {
bail!("value does not match the regex pattern"); bail!("value does not match the regex pattern");
} }
} }
ApiStringFormat::Enum(ref stringvec) => { ApiStringFormat::Enum(stringvec) => {
if stringvec.iter().find(|&e| *e == value) == None { if stringvec.iter().find(|&e| *e == value) == None {
bail!("value '{}' is not defined in the enumeration.", value); bail!("value '{}' is not defined in the enumeration.", value);
} }
} }
ApiStringFormat::Complex(ref subschema) => { ApiStringFormat::Complex(subschema) => {
parse_property_string(value, subschema)?; parse_property_string(value, subschema)?;
} }
ApiStringFormat::VerifyFn(verify_fn) => { ApiStringFormat::VerifyFn(verify_fn) => {
@ -214,20 +267,19 @@ impl StringSchema {
Ok(()) Ok(())
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct ArraySchema { pub struct ArraySchema {
pub description: &'static str, pub description: &'static str,
pub items: Arc<Schema>, pub items: &'static Schema,
pub min_length: Option<usize>, pub min_length: Option<usize>,
pub max_length: Option<usize>, pub max_length: Option<usize>,
} }
impl ArraySchema { impl ArraySchema {
pub fn new(description: &'static str, item_schema: Arc<Schema>) -> Self { pub const fn new(description: &'static str, item_schema: &'static Schema) -> Self {
ArraySchema { ArraySchema {
description, description,
items: item_schema, items: item_schema,
@ -236,16 +288,20 @@ impl ArraySchema {
} }
} }
pub fn min_length(mut self, min_length: usize) -> Self { pub const fn min_length(mut self, min_length: usize) -> Self {
self.min_length = Some(min_length); self.min_length = Some(min_length);
self self
} }
pub fn max_length(mut self, max_length: usize) -> Self { pub const fn max_length(mut self, max_length: usize) -> Self {
self.max_length = Some(max_length); self.max_length = Some(max_length);
self self
} }
pub const fn schema(self) -> Schema {
Schema::Array(self)
}
fn check_length(&self, length: usize) -> Result<(), Error> { fn check_length(&self, length: usize) -> Result<(), Error> {
if let Some(min_length) = self.min_length { if let Some(min_length) = self.min_length {
@ -264,44 +320,60 @@ impl ArraySchema {
} }
} }
/// 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)] #[derive(Debug)]
pub struct ObjectSchema { pub struct ObjectSchema {
pub description: &'static str, pub description: &'static str,
pub additional_properties: bool, pub additional_properties: bool,
pub properties: HashMap<&'static str, (bool, Arc<Schema>)>, pub properties: SchemaPropertyMap,
pub default_key: Option<&'static str>, pub default_key: Option<&'static str>,
} }
impl ObjectSchema { impl ObjectSchema {
pub fn new(description: &'static str) -> Self { pub const fn new(description: &'static str, properties: SchemaPropertyMap) -> Self {
let properties = HashMap::new();
ObjectSchema { ObjectSchema {
description, description,
additional_properties: false,
properties, properties,
additional_properties: false,
default_key: None, default_key: None,
} }
} }
pub fn additional_properties(mut self, additional_properties: bool) -> Self { pub const fn additional_properties(mut self, additional_properties: bool) -> Self {
self.additional_properties = additional_properties; self.additional_properties = additional_properties;
self self
} }
pub fn default_key(mut self, key: &'static str) -> Self { pub const fn default_key(mut self, key: &'static str) -> Self {
self.default_key = Some(key); self.default_key = Some(key);
self self
} }
pub fn required<S: Into<Arc<Schema>>>(mut self, name: &'static str, schema: S) -> Self { pub const fn schema(self) -> Schema {
self.properties.insert(name, (false, schema.into())); Schema::Object(self)
self
} }
pub fn optional<S: Into<Arc<Schema>>>(mut self, name: &'static str, schema: S) -> Self { pub fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
self.properties.insert(name, (true, schema.into())); if let Ok(ind) = self.properties.binary_search_by_key(&key, |(name, _, _)| name) {
self let (_name, optional, prop_schema) = self.properties[ind];
Some((optional, prop_schema))
} else {
None
}
} }
} }
@ -315,70 +387,10 @@ pub enum Schema {
Array(ArraySchema), Array(ArraySchema),
} }
impl From<StringSchema> for Schema {
fn from(string_schema: StringSchema) -> Self {
Schema::String(string_schema)
}
}
impl From<StringSchema> for Arc<Schema> {
fn from(string_schema: StringSchema) -> Self {
Arc::new(Schema::String(string_schema))
}
}
impl From<BooleanSchema> for Schema {
fn from(boolean_schema: BooleanSchema) -> Self {
Schema::Boolean(boolean_schema)
}
}
impl From<BooleanSchema> for Arc<Schema> {
fn from(boolean_schema: BooleanSchema) -> Self {
Arc::new(Schema::Boolean(boolean_schema))
}
}
impl From<IntegerSchema> for Schema {
fn from(integer_schema: IntegerSchema) -> Self {
Schema::Integer(integer_schema)
}
}
impl From<IntegerSchema> for Arc<Schema> {
fn from(integer_schema: IntegerSchema) -> Self {
Arc::new(Schema::Integer(integer_schema))
}
}
impl From<ObjectSchema> for Schema {
fn from(object_schema: ObjectSchema) -> Self {
Schema::Object(object_schema)
}
}
impl From<ObjectSchema> for Arc<Schema> {
fn from(object_schema: ObjectSchema) -> Self {
Arc::new(Schema::Object(object_schema))
}
}
impl From<ArraySchema> for Schema {
fn from(array_schema: ArraySchema) -> Self {
Schema::Array(array_schema)
}
}
impl From<ArraySchema> for Arc<Schema> {
fn from(array_schema: ArraySchema) -> Self {
Arc::new(Schema::Array(array_schema))
}
}
pub enum ApiStringFormat { pub enum ApiStringFormat {
Enum(&'static [&'static str]), Enum(&'static [&'static str]),
Pattern(&'static Regex), Pattern(&'static ConstRegexPattern),
Complex(Arc<Schema>), Complex(&'static Schema),
VerifyFn(fn(&str) -> Result<(), Error>), VerifyFn(fn(&str) -> Result<(), Error>),
} }
@ -480,12 +492,11 @@ pub fn parse_parameter_strings(data: &[(String, String)], schema: &ObjectSchema,
let mut errors = ParameterError::new(); let mut errors = ParameterError::new();
let properties = &schema.properties;
let additional_properties = schema.additional_properties; let additional_properties = schema.additional_properties;
for (key, value) in data { for (key, value) in data {
if let Some((_optional, prop_schema)) = properties.get::<str>(key) { if let Some((_optional, prop_schema)) = schema.lookup(&key) {
match prop_schema.as_ref() { match prop_schema {
Schema::Array(array_schema) => { Schema::Array(array_schema) => {
if params[key] == Value::Null { if params[key] == Value::Null {
params[key] = json!([]); params[key] = json!([]);
@ -533,7 +544,7 @@ pub fn parse_parameter_strings(data: &[(String, String)], schema: &ObjectSchema,
} }
if test_required && errors.len() == 0 { if test_required && errors.len() == 0 {
for (name, (optional, _prop_schema)) in properties { for (name, optional, _prop_schema) in schema.properties {
if !(*optional) && params[name] == Value::Null { if !(*optional) && params[name] == Value::Null {
errors.push(format_err!("parameter '{}': parameter is missing and it is not optional.", name)); errors.push(format_err!("parameter '{}': parameter is missing and it is not optional.", name));
} }
@ -624,12 +635,11 @@ pub fn verify_json_object(data: &Value, schema: &ObjectSchema) -> Result<(), Err
_ => bail!("Expected object - got scalar value."), _ => bail!("Expected object - got scalar value."),
}; };
let properties = &schema.properties;
let additional_properties = schema.additional_properties; let additional_properties = schema.additional_properties;
for (key, value) in map { for (key, value) in map {
if let Some((_optional, prop_schema)) = properties.get::<str>(key) { if let Some((_optional, prop_schema)) = schema.lookup(&key) {
match prop_schema.as_ref() { match prop_schema {
Schema::Object(object_schema) => { Schema::Object(object_schema) => {
verify_json_object(value, object_schema)?; verify_json_object(value, object_schema)?;
} }
@ -643,7 +653,7 @@ pub fn verify_json_object(data: &Value, schema: &ObjectSchema) -> Result<(), Err
} }
} }
for (name, (optional, _prop_schema)) in properties { for (name, optional, _prop_schema) in schema.properties {
if !(*optional) && data[name] == Value::Null { if !(*optional) && data[name] == Value::Null {
bail!("property '{}': property is missing and it is not optional.", name); bail!("property '{}': property is missing and it is not optional.", name);
} }
@ -657,11 +667,7 @@ fn test_schema1() {
let schema = Schema::Object(ObjectSchema { let schema = Schema::Object(ObjectSchema {
description: "TEST", description: "TEST",
additional_properties: false, additional_properties: false,
properties: { properties: &[],
let map = HashMap::new();
map
},
default_key: None, default_key: None,
}); });
@ -671,285 +677,342 @@ fn test_schema1() {
#[test] #[test]
fn test_query_string() { fn test_query_string() {
let schema = ObjectSchema::new("Parameters.") {
.required("name", StringSchema::new("Name.")); const SCHEMA: ObjectSchema = ObjectSchema::new(
"Parameters.",
let res = parse_query_string("", &schema, true); &[("name", false, &StringSchema::new("Name.").schema())]
assert!(res.is_err());
let schema = ObjectSchema::new("Parameters.")
.optional("name", StringSchema::new("Name."));
let res = parse_query_string("", &schema, true);
assert!(res.is_ok());
// TEST min_length and max_length
let schema = ObjectSchema::new("Parameters.")
.required(
"name", StringSchema::new("Name.")
.min_length(5)
.max_length(10)
); );
let res = parse_query_string("name=abcd", &schema, true); let res = parse_query_string("", &SCHEMA, true);
assert!(res.is_err()); 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
use lazy_static::lazy_static;
lazy_static! {
static ref TEST_REGEX: Regex = Regex::new("test").unwrap();
static ref TEST2_REGEX: Regex = Regex::new("^test$").unwrap();
} }
let schema = ObjectSchema::new("Parameters.") {
.required( const SCHEMA: ObjectSchema = ObjectSchema::new(
"name", StringSchema::new("Name.") "Parameters.",
.format(Arc::new(ApiStringFormat::Pattern(&TEST_REGEX))) &[("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); let res = parse_query_string("name=abcd", &SCHEMA, true);
assert!(res.is_err()); assert!(res.is_err());
let res = parse_query_string("name=ateststring", &schema, true); let res = parse_query_string("name=abcde", &SCHEMA, true);
assert!(res.is_ok()); assert!(res.is_ok());
let schema = ObjectSchema::new("Parameters.") let res = parse_query_string("name=abcdefghijk", &SCHEMA, true);
.required( assert!(res.is_err());
"name", StringSchema::new("Name.")
.format(Arc::new(ApiStringFormat::Pattern(&TEST2_REGEX)))
);
let res = parse_query_string("name=ateststring", &schema, true); let res = parse_query_string("name=abcdefghij", &SCHEMA, true);
assert!(res.is_err()); assert!(res.is_ok());
}
// TEST regex pattern
const_regex! {
TEST_REGEX = "test";
TEST2_REGEX = "^test$";
}
let res = parse_query_string("name=test", &schema, true); {
assert!(res.is_ok()); 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 // TEST string enums
{
const SCHEMA: ObjectSchema = ObjectSchema::new(
"Parameters.",
&[
("name", false, &StringSchema::new("Name.")
.format(&ApiStringFormat::Enum(&["ev1", "ev2"]))
.schema()
),
]);
let schema = ObjectSchema::new("Parameters.") let res = parse_query_string("name=noenum", &SCHEMA, true);
.required( assert!(res.is_err());
"name", StringSchema::new("Name.")
.format(Arc::new(ApiStringFormat::Enum(&["ev1", "ev2"])))
);
let res = parse_query_string("name=noenum", &schema, true); let res = parse_query_string("name=ev1", &SCHEMA, true);
assert!(res.is_err()); assert!(res.is_ok());
let res = parse_query_string("name=ev1", &schema, true); let res = parse_query_string("name=ev2", &SCHEMA, true);
assert!(res.is_ok()); 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());
let res = parse_query_string("name=ev3", &SCHEMA, true);
assert!(res.is_err());
}
} }
#[test] #[test]
fn test_query_integer() { fn test_query_integer() {
let schema = ObjectSchema::new("Parameters.") {
.required( const SCHEMA: ObjectSchema = ObjectSchema::new(
"count" , IntegerSchema::new("Count.") "Parameters.",
); &[
("count", false, &IntegerSchema::new("Count.").schema()),
]);
let res = parse_query_string("", &schema, true); let res = parse_query_string("", &SCHEMA, true);
assert!(res.is_err()); assert!(res.is_err());
}
let schema = ObjectSchema::new("Parameters.") {
.optional( const SCHEMA: ObjectSchema = ObjectSchema::new(
"count", IntegerSchema::new("Count.") "Parameters.",
.minimum(-3) &[
.maximum(50) ("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("", &schema, true); let res = parse_query_string("count=abc", &SCHEMA, false);
assert!(res.is_ok()); assert!(res.is_err());
let res = parse_query_string("count=abc", &schema, false); let res = parse_query_string("count=30", &SCHEMA, false);
assert!(res.is_err()); assert!(res.is_ok());
let res = parse_query_string("count=30", &schema, false); let res = parse_query_string("count=-1", &SCHEMA, false);
assert!(res.is_ok()); assert!(res.is_ok());
let res = parse_query_string("count=-1", &schema, false); let res = parse_query_string("count=300", &SCHEMA, false);
assert!(res.is_ok()); assert!(res.is_err());
let res = parse_query_string("count=300", &schema, false); let res = parse_query_string("count=-30", &SCHEMA, false);
assert!(res.is_err()); assert!(res.is_err());
let res = parse_query_string("count=-30", &schema, false); let res = parse_query_string("count=50", &SCHEMA, false);
assert!(res.is_err()); assert!(res.is_ok());
let res = parse_query_string("count=50", &schema, false); let res = parse_query_string("count=-3", &SCHEMA, false);
assert!(res.is_ok()); assert!(res.is_ok());
}
let res = parse_query_string("count=-3", &schema, false);
assert!(res.is_ok());
} }
#[test] #[test]
fn test_query_boolean() { fn test_query_boolean() {
let schema = ObjectSchema::new("Parameters.") {
.required( const SCHEMA: ObjectSchema = ObjectSchema::new(
"force", BooleanSchema::new("Force.") "Parameters.",
); &[
("force", false, &BooleanSchema::new("Force.").schema()),
]);
let res = parse_query_string("", &schema, true); let res = parse_query_string("", &SCHEMA, true);
assert!(res.is_err()); assert!(res.is_err());
}
let schema = ObjectSchema::new("Parameters.") {
.optional( const SCHEMA: ObjectSchema = ObjectSchema::new(
"force", BooleanSchema::new("Force.") "Parameters.",
); &[
("force", true, &BooleanSchema::new("Force.").schema()),
]);
let res = parse_query_string("", &SCHEMA, true);
assert!(res.is_ok());
let res = parse_query_string("", &schema, true); let res = parse_query_string("a=b", &SCHEMA, true);
assert!(res.is_ok()); assert!(res.is_err());
let res = parse_query_string("a=b", &schema, true); let res = parse_query_string("force", &SCHEMA, true);
assert!(res.is_err()); 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", &schema, true); let res = parse_query_string("force=NO", &SCHEMA, true);
assert!(res.is_err()); assert!(res.is_ok());
let res = parse_query_string("force=0", &SCHEMA, true);
let res = parse_query_string("force=yes", &schema, true); assert!(res.is_ok());
assert!(res.is_ok()); let res = parse_query_string("force=off", &SCHEMA, true);
let res = parse_query_string("force=1", &schema, true); assert!(res.is_ok());
assert!(res.is_ok()); let res = parse_query_string("force=False", &SCHEMA, true);
let res = parse_query_string("force=On", &schema, true); assert!(res.is_ok());
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] #[test]
fn test_verify_function() { fn test_verify_function() {
let schema = ObjectSchema::new("Parameters.") const SCHEMA: ObjectSchema = ObjectSchema::new(
.required( "Parameters.",
"p1", StringSchema::new("P1") &[
.format(ApiStringFormat::VerifyFn(|value| { ("p1", false, &StringSchema::new("P1")
if value == "test" { return Ok(()) }; .format(&ApiStringFormat::VerifyFn(|value| {
bail!("format error"); if value == "test" { return Ok(()) };
}).into()) bail!("format error");
); }))
.schema()
),
]);
let res = parse_query_string("p1=tes", &schema, true); let res = parse_query_string("p1=tes", &SCHEMA, true);
assert!(res.is_err()); assert!(res.is_err());
let res = parse_query_string("p1=test", &schema, true); let res = parse_query_string("p1=test", &SCHEMA, true);
assert!(res.is_ok()); assert!(res.is_ok());
} }
#[test] #[test]
fn test_verify_complex_object() { fn test_verify_complex_object() {
let nic_models = Arc::new(ApiStringFormat::Enum( const NIC_MODELS: ApiStringFormat = ApiStringFormat::Enum(&["e1000", "virtio"]);
&["e1000", "virtio"]));
let param_schema: Arc<Schema> = ObjectSchema::new("Properties.") 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") .default_key("model")
.required("model", StringSchema::new("Ethernet device Model.") .schema();
.format(nic_models))
.optional("enable", BooleanSchema::new("Enable device."))
.into();
let schema = ObjectSchema::new("Parameters.") const SCHEMA: ObjectSchema = ObjectSchema::new(
.required( "Parameters.",
"net0", StringSchema::new("First Network device.") &[
.format(ApiStringFormat::Complex(param_schema).into()) ("net0", false, &StringSchema::new("First Network device.")
); .format(&ApiStringFormat::Complex(&PARAM_SCHEMA))
.schema()
),
]);
let res = parse_query_string("", &schema, true); let res = parse_query_string("", &SCHEMA, true);
assert!(res.is_err()); assert!(res.is_err());
let res = parse_query_string("test=abc", &schema, true); let res = parse_query_string("test=abc", &SCHEMA, true);
assert!(res.is_err()); assert!(res.is_err());
let res = parse_query_string("net0=model=abc", &schema, true); let res = parse_query_string("net0=model=abc", &SCHEMA, true);
assert!(res.is_err()); assert!(res.is_err());
let res = parse_query_string("net0=model=virtio", &schema, true); 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()); assert!(res.is_ok());
let res = parse_query_string("net0=virtio,enable=no", &schema, true); 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()); assert!(res.is_ok());
} }
#[test] #[test]
fn test_verify_complex_array() { fn test_verify_complex_array() {
let param_schema: Arc<Schema> = ArraySchema::new( {
"Integer List.", Arc::new(IntegerSchema::new("Soemething").into())) const PARAM_SCHEMA: Schema = ArraySchema::new(
.into(); "Integer List.", &IntegerSchema::new("Soemething").schema())
.schema();
let schema = ObjectSchema::new("Parameters.") const SCHEMA: ObjectSchema = ObjectSchema::new(
.required( "Parameters.",
"list", StringSchema::new("A list on integers, comma separated.") &[
.format(ApiStringFormat::Complex(param_schema).into()) ("list", false, &StringSchema::new("A list on integers, comma separated.")
); .format(&ApiStringFormat::Complex(&PARAM_SCHEMA))
.schema()
),
]);
let res = parse_query_string("", &schema, true); let res = parse_query_string("", &SCHEMA, true);
assert!(res.is_err()); assert!(res.is_err());
let res = parse_query_string("list=", &schema, true); let res = parse_query_string("list=", &SCHEMA, true);
assert!(res.is_ok()); assert!(res.is_ok());
let res = parse_query_string("list=abc", &schema, true); let res = parse_query_string("list=abc", &SCHEMA, true);
assert!(res.is_err()); assert!(res.is_err());
let res = parse_query_string("list=1", &schema, true); let res = parse_query_string("list=1", &SCHEMA, true);
assert!(res.is_ok()); assert!(res.is_ok());
let res = parse_query_string("list=2,3,4,5", &schema, true); let res = parse_query_string("list=2,3,4,5", &SCHEMA, true);
assert!(res.is_ok()); assert!(res.is_ok());
}
let param_schema: Arc<Schema> = ArraySchema::new( {
"Integer List.", Arc::new(IntegerSchema::new("Soemething").into()))
.min_length(1)
.max_length(3)
.into();
let schema = ObjectSchema::new("Parameters.") const PARAM_SCHEMA: Schema = ArraySchema::new(
.required( "Integer List.", &IntegerSchema::new("Soemething").schema())
"list", StringSchema::new("A list on integers, comma separated.") .min_length(1)
.format(ApiStringFormat::Complex(param_schema).into()) .max_length(3)
); .schema();
let res = parse_query_string("list=", &schema, true); const SCHEMA: ObjectSchema = ObjectSchema::new(
assert!(res.is_err()); "Parameters.",
&[
("list", false, &StringSchema::new("A list on integers, comma separated.")
.format(&ApiStringFormat::Complex(&PARAM_SCHEMA))
.schema()
),
]);
let res = parse_query_string("list=1,2,3", &schema, true); let res = parse_query_string("list=", &SCHEMA, true);
assert!(res.is_ok()); assert!(res.is_err());
let res = parse_query_string("list=2,3,4,5", &schema, true); let res = parse_query_string("list=1,2,3", &SCHEMA, true);
assert!(res.is_err()); assert!(res.is_ok());
let res = parse_query_string("list=2,3,4,5", &SCHEMA, true);
assert!(res.is_err());
}
} }

View File

@ -5,7 +5,7 @@ use proxmox_backup::api_schema::format::*;
fn main() -> Result<(), Error> { fn main() -> Result<(), Error> {
let api = api2::backup::backup_api(); let api = api2::backup::BACKUP_API_ROUTER;
dump_api(&mut std::io::stdout(), &api, ".", 0)?; dump_api(&mut std::io::stdout(), &api, ".", 0)?;

View File

@ -1,6 +1,5 @@
use failure::*; use failure::*;
use futures::*; use futures::*;
use lazy_static::lazy_static;
use proxmox::tools::try_block; use proxmox::tools::try_block;
@ -44,13 +43,9 @@ async fn run() -> Result<(), Error> {
} }
let _ = csrf_secret(); // load with lazy_static let _ = csrf_secret(); // load with lazy_static
lazy_static!{
static ref ROUTER: Router = proxmox_backup::api2::router();
}
let config = ApiConfig::new( let config = ApiConfig::new(
buildcfg::JS_DIR, &ROUTER, RpcEnvironmentType::PRIVILEGED); buildcfg::JS_DIR, &proxmox_backup::api2::ROUTER, RpcEnvironmentType::PRIVILEGED);
let rest_server = RestServer::new(config); let rest_server = RestServer::new(config);
// http server future: // http server future:
@ -87,6 +82,6 @@ async fn run() -> Result<(), Error> {
server.await?; server.await?;
log::info!("done - exit server"); log::info!("done - exit server");
Ok(()) Ok(())
} }

View File

@ -1,4 +1,4 @@
//#[macro_use] #[macro_use]
extern crate proxmox_backup; extern crate proxmox_backup;
use failure::*; use failure::*;
@ -30,24 +30,20 @@ use proxmox_backup::pxar::{ self, catalog::* };
use serde_json::{json, Value}; use serde_json::{json, Value};
//use hyper::Body; //use hyper::Body;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use regex::Regex; //use regex::Regex;
use xdg::BaseDirectories; use xdg::BaseDirectories;
use lazy_static::lazy_static;
use futures::*; use futures::*;
use tokio::sync::mpsc; use tokio::sync::mpsc;
lazy_static! { proxmox_backup::const_regex! {
static ref BACKUPSPEC_REGEX: Regex = Regex::new(r"^([a-zA-Z0-9_-]+\.(?:pxar|img|conf|log)):(.+)$").unwrap(); BACKUPSPEC_REGEX = r"^([a-zA-Z0-9_-]+\.(?:pxar|img|conf|log)):(.+)$";
static ref REPO_URL_SCHEMA: Arc<Schema> = Arc::new(
StringSchema::new("Repository URL.")
.format(BACKUP_REPO_URL.clone())
.max_length(256)
.into()
);
} }
const REPO_URL_SCHEMA: Schema = StringSchema::new("Repository URL.")
.format(&BACKUP_REPO_URL)
.max_length(256)
.schema();
fn get_default_repository() -> Option<String> { fn get_default_repository() -> Option<String> {
std::env::var("PBS_REPOSITORY").ok() std::env::var("PBS_REPOSITORY").ok()
@ -556,7 +552,7 @@ fn start_garbage_collection(
fn parse_backupspec(value: &str) -> Result<(&str, &str), Error> { fn parse_backupspec(value: &str) -> Result<(&str, &str), Error> {
if let Some(caps) = BACKUPSPEC_REGEX.captures(value) { if let Some(caps) = (BACKUPSPEC_REGEX.regex_obj)().captures(value) {
return Ok((caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str())); return Ok((caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str()));
} }
bail!("unable to parse directory specification '{}'", value); bail!("unable to parse directory specification '{}'", value);
@ -1553,45 +1549,58 @@ fn key_change_passphrase(
fn key_mgmt_cli() -> CliCommandMap { fn key_mgmt_cli() -> CliCommandMap {
let kdf_schema: Arc<Schema> = Arc::new( const KDF_SCHEMA: Schema =
StringSchema::new("Key derivation function. Choose 'none' to store the key unecrypted.") StringSchema::new("Key derivation function. Choose 'none' to store the key unecrypted.")
.format(Arc::new(ApiStringFormat::Enum(&["scrypt", "none"]))) .format(&ApiStringFormat::Enum(&["scrypt", "none"]))
.default("scrypt") .default("scrypt")
.into() .schema();
const API_METHOD_KEY_CREATE: ApiMethod = ApiMethod::new(
&ApiHandler::Sync(&key_create),
&ObjectSchema::new(
"Create a new encryption key.",
&[
("path", false, &StringSchema::new("File system path.").schema()),
("kdf", true, &KDF_SCHEMA),
],
)
); );
let key_create_cmd_def = CliCommand::new( let key_create_cmd_def = CliCommand::new(&API_METHOD_KEY_CREATE)
ApiMethod::new(
key_create,
ObjectSchema::new("Create a new encryption key.")
.required("path", StringSchema::new("File system path."))
.optional("kdf", kdf_schema.clone())
))
.arg_param(vec!["path"]) .arg_param(vec!["path"])
.completion_cb("path", tools::complete_file_name); .completion_cb("path", tools::complete_file_name);
let key_change_passphrase_cmd_def = CliCommand::new( const API_METHOD_KEY_CHANGE_PASSPHRASE: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&key_change_passphrase),
key_change_passphrase, &ObjectSchema::new(
ObjectSchema::new("Change the passphrase required to decrypt the key.") "Change the passphrase required to decrypt the key.",
.required("path", StringSchema::new("File system path.")) &[
.optional("kdf", kdf_schema.clone()) ("path", false, &StringSchema::new("File system path.").schema()),
)) ("kdf", true, &KDF_SCHEMA),
],
)
);
let key_change_passphrase_cmd_def = CliCommand::new(&API_METHOD_KEY_CHANGE_PASSPHRASE)
.arg_param(vec!["path"]) .arg_param(vec!["path"])
.completion_cb("path", tools::complete_file_name); .completion_cb("path", tools::complete_file_name);
let key_create_master_key_cmd_def = CliCommand::new( const API_METHOD_KEY_CREATE_MASTER_KEY: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&key_create_master_key),
key_create_master_key, &ObjectSchema::new("Create a new 4096 bit RSA master pub/priv key pair.", &[])
ObjectSchema::new("Create a new 4096 bit RSA master pub/priv key pair.") );
));
let key_create_master_key_cmd_def = CliCommand::new(&API_METHOD_KEY_CREATE_MASTER_KEY);
let key_import_master_pubkey_cmd_def = CliCommand::new( const API_METHOD_KEY_IMPORT_MASTER_PUBKEY: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&key_import_master_pubkey),
key_import_master_pubkey, &ObjectSchema::new(
ObjectSchema::new("Import a new RSA public key and use it as master key. The key is expected to be in '.pem' format.") "Import a new RSA public key and use it as master key. The key is expected to be in '.pem' format.",
.required("path", StringSchema::new("File system path.")) &[ ("path", false, &StringSchema::new("File system path.").schema()) ],
)) )
);
let key_import_master_pubkey_cmd_def = CliCommand::new(&API_METHOD_KEY_IMPORT_MASTER_PUBKEY)
.arg_param(vec!["path"]) .arg_param(vec!["path"])
.completion_cb("path", tools::complete_file_name); .completion_cb("path", tools::complete_file_name);
@ -1602,7 +1611,6 @@ fn key_mgmt_cli() -> CliCommandMap {
.insert("change-passphrase".to_owned(), key_change_passphrase_cmd_def.into()) .insert("change-passphrase".to_owned(), key_change_passphrase_cmd_def.into())
} }
fn mount( fn mount(
param: Value, param: Value,
_info: &ApiMethod, _info: &ApiMethod,
@ -1742,223 +1750,330 @@ async fn mount_do(param: Value, pipe: Option<RawFd>) -> Result<Value, Error> {
fn main() { fn main() {
let backup_source_schema: Arc<Schema> = Arc::new( const BACKUP_SOURCE_SCHEMA: Schema = StringSchema::new("Backup source specification ([<label>:<path>]).")
StringSchema::new("Backup source specification ([<label>:<path>]).") .format(&ApiStringFormat::Pattern(&BACKUPSPEC_REGEX))
.format(Arc::new(ApiStringFormat::Pattern(&BACKUPSPEC_REGEX))) .schema();
.into()
);
let backup_cmd_def = CliCommand::new( const API_METHOD_CREATE_BACKUP: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&create_backup),
create_backup, &ObjectSchema::new(
ObjectSchema::new("Create (host) backup.") "Create (host) backup.",
.required( &[
(
"backupspec", "backupspec",
ArraySchema::new( false,
&ArraySchema::new(
"List of backup source specifications ([<label.ext>:<path>] ...)", "List of backup source specifications ([<label.ext>:<path>] ...)",
backup_source_schema, &BACKUP_SOURCE_SCHEMA,
).min_length(1) ).min_length(1).schema()
) ),
.optional("repository", REPO_URL_SCHEMA.clone()) (
.optional( "repository",
true,
&REPO_URL_SCHEMA
),
(
"include-dev", "include-dev",
ArraySchema::new( true,
&ArraySchema::new(
"Include mountpoints with same st_dev number (see ``man fstat``) as specified files.", "Include mountpoints with same st_dev number (see ``man fstat``) as specified files.",
StringSchema::new("Path to file.").into() &StringSchema::new("Path to file.").schema()
) ).schema()
) ),
.optional( (
"keyfile", "keyfile",
StringSchema::new("Path to encryption key. All data will be encrypted using this key.")) true,
.optional( &StringSchema::new("Path to encryption key. All data will be encrypted using this key.").schema()
),
(
"verbose", "verbose",
BooleanSchema::new("Verbose output.").default(false)) true,
.optional( &BooleanSchema::new("Verbose output.")
.default(false)
.schema()
),
(
"skip-lost-and-found", "skip-lost-and-found",
BooleanSchema::new("Skip lost+found directory").default(false)) true,
.optional( &BooleanSchema::new("Skip lost+found directory")
.default(false)
.schema()
),
(
"backup-type", "backup-type",
BACKUP_TYPE_SCHEMA.clone() true,
) &BACKUP_TYPE_SCHEMA,
.optional( ),
(
"backup-id", "backup-id",
BACKUP_ID_SCHEMA.clone() true,
) &BACKUP_ID_SCHEMA
.optional( ),
(
"backup-time", "backup-time",
BACKUP_TIME_SCHEMA.clone() true,
) &BACKUP_TIME_SCHEMA
.optional( ),
(
"chunk-size", "chunk-size",
IntegerSchema::new("Chunk size in KB. Must be a power of 2.") true,
&IntegerSchema::new("Chunk size in KB. Must be a power of 2.")
.minimum(64) .minimum(64)
.maximum(4096) .maximum(4096)
.default(4096) .default(4096)
) .schema()
)) ),
],
)
);
let backup_cmd_def = CliCommand::new(&API_METHOD_CREATE_BACKUP)
.arg_param(vec!["backupspec"]) .arg_param(vec!["backupspec"])
.completion_cb("repository", complete_repository) .completion_cb("repository", complete_repository)
.completion_cb("backupspec", complete_backup_source) .completion_cb("backupspec", complete_backup_source)
.completion_cb("keyfile", tools::complete_file_name) .completion_cb("keyfile", tools::complete_file_name)
.completion_cb("chunk-size", complete_chunk_size); .completion_cb("chunk-size", complete_chunk_size);
let upload_log_cmd_def = CliCommand::new( const API_METHOD_UPLOAD_LOG: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&upload_log),
upload_log, &ObjectSchema::new(
ObjectSchema::new("Upload backup log file.") "Upload backup log file.",
.required("snapshot", StringSchema::new("Snapshot path.")) &[
.required("logfile", StringSchema::new("The path to the log file you want to upload.")) (
.optional("repository", REPO_URL_SCHEMA.clone()) "snapshot",
.optional( false,
&StringSchema::new("Snapshot path.").schema()
),
(
"logfile",
false,
&StringSchema::new("The path to the log file you want to upload.").schema()
),
(
"repository",
true,
&REPO_URL_SCHEMA
),
(
"keyfile", "keyfile",
StringSchema::new("Path to encryption key. All data will be encrypted using this key.")) true,
)) &StringSchema::new("Path to encryption key. All data will be encrypted using this key.").schema()
),
],
)
);
let upload_log_cmd_def = CliCommand::new(&API_METHOD_UPLOAD_LOG)
.arg_param(vec!["snapshot", "logfile"]) .arg_param(vec!["snapshot", "logfile"])
.completion_cb("snapshot", complete_backup_snapshot) .completion_cb("snapshot", complete_backup_snapshot)
.completion_cb("logfile", tools::complete_file_name) .completion_cb("logfile", tools::complete_file_name)
.completion_cb("keyfile", tools::complete_file_name) .completion_cb("keyfile", tools::complete_file_name)
.completion_cb("repository", complete_repository); .completion_cb("repository", complete_repository);
let list_cmd_def = CliCommand::new( const API_METHOD_LIST_BACKUP_GROUPS: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&list_backup_groups),
list_backup_groups, &ObjectSchema::new(
ObjectSchema::new("List backup groups.") "List backup groups.",
.optional("repository", REPO_URL_SCHEMA.clone()) &[
.optional("output-format", OUTPUT_FORMAT.clone()) ("repository", true, &REPO_URL_SCHEMA),
)) ("output-format", true, &OUTPUT_FORMAT),
],
)
);
let list_cmd_def = CliCommand::new(&API_METHOD_LIST_BACKUP_GROUPS)
.completion_cb("repository", complete_repository); .completion_cb("repository", complete_repository);
let snapshots_cmd_def = CliCommand::new( const API_METHOD_LIST_SNAPSHOTS: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&list_snapshots),
list_snapshots, &ObjectSchema::new(
ObjectSchema::new("List backup snapshots.") "List backup snapshots.",
.optional("group", StringSchema::new("Backup group.")) &[
.optional("repository", REPO_URL_SCHEMA.clone()) ("group", true, &StringSchema::new("Backup group.").schema()),
.optional("output-format", OUTPUT_FORMAT.clone()) ("repository", true, &REPO_URL_SCHEMA),
)) ("output-format", true, &OUTPUT_FORMAT),
],
)
);
let snapshots_cmd_def = CliCommand::new(&API_METHOD_LIST_SNAPSHOTS)
.arg_param(vec!["group"]) .arg_param(vec!["group"])
.completion_cb("group", complete_backup_group) .completion_cb("group", complete_backup_group)
.completion_cb("repository", complete_repository); .completion_cb("repository", complete_repository);
let forget_cmd_def = CliCommand::new( const API_METHOD_FORGET_SNAPSHOTS: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&forget_snapshots),
forget_snapshots, &ObjectSchema::new(
ObjectSchema::new("Forget (remove) backup snapshots.") "Forget (remove) backup snapshots.",
.required("snapshot", StringSchema::new("Snapshot path.")) &[
.optional("repository", REPO_URL_SCHEMA.clone()) ("snapshot", false, &StringSchema::new("Snapshot path.").schema()),
)) ("repository", true, &REPO_URL_SCHEMA),
],
)
);
let forget_cmd_def = CliCommand::new(&API_METHOD_FORGET_SNAPSHOTS)
.arg_param(vec!["snapshot"]) .arg_param(vec!["snapshot"])
.completion_cb("repository", complete_repository) .completion_cb("repository", complete_repository)
.completion_cb("snapshot", complete_backup_snapshot); .completion_cb("snapshot", complete_backup_snapshot);
let garbage_collect_cmd_def = CliCommand::new( const API_METHOD_START_GARBAGE_COLLECTION: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&start_garbage_collection),
start_garbage_collection, &ObjectSchema::new(
ObjectSchema::new("Start garbage collection for a specific repository.") "Start garbage collection for a specific repository.",
.optional("repository", REPO_URL_SCHEMA.clone()) &[ ("repository", true, &REPO_URL_SCHEMA) ],
)) )
);
let garbage_collect_cmd_def = CliCommand::new(&API_METHOD_START_GARBAGE_COLLECTION)
.completion_cb("repository", complete_repository); .completion_cb("repository", complete_repository);
let restore_cmd_def = CliCommand::new( const API_METHOD_RESTORE: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&restore),
restore, &ObjectSchema::new(
ObjectSchema::new("Restore backup repository.") "Restore backup repository.",
.required("snapshot", StringSchema::new("Group/Snapshot path.")) &[
.required("archive-name", StringSchema::new("Backup archive name.")) ("snapshot", false, &StringSchema::new("Group/Snapshot path.").schema()),
.required("target", StringSchema::new(r###"Target directory path. Use '-' to write to stdandard output. ("archive-name", false, &StringSchema::new("Backup archive name.").schema()),
(
"target",
false,
&StringSchema::new(
r###"Target directory path. Use '-' to write to stdandard output.
We do not extraxt '.pxar' archives when writing to stdandard output. We do not extraxt '.pxar' archives when writing to stdandard output.
"### "###
)) ).schema()
.optional( ),
(
"allow-existing-dirs", "allow-existing-dirs",
BooleanSchema::new("Do not fail if directories already exists.").default(false)) true,
.optional("repository", REPO_URL_SCHEMA.clone()) &BooleanSchema::new("Do not fail if directories already exists.")
.optional("keyfile", StringSchema::new("Path to encryption key.")) .default(false)
.optional( .schema()
),
("repository", true, &REPO_URL_SCHEMA),
("keyfile", true, &StringSchema::new("Path to encryption key.").schema()),
(
"verbose", "verbose",
BooleanSchema::new("Verbose output.").default(false) true,
) &BooleanSchema::new("Verbose output.")
)) .default(false)
.schema()
),
],
)
);
let restore_cmd_def = CliCommand::new(&API_METHOD_RESTORE)
.arg_param(vec!["snapshot", "archive-name", "target"]) .arg_param(vec!["snapshot", "archive-name", "target"])
.completion_cb("repository", complete_repository) .completion_cb("repository", complete_repository)
.completion_cb("snapshot", complete_group_or_snapshot) .completion_cb("snapshot", complete_group_or_snapshot)
.completion_cb("archive-name", complete_archive_name) .completion_cb("archive-name", complete_archive_name)
.completion_cb("target", tools::complete_file_name); .completion_cb("target", tools::complete_file_name);
let files_cmd_def = CliCommand::new( const API_METHOD_LIST_SNAPSHOT_FILES: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&list_snapshot_files),
list_snapshot_files, &ObjectSchema::new(
ObjectSchema::new("List snapshot files.") "List snapshot files.",
.required("snapshot", StringSchema::new("Snapshot path.")) &[
.optional("repository", REPO_URL_SCHEMA.clone()) ("snapshot", false, &StringSchema::new("Snapshot path.").schema()),
.optional("output-format", OUTPUT_FORMAT.clone()) ("repository", true, &REPO_URL_SCHEMA),
)) ("output-format", true, &OUTPUT_FORMAT),
],
)
);
let files_cmd_def = CliCommand::new(&API_METHOD_LIST_SNAPSHOT_FILES)
.arg_param(vec!["snapshot"]) .arg_param(vec!["snapshot"])
.completion_cb("repository", complete_repository) .completion_cb("repository", complete_repository)
.completion_cb("snapshot", complete_backup_snapshot); .completion_cb("snapshot", complete_backup_snapshot);
let catalog_cmd_def = CliCommand::new( const API_METHOD_DUMP_CATALOG: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&dump_catalog),
dump_catalog, &ObjectSchema::new(
ObjectSchema::new("Dump catalog.") "Dump catalog.",
.required("snapshot", StringSchema::new("Snapshot path.")) &[
.optional("repository", REPO_URL_SCHEMA.clone()) ("snapshot", false, &StringSchema::new("Snapshot path.").schema()),
)) ("repository", true, &REPO_URL_SCHEMA),
],
)
);
let catalog_cmd_def = CliCommand::new(&API_METHOD_DUMP_CATALOG)
.arg_param(vec!["snapshot"]) .arg_param(vec!["snapshot"])
.completion_cb("repository", complete_repository) .completion_cb("repository", complete_repository)
.completion_cb("snapshot", complete_backup_snapshot); .completion_cb("snapshot", complete_backup_snapshot);
let prune_cmd_def = CliCommand::new( const API_METHOD_PRUNE: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&prune),
prune, &ObjectSchema::new(
proxmox_backup::api2::admin::datastore::add_common_prune_prameters( "Prune backup repository.",
ObjectSchema::new("Prune backup repository.") &proxmox_backup::add_common_prune_prameters!(
.required("group", StringSchema::new("Backup group.")) ("group", false, &StringSchema::new("Backup group.").schema()),
.optional("repository", REPO_URL_SCHEMA.clone()) ("repository", true, &REPO_URL_SCHEMA),
) )
)) )
);
let prune_cmd_def = CliCommand::new(&API_METHOD_PRUNE)
.arg_param(vec!["group"]) .arg_param(vec!["group"])
.completion_cb("group", complete_backup_group) .completion_cb("group", complete_backup_group)
.completion_cb("repository", complete_repository); .completion_cb("repository", complete_repository);
let status_cmd_def = CliCommand::new( const API_METHOD_STATUS: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&status),
status, &ObjectSchema::new(
ObjectSchema::new("Get repository status.") "Get repository status.",
.optional("repository", REPO_URL_SCHEMA.clone()) &[
.optional("output-format", OUTPUT_FORMAT.clone()) ("repository", true, &REPO_URL_SCHEMA),
)) ("output-format", true, &OUTPUT_FORMAT),
],
)
);
let status_cmd_def = CliCommand::new(&API_METHOD_STATUS)
.completion_cb("repository", complete_repository); .completion_cb("repository", complete_repository);
let login_cmd_def = CliCommand::new( const API_METHOD_API_LOGIN: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&api_login),
api_login, &ObjectSchema::new(
ObjectSchema::new("Try to login. If successful, store ticket.") "Try to login. If successful, store ticket.",
.optional("repository", REPO_URL_SCHEMA.clone()) &[ ("repository", true, &REPO_URL_SCHEMA) ],
)) )
);
let login_cmd_def = CliCommand::new(&API_METHOD_API_LOGIN)
.completion_cb("repository", complete_repository); .completion_cb("repository", complete_repository);
let logout_cmd_def = CliCommand::new( const API_METHOD_API_LOGOUT: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&api_logout),
api_logout, &ObjectSchema::new(
ObjectSchema::new("Logout (delete stored ticket).") "Logout (delete stored ticket).",
.optional("repository", REPO_URL_SCHEMA.clone()) &[ ("repository", true, &REPO_URL_SCHEMA) ],
)) )
);
let logout_cmd_def = CliCommand::new(&API_METHOD_API_LOGOUT)
.completion_cb("repository", complete_repository); .completion_cb("repository", complete_repository);
let mount_cmd_def = CliCommand::new( const API_METHOD_MOUNT: ApiMethod = ApiMethod::new(
ApiMethod::new( &ApiHandler::Sync(&mount),
mount, &ObjectSchema::new(
ObjectSchema::new("Mount pxar archive.") "Mount pxar archive.",
.required("snapshot", StringSchema::new("Group/Snapshot path.")) &[
.required("archive-name", StringSchema::new("Backup archive name.")) ("snapshot", false, &StringSchema::new("Group/Snapshot path.").schema()),
.required("target", StringSchema::new("Target directory path.")) ("archive-name", false, &StringSchema::new("Backup archive name.").schema()),
.optional("repository", REPO_URL_SCHEMA.clone()) ("target", false, &StringSchema::new("Target directory path.").schema()),
.optional("keyfile", StringSchema::new("Path to encryption key.")) ("repository", true, &REPO_URL_SCHEMA),
.optional("verbose", BooleanSchema::new("Verbose output.").default(false)) ("keyfile", true, &StringSchema::new("Path to encryption key.").schema()),
)) ("verbose", true, &BooleanSchema::new("Verbose output.").default(false).schema()),
],
)
);
let mount_cmd_def = CliCommand::new(&API_METHOD_MOUNT)
.arg_param(vec!["snapshot", "archive-name", "target"]) .arg_param(vec!["snapshot", "archive-name", "target"])
.completion_cb("repository", complete_repository) .completion_cb("repository", complete_repository)
.completion_cb("snapshot", complete_group_or_snapshot) .completion_cb("snapshot", complete_group_or_snapshot)

View File

@ -9,13 +9,13 @@ fn datastore_commands() -> CommandLineInterface {
use proxmox_backup::api2; use proxmox_backup::api2;
let cmd_def = CliCommandMap::new() let cmd_def = CliCommandMap::new()
.insert("list", CliCommand::new(api2::config::datastore::get()).into()) .insert("list", CliCommand::new(&api2::config::datastore::GET).into())
.insert("create", .insert("create",
CliCommand::new(api2::config::datastore::post()) CliCommand::new(&api2::config::datastore::POST)
.arg_param(vec!["name", "path"]) .arg_param(vec!["name", "path"])
.into()) .into())
.insert("remove", .insert("remove",
CliCommand::new(api2::config::datastore::delete()) CliCommand::new(&api2::config::datastore::DELETE)
.arg_param(vec!["name"]) .arg_param(vec!["name"])
.completion_cb("name", config::datastore::complete_datastore_name) .completion_cb("name", config::datastore::complete_datastore_name)
.into()); .into());
@ -32,12 +32,12 @@ fn garbage_collection_commands() -> CommandLineInterface {
let cmd_def = CliCommandMap::new() let cmd_def = CliCommandMap::new()
.insert("status", .insert("status",
CliCommand::new(api2::admin::datastore::api_method_garbage_collection_status()) CliCommand::new(&api2::admin::datastore::API_METHOD_GARBAGE_COLLECTION_STATUS)
.arg_param(vec!["store"]) .arg_param(vec!["store"])
.completion_cb("store", config::datastore::complete_datastore_name) .completion_cb("store", config::datastore::complete_datastore_name)
.into()) .into())
.insert("start", .insert("start",
CliCommand::new(api2::admin::datastore::api_method_start_garbage_collection()) CliCommand::new(&api2::admin::datastore::API_METHOD_START_GARBAGE_COLLECTION)
.arg_param(vec!["store"]) .arg_param(vec!["store"])
.completion_cb("store", config::datastore::complete_datastore_name) .completion_cb("store", config::datastore::complete_datastore_name)
.into()); .into());

View File

@ -9,7 +9,6 @@ use proxmox_backup::auth_helpers::*;
use failure::*; use failure::*;
use proxmox::tools::try_block; use proxmox::tools::try_block;
use lazy_static::lazy_static;
use futures::*; use futures::*;
@ -37,12 +36,8 @@ async fn run() -> Result<(), Error> {
let _ = public_auth_key(); // load with lazy_static let _ = public_auth_key(); // load with lazy_static
let _ = csrf_secret(); // load with lazy_static let _ = csrf_secret(); // load with lazy_static
lazy_static!{
static ref ROUTER: Router = proxmox_backup::api2::router();
}
let mut config = ApiConfig::new( let mut config = ApiConfig::new(
buildcfg::JS_DIR, &ROUTER, RpcEnvironmentType::PUBLIC); buildcfg::JS_DIR, &proxmox_backup::api2::ROUTER, RpcEnvironmentType::PUBLIC);
// add default dirs which includes jquery and bootstrap // add default dirs which includes jquery and bootstrap
// my $base = '/usr/share/libpve-http-server-perl'; // my $base = '/usr/share/libpve-http-server-perl';

View File

@ -13,7 +13,6 @@ use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::sync::Arc;
use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd; use std::os::unix::io::AsRawFd;
use std::collections::HashSet; use std::collections::HashSet;
@ -260,84 +259,247 @@ fn mount_archive(
Ok(Value::Null) Ok(Value::Null)
} }
const API_METHOD_CREATE_ARCHIVE: ApiMethod = ApiMethod::new(
&ApiHandler::Sync(&create_archive),
&ObjectSchema::new(
"Create new .pxar archive.",
&[
(
"archive",
false,
&StringSchema::new("Archive name").schema()
),
(
"source",
false,
&StringSchema::new("Source directory.").schema()
),
(
"verbose",
true,
&BooleanSchema::new("Verbose output.")
.default(false)
.schema()
),
(
"no-xattrs",
true,
&BooleanSchema::new("Ignore extended file attributes.")
.default(false)
.schema()
),
(
"no-fcaps",
true,
&BooleanSchema::new("Ignore file capabilities.")
.default(false)
.schema()
),
(
"no-acls",
true,
&BooleanSchema::new("Ignore access control list entries.")
.default(false)
.schema()
),
(
"all-file-systems",
true,
&BooleanSchema::new("Include mounted sudirs.")
.default(false)
.schema()
),
(
"no-device-nodes",
true,
&BooleanSchema::new("Ignore device nodes.")
.default(false)
.schema()
),
(
"no-fifos",
true,
&BooleanSchema::new("Ignore fifos.")
.default(false)
.schema()
),
(
"no-sockets",
true,
&BooleanSchema::new("Ignore sockets.")
.default(false)
.schema()
),
(
"exclude",
true,
&ArraySchema::new(
"List of paths or pattern matching files to exclude.",
&StringSchema::new("Path or pattern matching files to restore.").schema()
).schema()
),
],
)
);
const API_METHOD_EXTRACT_ARCHIVE: ApiMethod = ApiMethod::new(
&ApiHandler::Sync(&extract_archive),
&ObjectSchema::new(
"Extract an archive.",
&[
(
"archive",
false,
&StringSchema::new("Archive name.").schema()
),
(
"pattern",
true,
&ArraySchema::new(
"List of paths or pattern matching files to restore",
&StringSchema::new("Path or pattern matching files to restore.").schema()
).schema()
),
(
"target",
true,
&StringSchema::new("Target directory.").schema()
),
(
"verbose",
true,
&BooleanSchema::new("Verbose output.")
.default(false)
.schema()
),
(
"no-xattrs",
true,
&BooleanSchema::new("Ignore extended file attributes.")
.default(false)
.schema()
),
(
"no-fcaps",
true,
&BooleanSchema::new("Ignore file capabilities.")
.default(false)
.schema()
),
(
"no-acls",
true,
&BooleanSchema::new("Ignore access control list entries.")
.default(false)
.schema()
),
(
"allow-existing-dirs",
true,
&BooleanSchema::new("Allows directories to already exist on restore.")
.default(false)
.schema()
),
(
"files-from",
true,
&StringSchema::new("Match pattern for files to restore.").schema()
),
(
"no-device-nodes",
true,
&BooleanSchema::new("Ignore device nodes.")
.default(false)
.schema()
),
(
"no-fifos",
true,
&BooleanSchema::new("Ignore fifos.")
.default(false)
.schema()
),
(
"no-sockets",
true,
&BooleanSchema::new("Ignore sockets.")
.default(false)
.schema()
),
],
)
);
const API_METHOD_MOUNT_ARCHIVE: ApiMethod = ApiMethod::new(
&ApiHandler::Sync(&mount_archive),
&ObjectSchema::new(
"Mount the archive as filesystem via FUSE.",
&[
(
"archive",
false,
&StringSchema::new("Archive name.").schema()
),
(
"mountpoint",
false,
&StringSchema::new("Mountpoint for the filesystem root.").schema()
),
(
"verbose",
true,
&BooleanSchema::new("Verbose output, keeps process running in foreground (for debugging).")
.default(false)
.schema()
),
(
"no-mt",
true,
&BooleanSchema::new("Run in single threaded mode (for debugging).")
.default(false)
.schema()
),
],
)
);
const API_METHOD_DUMP_ARCHIVE: ApiMethod = ApiMethod::new(
&ApiHandler::Sync(&dump_archive),
&ObjectSchema::new(
"List the contents of an archive.",
&[
( "archive", false, &StringSchema::new("Archive name.").schema()),
( "verbose", true, &BooleanSchema::new("Verbose output.")
.default(false)
.schema()
),
]
)
);
fn main() { fn main() {
let cmd_def = CliCommandMap::new() let cmd_def = CliCommandMap::new()
.insert("create", CliCommand::new( .insert("create", CliCommand::new(&API_METHOD_CREATE_ARCHIVE)
ApiMethod::new(
create_archive,
ObjectSchema::new("Create new .pxar archive.")
.required("archive", StringSchema::new("Archive name"))
.required("source", StringSchema::new("Source directory."))
.optional("verbose", BooleanSchema::new("Verbose output.").default(false))
.optional("no-xattrs", BooleanSchema::new("Ignore extended file attributes.").default(false))
.optional("no-fcaps", BooleanSchema::new("Ignore file capabilities.").default(false))
.optional("no-acls", BooleanSchema::new("Ignore access control list entries.").default(false))
.optional("all-file-systems", BooleanSchema::new("Include mounted sudirs.").default(false))
.optional("no-device-nodes", BooleanSchema::new("Ignore device nodes.").default(false))
.optional("no-fifos", BooleanSchema::new("Ignore fifos.").default(false))
.optional("no-sockets", BooleanSchema::new("Ignore sockets.").default(false))
.optional("exclude", Arc::new(
ArraySchema::new(
"List of paths or pattern matching files to exclude.",
Arc::new(StringSchema::new("Path or pattern matching files to restore.").into())
).into()
))
))
.arg_param(vec!["archive", "source", "exclude"]) .arg_param(vec!["archive", "source", "exclude"])
.completion_cb("archive", tools::complete_file_name) .completion_cb("archive", tools::complete_file_name)
.completion_cb("source", tools::complete_file_name) .completion_cb("source", tools::complete_file_name)
.into() .into()
) )
.insert("extract", CliCommand::new( .insert("extract", CliCommand::new(&API_METHOD_EXTRACT_ARCHIVE)
ApiMethod::new(
extract_archive,
ObjectSchema::new("Extract an archive.")
.required("archive", StringSchema::new("Archive name."))
.optional("pattern", Arc::new(
ArraySchema::new(
"List of paths or pattern matching files to restore",
Arc::new(StringSchema::new("Path or pattern matching files to restore.").into())
).into()
))
.optional("target", StringSchema::new("Target directory."))
.optional("verbose", BooleanSchema::new("Verbose output.").default(false))
.optional("no-xattrs", BooleanSchema::new("Ignore extended file attributes.").default(false))
.optional("no-fcaps", BooleanSchema::new("Ignore file capabilities.").default(false))
.optional("no-acls", BooleanSchema::new("Ignore access control list entries.").default(false))
.optional("allow-existing-dirs", BooleanSchema::new("Allows directories to already exist on restore.").default(false))
.optional("files-from", StringSchema::new("Match pattern for files to restore."))
.optional("no-device-nodes", BooleanSchema::new("Ignore device nodes.").default(false))
.optional("no-fifos", BooleanSchema::new("Ignore fifos.").default(false))
.optional("no-sockets", BooleanSchema::new("Ignore sockets.").default(false))
))
.arg_param(vec!["archive", "pattern"]) .arg_param(vec!["archive", "pattern"])
.completion_cb("archive", tools::complete_file_name) .completion_cb("archive", tools::complete_file_name)
.completion_cb("target", tools::complete_file_name) .completion_cb("target", tools::complete_file_name)
.completion_cb("files-from", tools::complete_file_name) .completion_cb("files-from", tools::complete_file_name)
.into() .into()
) )
.insert("mount", CliCommand::new( .insert("mount", CliCommand::new(&API_METHOD_MOUNT_ARCHIVE)
ApiMethod::new(
mount_archive,
ObjectSchema::new("Mount the archive as filesystem via FUSE.")
.required("archive", StringSchema::new("Archive name."))
.required("mountpoint", StringSchema::new("Mountpoint for the filesystem root."))
.optional("verbose", BooleanSchema::new("Verbose output, keeps process running in foreground (for debugging).").default(false))
.optional("no-mt", BooleanSchema::new("Run in single threaded mode (for debugging).").default(false))
))
.arg_param(vec!["archive", "mountpoint"]) .arg_param(vec!["archive", "mountpoint"])
.completion_cb("archive", tools::complete_file_name) .completion_cb("archive", tools::complete_file_name)
.completion_cb("mountpoint", tools::complete_file_name) .completion_cb("mountpoint", tools::complete_file_name)
.into() .into()
) )
.insert("list", CliCommand::new( .insert("list", CliCommand::new(&API_METHOD_DUMP_ARCHIVE)
ApiMethod::new(
dump_archive,
ObjectSchema::new("List the contents of an archive.")
.required("archive", StringSchema::new("Archive name."))
.optional("verbose", BooleanSchema::new("Verbose output.").default(false))
))
.arg_param(vec!["archive"]) .arg_param(vec!["archive"])
.completion_cb("archive", tools::complete_file_name) .completion_cb("archive", tools::complete_file_name)
.into() .into()

View File

@ -1,11 +1,10 @@
use failure::*; use failure::*;
use lazy_static::lazy_static;
use std::sync::Arc;
use serde_json::Value; use serde_json::Value;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use crate::api_schema::*; use crate::api_schema::*;
use crate::api_schema::api_handler::*;
use crate::api_schema::router::*; use crate::api_schema::router::*;
use crate::api_schema::format::*; use crate::api_schema::format::*;
//use crate::api_schema::config::*; //use crate::api_schema::config::*;
@ -13,14 +12,10 @@ use super::environment::CliEnvironment;
use super::getopts; use super::getopts;
lazy_static!{ pub const OUTPUT_FORMAT: Schema =
StringSchema::new("Output format.")
pub static ref OUTPUT_FORMAT: Arc<Schema> = .format(&ApiStringFormat::Enum(&["text", "json", "json-pretty"]))
StringSchema::new("Output format.") .schema();
.format(Arc::new(ApiStringFormat::Enum(&["text", "json", "json-pretty"])))
.into();
}
/// Helper function to format and print result /// Helper function to format and print result
/// ///
@ -46,23 +41,22 @@ fn generate_usage_str(
let arg_param = &cli_cmd.arg_param; let arg_param = &cli_cmd.arg_param;
let fixed_param = &cli_cmd.fixed_param; let fixed_param = &cli_cmd.fixed_param;
let properties = &cli_cmd.info.parameters.properties; let schema = cli_cmd.info.parameters;
let description = &cli_cmd.info.parameters.description;
let mut done_hash = HashSet::<&str>::new(); let mut done_hash = HashSet::<&str>::new();
let mut args = String::new(); let mut args = String::new();
for positional_arg in arg_param { for positional_arg in arg_param {
match properties.get(positional_arg) { match schema.lookup(positional_arg) {
Some((optional, schema)) => { Some((optional, param_schema)) => {
args.push(' '); args.push(' ');
let is_array = if let Schema::Array(_) = schema.as_ref() { true } else { false }; let is_array = if let Schema::Array(_) = param_schema { true } else { false };
if *optional { args.push('['); } if optional { args.push('['); }
if is_array { args.push('{'); } if is_array { args.push('{'); }
args.push('<'); args.push_str(positional_arg); args.push('>'); args.push('<'); args.push_str(positional_arg); args.push('>');
if is_array { args.push('}'); } if is_array { args.push('}'); }
if *optional { args.push(']'); } if optional { args.push(']'); }
done_hash.insert(positional_arg); done_hash.insert(positional_arg);
} }
@ -72,28 +66,24 @@ fn generate_usage_str(
let mut arg_descr = String::new(); let mut arg_descr = String::new();
for positional_arg in arg_param { for positional_arg in arg_param {
let (_optional, schema) = properties.get(positional_arg).unwrap(); let (_optional, param_schema) = schema.lookup(positional_arg).unwrap();
let param_descr = get_property_description( let param_descr = get_property_description(
positional_arg, &schema, ParameterDisplayStyle::Fixed, format); positional_arg, param_schema, ParameterDisplayStyle::Fixed, format);
arg_descr.push_str(&param_descr); arg_descr.push_str(&param_descr);
} }
let mut options = String::new(); let mut options = String::new();
let mut prop_names: Vec<&str> = properties.keys().map(|v| *v).collect(); for (prop, optional, param_schema) in schema.properties {
prop_names.sort();
for prop in prop_names {
let (optional, schema) = properties.get(prop).unwrap();
if done_hash.contains(prop) { continue; } if done_hash.contains(prop) { continue; }
if fixed_param.contains_key(&prop) { continue; } if fixed_param.contains_key(prop) { continue; }
let type_text = get_schema_type_text(&schema, ParameterDisplayStyle::Arg); let type_text = get_schema_type_text(param_schema, ParameterDisplayStyle::Arg);
if *optional { if *optional {
if options.len() > 0 { options.push('\n'); } if options.len() > 0 { options.push('\n'); }
options.push_str(&get_property_description(prop, &schema, ParameterDisplayStyle::Arg, format)); options.push_str(&get_property_description(prop, param_schema, ParameterDisplayStyle::Arg, format));
} else { } else {
args.push_str(" --"); args.push_str(prop); args.push_str(" --"); args.push_str(prop);
@ -114,10 +104,10 @@ fn generate_usage_str(
format!("{}{}{}{}\n\n", indent, prefix, args, option_indicator) format!("{}{}{}{}\n\n", indent, prefix, args, option_indicator)
} }
DocumentationFormat::Full => { DocumentationFormat::Full => {
format!("{}{}{}{}\n\n{}\n\n", indent, prefix, args, option_indicator, description) format!("{}{}{}{}\n\n{}\n\n", indent, prefix, args, option_indicator, schema.description)
} }
DocumentationFormat::ReST => { DocumentationFormat::ReST => {
format!("``{}{}{}``\n\n{}\n\n", prefix, args, option_indicator, description) format!("``{}{}{}``\n\n{}\n\n", prefix, args, option_indicator, schema.description)
} }
}; };
@ -138,7 +128,7 @@ fn print_simple_usage_error(prefix: &str, cli_cmd: &CliCommand, err: Error) {
eprint!("Error: {}\nUsage: {}", err, usage); eprint!("Error: {}\nUsage: {}", err, usage);
} }
fn print_help( pub fn print_help(
top_def: &CommandLineInterface, top_def: &CommandLineInterface,
mut prefix: String, mut prefix: String,
args: &Vec<String>, args: &Vec<String>,
@ -175,7 +165,7 @@ fn print_help(
} }
fn handle_simple_command( fn handle_simple_command(
top_def: &CommandLineInterface, _top_def: &CommandLineInterface,
prefix: &str, prefix: &str,
cli_cmd: &CliCommand, cli_cmd: &CliCommand,
args: Vec<String>, args: Vec<String>,
@ -190,12 +180,6 @@ fn handle_simple_command(
} }
}; };
if cli_cmd.info.handler.is_none() {
let prefix = prefix.split(' ').next().unwrap().to_string();
print_help(top_def, prefix, &rest, params["verbose"].as_bool());
return;
}
if !rest.is_empty() { if !rest.is_empty() {
let err = format_err!("got additional arguments: {:?}", rest); let err = format_err!("got additional arguments: {:?}", rest);
print_simple_usage_error(prefix, cli_cmd, err); print_simple_usage_error(prefix, cli_cmd, err);
@ -204,17 +188,25 @@ fn handle_simple_command(
let mut rpcenv = CliEnvironment::new(); let mut rpcenv = CliEnvironment::new();
match (cli_cmd.info.handler.as_ref().unwrap())(params, &cli_cmd.info, &mut rpcenv) { match cli_cmd.info.handler {
Ok(value) => { ApiHandler::Sync(handler) => {
if value != Value::Null { match (handler)(params, &cli_cmd.info, &mut rpcenv) {
println!("Result: {}", serde_json::to_string_pretty(&value).unwrap()); Ok(value) => {
if value != Value::Null {
println!("Result: {}", serde_json::to_string_pretty(&value).unwrap());
}
}
Err(err) => {
eprintln!("Error: {}", err);
std::process::exit(-1);
}
} }
} }
Err(err) => { ApiHandler::Async(_) => {
eprintln!("Error: {}", err); //fixme
std::process::exit(-1); unimplemented!();
} }
} }
} }
fn find_command<'a>(def: &'a CliCommandMap, name: &str) -> Option<&'a CommandLineInterface> { fn find_command<'a>(def: &'a CliCommandMap, name: &str) -> Option<&'a CommandLineInterface> {
@ -335,8 +327,8 @@ fn print_property_completion(
} }
if let Schema::String(StringSchema { format: Some(format), ..} ) = schema { if let Schema::String(StringSchema { format: Some(format), ..} ) = schema {
if let ApiStringFormat::Enum(list) = *format.as_ref() { if let ApiStringFormat::Enum(list) = format {
for value in list { for value in list.iter() {
if value.starts_with(arg) { if value.starts_with(arg) {
println!("{}", value); println!("{}", value);
} }
@ -349,8 +341,8 @@ fn print_property_completion(
fn record_done_argument(done: &mut HashMap<String, String>, parameters: &ObjectSchema, key: &str, value: &str) { fn record_done_argument(done: &mut HashMap<String, String>, parameters: &ObjectSchema, key: &str, value: &str) {
if let Some((_, schema)) = parameters.properties.get::<str>(key) { if let Some((_, schema)) = parameters.lookup(key) {
match schema.as_ref() { match schema {
Schema::Array(_) => { /* do nothing ?? */ } Schema::Array(_) => { /* do nothing ?? */ }
_ => { done.insert(key.to_owned(), value.to_owned()); } _ => { done.insert(key.to_owned(), value.to_owned()); }
} }
@ -370,12 +362,12 @@ fn print_simple_completion(
if !arg_param.is_empty() { if !arg_param.is_empty() {
let prop_name = arg_param[0]; let prop_name = arg_param[0];
if args.len() > 1 { if args.len() > 1 {
record_done_argument(done, &cli_cmd.info.parameters, prop_name, &args[0]); record_done_argument(done, cli_cmd.info.parameters, prop_name, &args[0]);
print_simple_completion(cli_cmd, done, arg_param, &arg_param[1..], &args[1..]); print_simple_completion(cli_cmd, done, arg_param, &arg_param[1..], &args[1..]);
return; return;
} else if args.len() == 1 { } else if args.len() == 1 {
record_done_argument(done, &cli_cmd.info.parameters, prop_name, &args[0]); record_done_argument(done, cli_cmd.info.parameters, prop_name, &args[0]);
if let Some((_, schema)) = cli_cmd.info.parameters.properties.get(prop_name) { if let Some((_, schema)) = cli_cmd.info.parameters.lookup(prop_name) {
print_property_completion(schema, prop_name, &cli_cmd.completion_functions, &args[0], done); print_property_completion(schema, prop_name, &cli_cmd.completion_functions, &args[0], done);
} }
} }
@ -399,14 +391,14 @@ fn print_simple_completion(
let last = &args[args.len()-2]; let last = &args[args.len()-2];
if last.starts_with("--") && last.len() > 2 { if last.starts_with("--") && last.len() > 2 {
let prop_name = &last[2..]; let prop_name = &last[2..];
if let Some((_, schema)) = cli_cmd.info.parameters.properties.get(prop_name) { if let Some((_, schema)) = cli_cmd.info.parameters.lookup(prop_name) {
print_property_completion(schema, prop_name, &cli_cmd.completion_functions, &prefix, done); print_property_completion(schema, prop_name, &cli_cmd.completion_functions, &prefix, done);
} }
return; return;
} }
} }
for (name, (_optional, _schema)) in &cli_cmd.info.parameters.properties { for (name, _optional, _schema) in cli_cmd.info.parameters.properties {
if done.contains_key(*name) { continue; } if done.contains_key(*name) { continue; }
if all_arg_param.contains(name) { continue; } if all_arg_param.contains(name) { continue; }
let option = String::from("--") + name; let option = String::from("--") + name;
@ -524,13 +516,16 @@ pub fn print_bash_completion(def: &CommandLineInterface) {
} }
} }
const VERBOSE_HELP_SCHEMA: Schema = BooleanSchema::new("Verbose help.").schema();
const COMMAND_HELP: ObjectSchema = ObjectSchema::new(
"Get help about specified command.",
&[ ("verbose", true, &VERBOSE_HELP_SCHEMA) ]
);
const API_METHOD_COMMAND_HELP: ApiMethod = ApiMethod::new_dummy(&COMMAND_HELP);
fn help_command_def() -> CliCommand { fn help_command_def() -> CliCommand {
CliCommand::new( CliCommand::new(&API_METHOD_COMMAND_HELP)
ApiMethod::new_dummy(
ObjectSchema::new("Get help about specified command.")
.optional("verbose", BooleanSchema::new("Verbose help."))
)
)
} }
pub fn run_cli_command(def: CommandLineInterface) { pub fn run_cli_command(def: CommandLineInterface) {
@ -579,7 +574,7 @@ pub fn run_cli_command(def: CommandLineInterface) {
pub type CompletionFunction = fn(&str, &HashMap<String, String>) -> Vec<String>; pub type CompletionFunction = fn(&str, &HashMap<String, String>) -> Vec<String>;
pub struct CliCommand { pub struct CliCommand {
pub info: ApiMethod, pub info: &'static ApiMethod,
pub arg_param: Vec<&'static str>, pub arg_param: Vec<&'static str>,
pub fixed_param: HashMap<&'static str, String>, pub fixed_param: HashMap<&'static str, String>,
pub completion_functions: HashMap<String, CompletionFunction>, pub completion_functions: HashMap<String, CompletionFunction>,
@ -587,7 +582,7 @@ pub struct CliCommand {
impl CliCommand { impl CliCommand {
pub fn new(info: ApiMethod) -> Self { pub fn new(info: &'static ApiMethod) -> Self {
Self { Self {
info, arg_param: vec![], info, arg_param: vec![],
fixed_param: HashMap::new(), fixed_param: HashMap::new(),

View File

@ -66,8 +66,6 @@ pub (crate) fn parse_argument_list<T: AsRef<str>>(
let mut pos = 0; let mut pos = 0;
let properties = &schema.properties;
while pos < args.len() { while pos < args.len() {
match parse_argument(args[pos].as_ref()) { match parse_argument(args[pos].as_ref()) {
RawArgument::Separator => { RawArgument::Separator => {
@ -77,8 +75,8 @@ pub (crate) fn parse_argument_list<T: AsRef<str>>(
None => { None => {
let mut want_bool = false; let mut want_bool = false;
let mut can_default = false; let mut can_default = false;
if let Some((_optional, param_schema)) = properties.get::<str>(&name) { if let Some((_optional, param_schema)) = schema.lookup(&name) {
if let Schema::Boolean(boolean_schema) = param_schema.as_ref() { if let Schema::Boolean(boolean_schema) = param_schema {
want_bool = true; want_bool = true;
if let Some(default) = boolean_schema.default { if let Some(default) = boolean_schema.default {
if default == false { if default == false {
@ -153,8 +151,6 @@ pub fn parse_arguments<T: AsRef<str>>(
) -> Result<(Value, Vec<String>), ParameterError> { ) -> Result<(Value, Vec<String>), ParameterError> {
let mut errors = ParameterError::new(); let mut errors = ParameterError::new();
let properties = &schema.properties;
// first check if all arg_param exists in schema // first check if all arg_param exists in schema
let mut last_arg_param_is_optional = false; let mut last_arg_param_is_optional = false;
@ -162,13 +158,13 @@ pub fn parse_arguments<T: AsRef<str>>(
for i in 0..arg_param.len() { for i in 0..arg_param.len() {
let name = arg_param[i]; let name = arg_param[i];
if let Some((optional, param_schema)) = properties.get::<str>(&name) { if let Some((optional, param_schema)) = schema.lookup(&name) {
if i == arg_param.len() -1 { if i == arg_param.len() -1 {
last_arg_param_is_optional = *optional; last_arg_param_is_optional = optional;
if let Schema::Array(_) = param_schema.as_ref() { if let Schema::Array(_) = param_schema {
last_arg_param_is_array = true; last_arg_param_is_array = true;
} }
} else if *optional { } else if optional {
panic!("positional argument '{}' may not be optional", name); panic!("positional argument '{}' may not be optional", name);
} }
} else { } else {
@ -208,10 +204,11 @@ pub fn parse_arguments<T: AsRef<str>>(
#[test] #[test]
fn test_boolean_arg() { fn test_boolean_arg() {
let schema = ObjectSchema::new("Parameters:")
.required( const PARAMETERS: ObjectSchema = ObjectSchema::new(
"enable", BooleanSchema::new("Enable") "Parameters:",
); &[ ("enable", false, &BooleanSchema::new("Enable").schema()) ],
);
let mut variants: Vec<(Vec<&str>, bool)> = vec![]; let mut variants: Vec<(Vec<&str>, bool)> = vec![];
variants.push((vec!["-enable"], true)); variants.push((vec!["-enable"], true));
@ -228,7 +225,7 @@ fn test_boolean_arg() {
variants.push((vec!["--enable", "false"], false)); variants.push((vec!["--enable", "false"], false));
for (args, expect) in variants { for (args, expect) in variants {
let res = parse_arguments(&args, &vec![], &schema); let res = parse_arguments(&args, &vec![], &PARAMETERS);
assert!(res.is_ok()); assert!(res.is_ok());
if let Ok((options, rest)) = res { if let Ok((options, rest)) = res {
assert!(options["enable"] == expect); assert!(options["enable"] == expect);
@ -239,12 +236,17 @@ fn test_boolean_arg() {
#[test] #[test]
fn test_argument_paramenter() { fn test_argument_paramenter() {
let schema = ObjectSchema::new("Parameters:")
.required("enable", BooleanSchema::new("Enable.")) const PARAMETERS: ObjectSchema = ObjectSchema::new(
.required("storage", StringSchema::new("Storage.")); "Parameters:",
&[
("enable", false, &BooleanSchema::new("Enable.").schema()),
("storage", false, &StringSchema::new("Storage.").schema()),
],
);
let args = vec!["-enable", "local"]; let args = vec!["-enable", "local"];
let res = parse_arguments(&args, &vec!["storage"], &schema); let res = parse_arguments(&args, &vec!["storage"], &PARAMETERS);
assert!(res.is_ok()); assert!(res.is_ok());
if let Ok((options, rest)) = res { if let Ok((options, rest)) = res {
assert!(options["enable"] == true); assert!(options["enable"] == true);

View File

@ -2,21 +2,16 @@ use failure::*;
use crate::api_schema::*; use crate::api_schema::*;
use std::sync::Arc;
use lazy_static::lazy_static;
use regex::Regex;
use std::fmt; use std::fmt;
lazy_static! { const_regex! {
/// Regular expression to parse repository URLs /// Regular expression to parse repository URLs
pub static ref BACKUP_REPO_URL_REGEX: Regex = pub BACKUP_REPO_URL_REGEX = r"^(?:(?:([\w@]+)@)?([\w\-_.]+):)?(\w+)$";
Regex::new(r"^(?:(?:([\w@]+)@)?([\w\-_.]+):)?(\w+)$").unwrap();
/// API schema format definition for repository URLs
pub static ref BACKUP_REPO_URL: Arc<ApiStringFormat> =
ApiStringFormat::Pattern(&BACKUP_REPO_URL_REGEX).into();
} }
/// API schema format definition for repository URLs
pub const BACKUP_REPO_URL: ApiStringFormat = ApiStringFormat::Pattern(&BACKUP_REPO_URL_REGEX);
/// Reference remote backup locations /// Reference remote backup locations
/// ///
@ -73,7 +68,7 @@ impl std::str::FromStr for BackupRepository {
/// host, and `user` defaults to `root@pam`. /// host, and `user` defaults to `root@pam`.
fn from_str(url: &str) -> Result<Self, Self::Err> { fn from_str(url: &str) -> Result<Self, Self::Err> {
let cap = BACKUP_REPO_URL_REGEX.captures(url) let cap = (BACKUP_REPO_URL_REGEX.regex_obj)().captures(url)
.ok_or_else(|| format_err!("unable to parse repository url '{}'", url))?; .ok_or_else(|| format_err!("unable to parse repository url '{}'", url))?;
Ok(Self { Ok(Self {

View File

@ -6,25 +6,27 @@ use lazy_static::lazy_static;
use proxmox::tools::{fs::file_set_contents, try_block}; use proxmox::tools::{fs::file_set_contents, try_block};
use crate::api_schema::{ObjectSchema, StringSchema}; use crate::api_schema::{Schema, ObjectSchema, StringSchema};
use crate::section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin}; use crate::section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin};
lazy_static! { lazy_static! {
static ref CONFIG: SectionConfig = init(); static ref CONFIG: SectionConfig = init();
} }
const DIR_NAME_SCHEMA: Schema = StringSchema::new("Directory name").schema();
const DATASTORE_ID_SCHEMA: Schema = StringSchema::new("DataStore ID schema.")
.min_length(3)
.schema();
const DATASTORE_PROPERTIES: ObjectSchema = ObjectSchema::new(
"DataStore properties",
&[
("path", false, &DIR_NAME_SCHEMA)
]
);
fn init() -> SectionConfig { fn init() -> SectionConfig {
let plugin = SectionConfigPlugin::new( let plugin = SectionConfigPlugin::new("datastore".to_string(), &DATASTORE_PROPERTIES);
"datastore".to_string(), let mut config = SectionConfig::new(&DATASTORE_ID_SCHEMA);
ObjectSchema::new("DataStore properties")
.required("path", StringSchema::new("Directory name")),
);
let id_schema = StringSchema::new("DataStore ID schema.")
.min_length(3)
.into();
let mut config = SectionConfig::new(id_schema);
config.register_plugin(plugin); config.register_plugin(plugin);
config config

View File

@ -6,20 +6,18 @@ use std::collections::VecDeque;
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::sync::Arc;
use proxmox::tools::try_block; use proxmox::tools::try_block;
use crate::api_schema::*; use crate::api_schema::*;
pub struct SectionConfigPlugin { pub struct SectionConfigPlugin {
type_name: String, type_name: String,
properties: ObjectSchema, properties: &'static ObjectSchema,
} }
impl SectionConfigPlugin { impl SectionConfigPlugin {
pub fn new(type_name: String, properties: ObjectSchema) -> Self { pub fn new(type_name: String, properties: &'static ObjectSchema) -> Self {
Self { type_name, properties } Self { type_name, properties }
} }
@ -28,7 +26,7 @@ impl SectionConfigPlugin {
pub struct SectionConfig { pub struct SectionConfig {
plugins: HashMap<String, SectionConfigPlugin>, plugins: HashMap<String, SectionConfigPlugin>,
id_schema: Arc<Schema>, id_schema: &'static Schema,
parse_section_header: fn(&str) -> Option<(String, String)>, parse_section_header: fn(&str) -> Option<(String, String)>,
parse_section_content: fn(&str) -> Option<(String, String)>, parse_section_content: fn(&str) -> Option<(String, String)>,
format_section_header: fn(type_name: &str, section_id: &str, data: &Value) -> String, format_section_header: fn(type_name: &str, section_id: &str, data: &Value) -> String,
@ -75,7 +73,7 @@ impl SectionConfigData {
impl SectionConfig { impl SectionConfig {
pub fn new(id_schema: Arc<Schema>) -> Self { pub fn new(id_schema: &'static Schema) -> Self {
Self { Self {
plugins: HashMap::new(), plugins: HashMap::new(),
id_schema, id_schema,
@ -151,7 +149,7 @@ impl SectionConfig {
let mut state = ParseState::BeforeHeader; let mut state = ParseState::BeforeHeader;
let test_required_properties = |value: &Value, schema: &ObjectSchema| -> Result<(), Error> { let test_required_properties = |value: &Value, schema: &ObjectSchema| -> Result<(), Error> {
for (name, (optional, _prop_schema)) in &schema.properties { for (name, optional, _prop_schema) in schema.properties {
if *optional == false && value[name] == Value::Null { if *optional == false && value[name] == Value::Null {
return Err(format_err!("property '{}' is missing and it is not optional.", name)); return Err(format_err!("property '{}' is missing and it is not optional.", name));
} }
@ -206,7 +204,7 @@ impl SectionConfig {
if let Some((key, value)) = (self.parse_section_content)(line) { if let Some((key, value)) = (self.parse_section_content)(line) {
//println!("CONTENT: key: {} value: {}", key, value); //println!("CONTENT: key: {} value: {}", key, value);
if let Some((_optional, prop_schema)) = plugin.properties.properties.get::<str>(&key) { if let Some((_optional, prop_schema)) = plugin.properties.lookup(&key) {
match parse_simple_value(&value, prop_schema) { match parse_simple_value(&value, prop_schema) {
Ok(value) => { Ok(value) => {
if config[&key] == Value::Null { if config[&key] == Value::Null {
@ -309,19 +307,22 @@ fn test_section_config1() {
//let mut contents = String::new(); //let mut contents = String::new();
//file.read_to_string(&mut contents).unwrap(); //file.read_to_string(&mut contents).unwrap();
let plugin = SectionConfigPlugin::new( const PROPERTIES: ObjectSchema = ObjectSchema::new(
"lvmthin".to_string(), "lvmthin properties",
ObjectSchema::new("lvmthin properties") &[
.required("thinpool", StringSchema::new("LVM thin pool name.")) ("content", true, &StringSchema::new("Storage content types.").schema()),
.required("vgname", StringSchema::new("LVM volume group name.")) ("thinpool", false, &StringSchema::new("LVM thin pool name.").schema()),
.optional("content", StringSchema::new("Storage content types.")) ("vgname", false, &StringSchema::new("LVM volume group name.").schema()),
],
); );
let id_schema = StringSchema::new("Storage ID schema.") let plugin = SectionConfigPlugin::new("lvmthin".to_string(), &PROPERTIES);
.min_length(3)
.into();
let mut config = SectionConfig::new(id_schema); const ID_SCHEMA: Schema = StringSchema::new("Storage ID schema.")
.min_length(3)
.schema();
let mut config = SectionConfig::new(&ID_SCHEMA);
config.register_plugin(plugin); config.register_plugin(plugin);
let raw = r" let raw = r"

View File

@ -8,6 +8,7 @@ use futures::*;
use hyper::{Body, Request, Response, StatusCode}; use hyper::{Body, Request, Response, StatusCode};
use crate::tools; use crate::tools;
use crate::api_schema::api_handler::*;
use crate::api_schema::router::*; use crate::api_schema::router::*;
use crate::server::formatter::*; use crate::server::formatter::*;
use crate::server::WorkerTask; use crate::server::WorkerTask;
@ -52,17 +53,21 @@ impl <E: RpcEnvironment + Clone> H2Service<E> {
let formatter = &JSON_FORMATTER; let formatter = &JSON_FORMATTER;
match self.router.find_method(&components, method, &mut uri_param) { match self.router.find_method(&components, method, &mut uri_param) {
MethodDefinition::None => { None => {
let err = http_err!(NOT_FOUND, "Path not found.".to_string()); let err = http_err!(NOT_FOUND, "Path not found.".to_string());
Box::new(future::ok((formatter.format_error)(err))) Box::new(future::ok((formatter.format_error)(err)))
} }
MethodDefinition::Simple(api_method) => { Some(api_method) => {
crate::server::rest::handle_sync_api_request( match api_method.handler {
self.rpcenv.clone(), api_method, formatter, parts, body, uri_param) ApiHandler::Sync(_) => {
} crate::server::rest::handle_sync_api_request(
MethodDefinition::Async(async_method) => { self.rpcenv.clone(), api_method, formatter, parts, body, uri_param)
crate::server::rest::handle_async_api_request( }
self.rpcenv.clone(), async_method, formatter, parts, body, uri_param) ApiHandler::Async(_) => {
crate::server::rest::handle_async_api_request(
self.rpcenv.clone(), api_method, formatter, parts, body, uri_param)
}
}
} }
} }
} }

View File

@ -18,6 +18,8 @@ use url::form_urlencoded;
use super::environment::RestEnvironment; use super::environment::RestEnvironment;
use super::formatter::*; use super::formatter::*;
use crate::api_schema::rpc_environment::*;
use crate::api_schema::api_handler::*;
use crate::api_schema::config::*; use crate::api_schema::config::*;
use crate::api_schema::router::*; use crate::api_schema::router::*;
use crate::api_schema::*; use crate::api_schema::*;
@ -186,7 +188,7 @@ fn get_request_parameters_async<S: 'static + BuildHasher + Send>(
if is_json { if is_json {
let mut params: Value = serde_json::from_str(utf8)?; let mut params: Value = serde_json::from_str(utf8)?;
for (k, v) in uri_param { for (k, v) in uri_param {
if let Some((_optional, prop_schema)) = obj_schema.properties.get::<str>(&k) { if let Some((_optional, prop_schema)) = obj_schema.lookup(&k) {
params[&k] = parse_simple_value(&v, prop_schema)?; params[&k] = parse_simple_value(&v, prop_schema)?;
} }
} }
@ -269,6 +271,13 @@ pub fn handle_sync_api_request<Env: RpcEnvironment, S: 'static + BuildHasher + S
uri_param: HashMap<String, String, S>, uri_param: HashMap<String, String, S>,
) -> BoxFut ) -> BoxFut
{ {
let handler = match info.handler {
ApiHandler::Async(_) => {
panic!("fixme");
}
ApiHandler::Sync(handler) => handler,
};
let params = get_request_parameters_async(info, parts, req_body, uri_param); let params = get_request_parameters_async(info, parts, req_body, uri_param);
let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000); let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000);
@ -276,7 +285,8 @@ pub fn handle_sync_api_request<Env: RpcEnvironment, S: 'static + BuildHasher + S
let resp = Pin::from(params) let resp = Pin::from(params)
.and_then(move |params| { .and_then(move |params| {
let mut delay = false; let mut delay = false;
let resp = match (info.handler.as_ref().unwrap())(params, info, &mut rpcenv) {
let resp = match (handler)(params, info, &mut rpcenv) {
Ok(data) => (formatter.format_data)(data, &rpcenv), Ok(data) => (formatter.format_data)(data, &rpcenv),
Err(err) => { Err(err) => {
if let Some(httperr) = err.downcast_ref::<HttpError>() { if let Some(httperr) = err.downcast_ref::<HttpError>() {
@ -307,13 +317,20 @@ pub fn handle_sync_api_request<Env: RpcEnvironment, S: 'static + BuildHasher + S
pub fn handle_async_api_request<Env: RpcEnvironment>( pub fn handle_async_api_request<Env: RpcEnvironment>(
rpcenv: Env, rpcenv: Env,
info: &'static ApiAsyncMethod, info: &'static ApiMethod,
formatter: &'static OutputFormatter, formatter: &'static OutputFormatter,
parts: Parts, parts: Parts,
req_body: Body, req_body: Body,
uri_param: HashMap<String, String>, uri_param: HashMap<String, String>,
) -> BoxFut ) -> BoxFut
{ {
let handler = match info.handler {
ApiHandler::Sync(_) => {
panic!("fixme");
}
ApiHandler::Async(handler) => handler,
};
// fixme: convert parameters to Json // fixme: convert parameters to Json
let mut param_list: Vec<(String, String)> = vec![]; let mut param_list: Vec<(String, String)> = vec![];
@ -336,7 +353,7 @@ pub fn handle_async_api_request<Env: RpcEnvironment>(
} }
}; };
match (info.handler)(parts, req_body, params, info, Box::new(rpcenv)) { match (handler)(parts, req_body, params, info, Box::new(rpcenv)) {
Ok(future) => future, Ok(future) => future,
Err(err) => { Err(err) => {
let resp = (formatter.format_error)(err); let resp = (formatter.format_error)(err);
@ -594,20 +611,24 @@ pub fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> BoxFut {
} }
match api.find_method(&components[2..], method, &mut uri_param) { match api.find_method(&components[2..], method, &mut uri_param) {
MethodDefinition::None => { None => {
let err = http_err!(NOT_FOUND, "Path not found.".to_string()); let err = http_err!(NOT_FOUND, "Path not found.".to_string());
return Box::new(future::ok((formatter.format_error)(err))); return Box::new(future::ok((formatter.format_error)(err)));
} }
MethodDefinition::Simple(api_method) => { Some(api_method) => {
if api_method.protected && env_type == RpcEnvironmentType::PUBLIC { if api_method.protected && env_type == RpcEnvironmentType::PUBLIC {
return proxy_protected_request(api_method, parts, body); return proxy_protected_request(api_method, parts, body);
} else { } else {
return handle_sync_api_request(rpcenv, api_method, formatter, parts, body, uri_param); match api_method.handler {
ApiHandler::Sync(_) => {
return handle_sync_api_request(rpcenv, api_method, formatter, parts, body, uri_param);
}
ApiHandler::Async(_) => {
return handle_async_api_request(rpcenv, api_method, formatter, parts, body, uri_param);
}
}
} }
} }
MethodDefinition::Async(async_method) => {
return handle_async_api_request(rpcenv, async_method, formatter, parts, body, uri_param);
}
} }
} }
} else { } else {

View File

@ -9,21 +9,23 @@ lazy_static!{
static ref STORAGE_SECTION_CONFIG: SectionConfig = register_storage_plugins(); static ref STORAGE_SECTION_CONFIG: SectionConfig = register_storage_plugins();
} }
const ID_SCHEMA: Schema = StringSchema::new("Storage ID schema.")
.min_length(3)
.schema();
const LVMTHIN_PROPERTIES: ObjectSchema = ObjectSchema::new(
"lvmthin properties",
&[
("thinpool", false, &StringSchema::new("LVM thin pool name.").schema()),
("vgname", false, &StringSchema::new("LVM volume group name.").schema()),
("content", true, &StringSchema::new("Storage content types.").schema()),
],
);
fn register_storage_plugins() -> SectionConfig { fn register_storage_plugins() -> SectionConfig {
let plugin = SectionConfigPlugin::new( let plugin = SectionConfigPlugin::new("lvmthin".to_string(), &LVMTHIN_PROPERTIES);
"lvmthin".to_string(), let mut config = SectionConfig::new(&ID_SCHEMA);
ObjectSchema::new("lvmthin properties")
.required("thinpool", StringSchema::new("LVM thin pool name."))
.required("vgname", StringSchema::new("LVM volume group name."))
.optional("content", StringSchema::new("Storage content types."))
);
let id_schema = StringSchema::new("Storage ID schema.")
.min_length(3)
.into();
let mut config = SectionConfig::new(id_schema);
config.register_plugin(plugin); config.register_plugin(plugin);
config config