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
;
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 mod proxmox_backup_manager
;
18 use proxmox_backup_manager
::*;
20 async
fn view_task_result(
24 ) -> Result
<(), Error
> {
25 let data
= &result
["data"];
26 if output_format
== "text" {
27 if let Some(upid
) = data
.as_str() {
28 display_task_log(client
, upid
, true).await?
;
31 format_and_print_result(&data
, &output_format
);
37 fn connect() -> Result
<HttpClient
, Error
> {
39 let uid
= nix
::unistd
::Uid
::current();
41 let mut options
= HttpClientOptions
::new()
42 .prefix(Some("proxmox-backup".to_string()))
43 .verify_cert(false); // not required for connection to localhost
45 let client
= if uid
.is_root() {
46 let ticket
= assemble_rsa_ticket(private_auth_key(), "PBS", Some("root@pam"), None
)?
;
47 options
= options
.password(Some(ticket
));
48 HttpClient
::new("localhost", "root@pam", options
)?
50 options
= options
.ticket_cache(true).interactive(true);
51 HttpClient
::new("localhost", "root@pam", options
)?
61 schema
: OUTPUT_FORMAT
,
68 fn get_dns(mut param
: Value
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Value
, Error
> {
70 let output_format
= get_output_format(¶m
);
72 param
["node"] = "localhost".into();
74 let info
= &api2
::node
::dns
::API_METHOD_GET_DNS
;
75 let mut data
= match info
.handler
{
76 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
81 let options
= default_table_format_options()
82 .column(ColumnConfig
::new("search"))
83 .column(ColumnConfig
::new("dns1"))
84 .column(ColumnConfig
::new("dns2"))
85 .column(ColumnConfig
::new("dns3"));
87 format_and_print_result_full(&mut data
, info
.returns
, &output_format
, &options
);
92 fn dns_commands() -> CommandLineInterface
{
94 let cmd_def
= CliCommandMap
::new()
97 CliCommand
::new(&API_METHOD_GET_DNS
)
101 CliCommand
::new(&api2
::node
::dns
::API_METHOD_UPDATE_DNS
)
102 .fixed_param("node", String
::from("localhost"))
112 schema
: OUTPUT_FORMAT
,
119 fn list_datastores(param
: Value
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Value
, Error
> {
121 let output_format
= get_output_format(¶m
);
123 let info
= &api2
::config
::datastore
::API_METHOD_LIST_DATASTORES
;
124 let mut data
= match info
.handler
{
125 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
129 let options
= default_table_format_options()
130 .column(ColumnConfig
::new("name"))
131 .column(ColumnConfig
::new("path"))
132 .column(ColumnConfig
::new("comment"));
134 format_and_print_result_full(&mut data
, info
.returns
, &output_format
, &options
);
143 schema
: DATASTORE_SCHEMA
,
146 schema
: OUTPUT_FORMAT
,
152 /// Show datastore configuration
153 fn show_datastore(param
: Value
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Value
, Error
> {
155 let output_format
= get_output_format(¶m
);
157 let info
= &api2
::config
::datastore
::API_METHOD_READ_DATASTORE
;
158 let mut data
= match info
.handler
{
159 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
163 let options
= default_table_format_options();
164 format_and_print_result_full(&mut data
, info
.returns
, &output_format
, &options
);
169 fn datastore_commands() -> CommandLineInterface
{
171 let cmd_def
= CliCommandMap
::new()
172 .insert("list", CliCommand
::new(&API_METHOD_LIST_DATASTORES
))
174 CliCommand
::new(&API_METHOD_SHOW_DATASTORE
)
175 .arg_param(&["name"])
176 .completion_cb("name", config
::datastore
::complete_datastore_name
)
179 CliCommand
::new(&api2
::config
::datastore
::API_METHOD_CREATE_DATASTORE
)
180 .arg_param(&["name", "path"])
183 CliCommand
::new(&api2
::config
::datastore
::API_METHOD_UPDATE_DATASTORE
)
184 .arg_param(&["name"])
185 .completion_cb("name", config
::datastore
::complete_datastore_name
)
186 .completion_cb("gc-schedule", config
::datastore
::complete_calendar_event
)
187 .completion_cb("prune-schedule", config
::datastore
::complete_calendar_event
)
190 CliCommand
::new(&api2
::config
::datastore
::API_METHOD_DELETE_DATASTORE
)
191 .arg_param(&["name"])
192 .completion_cb("name", config
::datastore
::complete_datastore_name
)
203 schema
: DATASTORE_SCHEMA
,
206 schema
: OUTPUT_FORMAT
,
212 /// Start garbage collection for a specific datastore.
213 async
fn start_garbage_collection(param
: Value
) -> Result
<Value
, Error
> {
215 let output_format
= get_output_format(¶m
);
217 let store
= tools
::required_string_param(¶m
, "store")?
;
219 let mut client
= connect()?
;
221 let path
= format
!("api2/json/admin/datastore/{}/gc", store
);
223 let result
= client
.post(&path
, None
).await?
;
225 view_task_result(client
, result
, &output_format
).await?
;
234 schema
: DATASTORE_SCHEMA
,
237 schema
: OUTPUT_FORMAT
,
243 /// Show garbage collection status for a specific datastore.
244 async
fn garbage_collection_status(param
: Value
) -> Result
<Value
, Error
> {
246 let output_format
= get_output_format(¶m
);
248 let store
= tools
::required_string_param(¶m
, "store")?
;
250 let client
= connect()?
;
252 let path
= format
!("api2/json/admin/datastore/{}/gc", store
);
254 let mut result
= client
.get(&path
, None
).await?
;
255 let mut data
= result
["data"].take();
256 let schema
= api2
::admin
::datastore
::API_RETURN_SCHEMA_GARBAGE_COLLECTION_STATUS
;
258 let options
= default_table_format_options();
260 format_and_print_result_full(&mut data
, schema
, &output_format
, &options
);
265 fn garbage_collection_commands() -> CommandLineInterface
{
267 let cmd_def
= CliCommandMap
::new()
269 CliCommand
::new(&API_METHOD_GARBAGE_COLLECTION_STATUS
)
270 .arg_param(&["store"])
271 .completion_cb("store", config
::datastore
::complete_datastore_name
)
274 CliCommand
::new(&API_METHOD_START_GARBAGE_COLLECTION
)
275 .arg_param(&["store"])
276 .completion_cb("store", config
::datastore
::complete_datastore_name
)
286 description
: "The maximal number of tasks to list.",
294 schema
: OUTPUT_FORMAT
,
299 description
: "Also list stopped tasks.",
305 /// List running server tasks.
306 async
fn task_list(param
: Value
) -> Result
<Value
, Error
> {
308 let output_format
= get_output_format(¶m
);
310 let client
= connect()?
;
312 let limit
= param
["limit"].as_u64().unwrap_or(50) as usize;
313 let running
= !param
["all"].as_bool().unwrap_or(false);
319 let mut result
= client
.get("api2/json/nodes/localhost/tasks", Some(args
)).await?
;
321 let mut data
= result
["data"].take();
322 let schema
= api2
::node
::tasks
::API_RETURN_SCHEMA_LIST_TASKS
;
324 let options
= default_table_format_options()
325 .column(ColumnConfig
::new("starttime").right_align(false).renderer(tools
::format
::render_epoch
))
326 .column(ColumnConfig
::new("endtime").right_align(false).renderer(tools
::format
::render_epoch
))
327 .column(ColumnConfig
::new("upid"))
328 .column(ColumnConfig
::new("status").renderer(tools
::format
::render_task_status
));
330 format_and_print_result_full(&mut data
, schema
, &output_format
, &options
);
344 /// Display the task log.
345 async
fn task_log(param
: Value
) -> Result
<Value
, Error
> {
347 let upid
= tools
::required_string_param(¶m
, "upid")?
;
349 let client
= connect()?
;
351 display_task_log(client
, upid
, true).await?
;
365 /// Try to stop a specific task.
366 async
fn task_stop(param
: Value
) -> Result
<Value
, Error
> {
368 let upid_str
= tools
::required_string_param(¶m
, "upid")?
;
370 let mut client
= connect()?
;
372 let path
= format
!("api2/json/nodes/localhost/tasks/{}", upid_str
);
373 let _
= client
.delete(&path
, None
).await?
;
378 fn task_mgmt_cli() -> CommandLineInterface
{
380 let task_log_cmd_def
= CliCommand
::new(&API_METHOD_TASK_LOG
)
381 .arg_param(&["upid"]);
383 let task_stop_cmd_def
= CliCommand
::new(&API_METHOD_TASK_STOP
)
384 .arg_param(&["upid"]);
386 let cmd_def
= CliCommandMap
::new()
387 .insert("list", CliCommand
::new(&API_METHOD_TASK_LIST
))
388 .insert("log", task_log_cmd_def
)
389 .insert("stop", task_stop_cmd_def
);
394 fn x509name_to_string(name
: &openssl
::x509
::X509NameRef
) -> Result
<String
, Error
> {
395 let mut parts
= Vec
::new();
396 for entry
in name
.entries() {
397 parts
.push(format
!("{} = {}", entry
.object().nid().short_name()?
, entry
.data().as_utf8()?
));
403 /// Diplay node certificate information.
404 fn cert_info() -> Result
<(), Error
> {
406 let cert_path
= PathBuf
::from(configdir
!("/proxy.pem"));
408 let cert_pem
= proxmox
::tools
::fs
::file_get_contents(&cert_path
)?
;
410 let cert
= openssl
::x509
::X509
::from_pem(&cert_pem
)?
;
412 println
!("Subject: {}", x509name_to_string(cert
.subject_name())?
);
414 if let Some(san
) = cert
.subject_alt_names() {
415 for name
in san
.iter() {
416 if let Some(v
) = name
.dnsname() {
417 println
!(" DNS:{}", v
);
418 } else if let Some(v
) = name
.ipaddress() {
419 println
!(" IP:{:?}", v
);
420 } else if let Some(v
) = name
.email() {
421 println
!(" EMAIL:{}", v
);
422 } else if let Some(v
) = name
.uri() {
423 println
!(" URI:{}", v
);
428 println
!("Issuer: {}", x509name_to_string(cert
.issuer_name())?
);
429 println
!("Validity:");
430 println
!(" Not Before: {}", cert
.not_before());
431 println
!(" Not After : {}", cert
.not_after());
433 let fp
= cert
.digest(openssl
::hash
::MessageDigest
::sha256())?
;
434 let fp_string
= proxmox
::tools
::digest_to_hex(&fp
);
435 let fp_string
= fp_string
.as_bytes().chunks(2).map(|v
| std
::str::from_utf8(v
).unwrap())
436 .collect
::<Vec
<&str>>().join(":");
438 println
!("Fingerprint (sha256): {}", fp_string
);
440 let pubkey
= cert
.public_key()?
;
441 println
!("Public key type: {}", openssl
::nid
::Nid
::from_raw(pubkey
.id().as_raw()).long_name()?
);
442 println
!("Public key bits: {}", pubkey
.bits());
451 description
: "Force generation of new SSL certifate.",
458 /// Update node certificates and generate all needed files/directories.
459 fn update_certs(force
: Option
<bool
>) -> Result
<(), Error
> {
461 config
::create_configdir()?
;
463 if let Err(err
) = generate_auth_key() {
464 bail
!("unable to generate auth key - {}", err
);
467 if let Err(err
) = generate_csrf_key() {
468 bail
!("unable to generate csrf key - {}", err
);
471 config
::update_self_signed_cert(force
.unwrap_or(false))?
;
476 fn cert_mgmt_cli() -> CommandLineInterface
{
478 let cmd_def
= CliCommandMap
::new()
479 .insert("info", CliCommand
::new(&API_METHOD_CERT_INFO
))
480 .insert("update", CliCommand
::new(&API_METHOD_UPDATE_CERTS
));
485 // fixme: avoid API redefinition
490 schema
: DATASTORE_SCHEMA
,
493 schema
: REMOTE_ID_SCHEMA
,
496 schema
: DATASTORE_SCHEMA
,
499 description
: "Delete vanished backups. This remove the local copy if the remote backup was deleted.",
505 schema
: OUTPUT_FORMAT
,
511 /// Sync datastore from another repository
512 async
fn pull_datastore(
514 remote_store
: String
,
516 delete
: Option
<bool
>,
518 ) -> Result
<Value
, Error
> {
520 let output_format
= get_output_format(¶m
);
522 let mut client
= connect()?
;
524 let mut args
= json
!({
525 "store": local_store
,
527 "remote-store": remote_store
,
530 if let Some(delete
) = delete
{
531 args
["delete"] = delete
.into();
534 let result
= client
.post("api2/json/pull", Some(args
)).await?
;
536 view_task_result(client
, result
, &output_format
).await?
;
543 let cmd_def
= CliCommandMap
::new()
544 .insert("acl", acl_commands())
545 .insert("datastore", datastore_commands())
546 .insert("dns", dns_commands())
547 .insert("network", network_commands())
548 .insert("user", user_commands())
549 .insert("remote", remote_commands())
550 .insert("garbage-collection", garbage_collection_commands())
551 .insert("cert", cert_mgmt_cli())
552 .insert("task", task_mgmt_cli())
555 CliCommand
::new(&API_METHOD_PULL_DATASTORE
)
556 .arg_param(&["remote", "remote-store", "local-store"])
557 .completion_cb("local-store", config
::datastore
::complete_datastore_name
)
558 .completion_cb("remote", config
::remote
::complete_remote_name
)
559 .completion_cb("remote-store", complete_remote_datastore_name
)
562 let mut rpcenv
= CliEnvironment
::new();
563 rpcenv
.set_user(Some(String
::from("root@pam")));
565 proxmox_backup
::tools
::runtime
::main(run_async_cli_command(cmd_def
, rpcenv
));
568 // shell completion helper
569 pub fn complete_remote_datastore_name(_arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
571 let mut list
= Vec
::new();
573 let _
= proxmox
::try_block
!({
574 let remote
= param
.get("remote").ok_or_else(|| format_err
!("no remote"))?
;
575 let (remote_config
, _digest
) = config
::remote
::config()?
;
577 let remote
: config
::remote
::Remote
= remote_config
.lookup("remote", &remote
)?
;
579 let options
= HttpClientOptions
::new()
580 .password(Some(remote
.password
.clone()))
581 .fingerprint(remote
.fingerprint
.clone());
583 let client
= HttpClient
::new(
589 let result
= crate::tools
::runtime
::block_on(client
.get("api2/json/admin/datastore", None
))?
;
591 if let Some(data
) = result
["data"].as_array() {
593 if let Some(store
) = item
["store"].as_str() {
594 list
.push(store
.to_owned());
600 }).map_err(|_err
: Error
| { /* ignore */ }
);