1 use anyhow
::{bail, format_err, Error}
;
2 use futures
::FutureExt
;
4 use serde
::{Deserialize, Serialize}
;
5 use serde_json
::{json, Value}
;
6 use tokio
::signal
::unix
::{signal, SignalKind}
;
8 use std
::collections
::HashMap
;
10 use proxmox_router
::{cli::*, ApiHandler, ApiMethod, RpcEnvironment, SubRoute}
;
11 use proxmox_schema
::{api, ApiType, ParameterSchema, Schema}
;
12 use proxmox_schema
::format
::DocumentationFormat
;
14 use pbs_api_types
::{PROXMOX_UPID_REGEX, UPID}
;
15 use pbs_client
::view_task_result
;
16 use proxmox_rest_server
::normalize_uri_path
;
18 use proxmox_backup
::client_helpers
::connect_to_localhost
;
20 const PROG_NAME
: &str = "proxmox-backup-debug api";
21 const URL_ASCIISET
: percent_encoding
::AsciiSet
= percent_encoding
::NON_ALPHANUMERIC
.remove(b'
/'
);
23 macro_rules
! complete_api_path
{
24 ($capability
:expr
) => {
25 |complete_me
: &str, _map
: &HashMap
<String
, String
>| {
26 proxmox_async
::runtime
::block_on(async { complete_api_path_do(complete_me, $capability).await }
)
31 async
fn complete_api_path_do(mut complete_me
: &str, capability
: Option
<&str>) -> Vec
<String
> {
32 if complete_me
.is_empty() {
36 let mut list
= Vec
::new();
38 let mut lookup_path
= complete_me
.to_string();
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)..];
50 let uid
= nix
::unistd
::Uid
::current();
52 let username
= match nix
::unistd
::User
::from_uid(uid
) {
53 Ok(Some(user
)) => user
.name
,
54 _
=> "root@pam".to_string(),
56 let mut rpcenv
= CliEnvironment
::new();
57 rpcenv
.set_auth_id(Some(format
!("{}@pam", username
)));
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
;
65 if filter
.is_empty() || name
.starts_with(filter
) {
66 let mut path
= format
!("{}{}", lookup_path
, name
);
67 if caps
.contains('D'
) {
69 list
.push(path
.clone());
70 } else if let Some(cap
) = capability
{
71 if caps
.contains(cap
) {
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();
93 async
fn get_child_links(
95 rpcenv
: &mut dyn RpcEnvironment
,
96 ) -> Result
<Vec
<String
>, Error
> {
97 let (path
, components
) = normalize_uri_path(&path
)?
;
99 let info
= &proxmox_backup
::api2
::ROUTER
100 .find_route(&components
, &mut HashMap
::new())
101 .ok_or_else(|| format_err
!("no such resource"))?
;
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?
;
109 .ok_or_else(|| format_err
!("{} did not return an array", path
))?
114 .map(|c
| c
.to_string())
115 .ok_or_else(|| format_err
!("no such property {}", param_name
))
117 .collect
::<Result
<Vec
<_
>, _
>>()?
)
119 None
=> bail
!("link does not define child links"),
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
,
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
)
139 Ok((method
, uri_param
))
141 bail
!("no {} handler defined for '{}'", method
, path
);
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
![];
152 for (k
, v
) in uri_param
{
153 param_list
.push((k
.clone(), v
.clone()));
156 let param
= param
.unwrap_or(json
!({}
));
158 if let Some(map
) = param
.as_object() {
160 param_list
.push((k
.clone(), v
.as_str().unwrap().to_string()));
164 let params
= schema
.parse_parameter_strings(¶m_list
, true)?
;
169 fn use_http_client() -> bool
{
170 match std
::env
::var("PROXMOX_DEBUG_API_CODE") {
171 Ok(var
) => var
!= "1",
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
;
186 let (method
, uri_param
) = get_api_method(method
, path
)?
;
187 let params
= merge_parameters(uri_param
, params
, method
.parameters
)?
;
189 call_api_code(method
, rpcenv
, params
).await
192 async
fn call_api_http(method
: &str, path
: &str, params
: Option
<Value
>) -> Result
<Value
, Error
> {
193 let client
= connect_to_localhost()?
;
197 percent_encoding
::utf8_percent_encode(path
, &URL_ASCIISET
)
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
,
207 .map(|mut res
| res
["data"].take())
210 async
fn call_api_code(
211 method
: &'
static ApiMethod
,
212 rpcenv
: &mut dyn RpcEnvironment
,
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
)?
;
221 match method
.handler
{
222 ApiHandler
::AsyncHttp(_handler
) => {
223 bail
!("not implemented");
225 ApiHandler
::Sync(handler
) => (handler
)(params
, method
, rpcenv
),
226 ApiHandler
::Async(handler
) => (handler
)(params
, method
, rpcenv
).await
,
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)");
236 proxmox_rest_server
::abort_local_worker(upid
.clone());
241 let result_future
= proxmox_rest_server
::wait_for_local_worker(upid_str
);
244 result
= result_future
.fuse() => result?
,
245 abort
= abort_future
.fuse() => abort?
,
251 async
fn call_api_and_format_result(
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?
;
260 if let Some(upid
) = result
.as_str() {
261 if PROXMOX_UPID_REGEX
.is_match(upid
) {
262 if use_http_client() {
263 let client
= connect_to_localhost()?
;
264 view_task_result(&client
, json
!({ "data": upid }
), &output_format
).await?
;
268 handle_worker(upid
).await?
;
270 if output_format
== "text" {
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();
283 format_and_print_result_full(&mut result
, return_type
, &output_format
, &options
);
290 additional_properties
: true,
294 description
: "The Method",
298 description
: "API path.",
301 schema
: OUTPUT_FORMAT
,
307 /// Call API on <api-path>
312 rpcenv
: &mut dyn RpcEnvironment
,
313 ) -> Result
<(), Error
> {
314 call_api_and_format_result(method
, api_path
, param
, rpcenv
).await
322 description
: "API path.",
326 description
: "Verbose output format.",
333 /// Get API usage information for <path>
338 _rpcenv
: &mut dyn RpcEnvironment
,
339 ) -> Result
<(), Error
> {
340 let docformat
= if verbose
{
341 DocumentationFormat
::Full
343 DocumentationFormat
::Short
345 let mut found
= false;
346 for command
in &["get", "set", "create", "delete"] {
347 let (info
, uri_params
) = match get_api_method(command
, &path
) {
353 let skip_params
: Vec
<&str> = uri_params
.keys().map(|s
| &**s
).collect();
355 let cmd
= CliCommand
::new(info
);
356 let prefix
= format
!("USAGE: {} {} {}", PROG_NAME
, command
, path
);
360 generate_usage_str(&prefix
, &cmd
, docformat
, "", &skip_params
)
365 bail
!("no such resource '{}'", path
);
371 #[derive(Debug, Serialize, Deserialize)]
372 /// A child link with capabilities
374 /// The name of the link
376 /// The capabilities of the path (format Drwcd)
377 capabilities
: String
,
380 const LS_SCHEMA
: &proxmox_schema
::Schema
=
381 &proxmox_schema
::ArraySchema
::new("List of child links", &ApiDirEntry
::API_SCHEMA
)
384 async
fn get_api_children(
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();
394 if get_child_links(&path
, rpcenv
).await
.is_ok() {
400 let cap_list
= &[("get", 'r'
), ("set", 'w'
), ("create", 'c'
), ("delete", 'd'
)];
402 for (method
, c
) in cap_list
{
403 if get_api_method(method
, &path
).is_ok() {
410 res
.push(ApiDirEntry
{
411 name
: link
.to_string(),
424 description
: "API path.",
428 schema
: OUTPUT_FORMAT
,
434 /// Get API usage information for <path>
435 async
fn ls(path
: Option
<String
>, mut param
: Value
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<(), Error
> {
436 let output_format
= extract_output_format(&mut param
);
438 let options
= TableFormatOptions
::new()
441 .sortby("name", false);
443 let res
= get_api_children(path
.unwrap_or(String
::from("/")), rpcenv
).await?
;
445 format_and_print_result_full(
446 &mut serde_json
::to_value(res
)?
,
447 &proxmox_schema
::ReturnType
{
458 pub fn api_commands() -> CommandLineInterface
{
459 let cmd_def
= CliCommandMap
::new()
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"))),
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"))),
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"))),
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"))),
490 CliCommand
::new(&API_METHOD_LS
)
491 .arg_param(&["path"])
492 .completion_cb("path", complete_api_path
!(Some("D"))),
496 CliCommand
::new(&API_METHOD_USAGE
)
497 .arg_param(&["path"])
498 .completion_cb("path", complete_api_path
!(None
)),