use anyhow::{bail, format_err, Error}; use futures::FutureExt; use hyper::Method; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use tokio::signal::unix::{signal, SignalKind}; use std::collections::HashMap; use proxmox_router::{cli::*, ApiHandler, ApiMethod, RpcEnvironment, SubRoute}; use proxmox_schema::{api, parse_parameter_strings, ApiType, ParameterSchema, Schema}; use proxmox_schema::format::DocumentationFormat; use pbs_api_types::{PROXMOX_UPID_REGEX, UPID}; use pbs_client::view_task_result; use proxmox_rest_server::normalize_uri_path; use proxmox_backup::client_helpers::connect_to_localhost; const PROG_NAME: &str = "proxmox-backup-debug api"; const URL_ASCIISET: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC.remove(b'/'); macro_rules! complete_api_path { ($capability:expr) => { |complete_me: &str, _map: &HashMap| { proxmox_async::runtime::block_on(async { complete_api_path_do(complete_me, $capability).await }) } }; } async fn complete_api_path_do(mut complete_me: &str, capability: Option<&str>) -> Vec { if complete_me.is_empty() { complete_me = "/"; } let mut list = Vec::new(); let mut lookup_path = complete_me.to_string(); let mut filter = ""; let last_path_index = complete_me.rfind('/'); if let Some(index) = last_path_index { if index != complete_me.len() - 1 { lookup_path = complete_me[..(index + 1)].to_string(); if index < complete_me.len() - 1 { filter = &complete_me[(index + 1)..]; } } } let uid = nix::unistd::Uid::current(); let username = match nix::unistd::User::from_uid(uid) { Ok(Some(user)) => user.name, _ => "root@pam".to_string(), }; let mut rpcenv = CliEnvironment::new(); rpcenv.set_auth_id(Some(format!("{}@pam", username))); while let Ok(children) = get_api_children(lookup_path.clone(), &mut rpcenv).await { let old_len = list.len(); for entry in children { let name = entry.name; let caps = entry.capabilities; if filter.is_empty() || name.starts_with(filter) { let mut path = format!("{}{}", lookup_path, name); if caps.contains('D') { path.push('/'); list.push(path.clone()); } else if let Some(cap) = capability { if caps.contains(cap) { list.push(path); } } else { list.push(path); } } } if list.len() == 1 && old_len != 1 && list[0].ends_with('/') { // we added only one match and it was a directory, lookup again lookup_path = list[0].clone(); filter = ""; continue; } break; } list } async fn get_child_links( path: &str, rpcenv: &mut dyn RpcEnvironment, ) -> Result, Error> { let (path, components) = normalize_uri_path(&path)?; let info = &proxmox_backup::api2::ROUTER .find_route(&components, &mut HashMap::new()) .ok_or_else(|| format_err!("no such resource"))?; match info.subroute { Some(SubRoute::Map(map)) => Ok(map.iter().map(|(name, _)| name.to_string()).collect()), Some(SubRoute::MatchAll { param_name, .. }) => { let list = call_api("get", &path, rpcenv, None).await?; Ok(list .as_array() .ok_or_else(|| format_err!("{} did not return an array", path))? .iter() .map(|item| { item[param_name] .as_str() .map(|c| c.to_string()) .ok_or_else(|| format_err!("no such property {}", param_name)) }) .collect::, _>>()?) } None => bail!("link does not define child links"), } } fn get_api_method( method: &str, path: &str, ) -> Result<(&'static ApiMethod, HashMap), Error> { let method = match method { "get" => Method::GET, "set" => Method::PUT, "create" => Method::POST, "delete" => Method::DELETE, _ => unreachable!(), }; let mut uri_param = HashMap::new(); let (path, components) = normalize_uri_path(&path)?; if let Some(method) = &proxmox_backup::api2::ROUTER.find_method(&components, method.clone(), &mut uri_param) { Ok((method, uri_param)) } else { bail!("no {} handler defined for '{}'", method, path); } } fn merge_parameters( uri_param: HashMap, param: Option, schema: ParameterSchema, ) -> Result { let mut param_list: Vec<(String, String)> = vec![]; for (k, v) in uri_param { param_list.push((k.clone(), v.clone())); } let param = param.unwrap_or(json!({})); if let Some(map) = param.as_object() { for (k, v) in map { param_list.push((k.clone(), v.as_str().unwrap().to_string())); } } let params = parse_parameter_strings(¶m_list, schema, true)?; Ok(params) } fn use_http_client() -> bool { match std::env::var("PROXMOX_DEBUG_API_CODE") { Ok(var) => var != "1", _ => true, } } async fn call_api( method: &str, path: &str, rpcenv: &mut dyn RpcEnvironment, params: Option, ) -> Result { if use_http_client() { return call_api_http(method, path, params).await; } let (method, uri_param) = get_api_method(method, path)?; let params = merge_parameters(uri_param, params, method.parameters)?; call_api_code(method, rpcenv, params).await } async fn call_api_http(method: &str, path: &str, params: Option) -> Result { let client = connect_to_localhost()?; let path = format!( "api2/json/{}", percent_encoding::utf8_percent_encode(path, &URL_ASCIISET) ); match method { "get" => client.get(&path, params).await, "create" => client.post(&path, params).await, "set" => client.put(&path, params).await, "delete" => client.delete(&path, params).await, _ => unreachable!(), } .map(|mut res| res["data"].take()) } async fn call_api_code( method: &'static ApiMethod, rpcenv: &mut dyn RpcEnvironment, params: Value, ) -> Result { if !method.protected { // drop privileges if we call non-protected code directly let backup_user = pbs_config::backup_user()?; nix::unistd::setgid(backup_user.gid)?; nix::unistd::setuid(backup_user.uid)?; } match method.handler { ApiHandler::AsyncHttp(_handler) => { bail!("not implemented"); } ApiHandler::Sync(handler) => (handler)(params, method, rpcenv), ApiHandler::Async(handler) => (handler)(params, method, rpcenv).await, } } async fn handle_worker(upid_str: &str) -> Result<(), Error> { let upid: UPID = upid_str.parse()?; let mut signal_stream = signal(SignalKind::interrupt())?; let abort_future = async move { while signal_stream.recv().await.is_some() { println!("got shutdown request (SIGINT)"); proxmox_rest_server::abort_local_worker(upid.clone()); } Ok::<_, Error>(()) }; let result_future = proxmox_rest_server::wait_for_local_worker(upid_str); futures::select! { result = result_future.fuse() => result?, abort = abort_future.fuse() => abort?, }; Ok(()) } async fn call_api_and_format_result( method: String, path: String, mut param: Value, rpcenv: &mut dyn RpcEnvironment, ) -> Result<(), Error> { let mut output_format = extract_output_format(&mut param); let mut result = call_api(&method, &path, rpcenv, Some(param)).await?; if let Some(upid) = result.as_str() { if PROXMOX_UPID_REGEX.is_match(upid) { if use_http_client() { let client = connect_to_localhost()?; view_task_result(&client, json!({ "data": upid }), &output_format).await?; return Ok(()); } handle_worker(upid).await?; if output_format == "text" { return Ok(()); } } } let (method, _) = get_api_method(&method, &path)?; let options = default_table_format_options(); let return_type = &method.returns; if matches!(return_type.schema, Schema::Null) { output_format = "json-pretty".to_string(); } format_and_print_result_full(&mut result, return_type, &output_format, &options); Ok(()) } #[api( input: { additional_properties: true, properties: { method: { type: String, description: "The Method", }, "api-path": { type: String, description: "API path.", }, "output-format": { schema: OUTPUT_FORMAT, optional: true, }, }, }, )] /// Call API on async fn api_call( method: String, api_path: String, param: Value, rpcenv: &mut dyn RpcEnvironment, ) -> Result<(), Error> { call_api_and_format_result(method, api_path, param, rpcenv).await } #[api( input: { properties: { path: { type: String, description: "API path.", }, verbose: { type: Boolean, description: "Verbose output format.", optional: true, default: false, } }, }, )] /// Get API usage information for async fn usage( path: String, verbose: bool, _param: Value, _rpcenv: &mut dyn RpcEnvironment, ) -> Result<(), Error> { let docformat = if verbose { DocumentationFormat::Full } else { DocumentationFormat::Short }; let mut found = false; for command in &["get", "set", "create", "delete"] { let (info, uri_params) = match get_api_method(command, &path) { Ok(some) => some, Err(_) => continue, }; found = true; let skip_params: Vec<&str> = uri_params.keys().map(|s| &**s).collect(); let cmd = CliCommand::new(info); let prefix = format!("USAGE: {} {} {}", PROG_NAME, command, path); print!( "{}", generate_usage_str(&prefix, &cmd, docformat, "", &skip_params) ); } if !found { bail!("no such resource '{}'", path); } Ok(()) } #[api()] #[derive(Debug, Serialize, Deserialize)] /// A child link with capabilities struct ApiDirEntry { /// The name of the link name: String, /// The capabilities of the path (format Drwcd) capabilities: String, } const LS_SCHEMA: &proxmox_schema::Schema = &proxmox_schema::ArraySchema::new("List of child links", &ApiDirEntry::API_SCHEMA) .schema(); async fn get_api_children( path: String, rpcenv: &mut dyn RpcEnvironment, ) -> Result, Error> { let mut res = Vec::new(); for link in get_child_links(&path, rpcenv).await? { let path = format!("{}/{}", path, link); let (path, _) = normalize_uri_path(&path)?; let mut cap = String::new(); if get_child_links(&path, rpcenv).await.is_ok() { cap.push('D'); } else { cap.push('-'); } let cap_list = &[("get", 'r'), ("set", 'w'), ("create", 'c'), ("delete", 'd')]; for (method, c) in cap_list { if get_api_method(method, &path).is_ok() { cap.push(*c); } else { cap.push('-'); } } res.push(ApiDirEntry { name: link.to_string(), capabilities: cap, }); } Ok(res) } #[api( input: { properties: { path: { type: String, description: "API path.", optional: true, }, "output-format": { schema: OUTPUT_FORMAT, optional: true, }, }, }, )] /// Get API usage information for async fn ls(path: Option, mut param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> { let output_format = extract_output_format(&mut param); let options = TableFormatOptions::new() .noborder(true) .noheader(true) .sortby("name", false); let res = get_api_children(path.unwrap_or(String::from("/")), rpcenv).await?; format_and_print_result_full( &mut serde_json::to_value(res)?, &proxmox_schema::ReturnType { optional: false, schema: &LS_SCHEMA, }, &output_format, &options, ); Ok(()) } pub fn api_commands() -> CommandLineInterface { let cmd_def = CliCommandMap::new() .insert( "get", CliCommand::new(&API_METHOD_API_CALL) .fixed_param("method", "get".to_string()) .arg_param(&["api-path"]) .completion_cb("api-path", complete_api_path!(Some("r"))), ) .insert( "set", CliCommand::new(&API_METHOD_API_CALL) .fixed_param("method", "set".to_string()) .arg_param(&["api-path"]) .completion_cb("api-path", complete_api_path!(Some("w"))), ) .insert( "create", CliCommand::new(&API_METHOD_API_CALL) .fixed_param("method", "create".to_string()) .arg_param(&["api-path"]) .completion_cb("api-path", complete_api_path!(Some("c"))), ) .insert( "delete", CliCommand::new(&API_METHOD_API_CALL) .fixed_param("method", "delete".to_string()) .arg_param(&["api-path"]) .completion_cb("api-path", complete_api_path!(Some("d"))), ) .insert( "ls", CliCommand::new(&API_METHOD_LS) .arg_param(&["path"]) .completion_cb("path", complete_api_path!(Some("D"))), ) .insert( "usage", CliCommand::new(&API_METHOD_USAGE) .arg_param(&["path"]) .completion_cb("path", complete_api_path!(None)), ); cmd_def.into() }