From: Dominik Csapak <d.csapak@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [PATCH proxmox-backup v4 3/7] proxmox-backup-debug: add 'api' subcommands
Date: Tue, 21 Sep 2021 12:11:14 +0200 [thread overview]
Message-ID: <20210921101118.2640200-4-d.csapak@proxmox.com> (raw)
In-Reply-To: <20210921101118.2640200-1-d.csapak@proxmox.com>
this provides some generic api call mechanisms like pvesh/pmgsh.
by default it uses the https api on localhost (creating a token
if called as root, else requesting the root@pam password interactively)
this is mainly intended for debugging, but it is also useful for
situations where some api calls do not have an equivalent in a binary
and a user does not want to go through the api
not implemented are the http2 api calls (since it is a separate api an
it wouldn't be that easy to do)
there are a few quirks though, related to the 'ls' command:
i extract the 'child-link' from the property name of the
'match_all' statement of the router, but this does not
always match with the property from the relevant 'get' api call
so it fails there (e.g. /tape/drive )
this can be fixed in the respective api calls (e.g. by renaming
the parameter that comes from the path)
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/bin/proxmox-backup-debug.rs | 17 +-
src/bin/proxmox_backup_debug/api.rs | 503 ++++++++++++++++++++++++++++
src/bin/proxmox_backup_debug/mod.rs | 1 +
3 files changed, 518 insertions(+), 3 deletions(-)
create mode 100644 src/bin/proxmox_backup_debug/api.rs
diff --git a/src/bin/proxmox-backup-debug.rs b/src/bin/proxmox-backup-debug.rs
index 4d6164ef..0ef37525 100644
--- a/src/bin/proxmox-backup-debug.rs
+++ b/src/bin/proxmox-backup-debug.rs
@@ -1,4 +1,7 @@
-use proxmox::api::cli::{run_cli_command, CliCommandMap, CliEnvironment};
+use proxmox::api::{
+ cli::{run_cli_command, CliCommandMap, CliEnvironment},
+ RpcEnvironment,
+};
mod proxmox_backup_debug;
use proxmox_backup_debug::*;
@@ -6,8 +9,16 @@ use proxmox_backup_debug::*;
fn main() {
let cmd_def = CliCommandMap::new()
.insert("inspect", inspect::inspect_commands())
- .insert("recover", recover::recover_commands());
+ .insert("recover", recover::recover_commands())
+ .insert("api", api::api_commands());
+
+ 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)));
- let rpcenv = CliEnvironment::new();
run_cli_command(cmd_def, rpcenv, Some(|future| pbs_runtime::main(future)));
}
diff --git a/src/bin/proxmox_backup_debug/api.rs b/src/bin/proxmox_backup_debug/api.rs
new file mode 100644
index 00000000..0292e628
--- /dev/null
+++ b/src/bin/proxmox_backup_debug/api.rs
@@ -0,0 +1,503 @@
+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::api::{
+ api,
+ cli::*,
+ format::DocumentationFormat,
+ schema::{parse_parameter_strings, ApiType, ParameterSchema, Schema},
+ ApiHandler, ApiMethod, RpcEnvironment, SubRoute,
+};
+
+use pbs_api_types::{PROXMOX_UPID_REGEX, UPID};
+use pbs_client::{connect_to_localhost, view_task_result};
+use proxmox_rest_server::normalize_uri_path;
+
+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<String, String>| {
+ pbs_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<String> {
+ 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<Vec<String>, 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::<Result<Vec<_>, _>>()?)
+ }
+ None => bail!("link does not define child links"),
+ }
+}
+
+fn get_api_method(
+ method: &str,
+ path: &str,
+) -> Result<(&'static ApiMethod, HashMap<String, String>), 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<String, String>,
+ param: Option<Value>,
+ schema: ParameterSchema,
+) -> Result<Value, Error> {
+ 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<Value>,
+) -> Result<Value, Error> {
+ 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<Value>) -> Result<Value, Error> {
+ let mut 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<Value, Error> {
+ 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_backup::server::abort_local_worker(upid.clone());
+ }
+ Ok::<_, Error>(())
+ };
+
+ let result_future = proxmox_backup::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 mut client = connect_to_localhost()?;
+ view_task_result(&mut 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 <api-path>
+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 <path>
+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::api::schema::Schema =
+ &proxmox::api::schema::ArraySchema::new("List of child links", &ApiDirEntry::API_SCHEMA)
+ .schema();
+
+async fn get_api_children(
+ path: String,
+ rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Vec<ApiDirEntry>, 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.",
+ },
+ "output-format": {
+ schema: OUTPUT_FORMAT,
+ optional: true,
+ },
+ },
+ },
+)]
+/// Get API usage information for <path>
+async fn ls(path: String, 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, rpcenv).await?;
+
+ format_and_print_result_full(
+ &mut serde_json::to_value(res)?,
+ &proxmox::api::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()
+}
diff --git a/src/bin/proxmox_backup_debug/mod.rs b/src/bin/proxmox_backup_debug/mod.rs
index bbaca751..a3a526dd 100644
--- a/src/bin/proxmox_backup_debug/mod.rs
+++ b/src/bin/proxmox_backup_debug/mod.rs
@@ -1,2 +1,3 @@
pub mod inspect;
pub mod recover;
+pub mod api;
--
2.30.2
next prev parent reply other threads:[~2021-09-21 10:11 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2021-09-21 10:11 [pbs-devel] [PATCH proxmox-backup v4 0/7] add 'proxmox-backup-debug api' commands Dominik Csapak
2021-09-21 10:11 ` [pbs-devel] [PATCH proxmox-backup v4 1/7] server: refactor abort_local_worker Dominik Csapak
2021-09-21 10:11 ` [pbs-devel] [PATCH proxmox-backup v4 2/7] move proxmox-backup-debug back to main crate Dominik Csapak
2021-09-21 10:11 ` Dominik Csapak [this message]
2021-09-21 10:11 ` [pbs-devel] [PATCH proxmox-backup v4 4/7] api2: add missing token list match_all property Dominik Csapak
2021-09-21 10:11 ` [pbs-devel] [PATCH proxmox-backup v4 5/7] api2: make some workers log on CLI Dominik Csapak
2021-09-21 10:11 ` [pbs-devel] [PATCH proxmox-backup v4 6/7] docs: add proxmox-backup-debug to the list of command line tools Dominik Csapak
2021-09-21 10:11 ` [pbs-devel] [PATCH proxmox-backup v4 7/7] docs: proxmox-backup-debug: add info about the 'api' subcommand Dominik Csapak
2021-09-21 13:45 ` [pbs-devel] applied-series: [PATCH proxmox-backup v4 0/7] add 'proxmox-backup-debug api' commands Thomas Lamprecht
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20210921101118.2640200-4-d.csapak@proxmox.com \
--to=d.csapak@proxmox.com \
--cc=pbs-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal