]>
Commit | Line | Data |
---|---|---|
2322a980 | 1 | use anyhow::{bail, Error}; |
fee0fe54 | 2 | use serde_json::{json, Value}; |
2322a980 | 3 | |
6ef1b649 WB |
4 | use proxmox_router::{ApiAccess, ApiHandler, ApiMethod, Permission, Router, SubRoute}; |
5 | use proxmox_schema::format::{dump_enum_properties, get_property_string_type_text}; | |
6 | use proxmox_schema::{ApiStringFormat, ApiType, ObjectSchemaType, Schema}; | |
7 | use proxmox_section_config::dump_section_config; | |
2322a980 | 8 | |
8cc3760e DM |
9 | use pbs_api_types::PRIVILEGES; |
10 | ||
e7d4be9d | 11 | use proxmox_backup::api2; |
2322a980 | 12 | |
2322a980 DM |
13 | fn get_args() -> (String, Vec<String>) { |
14 | ||
15 | let mut args = std::env::args(); | |
16 | let prefix = args.next().unwrap(); | |
17 | let prefix = prefix.rsplit('/').next().unwrap().to_string(); // without path | |
18 | let args: Vec<String> = args.collect(); | |
19 | ||
20 | (prefix, args) | |
21 | } | |
22 | ||
23 | fn main() -> Result<(), Error> { | |
24 | ||
25 | let (_prefix, args) = get_args(); | |
26 | ||
3afecb84 | 27 | if args.is_empty() { |
2322a980 DM |
28 | bail!("missing arguments"); |
29 | } | |
fee0fe54 | 30 | |
2322a980 | 31 | for arg in args.iter() { |
2ca396c0 | 32 | let text = match arg.as_ref() { |
fee0fe54 | 33 | "apidata.js" => generate_api_tree(), |
e7d4be9d | 34 | "datastore.cfg" => dump_section_config(&pbs_config::datastore::CONFIG), |
1ce8e905 | 35 | "tape.cfg" => dump_section_config(&pbs_config::drive::CONFIG), |
e3619d41 | 36 | "tape-job.cfg" => dump_section_config(&pbs_config::tape_job::CONFIG), |
ba3d7e19 | 37 | "user.cfg" => dump_section_config(&pbs_config::user::CONFIG), |
6afdda88 | 38 | "remote.cfg" => dump_section_config(&pbs_config::remote::CONFIG), |
a4e5a0fc | 39 | "sync.cfg" => dump_section_config(&pbs_config::sync::CONFIG), |
802189f7 | 40 | "verification.cfg" => dump_section_config(&pbs_config::verify::CONFIG), |
aad2d162 | 41 | "media-pool.cfg" => dump_section_config(&pbs_config::media_pool::CONFIG), |
8cc3760e | 42 | "config::acl::Role" => dump_enum_properties(&pbs_api_types::Role::API_SCHEMA)?, |
2322a980 | 43 | _ => bail!("docgen: got unknown type"), |
2ca396c0 DM |
44 | }; |
45 | println!("{}", text); | |
2322a980 | 46 | } |
fee0fe54 | 47 | |
2322a980 DM |
48 | Ok(()) |
49 | } | |
fee0fe54 DM |
50 | |
51 | fn generate_api_tree() -> String { | |
52 | ||
fee0fe54 | 53 | let mut tree = Vec::new(); |
0bf4b813 DM |
54 | |
55 | let mut data = dump_api_schema(& api2::ROUTER, "."); | |
fee0fe54 | 56 | data["path"] = "/".into(); |
0bf4b813 DM |
57 | // hack: add invisible space to sort as first entry |
58 | data["text"] = "​Management API (HTTP)".into(); | |
fee0fe54 DM |
59 | data["expanded"] = true.into(); |
60 | ||
61 | tree.push(data); | |
62 | ||
451856d2 DM |
63 | let mut data = dump_api_schema(&api2::backup::BACKUP_API_ROUTER, "/backup/_upgrade_"); |
64 | data["path"] = "/backup/_upgrade_".into(); | |
0bf4b813 DM |
65 | data["text"] = "Backup API (HTTP/2)".into(); |
66 | tree.push(data); | |
67 | ||
451856d2 DM |
68 | let mut data = dump_api_schema(&api2::reader::READER_API_ROUTER, "/reader/_upgrade_"); |
69 | data["path"] = "/reader/_upgrade_".into(); | |
0bf4b813 DM |
70 | data["text"] = "Restore API (HTTP/2)".into(); |
71 | tree.push(data); | |
72 | ||
85417b2a | 73 | format!("var apiSchema = {};", serde_json::to_string_pretty(&tree).unwrap()) |
fee0fe54 DM |
74 | } |
75 | ||
bc235831 DM |
76 | pub fn dump_schema(schema: &Schema) -> Value { |
77 | ||
78 | let mut data; | |
79 | ||
80 | match schema { | |
81 | Schema::Null => { | |
82 | data = json!({ | |
83 | "type": "null", | |
84 | }); | |
85 | } | |
86 | Schema::Boolean(boolean_schema) => { | |
87 | data = json!({ | |
88 | "type": "boolean", | |
89 | "description": boolean_schema.description, | |
90 | }); | |
91 | if let Some(default) = boolean_schema.default { | |
92 | data["default"] = default.into(); | |
93 | } | |
94 | } | |
95 | Schema::String(string_schema) => { | |
96 | data = json!({ | |
97 | "type": "string", | |
98 | "description": string_schema.description, | |
99 | }); | |
100 | if let Some(default) = string_schema.default { | |
101 | data["default"] = default.into(); | |
102 | } | |
103 | if let Some(min_length) = string_schema.min_length { | |
104 | data["minLength"] = min_length.into(); | |
105 | } | |
106 | if let Some(max_length) = string_schema.max_length { | |
107 | data["maxLength"] = max_length.into(); | |
108 | } | |
109 | if let Some(type_text) = string_schema.type_text { | |
110 | data["typetext"] = type_text.into(); | |
111 | } | |
8616a4af DM |
112 | match string_schema.format { |
113 | None | Some(ApiStringFormat::VerifyFn(_)) => { /* do nothing */ } | |
114 | Some(ApiStringFormat::Pattern(const_regex)) => { | |
aa30663c DM |
115 | data["pattern"] = format!("/{}/", const_regex.regex_string) |
116 | .into(); | |
8616a4af DM |
117 | } |
118 | Some(ApiStringFormat::Enum(variants)) => { | |
119 | let variants: Vec<String> = variants | |
120 | .iter() | |
121 | .map(|e| e.value.to_string()) | |
122 | .collect(); | |
123 | data["enum"] = serde_json::to_value(variants).unwrap(); | |
124 | } | |
125 | Some(ApiStringFormat::PropertyString(subschema)) => { | |
126 | ||
127 | match subschema { | |
128 | Schema::Object(_) | Schema::Array(_) => { | |
129 | data["format"] = dump_schema(subschema); | |
130 | data["typetext"] = get_property_string_type_text(subschema) | |
131 | .into(); | |
132 | } | |
133 | _ => { /* do nothing - shouldnot happen */ } | |
134 | }; | |
135 | } | |
136 | } | |
bc235831 DM |
137 | // fixme: dump format |
138 | } | |
139 | Schema::Integer(integer_schema) => { | |
140 | data = json!({ | |
141 | "type": "integer", | |
142 | "description": integer_schema.description, | |
143 | }); | |
144 | if let Some(default) = integer_schema.default { | |
145 | data["default"] = default.into(); | |
146 | } | |
147 | if let Some(minimum) = integer_schema.minimum { | |
148 | data["minimum"] = minimum.into(); | |
149 | } | |
150 | if let Some(maximum) = integer_schema.maximum { | |
151 | data["maximum"] = maximum.into(); | |
152 | } | |
153 | } | |
154 | Schema::Number(number_schema) => { | |
155 | data = json!({ | |
156 | "type": "number", | |
157 | "description": number_schema.description, | |
158 | }); | |
159 | if let Some(default) = number_schema.default { | |
160 | data["default"] = default.into(); | |
161 | } | |
162 | if let Some(minimum) = number_schema.minimum { | |
163 | data["minimum"] = minimum.into(); | |
164 | } | |
165 | if let Some(maximum) = number_schema.maximum { | |
166 | data["maximum"] = maximum.into(); | |
167 | } | |
168 | } | |
169 | Schema::Object(object_schema) => { | |
170 | data = dump_property_schema(object_schema); | |
171 | data["type"] = "object".into(); | |
8616a4af DM |
172 | if let Some(default_key) = object_schema.default_key { |
173 | data["default_key"] = default_key.into(); | |
174 | } | |
bc235831 DM |
175 | } |
176 | Schema::Array(array_schema) => { | |
177 | data = json!({ | |
178 | "type": "array", | |
179 | "description": array_schema.description, | |
180 | "items": dump_schema(array_schema.items), | |
181 | }); | |
182 | if let Some(min_length) = array_schema.min_length { | |
183 | data["minLength"] = min_length.into(); | |
184 | } | |
185 | if let Some(max_length) = array_schema.min_length { | |
186 | data["maxLength"] = max_length.into(); | |
187 | } | |
188 | } | |
189 | Schema::AllOf(alloff_schema) => { | |
190 | data = dump_property_schema(alloff_schema); | |
191 | data["type"] = "object".into(); | |
192 | } | |
193 | }; | |
194 | ||
195 | data | |
196 | } | |
197 | ||
3554fe64 | 198 | pub fn dump_property_schema(param: &dyn ObjectSchemaType) -> Value { |
bc235831 DM |
199 | let mut properties = json!({}); |
200 | ||
201 | for (prop, optional, schema) in param.properties() { | |
202 | let mut property = dump_schema(schema); | |
203 | if *optional { | |
204 | property["optional"] = 1.into(); | |
205 | } | |
206 | properties[prop] = property; | |
207 | } | |
208 | ||
209 | let data = json!({ | |
210 | "description": param.description(), | |
211 | "additionalProperties": param.additional_properties(), | |
212 | "properties": properties, | |
213 | }); | |
214 | ||
215 | data | |
216 | } | |
217 | ||
2037d9af DM |
218 | fn dump_api_permission(permission: &Permission) -> Value { |
219 | ||
220 | match permission { | |
221 | Permission::Superuser => json!({ "user": "root@pam" }), | |
222 | Permission::User(user) => json!({ "user": user }), | |
223 | Permission::Anybody => json!({ "user": "all" }), | |
224 | Permission::World => json!({ "user": "world" }), | |
225 | Permission::UserParam(param) => json!({ "userParam": param }), | |
226 | Permission::Group(group) => json!({ "group": group }), | |
227 | Permission::WithParam(param, sub_permission) => { | |
228 | json!({ | |
229 | "withParam": { | |
230 | "name": param, | |
231 | "permissions": dump_api_permission(sub_permission), | |
232 | }, | |
233 | }) | |
234 | } | |
235 | Permission::Privilege(name, value, partial) => { | |
236 | ||
237 | let mut privs = Vec::new(); | |
238 | for (name, v) in PRIVILEGES { | |
239 | if (value & v) != 0 { | |
240 | privs.push(name.to_string()); | |
241 | } | |
242 | } | |
243 | ||
244 | json!({ | |
245 | "check": { | |
246 | "path": name, | |
247 | "privs": privs, | |
248 | "partial": partial, | |
249 | } | |
250 | }) | |
251 | } | |
252 | Permission::And(list) => { | |
253 | let list: Vec<Value> = list.iter().map(|p| dump_api_permission(p)).collect(); | |
254 | json!({ "and": list }) | |
255 | } | |
256 | Permission::Or(list) => { | |
257 | let list: Vec<Value> = list.iter().map(|p| dump_api_permission(p)).collect(); | |
258 | json!({ "or": list }) | |
259 | } | |
260 | } | |
261 | } | |
262 | ||
fee0fe54 DM |
263 | fn dump_api_method_schema( |
264 | method: &str, | |
265 | api_method: &ApiMethod, | |
266 | ) -> Value { | |
267 | let mut data = json!({ | |
268 | "description": api_method.parameters.description(), | |
269 | }); | |
270 | ||
bc235831 | 271 | data["parameters"] = dump_property_schema(&api_method.parameters); |
fee0fe54 | 272 | |
9a37bd6c | 273 | let mut returns = dump_schema(api_method.returns.schema); |
bc235831 DM |
274 | if api_method.returns.optional { |
275 | returns["optional"] = 1.into(); | |
276 | } | |
277 | data["returns"] = returns; | |
fee0fe54 | 278 | |
2037d9af DM |
279 | match api_method.access { |
280 | ApiAccess { description: None, permission: Permission::Superuser } => { | |
281 | // no need to output default | |
282 | } | |
283 | ApiAccess { description, permission } => { | |
284 | let mut permissions = dump_api_permission(permission); | |
285 | if let Some(description) = description { | |
286 | permissions["description"] = description.into(); | |
287 | } | |
288 | data["permissions"] = permissions; | |
289 | } | |
290 | } | |
291 | ||
fee0fe54 DM |
292 | let mut method = method; |
293 | ||
294 | if let ApiHandler::AsyncHttp(_) = api_method.handler { | |
295 | method = if method == "POST" { "UPLOAD" } else { method }; | |
296 | method = if method == "GET" { "DOWNLOAD" } else { method }; | |
297 | } | |
298 | ||
299 | data["method"] = method.into(); | |
300 | ||
301 | data | |
302 | } | |
303 | ||
304 | pub fn dump_api_schema( | |
305 | router: &Router, | |
306 | path: &str, | |
307 | ) -> Value { | |
308 | ||
309 | let mut data = json!({}); | |
310 | ||
311 | let mut info = json!({}); | |
312 | if let Some(api_method) = router.get { | |
313 | info["GET"] = dump_api_method_schema("GET", api_method); | |
314 | } | |
315 | if let Some(api_method) = router.post { | |
316 | info["POST"] = dump_api_method_schema("POST", api_method); | |
317 | } | |
318 | if let Some(api_method) = router.put { | |
319 | info["PUT"] = dump_api_method_schema("PUT", api_method); | |
320 | } | |
321 | if let Some(api_method) = router.delete { | |
322 | info["DELETE"] = dump_api_method_schema("DELETE", api_method); | |
323 | } | |
324 | ||
325 | data["info"] = info; | |
326 | ||
327 | match &router.subroute { | |
328 | None => { | |
329 | data["leaf"] = 1.into(); | |
330 | }, | |
331 | Some(SubRoute::MatchAll { router, param_name }) => { | |
332 | let sub_path = if path == "." { | |
333 | format!("/{{{}}}", param_name) | |
334 | } else { | |
335 | format!("{}/{{{}}}", path, param_name) | |
336 | }; | |
337 | let mut child = dump_api_schema(router, &sub_path); | |
338 | child["path"] = sub_path.into(); | |
339 | child["text"] = format!("{{{}}}", param_name).into(); | |
340 | ||
341 | let mut children = Vec::new(); | |
342 | children.push(child); | |
343 | data["children"] = children.into(); | |
344 | data["leaf"] = 0.into(); | |
345 | } | |
346 | Some(SubRoute::Map(dirmap)) => { | |
347 | ||
348 | let mut children = Vec::new(); | |
349 | ||
350 | for (key, sub_router) in dirmap.iter() { | |
351 | let sub_path = if path == "." { | |
352 | format!("/{}", key) | |
353 | } else { | |
354 | format!("{}/{}", path, key) | |
355 | }; | |
356 | let mut child = dump_api_schema(sub_router, &sub_path); | |
357 | child["path"] = sub_path.into(); | |
358 | child["text"] = key.to_string().into(); | |
359 | children.push(child); | |
360 | } | |
361 | ||
362 | data["children"] = children.into(); | |
363 | data["leaf"] = 0.into(); | |
364 | } | |
365 | } | |
366 | ||
367 | data | |
368 | } |