1 use std
::path
::PathBuf
;
2 use std
::collections
::HashMap
;
5 use serde_json
::{json, Value}
;
6 use chrono
::{Local, TimeZone}
;
8 use proxmox
::api
::{api, cli::*, RpcEnvironment, ApiHandler}
;
10 use proxmox_backup
::configdir
;
11 use proxmox_backup
::tools
;
12 use proxmox_backup
::config
::{self, remote::{self, Remote}
};
13 use proxmox_backup
::api2
::{self, types::* }
;
14 use proxmox_backup
::client
::*;
15 use proxmox_backup
::tools
::ticket
::*;
16 use proxmox_backup
::auth_helpers
::*;
18 fn render_epoch(value
: &Value
, _record
: &Value
) -> Result
<String
, Error
> {
19 if value
.is_null() { return Ok(String::new()); }
20 let text
= match value
.as_i64() {
22 Local
.timestamp(epoch
, 0).format("%c").to_string()
31 fn render_status(value
: &Value
, record
: &Value
) -> Result
<String
, Error
> {
32 if record
["endtime"].is_null() {
33 Ok(value
.as_str().unwrap_or("running").to_string())
35 Ok(value
.as_str().unwrap_or("unknown").to_string())
39 async
fn view_task_result(
43 ) -> Result
<(), Error
> {
44 let data
= &result
["data"];
45 if output_format
== "text" {
46 if let Some(upid
) = data
.as_str() {
47 display_task_log(client
, upid
, true).await?
;
50 format_and_print_result(&data
, &output_format
);
56 fn connect() -> Result
<HttpClient
, Error
> {
58 let uid
= nix
::unistd
::Uid
::current();
60 let mut options
= HttpClientOptions
::new()
61 .prefix(Some("proxmox-backup".to_string()))
62 .verify_cert(false); // not required for connection to localhost
64 let client
= if uid
.is_root() {
65 let ticket
= assemble_rsa_ticket(private_auth_key(), "PBS", Some("root@pam"), None
)?
;
66 options
= options
.password(Some(ticket
));
67 HttpClient
::new("localhost", "root@pam", options
)?
69 options
= options
.ticket_cache(true).interactive(true);
70 HttpClient
::new("localhost", "root@pam", options
)?
80 schema
: OUTPUT_FORMAT
,
86 /// List configured remotes.
87 fn list_remotes(param
: Value
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Value
, Error
> {
89 let output_format
= param
["output-format"].as_str().unwrap_or("text").to_owned();
91 let info
= &api2
::config
::remote
::API_METHOD_LIST_REMOTES
;
92 let mut data
= match info
.handler
{
93 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
97 let mut column_config
= Vec
::new();
98 column_config
.push(ColumnConfig
::new("name"));
99 column_config
.push(ColumnConfig
::new("host"));
100 column_config
.push(ColumnConfig
::new("userid"));
101 column_config
.push(ColumnConfig
::new("fingerprint"));
102 column_config
.push(ColumnConfig
::new("comment"));
104 let options
= TableFormatOptions
::new()
107 .column_config(column_config
);
110 format_and_print_result_full(&mut data
, info
.returns
, &output_format
, &options
);
115 fn remote_commands() -> CommandLineInterface
{
117 let cmd_def
= CliCommandMap
::new()
118 //.insert("list", CliCommand::new(&api2::config::remote::API_METHOD_LIST_REMOTES))
119 .insert("list", CliCommand
::new(&&API_METHOD_LIST_REMOTES
))
122 // fixme: howto handle password parameter?
123 CliCommand
::new(&api2
::config
::remote
::API_METHOD_CREATE_REMOTE
)
124 .arg_param(&["name"])
128 CliCommand
::new(&api2
::config
::remote
::API_METHOD_UPDATE_REMOTE
)
129 .arg_param(&["name"])
130 .completion_cb("name", config
::remote
::complete_remote_name
)
134 CliCommand
::new(&api2
::config
::remote
::API_METHOD_DELETE_REMOTE
)
135 .arg_param(&["name"])
136 .completion_cb("name", config
::remote
::complete_remote_name
)
142 fn datastore_commands() -> CommandLineInterface
{
144 let cmd_def
= CliCommandMap
::new()
145 .insert("list", CliCommand
::new(&api2
::config
::datastore
::API_METHOD_LIST_DATASTORES
))
147 CliCommand
::new(&api2
::config
::datastore
::API_METHOD_CREATE_DATASTORE
)
148 .arg_param(&["name", "path"])
151 CliCommand
::new(&api2
::config
::datastore
::API_METHOD_UPDATE_DATASTORE
)
152 .arg_param(&["name"])
153 .completion_cb("name", config
::datastore
::complete_datastore_name
)
156 CliCommand
::new(&api2
::config
::datastore
::API_METHOD_DELETE_DATASTORE
)
157 .arg_param(&["name"])
158 .completion_cb("name", config
::datastore
::complete_datastore_name
)
169 schema
: DATASTORE_SCHEMA
,
172 schema
: OUTPUT_FORMAT
,
178 /// Start garbage collection for a specific datastore.
179 async
fn start_garbage_collection(param
: Value
) -> Result
<Value
, Error
> {
181 let output_format
= param
["output-format"].as_str().unwrap_or("text").to_owned();
183 let store
= tools
::required_string_param(¶m
, "store")?
;
185 let mut client
= connect()?
;
187 let path
= format
!("api2/json/admin/datastore/{}/gc", store
);
189 let result
= client
.post(&path
, None
).await?
;
191 view_task_result(client
, result
, &output_format
).await?
;
200 schema
: DATASTORE_SCHEMA
,
203 schema
: OUTPUT_FORMAT
,
209 /// Show garbage collection status for a specific datastore.
210 async
fn garbage_collection_status(param
: Value
) -> Result
<Value
, Error
> {
212 let output_format
= param
["output-format"].as_str().unwrap_or("text").to_owned();
214 let store
= tools
::required_string_param(¶m
, "store")?
;
216 let client
= connect()?
;
218 let path
= format
!("api2/json/admin/datastore/{}/gc", store
);
220 let mut result
= client
.get(&path
, None
).await?
;
221 let mut data
= result
["data"].take();
222 let schema
= api2
::admin
::datastore
::API_RETURN_SCHEMA_GARBAGE_COLLECTION_STATUS
;
224 let options
= TableFormatOptions
::new()
228 format_and_print_result_full(&mut data
, schema
, &output_format
, &options
);
233 fn garbage_collection_commands() -> CommandLineInterface
{
235 let cmd_def
= CliCommandMap
::new()
237 CliCommand
::new(&API_METHOD_GARBAGE_COLLECTION_STATUS
)
238 .arg_param(&["store"])
239 .completion_cb("store", config
::datastore
::complete_datastore_name
)
242 CliCommand
::new(&API_METHOD_START_GARBAGE_COLLECTION
)
243 .arg_param(&["store"])
244 .completion_cb("store", config
::datastore
::complete_datastore_name
)
254 description
: "The maximal number of tasks to list.",
262 schema
: OUTPUT_FORMAT
,
267 description
: "Also list stopped tasks.",
273 /// List running server tasks.
274 async
fn task_list(param
: Value
) -> Result
<Value
, Error
> {
276 let output_format
= param
["output-format"].as_str().unwrap_or("text").to_owned();
278 let client
= connect()?
;
280 let limit
= param
["limit"].as_u64().unwrap_or(50) as usize;
281 let running
= !param
["all"].as_bool().unwrap_or(false);
287 let mut result
= client
.get("api2/json/nodes/localhost/tasks", Some(args
)).await?
;
289 let mut data
= result
["data"].take();
290 let schema
= api2
::node
::tasks
::API_RETURN_SCHEMA_LIST_TASKS
;
292 let mut column_config
= Vec
::new();
293 column_config
.push(ColumnConfig
::new("starttime").right_align(false).renderer(render_epoch
));
294 column_config
.push(ColumnConfig
::new("endtime").right_align(false).renderer(render_epoch
));
295 column_config
.push(ColumnConfig
::new("upid"));
296 column_config
.push(ColumnConfig
::new("status").renderer(render_status
));
298 let options
= TableFormatOptions
::new()
301 .column_config(column_config
);
303 format_and_print_result_full(&mut data
, schema
, &output_format
, &options
);
317 /// Display the task log.
318 async
fn task_log(param
: Value
) -> Result
<Value
, Error
> {
320 let upid
= tools
::required_string_param(¶m
, "upid")?
;
322 let client
= connect()?
;
324 display_task_log(client
, upid
, true).await?
;
338 /// Try to stop a specific task.
339 async
fn task_stop(param
: Value
) -> Result
<Value
, Error
> {
341 let upid_str
= tools
::required_string_param(¶m
, "upid")?
;
343 let mut client
= connect()?
;
345 let path
= format
!("api2/json/nodes/localhost/tasks/{}", upid_str
);
346 let _
= client
.delete(&path
, None
).await?
;
351 fn task_mgmt_cli() -> CommandLineInterface
{
353 let task_log_cmd_def
= CliCommand
::new(&API_METHOD_TASK_LOG
)
354 .arg_param(&["upid"]);
356 let task_stop_cmd_def
= CliCommand
::new(&API_METHOD_TASK_STOP
)
357 .arg_param(&["upid"]);
359 let cmd_def
= CliCommandMap
::new()
360 .insert("list", CliCommand
::new(&API_METHOD_TASK_LIST
))
361 .insert("log", task_log_cmd_def
)
362 .insert("stop", task_stop_cmd_def
);
367 fn x509name_to_string(name
: &openssl
::x509
::X509NameRef
) -> Result
<String
, Error
> {
368 let mut parts
= Vec
::new();
369 for entry
in name
.entries() {
370 parts
.push(format
!("{} = {}", entry
.object().nid().short_name()?
, entry
.data().as_utf8()?
));
376 /// Diplay node certificate information.
377 fn cert_info() -> Result
<(), Error
> {
379 let cert_path
= PathBuf
::from(configdir
!("/proxy.pem"));
381 let cert_pem
= proxmox
::tools
::fs
::file_get_contents(&cert_path
)?
;
383 let cert
= openssl
::x509
::X509
::from_pem(&cert_pem
)?
;
385 println
!("Subject: {}", x509name_to_string(cert
.subject_name())?
);
387 if let Some(san
) = cert
.subject_alt_names() {
388 for name
in san
.iter() {
389 if let Some(v
) = name
.dnsname() {
390 println
!(" DNS:{}", v
);
391 } else if let Some(v
) = name
.ipaddress() {
392 println
!(" IP:{:?}", v
);
393 } else if let Some(v
) = name
.email() {
394 println
!(" EMAIL:{}", v
);
395 } else if let Some(v
) = name
.uri() {
396 println
!(" URI:{}", v
);
401 println
!("Issuer: {}", x509name_to_string(cert
.issuer_name())?
);
402 println
!("Validity:");
403 println
!(" Not Before: {}", cert
.not_before());
404 println
!(" Not After : {}", cert
.not_after());
406 let fp
= cert
.digest(openssl
::hash
::MessageDigest
::sha256())?
;
407 let fp_string
= proxmox
::tools
::digest_to_hex(&fp
);
408 let fp_string
= fp_string
.as_bytes().chunks(2).map(|v
| std
::str::from_utf8(v
).unwrap())
409 .collect
::<Vec
<&str>>().join(":");
411 println
!("Fingerprint (sha256): {}", fp_string
);
413 let pubkey
= cert
.public_key()?
;
414 println
!("Public key type: {}", openssl
::nid
::Nid
::from_raw(pubkey
.id().as_raw()).long_name()?
);
415 println
!("Public key bits: {}", pubkey
.bits());
424 description
: "Force generation of new SSL certifate.",
431 /// Update node certificates and generate all needed files/directories.
432 fn update_certs(force
: Option
<bool
>) -> Result
<(), Error
> {
434 config
::create_configdir()?
;
436 if let Err(err
) = generate_auth_key() {
437 bail
!("unable to generate auth key - {}", err
);
440 if let Err(err
) = generate_csrf_key() {
441 bail
!("unable to generate csrf key - {}", err
);
444 config
::update_self_signed_cert(force
.unwrap_or(false))?
;
449 fn cert_mgmt_cli() -> CommandLineInterface
{
451 let cmd_def
= CliCommandMap
::new()
452 .insert("info", CliCommand
::new(&API_METHOD_CERT_INFO
))
453 .insert("update", CliCommand
::new(&API_METHOD_UPDATE_CERTS
));
458 // fixme: avoid API redefinition
463 schema
: DATASTORE_SCHEMA
,
466 schema
: REMOTE_ID_SCHEMA
,
469 schema
: DATASTORE_SCHEMA
,
472 description
: "Delete vanished backups. This remove the local copy if the remote backup was deleted.",
478 schema
: OUTPUT_FORMAT
,
484 /// Sync datastore from another repository
485 async
fn pull_datastore(
487 remote_store
: String
,
489 delete
: Option
<bool
>,
490 output_format
: Option
<String
>,
491 ) -> Result
<Value
, Error
> {
493 let output_format
= output_format
.unwrap_or("text".to_string());
495 let mut client
= connect()?
;
497 let mut args
= json
!({
498 "store": local_store
,
500 "remote-store": remote_store
,
503 if let Some(delete
) = delete
{
504 args
["delete"] = delete
.into();
507 let result
= client
.post("api2/json/pull", Some(args
)).await?
;
509 view_task_result(client
, result
, &output_format
).await?
;
516 let cmd_def
= CliCommandMap
::new()
517 .insert("datastore", datastore_commands())
518 .insert("remote", remote_commands())
519 .insert("garbage-collection", garbage_collection_commands())
520 .insert("cert", cert_mgmt_cli())
521 .insert("task", task_mgmt_cli())
524 CliCommand
::new(&API_METHOD_PULL_DATASTORE
)
525 .arg_param(&["remote", "remote-store", "local-store"])
526 .completion_cb("local-store", config
::datastore
::complete_datastore_name
)
527 .completion_cb("remote", config
::remote
::complete_remote_name
)
528 .completion_cb("remote-store", complete_remote_datastore_name
)
531 proxmox_backup
::tools
::runtime
::main(run_async_cli_command(cmd_def
));
534 // shell completion helper
535 pub fn complete_remote_datastore_name(_arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
537 let mut list
= Vec
::new();
539 let _
= proxmox
::try_block
!({
540 let remote
= param
.get("remote").ok_or_else(|| format_err
!("no remote"))?
;
541 let (remote_config
, _digest
) = remote
::config()?
;
543 let remote
: Remote
= remote_config
.lookup("remote", &remote
)?
;
545 let options
= HttpClientOptions
::new()
546 .password(Some(remote
.password
.clone()))
547 .fingerprint(remote
.fingerprint
.clone());
549 let client
= HttpClient
::new(
555 let mut rt
= tokio
::runtime
::Runtime
::new().unwrap();
556 let result
= rt
.block_on(client
.get("api2/json/admin/datastore", None
))?
;
558 if let Some(data
) = result
["data"].as_array() {
560 if let Some(store
) = item
["store"].as_str() {
561 list
.push(store
.to_owned());
567 }).map_err(|_err
: Error
| { /* ignore */ }
);