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