]> git.proxmox.com Git - proxmox-backup.git/blame - src/bin/proxmox_backup_debug/api.rs
cleanup schema function calls
[proxmox-backup.git] / src / bin / proxmox_backup_debug / api.rs
CommitLineData
efb7c534
DC
1use anyhow::{bail, format_err, Error};
2use futures::FutureExt;
3use hyper::Method;
4use serde::{Deserialize, Serialize};
5use serde_json::{json, Value};
6use tokio::signal::unix::{signal, SignalKind};
7
8use std::collections::HashMap;
9
6ef1b649 10use proxmox_router::{cli::*, ApiHandler, ApiMethod, RpcEnvironment, SubRoute};
9fa3026a 11use proxmox_schema::{api, ApiType, ParameterSchema, Schema};
6ef1b649 12use proxmox_schema::format::DocumentationFormat;
efb7c534
DC
13
14use pbs_api_types::{PROXMOX_UPID_REGEX, UPID};
01a08021 15use pbs_client::view_task_result;
efb7c534
DC
16use proxmox_rest_server::normalize_uri_path;
17
01a08021
WB
18use proxmox_backup::client_helpers::connect_to_localhost;
19
efb7c534
DC
20const PROG_NAME: &str = "proxmox-backup-debug api";
21const URL_ASCIISET: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC.remove(b'/');
22
23macro_rules! complete_api_path {
24 ($capability:expr) => {
25 |complete_me: &str, _map: &HashMap<String, String>| {
9a1b24b6 26 proxmox_async::runtime::block_on(async { complete_api_path_do(complete_me, $capability).await })
efb7c534
DC
27 }
28 };
29}
30
31async fn complete_api_path_do(mut complete_me: &str, capability: Option<&str>) -> Vec<String> {
32 if complete_me.is_empty() {
33 complete_me = "/";
34 }
35
36 let mut list = Vec::new();
37
38 let mut lookup_path = complete_me.to_string();
39 let mut filter = "";
40 let last_path_index = complete_me.rfind('/');
41 if let Some(index) = last_path_index {
42 if index != complete_me.len() - 1 {
43 lookup_path = complete_me[..(index + 1)].to_string();
44 if index < complete_me.len() - 1 {
45 filter = &complete_me[(index + 1)..];
46 }
47 }
48 }
49
50 let uid = nix::unistd::Uid::current();
51
52 let username = match nix::unistd::User::from_uid(uid) {
53 Ok(Some(user)) => user.name,
54 _ => "root@pam".to_string(),
55 };
56 let mut rpcenv = CliEnvironment::new();
57 rpcenv.set_auth_id(Some(format!("{}@pam", username)));
58
59 while let Ok(children) = get_api_children(lookup_path.clone(), &mut rpcenv).await {
60 let old_len = list.len();
61 for entry in children {
62 let name = entry.name;
63 let caps = entry.capabilities;
64
65 if filter.is_empty() || name.starts_with(filter) {
66 let mut path = format!("{}{}", lookup_path, name);
67 if caps.contains('D') {
68 path.push('/');
69 list.push(path.clone());
70 } else if let Some(cap) = capability {
71 if caps.contains(cap) {
72 list.push(path);
73 }
74 } else {
75 list.push(path);
76 }
77 }
78 }
79
80 if list.len() == 1 && old_len != 1 && list[0].ends_with('/') {
81 // we added only one match and it was a directory, lookup again
82 lookup_path = list[0].clone();
83 filter = "";
84 continue;
85 }
86
87 break;
88 }
89
90 list
91}
92
93async fn get_child_links(
94 path: &str,
95 rpcenv: &mut dyn RpcEnvironment,
96) -> Result<Vec<String>, Error> {
97 let (path, components) = normalize_uri_path(&path)?;
98
99 let info = &proxmox_backup::api2::ROUTER
100 .find_route(&components, &mut HashMap::new())
101 .ok_or_else(|| format_err!("no such resource"))?;
102
103 match info.subroute {
104 Some(SubRoute::Map(map)) => Ok(map.iter().map(|(name, _)| name.to_string()).collect()),
105 Some(SubRoute::MatchAll { param_name, .. }) => {
106 let list = call_api("get", &path, rpcenv, None).await?;
107 Ok(list
108 .as_array()
109 .ok_or_else(|| format_err!("{} did not return an array", path))?
110 .iter()
111 .map(|item| {
112 item[param_name]
113 .as_str()
114 .map(|c| c.to_string())
115 .ok_or_else(|| format_err!("no such property {}", param_name))
116 })
117 .collect::<Result<Vec<_>, _>>()?)
118 }
119 None => bail!("link does not define child links"),
120 }
121}
122
123fn get_api_method(
124 method: &str,
125 path: &str,
126) -> Result<(&'static ApiMethod, HashMap<String, String>), Error> {
127 let method = match method {
128 "get" => Method::GET,
129 "set" => Method::PUT,
130 "create" => Method::POST,
131 "delete" => Method::DELETE,
132 _ => unreachable!(),
133 };
134 let mut uri_param = HashMap::new();
135 let (path, components) = normalize_uri_path(&path)?;
136 if let Some(method) =
137 &proxmox_backup::api2::ROUTER.find_method(&components, method.clone(), &mut uri_param)
138 {
139 Ok((method, uri_param))
140 } else {
141 bail!("no {} handler defined for '{}'", method, path);
142 }
143}
144
145fn merge_parameters(
146 uri_param: HashMap<String, String>,
147 param: Option<Value>,
148 schema: ParameterSchema,
149) -> Result<Value, Error> {
150 let mut param_list: Vec<(String, String)> = vec![];
151
152 for (k, v) in uri_param {
153 param_list.push((k.clone(), v.clone()));
154 }
155
156 let param = param.unwrap_or(json!({}));
157
158 if let Some(map) = param.as_object() {
159 for (k, v) in map {
160 param_list.push((k.clone(), v.as_str().unwrap().to_string()));
161 }
162 }
163
9fa3026a 164 let params = schema.parse_parameter_strings(&param_list, true)?;
efb7c534
DC
165
166 Ok(params)
167}
168
169fn use_http_client() -> bool {
170 match std::env::var("PROXMOX_DEBUG_API_CODE") {
171 Ok(var) => var != "1",
172 _ => true,
173 }
174}
175
176async fn call_api(
177 method: &str,
178 path: &str,
179 rpcenv: &mut dyn RpcEnvironment,
180 params: Option<Value>,
181) -> Result<Value, Error> {
182 if use_http_client() {
183 return call_api_http(method, path, params).await;
184 }
185
186 let (method, uri_param) = get_api_method(method, path)?;
187 let params = merge_parameters(uri_param, params, method.parameters)?;
188
189 call_api_code(method, rpcenv, params).await
190}
191
192async fn call_api_http(method: &str, path: &str, params: Option<Value>) -> Result<Value, Error> {
d4877712 193 let client = connect_to_localhost()?;
efb7c534
DC
194
195 let path = format!(
196 "api2/json/{}",
197 percent_encoding::utf8_percent_encode(path, &URL_ASCIISET)
198 );
199
200 match method {
201 "get" => client.get(&path, params).await,
202 "create" => client.post(&path, params).await,
203 "set" => client.put(&path, params).await,
204 "delete" => client.delete(&path, params).await,
205 _ => unreachable!(),
206 }
207 .map(|mut res| res["data"].take())
208}
209
210async fn call_api_code(
211 method: &'static ApiMethod,
212 rpcenv: &mut dyn RpcEnvironment,
213 params: Value,
214) -> Result<Value, Error> {
215 if !method.protected {
216 // drop privileges if we call non-protected code directly
217 let backup_user = pbs_config::backup_user()?;
218 nix::unistd::setgid(backup_user.gid)?;
219 nix::unistd::setuid(backup_user.uid)?;
220 }
221 match method.handler {
222 ApiHandler::AsyncHttp(_handler) => {
223 bail!("not implemented");
224 }
225 ApiHandler::Sync(handler) => (handler)(params, method, rpcenv),
226 ApiHandler::Async(handler) => (handler)(params, method, rpcenv).await,
227 }
228}
229
230async fn handle_worker(upid_str: &str) -> Result<(), Error> {
231 let upid: UPID = upid_str.parse()?;
232 let mut signal_stream = signal(SignalKind::interrupt())?;
233 let abort_future = async move {
234 while signal_stream.recv().await.is_some() {
235 println!("got shutdown request (SIGINT)");
b9700a9f 236 proxmox_rest_server::abort_local_worker(upid.clone());
efb7c534
DC
237 }
238 Ok::<_, Error>(())
239 };
240
b9700a9f 241 let result_future = proxmox_rest_server::wait_for_local_worker(upid_str);
efb7c534
DC
242
243 futures::select! {
244 result = result_future.fuse() => result?,
245 abort = abort_future.fuse() => abort?,
246 };
247
248 Ok(())
249}
250
251async fn call_api_and_format_result(
252 method: String,
253 path: String,
254 mut param: Value,
255 rpcenv: &mut dyn RpcEnvironment,
256) -> Result<(), Error> {
257 let mut output_format = extract_output_format(&mut param);
258 let mut result = call_api(&method, &path, rpcenv, Some(param)).await?;
259
260 if let Some(upid) = result.as_str() {
261 if PROXMOX_UPID_REGEX.is_match(upid) {
262 if use_http_client() {
d4877712
DM
263 let client = connect_to_localhost()?;
264 view_task_result(&client, json!({ "data": upid }), &output_format).await?;
efb7c534
DC
265 return Ok(());
266 }
267
268 handle_worker(upid).await?;
269
270 if output_format == "text" {
271 return Ok(());
272 }
273 }
274 }
275
276 let (method, _) = get_api_method(&method, &path)?;
277 let options = default_table_format_options();
278 let return_type = &method.returns;
279 if matches!(return_type.schema, Schema::Null) {
280 output_format = "json-pretty".to_string();
281 }
282
283 format_and_print_result_full(&mut result, return_type, &output_format, &options);
284
285 Ok(())
286}
287
288#[api(
289 input: {
290 additional_properties: true,
291 properties: {
292 method: {
293 type: String,
294 description: "The Method",
295 },
296 "api-path": {
297 type: String,
298 description: "API path.",
299 },
300 "output-format": {
301 schema: OUTPUT_FORMAT,
302 optional: true,
303 },
304 },
305 },
306)]
307/// Call API on <api-path>
308async fn api_call(
309 method: String,
310 api_path: String,
311 param: Value,
312 rpcenv: &mut dyn RpcEnvironment,
313) -> Result<(), Error> {
314 call_api_and_format_result(method, api_path, param, rpcenv).await
315}
316
317#[api(
318 input: {
319 properties: {
320 path: {
321 type: String,
322 description: "API path.",
323 },
324 verbose: {
325 type: Boolean,
326 description: "Verbose output format.",
327 optional: true,
328 default: false,
329 }
330 },
331 },
332)]
333/// Get API usage information for <path>
334async fn usage(
335 path: String,
336 verbose: bool,
337 _param: Value,
338 _rpcenv: &mut dyn RpcEnvironment,
339) -> Result<(), Error> {
340 let docformat = if verbose {
341 DocumentationFormat::Full
342 } else {
343 DocumentationFormat::Short
344 };
345 let mut found = false;
346 for command in &["get", "set", "create", "delete"] {
347 let (info, uri_params) = match get_api_method(command, &path) {
348 Ok(some) => some,
349 Err(_) => continue,
350 };
351 found = true;
352
353 let skip_params: Vec<&str> = uri_params.keys().map(|s| &**s).collect();
354
355 let cmd = CliCommand::new(info);
356 let prefix = format!("USAGE: {} {} {}", PROG_NAME, command, path);
357
358 print!(
359 "{}",
360 generate_usage_str(&prefix, &cmd, docformat, "", &skip_params)
361 );
362 }
363
364 if !found {
365 bail!("no such resource '{}'", path);
366 }
367 Ok(())
368}
369
370#[api()]
371#[derive(Debug, Serialize, Deserialize)]
372/// A child link with capabilities
373struct ApiDirEntry {
374 /// The name of the link
375 name: String,
376 /// The capabilities of the path (format Drwcd)
377 capabilities: String,
378}
379
6ef1b649
WB
380const LS_SCHEMA: &proxmox_schema::Schema =
381 &proxmox_schema::ArraySchema::new("List of child links", &ApiDirEntry::API_SCHEMA)
efb7c534
DC
382 .schema();
383
384async fn get_api_children(
385 path: String,
386 rpcenv: &mut dyn RpcEnvironment,
387) -> Result<Vec<ApiDirEntry>, Error> {
388 let mut res = Vec::new();
389 for link in get_child_links(&path, rpcenv).await? {
390 let path = format!("{}/{}", path, link);
391 let (path, _) = normalize_uri_path(&path)?;
392 let mut cap = String::new();
393
394 if get_child_links(&path, rpcenv).await.is_ok() {
395 cap.push('D');
396 } else {
397 cap.push('-');
398 }
399
400 let cap_list = &[("get", 'r'), ("set", 'w'), ("create", 'c'), ("delete", 'd')];
401
402 for (method, c) in cap_list {
403 if get_api_method(method, &path).is_ok() {
404 cap.push(*c);
405 } else {
406 cap.push('-');
407 }
408 }
409
410 res.push(ApiDirEntry {
411 name: link.to_string(),
412 capabilities: cap,
413 });
414 }
415
416 Ok(res)
417}
418
419#[api(
420 input: {
421 properties: {
422 path: {
423 type: String,
424 description: "API path.",
c25ea25f 425 optional: true,
efb7c534
DC
426 },
427 "output-format": {
428 schema: OUTPUT_FORMAT,
429 optional: true,
430 },
431 },
432 },
433)]
434/// Get API usage information for <path>
c25ea25f 435async fn ls(path: Option<String>, mut param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
efb7c534
DC
436 let output_format = extract_output_format(&mut param);
437
438 let options = TableFormatOptions::new()
439 .noborder(true)
440 .noheader(true)
441 .sortby("name", false);
442
c25ea25f 443 let res = get_api_children(path.unwrap_or(String::from("/")), rpcenv).await?;
efb7c534
DC
444
445 format_and_print_result_full(
446 &mut serde_json::to_value(res)?,
6ef1b649 447 &proxmox_schema::ReturnType {
efb7c534
DC
448 optional: false,
449 schema: &LS_SCHEMA,
450 },
451 &output_format,
452 &options,
453 );
454
455 Ok(())
456}
457
458pub fn api_commands() -> CommandLineInterface {
459 let cmd_def = CliCommandMap::new()
460 .insert(
461 "get",
462 CliCommand::new(&API_METHOD_API_CALL)
463 .fixed_param("method", "get".to_string())
464 .arg_param(&["api-path"])
465 .completion_cb("api-path", complete_api_path!(Some("r"))),
466 )
467 .insert(
468 "set",
469 CliCommand::new(&API_METHOD_API_CALL)
470 .fixed_param("method", "set".to_string())
471 .arg_param(&["api-path"])
472 .completion_cb("api-path", complete_api_path!(Some("w"))),
473 )
474 .insert(
475 "create",
476 CliCommand::new(&API_METHOD_API_CALL)
477 .fixed_param("method", "create".to_string())
478 .arg_param(&["api-path"])
479 .completion_cb("api-path", complete_api_path!(Some("c"))),
480 )
481 .insert(
482 "delete",
483 CliCommand::new(&API_METHOD_API_CALL)
484 .fixed_param("method", "delete".to_string())
485 .arg_param(&["api-path"])
486 .completion_cb("api-path", complete_api_path!(Some("d"))),
487 )
488 .insert(
489 "ls",
490 CliCommand::new(&API_METHOD_LS)
491 .arg_param(&["path"])
492 .completion_cb("path", complete_api_path!(Some("D"))),
493 )
494 .insert(
495 "usage",
496 CliCommand::new(&API_METHOD_USAGE)
497 .arg_param(&["path"])
498 .completion_cb("path", complete_api_path!(None)),
499 );
500
501 cmd_def.into()
502}