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