]>
Commit | Line | Data |
---|---|---|
efb7c534 DC |
1 | use anyhow::{bail, format_err, Error}; |
2 | use futures::FutureExt; | |
3 | use hyper::Method; | |
4 | use serde::{Deserialize, Serialize}; | |
5 | use serde_json::{json, Value}; | |
6 | use tokio::signal::unix::{signal, SignalKind}; | |
7 | ||
8 | use std::collections::HashMap; | |
9 | ||
6ef1b649 | 10 | use proxmox_router::{cli::*, ApiHandler, ApiMethod, RpcEnvironment, SubRoute}; |
9fa3026a | 11 | use proxmox_schema::{api, ApiType, ParameterSchema, Schema}; |
6ef1b649 | 12 | use proxmox_schema::format::DocumentationFormat; |
efb7c534 DC |
13 | |
14 | use pbs_api_types::{PROXMOX_UPID_REGEX, UPID}; | |
01a08021 | 15 | use pbs_client::view_task_result; |
efb7c534 DC |
16 | use proxmox_rest_server::normalize_uri_path; |
17 | ||
01a08021 WB |
18 | use proxmox_backup::client_helpers::connect_to_localhost; |
19 | ||
efb7c534 DC |
20 | const PROG_NAME: &str = "proxmox-backup-debug api"; |
21 | const URL_ASCIISET: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC.remove(b'/'); | |
22 | ||
23 | macro_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 | ||
31 | async 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 | ||
93 | async 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 | ||
123 | fn 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 | ||
145 | fn 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(¶m_list, true)?; |
efb7c534 DC |
165 | |
166 | Ok(params) | |
167 | } | |
168 | ||
169 | fn use_http_client() -> bool { | |
170 | match std::env::var("PROXMOX_DEBUG_API_CODE") { | |
171 | Ok(var) => var != "1", | |
172 | _ => true, | |
173 | } | |
174 | } | |
175 | ||
176 | async 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 | ||
192 | async 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 | ||
210 | async 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 | ||
230 | async 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 | ||
251 | async 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> | |
308 | async 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> | |
334 | async 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 | |
373 | struct 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 |
380 | const LS_SCHEMA: &proxmox_schema::Schema = |
381 | &proxmox_schema::ArraySchema::new("List of child links", &ApiDirEntry::API_SCHEMA) | |
efb7c534 DC |
382 | .schema(); |
383 | ||
384 | async 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 | 435 | async 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 | ||
458 | pub 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 | } |