2021-02-10 09:27:40 +00:00
|
|
|
use anyhow::{bail, Error};
|
2021-02-21 07:45:42 +00:00
|
|
|
use serde_json::{json, Value};
|
2021-02-10 09:27:40 +00:00
|
|
|
|
2021-10-08 09:19:37 +00:00
|
|
|
use proxmox_router::{ApiAccess, ApiHandler, ApiMethod, Permission, Router, SubRoute};
|
|
|
|
use proxmox_schema::format::{dump_enum_properties, get_property_string_type_text};
|
|
|
|
use proxmox_schema::{ApiStringFormat, ApiType, ObjectSchemaType, Schema};
|
|
|
|
use proxmox_section_config::dump_section_config;
|
2021-02-10 09:27:40 +00:00
|
|
|
|
2021-09-09 08:32:44 +00:00
|
|
|
use pbs_api_types::PRIVILEGES;
|
|
|
|
|
2021-09-10 06:40:58 +00:00
|
|
|
use proxmox_backup::api2;
|
2021-02-10 09:27:40 +00:00
|
|
|
|
|
|
|
fn get_args() -> (String, Vec<String>) {
|
|
|
|
|
|
|
|
let mut args = std::env::args();
|
|
|
|
let prefix = args.next().unwrap();
|
|
|
|
let prefix = prefix.rsplit('/').next().unwrap().to_string(); // without path
|
|
|
|
let args: Vec<String> = args.collect();
|
|
|
|
|
|
|
|
(prefix, args)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() -> Result<(), Error> {
|
|
|
|
|
|
|
|
let (_prefix, args) = get_args();
|
|
|
|
|
2021-12-30 13:49:11 +00:00
|
|
|
if args.is_empty() {
|
2021-02-10 09:27:40 +00:00
|
|
|
bail!("missing arguments");
|
|
|
|
}
|
2021-02-21 07:45:42 +00:00
|
|
|
|
2021-02-10 09:27:40 +00:00
|
|
|
for arg in args.iter() {
|
2021-02-11 12:13:34 +00:00
|
|
|
let text = match arg.as_ref() {
|
2021-02-21 07:45:42 +00:00
|
|
|
"apidata.js" => generate_api_tree(),
|
2021-09-10 06:40:58 +00:00
|
|
|
"datastore.cfg" => dump_section_config(&pbs_config::datastore::CONFIG),
|
2021-09-03 07:10:18 +00:00
|
|
|
"tape.cfg" => dump_section_config(&pbs_config::drive::CONFIG),
|
2021-09-07 10:32:01 +00:00
|
|
|
"tape-job.cfg" => dump_section_config(&pbs_config::tape_job::CONFIG),
|
2021-09-10 04:53:53 +00:00
|
|
|
"user.cfg" => dump_section_config(&pbs_config::user::CONFIG),
|
2021-09-02 12:25:15 +00:00
|
|
|
"remote.cfg" => dump_section_config(&pbs_config::remote::CONFIG),
|
2021-09-08 04:57:23 +00:00
|
|
|
"sync.cfg" => dump_section_config(&pbs_config::sync::CONFIG),
|
2021-09-08 06:01:07 +00:00
|
|
|
"verification.cfg" => dump_section_config(&pbs_config::verify::CONFIG),
|
2021-09-06 06:56:04 +00:00
|
|
|
"media-pool.cfg" => dump_section_config(&pbs_config::media_pool::CONFIG),
|
2021-09-09 08:32:44 +00:00
|
|
|
"config::acl::Role" => dump_enum_properties(&pbs_api_types::Role::API_SCHEMA)?,
|
2021-02-10 09:27:40 +00:00
|
|
|
_ => bail!("docgen: got unknown type"),
|
2021-02-11 12:13:34 +00:00
|
|
|
};
|
|
|
|
println!("{}", text);
|
2021-02-10 09:27:40 +00:00
|
|
|
}
|
2021-02-21 07:45:42 +00:00
|
|
|
|
2021-02-10 09:27:40 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
2021-02-21 07:45:42 +00:00
|
|
|
|
|
|
|
fn generate_api_tree() -> String {
|
|
|
|
|
|
|
|
let mut tree = Vec::new();
|
2021-02-21 09:38:33 +00:00
|
|
|
|
|
|
|
let mut data = dump_api_schema(& api2::ROUTER, ".");
|
2021-02-21 07:45:42 +00:00
|
|
|
data["path"] = "/".into();
|
2021-02-21 09:38:33 +00:00
|
|
|
// hack: add invisible space to sort as first entry
|
|
|
|
data["text"] = "​Management API (HTTP)".into();
|
2021-02-21 07:45:42 +00:00
|
|
|
data["expanded"] = true.into();
|
|
|
|
|
|
|
|
tree.push(data);
|
|
|
|
|
2021-02-22 07:38:27 +00:00
|
|
|
let mut data = dump_api_schema(&api2::backup::BACKUP_API_ROUTER, "/backup/_upgrade_");
|
|
|
|
data["path"] = "/backup/_upgrade_".into();
|
2021-02-21 09:38:33 +00:00
|
|
|
data["text"] = "Backup API (HTTP/2)".into();
|
|
|
|
tree.push(data);
|
|
|
|
|
2021-02-22 07:38:27 +00:00
|
|
|
let mut data = dump_api_schema(&api2::reader::READER_API_ROUTER, "/reader/_upgrade_");
|
|
|
|
data["path"] = "/reader/_upgrade_".into();
|
2021-02-21 09:38:33 +00:00
|
|
|
data["text"] = "Restore API (HTTP/2)".into();
|
|
|
|
tree.push(data);
|
|
|
|
|
2021-06-04 12:35:30 +00:00
|
|
|
format!("var apiSchema = {};", serde_json::to_string_pretty(&tree).unwrap())
|
2021-02-21 07:45:42 +00:00
|
|
|
}
|
|
|
|
|
2021-02-21 11:00:06 +00:00
|
|
|
pub fn dump_schema(schema: &Schema) -> Value {
|
|
|
|
|
|
|
|
let mut data;
|
|
|
|
|
|
|
|
match schema {
|
|
|
|
Schema::Null => {
|
|
|
|
data = json!({
|
|
|
|
"type": "null",
|
|
|
|
});
|
|
|
|
}
|
|
|
|
Schema::Boolean(boolean_schema) => {
|
|
|
|
data = json!({
|
|
|
|
"type": "boolean",
|
|
|
|
"description": boolean_schema.description,
|
|
|
|
});
|
|
|
|
if let Some(default) = boolean_schema.default {
|
|
|
|
data["default"] = default.into();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Schema::String(string_schema) => {
|
|
|
|
data = json!({
|
|
|
|
"type": "string",
|
|
|
|
"description": string_schema.description,
|
|
|
|
});
|
|
|
|
if let Some(default) = string_schema.default {
|
|
|
|
data["default"] = default.into();
|
|
|
|
}
|
|
|
|
if let Some(min_length) = string_schema.min_length {
|
|
|
|
data["minLength"] = min_length.into();
|
|
|
|
}
|
|
|
|
if let Some(max_length) = string_schema.max_length {
|
|
|
|
data["maxLength"] = max_length.into();
|
|
|
|
}
|
|
|
|
if let Some(type_text) = string_schema.type_text {
|
|
|
|
data["typetext"] = type_text.into();
|
|
|
|
}
|
2021-02-21 14:52:38 +00:00
|
|
|
match string_schema.format {
|
|
|
|
None | Some(ApiStringFormat::VerifyFn(_)) => { /* do nothing */ }
|
|
|
|
Some(ApiStringFormat::Pattern(const_regex)) => {
|
2021-02-21 15:14:11 +00:00
|
|
|
data["pattern"] = format!("/{}/", const_regex.regex_string)
|
|
|
|
.into();
|
2021-02-21 14:52:38 +00:00
|
|
|
}
|
|
|
|
Some(ApiStringFormat::Enum(variants)) => {
|
|
|
|
let variants: Vec<String> = variants
|
|
|
|
.iter()
|
|
|
|
.map(|e| e.value.to_string())
|
|
|
|
.collect();
|
|
|
|
data["enum"] = serde_json::to_value(variants).unwrap();
|
|
|
|
}
|
|
|
|
Some(ApiStringFormat::PropertyString(subschema)) => {
|
|
|
|
|
|
|
|
match subschema {
|
|
|
|
Schema::Object(_) | Schema::Array(_) => {
|
|
|
|
data["format"] = dump_schema(subschema);
|
|
|
|
data["typetext"] = get_property_string_type_text(subschema)
|
|
|
|
.into();
|
|
|
|
}
|
|
|
|
_ => { /* do nothing - shouldnot happen */ }
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
2021-02-21 11:00:06 +00:00
|
|
|
// fixme: dump format
|
|
|
|
}
|
|
|
|
Schema::Integer(integer_schema) => {
|
|
|
|
data = json!({
|
|
|
|
"type": "integer",
|
|
|
|
"description": integer_schema.description,
|
|
|
|
});
|
|
|
|
if let Some(default) = integer_schema.default {
|
|
|
|
data["default"] = default.into();
|
|
|
|
}
|
|
|
|
if let Some(minimum) = integer_schema.minimum {
|
|
|
|
data["minimum"] = minimum.into();
|
|
|
|
}
|
|
|
|
if let Some(maximum) = integer_schema.maximum {
|
|
|
|
data["maximum"] = maximum.into();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Schema::Number(number_schema) => {
|
|
|
|
data = json!({
|
|
|
|
"type": "number",
|
|
|
|
"description": number_schema.description,
|
|
|
|
});
|
|
|
|
if let Some(default) = number_schema.default {
|
|
|
|
data["default"] = default.into();
|
|
|
|
}
|
|
|
|
if let Some(minimum) = number_schema.minimum {
|
|
|
|
data["minimum"] = minimum.into();
|
|
|
|
}
|
|
|
|
if let Some(maximum) = number_schema.maximum {
|
|
|
|
data["maximum"] = maximum.into();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Schema::Object(object_schema) => {
|
|
|
|
data = dump_property_schema(object_schema);
|
|
|
|
data["type"] = "object".into();
|
2021-02-21 14:52:38 +00:00
|
|
|
if let Some(default_key) = object_schema.default_key {
|
|
|
|
data["default_key"] = default_key.into();
|
|
|
|
}
|
2021-02-21 11:00:06 +00:00
|
|
|
}
|
|
|
|
Schema::Array(array_schema) => {
|
|
|
|
data = json!({
|
|
|
|
"type": "array",
|
|
|
|
"description": array_schema.description,
|
|
|
|
"items": dump_schema(array_schema.items),
|
|
|
|
});
|
|
|
|
if let Some(min_length) = array_schema.min_length {
|
|
|
|
data["minLength"] = min_length.into();
|
|
|
|
}
|
|
|
|
if let Some(max_length) = array_schema.min_length {
|
|
|
|
data["maxLength"] = max_length.into();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Schema::AllOf(alloff_schema) => {
|
|
|
|
data = dump_property_schema(alloff_schema);
|
|
|
|
data["type"] = "object".into();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
data
|
|
|
|
}
|
|
|
|
|
2021-02-24 13:50:08 +00:00
|
|
|
pub fn dump_property_schema(param: &dyn ObjectSchemaType) -> Value {
|
2021-02-21 11:00:06 +00:00
|
|
|
let mut properties = json!({});
|
|
|
|
|
|
|
|
for (prop, optional, schema) in param.properties() {
|
|
|
|
let mut property = dump_schema(schema);
|
|
|
|
if *optional {
|
|
|
|
property["optional"] = 1.into();
|
|
|
|
}
|
|
|
|
properties[prop] = property;
|
|
|
|
}
|
|
|
|
|
|
|
|
let data = json!({
|
|
|
|
"description": param.description(),
|
|
|
|
"additionalProperties": param.additional_properties(),
|
|
|
|
"properties": properties,
|
|
|
|
});
|
|
|
|
|
|
|
|
data
|
|
|
|
}
|
|
|
|
|
2021-02-22 11:01:24 +00:00
|
|
|
fn dump_api_permission(permission: &Permission) -> Value {
|
|
|
|
|
|
|
|
match permission {
|
|
|
|
Permission::Superuser => json!({ "user": "root@pam" }),
|
|
|
|
Permission::User(user) => json!({ "user": user }),
|
|
|
|
Permission::Anybody => json!({ "user": "all" }),
|
|
|
|
Permission::World => json!({ "user": "world" }),
|
|
|
|
Permission::UserParam(param) => json!({ "userParam": param }),
|
|
|
|
Permission::Group(group) => json!({ "group": group }),
|
|
|
|
Permission::WithParam(param, sub_permission) => {
|
|
|
|
json!({
|
|
|
|
"withParam": {
|
|
|
|
"name": param,
|
|
|
|
"permissions": dump_api_permission(sub_permission),
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
Permission::Privilege(name, value, partial) => {
|
|
|
|
|
|
|
|
let mut privs = Vec::new();
|
|
|
|
for (name, v) in PRIVILEGES {
|
|
|
|
if (value & v) != 0 {
|
|
|
|
privs.push(name.to_string());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
json!({
|
|
|
|
"check": {
|
|
|
|
"path": name,
|
|
|
|
"privs": privs,
|
|
|
|
"partial": partial,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
Permission::And(list) => {
|
|
|
|
let list: Vec<Value> = list.iter().map(|p| dump_api_permission(p)).collect();
|
|
|
|
json!({ "and": list })
|
|
|
|
}
|
|
|
|
Permission::Or(list) => {
|
|
|
|
let list: Vec<Value> = list.iter().map(|p| dump_api_permission(p)).collect();
|
|
|
|
json!({ "or": list })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-21 07:45:42 +00:00
|
|
|
fn dump_api_method_schema(
|
|
|
|
method: &str,
|
|
|
|
api_method: &ApiMethod,
|
|
|
|
) -> Value {
|
|
|
|
let mut data = json!({
|
|
|
|
"description": api_method.parameters.description(),
|
|
|
|
});
|
|
|
|
|
2021-02-21 11:00:06 +00:00
|
|
|
data["parameters"] = dump_property_schema(&api_method.parameters);
|
2021-02-21 07:45:42 +00:00
|
|
|
|
2021-12-30 11:57:37 +00:00
|
|
|
let mut returns = dump_schema(api_method.returns.schema);
|
2021-02-21 11:00:06 +00:00
|
|
|
if api_method.returns.optional {
|
|
|
|
returns["optional"] = 1.into();
|
|
|
|
}
|
|
|
|
data["returns"] = returns;
|
2021-02-21 07:45:42 +00:00
|
|
|
|
2021-02-22 11:01:24 +00:00
|
|
|
match api_method.access {
|
|
|
|
ApiAccess { description: None, permission: Permission::Superuser } => {
|
|
|
|
// no need to output default
|
|
|
|
}
|
|
|
|
ApiAccess { description, permission } => {
|
|
|
|
let mut permissions = dump_api_permission(permission);
|
|
|
|
if let Some(description) = description {
|
|
|
|
permissions["description"] = description.into();
|
|
|
|
}
|
|
|
|
data["permissions"] = permissions;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-21 07:45:42 +00:00
|
|
|
let mut method = method;
|
|
|
|
|
|
|
|
if let ApiHandler::AsyncHttp(_) = api_method.handler {
|
|
|
|
method = if method == "POST" { "UPLOAD" } else { method };
|
|
|
|
method = if method == "GET" { "DOWNLOAD" } else { method };
|
|
|
|
}
|
|
|
|
|
|
|
|
data["method"] = method.into();
|
|
|
|
|
|
|
|
data
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn dump_api_schema(
|
|
|
|
router: &Router,
|
|
|
|
path: &str,
|
|
|
|
) -> Value {
|
|
|
|
|
|
|
|
let mut data = json!({});
|
|
|
|
|
|
|
|
let mut info = json!({});
|
|
|
|
if let Some(api_method) = router.get {
|
|
|
|
info["GET"] = dump_api_method_schema("GET", api_method);
|
|
|
|
}
|
|
|
|
if let Some(api_method) = router.post {
|
|
|
|
info["POST"] = dump_api_method_schema("POST", api_method);
|
|
|
|
}
|
|
|
|
if let Some(api_method) = router.put {
|
|
|
|
info["PUT"] = dump_api_method_schema("PUT", api_method);
|
|
|
|
}
|
|
|
|
if let Some(api_method) = router.delete {
|
|
|
|
info["DELETE"] = dump_api_method_schema("DELETE", api_method);
|
|
|
|
}
|
|
|
|
|
|
|
|
data["info"] = info;
|
|
|
|
|
|
|
|
match &router.subroute {
|
|
|
|
None => {
|
|
|
|
data["leaf"] = 1.into();
|
|
|
|
},
|
|
|
|
Some(SubRoute::MatchAll { router, param_name }) => {
|
|
|
|
let sub_path = if path == "." {
|
|
|
|
format!("/{{{}}}", param_name)
|
|
|
|
} else {
|
|
|
|
format!("{}/{{{}}}", path, param_name)
|
|
|
|
};
|
|
|
|
let mut child = dump_api_schema(router, &sub_path);
|
|
|
|
child["path"] = sub_path.into();
|
|
|
|
child["text"] = format!("{{{}}}", param_name).into();
|
|
|
|
|
|
|
|
let mut children = Vec::new();
|
|
|
|
children.push(child);
|
|
|
|
data["children"] = children.into();
|
|
|
|
data["leaf"] = 0.into();
|
|
|
|
}
|
|
|
|
Some(SubRoute::Map(dirmap)) => {
|
|
|
|
|
|
|
|
let mut children = Vec::new();
|
|
|
|
|
|
|
|
for (key, sub_router) in dirmap.iter() {
|
|
|
|
let sub_path = if path == "." {
|
|
|
|
format!("/{}", key)
|
|
|
|
} else {
|
|
|
|
format!("{}/{}", path, key)
|
|
|
|
};
|
|
|
|
let mut child = dump_api_schema(sub_router, &sub_path);
|
|
|
|
child["path"] = sub_path.into();
|
|
|
|
child["text"] = key.to_string().into();
|
|
|
|
children.push(child);
|
|
|
|
}
|
|
|
|
|
|
|
|
data["children"] = children.into();
|
|
|
|
data["leaf"] = 0.into();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
data
|
|
|
|
}
|