1 use std
::path
::PathBuf
;
2 use std
::collections
::HashMap
;
4 use anyhow
::{bail, format_err, Error}
;
5 use serde_json
::{json, Value}
;
7 use proxmox
::api
::{api, cli::*, RpcEnvironment, ApiHandler}
;
9 use proxmox_backup
::configdir
;
10 use proxmox_backup
::tools
;
11 use proxmox_backup
::config
::{self, remote::{self, Remote}
};
12 use proxmox_backup
::api2
::{self, types::* }
;
13 use proxmox_backup
::client
::*;
14 use proxmox_backup
::tools
::ticket
::*;
15 use proxmox_backup
::auth_helpers
::*;
17 async
fn view_task_result(
21 ) -> Result
<(), Error
> {
22 let data
= &result
["data"];
23 if output_format
== "text" {
24 if let Some(upid
) = data
.as_str() {
25 display_task_log(client
, upid
, true).await?
;
28 format_and_print_result(&data
, &output_format
);
34 fn connect() -> Result
<HttpClient
, Error
> {
36 let uid
= nix
::unistd
::Uid
::current();
38 let mut options
= HttpClientOptions
::new()
39 .prefix(Some("proxmox-backup".to_string()))
40 .verify_cert(false); // not required for connection to localhost
42 let client
= if uid
.is_root() {
43 let ticket
= assemble_rsa_ticket(private_auth_key(), "PBS", Some("root@pam"), None
)?
;
44 options
= options
.password(Some(ticket
));
45 HttpClient
::new("localhost", "root@pam", options
)?
47 options
= options
.ticket_cache(true).interactive(true);
48 HttpClient
::new("localhost", "root@pam", options
)?
58 schema
: OUTPUT_FORMAT
,
64 /// List configured remotes.
65 fn list_remotes(param
: Value
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Value
, Error
> {
67 let output_format
= get_output_format(¶m
);
69 let info
= &api2
::config
::remote
::API_METHOD_LIST_REMOTES
;
70 let mut data
= match info
.handler
{
71 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
75 let options
= default_table_format_options()
76 .column(ColumnConfig
::new("name"))
77 .column(ColumnConfig
::new("host"))
78 .column(ColumnConfig
::new("userid"))
79 .column(ColumnConfig
::new("fingerprint"))
80 .column(ColumnConfig
::new("comment"));
82 format_and_print_result_full(&mut data
, info
.returns
, &output_format
, &options
);
87 fn remote_commands() -> CommandLineInterface
{
89 let cmd_def
= CliCommandMap
::new()
90 .insert("list", CliCommand
::new(&&API_METHOD_LIST_REMOTES
))
93 // fixme: howto handle password parameter?
94 CliCommand
::new(&api2
::config
::remote
::API_METHOD_CREATE_REMOTE
)
99 CliCommand
::new(&api2
::config
::remote
::API_METHOD_UPDATE_REMOTE
)
100 .arg_param(&["name"])
101 .completion_cb("name", config
::remote
::complete_remote_name
)
105 CliCommand
::new(&api2
::config
::remote
::API_METHOD_DELETE_REMOTE
)
106 .arg_param(&["name"])
107 .completion_cb("name", config
::remote
::complete_remote_name
)
117 schema
: OUTPUT_FORMAT
,
123 /// List configured users.
124 fn list_users(param
: Value
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Value
, Error
> {
126 let output_format
= get_output_format(¶m
);
128 let info
= &api2
::access
::user
::API_METHOD_LIST_USERS
;
129 let mut data
= match info
.handler
{
130 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
134 let options
= default_table_format_options()
135 .column(ColumnConfig
::new("userid"))
137 ColumnConfig
::new("enable")
138 .renderer(tools
::format
::render_bool_with_default_true
)
141 ColumnConfig
::new("expire")
142 .renderer(tools
::format
::render_epoch
)
144 .column(ColumnConfig
::new("firstname"))
145 .column(ColumnConfig
::new("lastname"))
146 .column(ColumnConfig
::new("email"))
147 .column(ColumnConfig
::new("comment"));
149 format_and_print_result_full(&mut data
, info
.returns
, &output_format
, &options
);
154 fn user_commands() -> CommandLineInterface
{
156 let cmd_def
= CliCommandMap
::new()
157 .insert("list", CliCommand
::new(&&API_METHOD_LIST_USERS
))
160 // fixme: howto handle password parameter?
161 CliCommand
::new(&api2
::access
::user
::API_METHOD_CREATE_USER
)
162 .arg_param(&["userid"])
166 CliCommand
::new(&api2
::access
::user
::API_METHOD_UPDATE_USER
)
167 .arg_param(&["userid"])
168 .completion_cb("userid", config
::user
::complete_user_name
)
172 CliCommand
::new(&api2
::access
::user
::API_METHOD_DELETE_USER
)
173 .arg_param(&["userid"])
174 .completion_cb("userid", config
::user
::complete_user_name
)
184 schema
: OUTPUT_FORMAT
,
190 /// Access Control list.
191 fn list_acls(param
: Value
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Value
, Error
> {
193 let output_format
= get_output_format(¶m
);
195 let info
= &api2
::access
::acl
::API_METHOD_READ_ACL
;
196 let mut data
= match info
.handler
{
197 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
201 fn render_ugid(value
: &Value
, record
: &Value
) -> Result
<String
, Error
> {
202 if value
.is_null() { return Ok(String::new()); }
203 let ugid
= value
.as_str().unwrap();
204 let ugid_type
= record
["ugid_type"].as_str().unwrap();
206 if ugid_type
== "user" {
208 } else if ugid_type
== "group" {
209 Ok(format
!("@{}", ugid
))
211 bail
!("render_ugid: got unknown ugid_type");
215 let options
= default_table_format_options()
216 .column(ColumnConfig
::new("ugid").renderer(render_ugid
))
217 .column(ColumnConfig
::new("path"))
218 .column(ColumnConfig
::new("propagate"))
219 .column(ColumnConfig
::new("roleid"));
221 format_and_print_result_full(&mut data
, info
.returns
, &output_format
, &options
);
226 fn acl_commands() -> CommandLineInterface
{
228 let cmd_def
= CliCommandMap
::new()
229 .insert("list", CliCommand
::new(&&API_METHOD_LIST_ACLS
))
232 CliCommand
::new(&api2
::access
::acl
::API_METHOD_UPDATE_ACL
)
233 .arg_param(&["path", "role"])
234 .completion_cb("userid", config
::user
::complete_user_name
)
235 .completion_cb("path", config
::datastore
::complete_acl_path
)
246 schema
: OUTPUT_FORMAT
,
252 /// Network device list.
253 fn list_network_devices(mut param
: Value
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Value
, Error
> {
255 let output_format
= get_output_format(¶m
);
257 param
["node"] = "localhost".into();
259 let info
= &api2
::node
::network
::API_METHOD_LIST_NETWORK_DEVICES
;
260 let mut data
= match info
.handler
{
261 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
265 if let Some(changes
) = rpcenv
.get_result_attrib("changes") {
266 if let Some(diff
) = changes
.as_str() {
267 if output_format
== "text" {
268 eprintln
!("pending changes:\n{}\n", diff
);
273 fn render_address(_value
: &Value
, record
: &Value
) -> Result
<String
, Error
> {
274 let mut text
= String
::new();
276 if let Some(cidr
) = record
["cidr"].as_str() {
279 if let Some(cidr
) = record
["cidr6"].as_str() {
280 if !text
.is_empty() { text.push('\n'); }
287 fn render_gateway(_value
: &Value
, record
: &Value
) -> Result
<String
, Error
> {
288 let mut text
= String
::new();
290 if let Some(gateway
) = record
["gateway"].as_str() {
291 text
.push_str(gateway
);
293 if let Some(gateway
) = record
["gateway6"].as_str() {
294 if !text
.is_empty() { text.push('\n'); }
295 text
.push_str(gateway
);
301 let options
= default_table_format_options()
302 .column(ColumnConfig
::new("type").header("type"))
303 .column(ColumnConfig
::new("name"))
304 .column(ColumnConfig
::new("autostart"))
305 .column(ColumnConfig
::new("method"))
306 .column(ColumnConfig
::new("method6"))
307 .column(ColumnConfig
::new("cidr").header("address").renderer(render_address
))
308 .column(ColumnConfig
::new("gateway").header("gateway").renderer(render_gateway
));
310 format_and_print_result_full(&mut data
, info
.returns
, &output_format
, &options
);
316 /// Show pending configuration changes (diff)
317 fn pending_network_changes(mut param
: Value
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Value
, Error
> {
318 param
["node"] = "localhost".into();
320 let info
= &api2
::node
::network
::API_METHOD_LIST_NETWORK_DEVICES
;
321 let _data
= match info
.handler
{
322 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
326 if let Some(changes
) = rpcenv
.get_result_attrib("changes") {
327 if let Some(diff
) = changes
.as_str() {
328 println
!("{}", diff
);
335 fn network_commands() -> CommandLineInterface
{
337 let cmd_def
= CliCommandMap
::new()
340 CliCommand
::new(&API_METHOD_LIST_NETWORK_DEVICES
)
344 CliCommand
::new(&API_METHOD_PENDING_NETWORK_CHANGES
)
348 CliCommand
::new(&api2
::node
::network
::API_METHOD_UPDATE_INTERFACE
)
349 .fixed_param("node", String
::from("localhost"))
350 .arg_param(&["iface"])
351 .completion_cb("iface", config
::network
::complete_interface_name
)
355 CliCommand
::new(&api2
::node
::network
::API_METHOD_DELETE_INTERFACE
)
356 .fixed_param("node", String
::from("localhost"))
357 .arg_param(&["iface"])
358 .completion_cb("iface", config
::network
::complete_interface_name
)
362 CliCommand
::new(&api2
::node
::network
::API_METHOD_REVERT_NETWORK_CONFIG
)
363 .fixed_param("node", String
::from("localhost"))
367 CliCommand
::new(&api2
::node
::network
::API_METHOD_RELOAD_NETWORK_CONFIG
)
368 .fixed_param("node", String
::from("localhost"))
378 schema
: OUTPUT_FORMAT
,
384 /// Read DNS settings
385 fn get_dns(mut param
: Value
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Value
, Error
> {
387 let output_format
= get_output_format(¶m
);
389 param
["node"] = "localhost".into();
391 let info
= &api2
::node
::dns
::API_METHOD_GET_DNS
;
392 let mut data
= match info
.handler
{
393 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
398 let options
= default_table_format_options()
399 .column(ColumnConfig
::new("search"))
400 .column(ColumnConfig
::new("dns1"))
401 .column(ColumnConfig
::new("dns2"))
402 .column(ColumnConfig
::new("dns3"));
404 format_and_print_result_full(&mut data
, info
.returns
, &output_format
, &options
);
409 fn dns_commands() -> CommandLineInterface
{
411 let cmd_def
= CliCommandMap
::new()
414 CliCommand
::new(&API_METHOD_GET_DNS
)
418 CliCommand
::new(&api2
::node
::dns
::API_METHOD_UPDATE_DNS
)
419 .fixed_param("node", String
::from("localhost"))
429 schema
: OUTPUT_FORMAT
,
436 fn list_datastores(param
: Value
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Value
, Error
> {
438 let output_format
= get_output_format(¶m
);
440 let info
= &api2
::config
::datastore
::API_METHOD_LIST_DATASTORES
;
441 let mut data
= match info
.handler
{
442 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
446 let options
= default_table_format_options()
447 .column(ColumnConfig
::new("name"))
448 .column(ColumnConfig
::new("path"))
449 .column(ColumnConfig
::new("comment"));
451 format_and_print_result_full(&mut data
, info
.returns
, &output_format
, &options
);
456 fn datastore_commands() -> CommandLineInterface
{
458 let cmd_def
= CliCommandMap
::new()
459 .insert("list", CliCommand
::new(&API_METHOD_LIST_DATASTORES
))
461 CliCommand
::new(&api2
::config
::datastore
::API_METHOD_CREATE_DATASTORE
)
462 .arg_param(&["name", "path"])
465 CliCommand
::new(&api2
::config
::datastore
::API_METHOD_UPDATE_DATASTORE
)
466 .arg_param(&["name"])
467 .completion_cb("name", config
::datastore
::complete_datastore_name
)
470 CliCommand
::new(&api2
::config
::datastore
::API_METHOD_DELETE_DATASTORE
)
471 .arg_param(&["name"])
472 .completion_cb("name", config
::datastore
::complete_datastore_name
)
483 schema
: DATASTORE_SCHEMA
,
486 schema
: OUTPUT_FORMAT
,
492 /// Start garbage collection for a specific datastore.
493 async
fn start_garbage_collection(param
: Value
) -> Result
<Value
, Error
> {
495 let output_format
= get_output_format(¶m
);
497 let store
= tools
::required_string_param(¶m
, "store")?
;
499 let mut client
= connect()?
;
501 let path
= format
!("api2/json/admin/datastore/{}/gc", store
);
503 let result
= client
.post(&path
, None
).await?
;
505 view_task_result(client
, result
, &output_format
).await?
;
514 schema
: DATASTORE_SCHEMA
,
517 schema
: OUTPUT_FORMAT
,
523 /// Show garbage collection status for a specific datastore.
524 async
fn garbage_collection_status(param
: Value
) -> Result
<Value
, Error
> {
526 let output_format
= get_output_format(¶m
);
528 let store
= tools
::required_string_param(¶m
, "store")?
;
530 let client
= connect()?
;
532 let path
= format
!("api2/json/admin/datastore/{}/gc", store
);
534 let mut result
= client
.get(&path
, None
).await?
;
535 let mut data
= result
["data"].take();
536 let schema
= api2
::admin
::datastore
::API_RETURN_SCHEMA_GARBAGE_COLLECTION_STATUS
;
538 let options
= default_table_format_options();
540 format_and_print_result_full(&mut data
, schema
, &output_format
, &options
);
545 fn garbage_collection_commands() -> CommandLineInterface
{
547 let cmd_def
= CliCommandMap
::new()
549 CliCommand
::new(&API_METHOD_GARBAGE_COLLECTION_STATUS
)
550 .arg_param(&["store"])
551 .completion_cb("store", config
::datastore
::complete_datastore_name
)
554 CliCommand
::new(&API_METHOD_START_GARBAGE_COLLECTION
)
555 .arg_param(&["store"])
556 .completion_cb("store", config
::datastore
::complete_datastore_name
)
566 description
: "The maximal number of tasks to list.",
574 schema
: OUTPUT_FORMAT
,
579 description
: "Also list stopped tasks.",
585 /// List running server tasks.
586 async
fn task_list(param
: Value
) -> Result
<Value
, Error
> {
588 let output_format
= get_output_format(¶m
);
590 let client
= connect()?
;
592 let limit
= param
["limit"].as_u64().unwrap_or(50) as usize;
593 let running
= !param
["all"].as_bool().unwrap_or(false);
599 let mut result
= client
.get("api2/json/nodes/localhost/tasks", Some(args
)).await?
;
601 let mut data
= result
["data"].take();
602 let schema
= api2
::node
::tasks
::API_RETURN_SCHEMA_LIST_TASKS
;
604 let options
= default_table_format_options()
605 .column(ColumnConfig
::new("starttime").right_align(false).renderer(tools
::format
::render_epoch
))
606 .column(ColumnConfig
::new("endtime").right_align(false).renderer(tools
::format
::render_epoch
))
607 .column(ColumnConfig
::new("upid"))
608 .column(ColumnConfig
::new("status").renderer(tools
::format
::render_task_status
));
610 format_and_print_result_full(&mut data
, schema
, &output_format
, &options
);
624 /// Display the task log.
625 async
fn task_log(param
: Value
) -> Result
<Value
, Error
> {
627 let upid
= tools
::required_string_param(¶m
, "upid")?
;
629 let client
= connect()?
;
631 display_task_log(client
, upid
, true).await?
;
645 /// Try to stop a specific task.
646 async
fn task_stop(param
: Value
) -> Result
<Value
, Error
> {
648 let upid_str
= tools
::required_string_param(¶m
, "upid")?
;
650 let mut client
= connect()?
;
652 let path
= format
!("api2/json/nodes/localhost/tasks/{}", upid_str
);
653 let _
= client
.delete(&path
, None
).await?
;
658 fn task_mgmt_cli() -> CommandLineInterface
{
660 let task_log_cmd_def
= CliCommand
::new(&API_METHOD_TASK_LOG
)
661 .arg_param(&["upid"]);
663 let task_stop_cmd_def
= CliCommand
::new(&API_METHOD_TASK_STOP
)
664 .arg_param(&["upid"]);
666 let cmd_def
= CliCommandMap
::new()
667 .insert("list", CliCommand
::new(&API_METHOD_TASK_LIST
))
668 .insert("log", task_log_cmd_def
)
669 .insert("stop", task_stop_cmd_def
);
674 fn x509name_to_string(name
: &openssl
::x509
::X509NameRef
) -> Result
<String
, Error
> {
675 let mut parts
= Vec
::new();
676 for entry
in name
.entries() {
677 parts
.push(format
!("{} = {}", entry
.object().nid().short_name()?
, entry
.data().as_utf8()?
));
683 /// Diplay node certificate information.
684 fn cert_info() -> Result
<(), Error
> {
686 let cert_path
= PathBuf
::from(configdir
!("/proxy.pem"));
688 let cert_pem
= proxmox
::tools
::fs
::file_get_contents(&cert_path
)?
;
690 let cert
= openssl
::x509
::X509
::from_pem(&cert_pem
)?
;
692 println
!("Subject: {}", x509name_to_string(cert
.subject_name())?
);
694 if let Some(san
) = cert
.subject_alt_names() {
695 for name
in san
.iter() {
696 if let Some(v
) = name
.dnsname() {
697 println
!(" DNS:{}", v
);
698 } else if let Some(v
) = name
.ipaddress() {
699 println
!(" IP:{:?}", v
);
700 } else if let Some(v
) = name
.email() {
701 println
!(" EMAIL:{}", v
);
702 } else if let Some(v
) = name
.uri() {
703 println
!(" URI:{}", v
);
708 println
!("Issuer: {}", x509name_to_string(cert
.issuer_name())?
);
709 println
!("Validity:");
710 println
!(" Not Before: {}", cert
.not_before());
711 println
!(" Not After : {}", cert
.not_after());
713 let fp
= cert
.digest(openssl
::hash
::MessageDigest
::sha256())?
;
714 let fp_string
= proxmox
::tools
::digest_to_hex(&fp
);
715 let fp_string
= fp_string
.as_bytes().chunks(2).map(|v
| std
::str::from_utf8(v
).unwrap())
716 .collect
::<Vec
<&str>>().join(":");
718 println
!("Fingerprint (sha256): {}", fp_string
);
720 let pubkey
= cert
.public_key()?
;
721 println
!("Public key type: {}", openssl
::nid
::Nid
::from_raw(pubkey
.id().as_raw()).long_name()?
);
722 println
!("Public key bits: {}", pubkey
.bits());
731 description
: "Force generation of new SSL certifate.",
738 /// Update node certificates and generate all needed files/directories.
739 fn update_certs(force
: Option
<bool
>) -> Result
<(), Error
> {
741 config
::create_configdir()?
;
743 if let Err(err
) = generate_auth_key() {
744 bail
!("unable to generate auth key - {}", err
);
747 if let Err(err
) = generate_csrf_key() {
748 bail
!("unable to generate csrf key - {}", err
);
751 config
::update_self_signed_cert(force
.unwrap_or(false))?
;
756 fn cert_mgmt_cli() -> CommandLineInterface
{
758 let cmd_def
= CliCommandMap
::new()
759 .insert("info", CliCommand
::new(&API_METHOD_CERT_INFO
))
760 .insert("update", CliCommand
::new(&API_METHOD_UPDATE_CERTS
));
765 // fixme: avoid API redefinition
770 schema
: DATASTORE_SCHEMA
,
773 schema
: REMOTE_ID_SCHEMA
,
776 schema
: DATASTORE_SCHEMA
,
779 description
: "Delete vanished backups. This remove the local copy if the remote backup was deleted.",
785 schema
: OUTPUT_FORMAT
,
791 /// Sync datastore from another repository
792 async
fn pull_datastore(
794 remote_store
: String
,
796 delete
: Option
<bool
>,
798 ) -> Result
<Value
, Error
> {
800 let output_format
= get_output_format(¶m
);
802 let mut client
= connect()?
;
804 let mut args
= json
!({
805 "store": local_store
,
807 "remote-store": remote_store
,
810 if let Some(delete
) = delete
{
811 args
["delete"] = delete
.into();
814 let result
= client
.post("api2/json/pull", Some(args
)).await?
;
816 view_task_result(client
, result
, &output_format
).await?
;
823 let cmd_def
= CliCommandMap
::new()
824 .insert("acl", acl_commands())
825 .insert("datastore", datastore_commands())
826 .insert("dns", dns_commands())
827 .insert("network", network_commands())
828 .insert("user", user_commands())
829 .insert("remote", remote_commands())
830 .insert("garbage-collection", garbage_collection_commands())
831 .insert("cert", cert_mgmt_cli())
832 .insert("task", task_mgmt_cli())
835 CliCommand
::new(&API_METHOD_PULL_DATASTORE
)
836 .arg_param(&["remote", "remote-store", "local-store"])
837 .completion_cb("local-store", config
::datastore
::complete_datastore_name
)
838 .completion_cb("remote", config
::remote
::complete_remote_name
)
839 .completion_cb("remote-store", complete_remote_datastore_name
)
842 let mut rpcenv
= CliEnvironment
::new();
843 rpcenv
.set_user(Some(String
::from("root@pam")));
845 proxmox_backup
::tools
::runtime
::main(run_async_cli_command(cmd_def
, rpcenv
));
848 // shell completion helper
849 pub fn complete_remote_datastore_name(_arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
851 let mut list
= Vec
::new();
853 let _
= proxmox
::try_block
!({
854 let remote
= param
.get("remote").ok_or_else(|| format_err
!("no remote"))?
;
855 let (remote_config
, _digest
) = remote
::config()?
;
857 let remote
: Remote
= remote_config
.lookup("remote", &remote
)?
;
859 let options
= HttpClientOptions
::new()
860 .password(Some(remote
.password
.clone()))
861 .fingerprint(remote
.fingerprint
.clone());
863 let client
= HttpClient
::new(
869 let result
= crate::tools
::runtime
::block_on(client
.get("api2/json/admin/datastore", None
))?
;
871 if let Some(data
) = result
["data"].as_array() {
873 if let Some(store
) = item
["store"].as_str() {
874 list
.push(store
.to_owned());
880 }).map_err(|_err
: Error
| { /* ignore */ }
);