]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/proxmox-backup-manager.rs
src/config/network.rs: make it compatible with pve
[proxmox-backup.git] / src / bin / proxmox-backup-manager.rs
1 use std::path::PathBuf;
2 use std::collections::HashMap;
3
4 use anyhow::{bail, format_err, Error};
5 use serde_json::{json, Value};
6
7 use proxmox::api::{api, cli::*, RpcEnvironment, ApiHandler};
8
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::*;
16
17 async fn view_task_result(
18 client: HttpClient,
19 result: Value,
20 output_format: &str,
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?;
26 }
27 } else {
28 format_and_print_result(&data, &output_format);
29 }
30
31 Ok(())
32 }
33
34 fn connect() -> Result<HttpClient, Error> {
35
36 let uid = nix::unistd::Uid::current();
37
38 let mut options = HttpClientOptions::new()
39 .prefix(Some("proxmox-backup".to_string()))
40 .verify_cert(false); // not required for connection to localhost
41
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)?
46 } else {
47 options = options.ticket_cache(true).interactive(true);
48 HttpClient::new("localhost", "root@pam", options)?
49 };
50
51 Ok(client)
52 }
53
54 #[api(
55 input: {
56 properties: {
57 "output-format": {
58 schema: OUTPUT_FORMAT,
59 optional: true,
60 },
61 }
62 }
63 )]
64 /// List configured remotes.
65 fn list_remotes(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
66
67 let output_format = get_output_format(&param);
68
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)?,
72 _ => unreachable!(),
73 };
74
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"));
81
82 format_and_print_result_full(&mut data, info.returns, &output_format, &options);
83
84 Ok(Value::Null)
85 }
86
87 fn remote_commands() -> CommandLineInterface {
88
89 let cmd_def = CliCommandMap::new()
90 .insert("list", CliCommand::new(&&API_METHOD_LIST_REMOTES))
91 .insert(
92 "create",
93 // fixme: howto handle password parameter?
94 CliCommand::new(&api2::config::remote::API_METHOD_CREATE_REMOTE)
95 .arg_param(&["name"])
96 )
97 .insert(
98 "update",
99 CliCommand::new(&api2::config::remote::API_METHOD_UPDATE_REMOTE)
100 .arg_param(&["name"])
101 .completion_cb("name", config::remote::complete_remote_name)
102 )
103 .insert(
104 "remove",
105 CliCommand::new(&api2::config::remote::API_METHOD_DELETE_REMOTE)
106 .arg_param(&["name"])
107 .completion_cb("name", config::remote::complete_remote_name)
108 );
109
110 cmd_def.into()
111 }
112
113 #[api(
114 input: {
115 properties: {
116 "output-format": {
117 schema: OUTPUT_FORMAT,
118 optional: true,
119 },
120 }
121 }
122 )]
123 /// List configured users.
124 fn list_users(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
125
126 let output_format = get_output_format(&param);
127
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)?,
131 _ => unreachable!(),
132 };
133
134 let options = default_table_format_options()
135 .column(ColumnConfig::new("userid"))
136 .column(
137 ColumnConfig::new("enable")
138 .renderer(tools::format::render_bool_with_default_true)
139 )
140 .column(
141 ColumnConfig::new("expire")
142 .renderer(tools::format::render_epoch)
143 )
144 .column(ColumnConfig::new("firstname"))
145 .column(ColumnConfig::new("lastname"))
146 .column(ColumnConfig::new("email"))
147 .column(ColumnConfig::new("comment"));
148
149 format_and_print_result_full(&mut data, info.returns, &output_format, &options);
150
151 Ok(Value::Null)
152 }
153
154 fn user_commands() -> CommandLineInterface {
155
156 let cmd_def = CliCommandMap::new()
157 .insert("list", CliCommand::new(&&API_METHOD_LIST_USERS))
158 .insert(
159 "create",
160 // fixme: howto handle password parameter?
161 CliCommand::new(&api2::access::user::API_METHOD_CREATE_USER)
162 .arg_param(&["userid"])
163 )
164 .insert(
165 "update",
166 CliCommand::new(&api2::access::user::API_METHOD_UPDATE_USER)
167 .arg_param(&["userid"])
168 .completion_cb("userid", config::user::complete_user_name)
169 )
170 .insert(
171 "remove",
172 CliCommand::new(&api2::access::user::API_METHOD_DELETE_USER)
173 .arg_param(&["userid"])
174 .completion_cb("userid", config::user::complete_user_name)
175 );
176
177 cmd_def.into()
178 }
179
180 #[api(
181 input: {
182 properties: {
183 "output-format": {
184 schema: OUTPUT_FORMAT,
185 optional: true,
186 },
187 }
188 }
189 )]
190 /// Access Control list.
191 fn list_acls(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
192
193 let output_format = get_output_format(&param);
194
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)?,
198 _ => unreachable!(),
199 };
200
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();
205
206 if ugid_type == "user" {
207 Ok(ugid.to_string())
208 } else if ugid_type == "group" {
209 Ok(format!("@{}", ugid))
210 } else {
211 bail!("render_ugid: got unknown ugid_type");
212 }
213 }
214
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"));
220
221 format_and_print_result_full(&mut data, info.returns, &output_format, &options);
222
223 Ok(Value::Null)
224 }
225
226 fn acl_commands() -> CommandLineInterface {
227
228 let cmd_def = CliCommandMap::new()
229 .insert("list", CliCommand::new(&&API_METHOD_LIST_ACLS))
230 .insert(
231 "update",
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)
236
237 );
238
239 cmd_def.into()
240 }
241
242 #[api(
243 input: {
244 properties: {
245 "output-format": {
246 schema: OUTPUT_FORMAT,
247 optional: true,
248 },
249 }
250 }
251 )]
252 /// Network device list.
253 fn list_network_devices(mut param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
254
255 let output_format = get_output_format(&param);
256
257 param["node"] = "localhost".into();
258
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)?,
262 _ => unreachable!(),
263 };
264
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);
269 }
270 }
271 }
272
273 fn render_address(_value: &Value, record: &Value) -> Result<String, Error> {
274 let mut text = String::new();
275
276 if let Some(cidr) = record["cidr"].as_str() {
277 text.push_str(cidr);
278 }
279 if let Some(cidr) = record["cidr6"].as_str() {
280 if !text.is_empty() { text.push('\n'); }
281 text.push_str(cidr);
282 }
283
284 Ok(text)
285 }
286
287 fn render_gateway(_value: &Value, record: &Value) -> Result<String, Error> {
288 let mut text = String::new();
289
290 if let Some(gateway) = record["gateway"].as_str() {
291 text.push_str(gateway);
292 }
293 if let Some(gateway) = record["gateway6"].as_str() {
294 if !text.is_empty() { text.push('\n'); }
295 text.push_str(gateway);
296 }
297
298 Ok(text)
299 }
300
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));
309
310 format_and_print_result_full(&mut data, info.returns, &output_format, &options);
311
312 Ok(Value::Null)
313 }
314
315 #[api()]
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();
319
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)?,
323 _ => unreachable!(),
324 };
325
326 if let Some(changes) = rpcenv.get_result_attrib("changes") {
327 if let Some(diff) = changes.as_str() {
328 println!("{}", diff);
329 }
330 }
331
332 Ok(Value::Null)
333 }
334
335 fn network_commands() -> CommandLineInterface {
336
337 let cmd_def = CliCommandMap::new()
338 .insert(
339 "list",
340 CliCommand::new(&API_METHOD_LIST_NETWORK_DEVICES)
341 )
342 .insert(
343 "changes",
344 CliCommand::new(&API_METHOD_PENDING_NETWORK_CHANGES)
345 )
346 .insert(
347 "update",
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)
352 )
353 .insert(
354 "remove",
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)
359 )
360 .insert(
361 "revert",
362 CliCommand::new(&api2::node::network::API_METHOD_REVERT_NETWORK_CONFIG)
363 .fixed_param("node", String::from("localhost"))
364 )
365 .insert(
366 "reload",
367 CliCommand::new(&api2::node::network::API_METHOD_RELOAD_NETWORK_CONFIG)
368 .fixed_param("node", String::from("localhost"))
369 );
370
371 cmd_def.into()
372 }
373
374 #[api(
375 input: {
376 properties: {
377 "output-format": {
378 schema: OUTPUT_FORMAT,
379 optional: true,
380 },
381 }
382 }
383 )]
384 /// Read DNS settings
385 fn get_dns(mut param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
386
387 let output_format = get_output_format(&param);
388
389 param["node"] = "localhost".into();
390
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)?,
394 _ => unreachable!(),
395 };
396
397
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"));
403
404 format_and_print_result_full(&mut data, info.returns, &output_format, &options);
405
406 Ok(Value::Null)
407 }
408
409 fn dns_commands() -> CommandLineInterface {
410
411 let cmd_def = CliCommandMap::new()
412 .insert(
413 "get",
414 CliCommand::new(&API_METHOD_GET_DNS)
415 )
416 .insert(
417 "set",
418 CliCommand::new(&api2::node::dns::API_METHOD_UPDATE_DNS)
419 .fixed_param("node", String::from("localhost"))
420 );
421
422 cmd_def.into()
423 }
424
425 #[api(
426 input: {
427 properties: {
428 "output-format": {
429 schema: OUTPUT_FORMAT,
430 optional: true,
431 },
432 }
433 }
434 )]
435 /// Datastore list.
436 fn list_datastores(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
437
438 let output_format = get_output_format(&param);
439
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)?,
443 _ => unreachable!(),
444 };
445
446 let options = default_table_format_options()
447 .column(ColumnConfig::new("name"))
448 .column(ColumnConfig::new("path"))
449 .column(ColumnConfig::new("comment"));
450
451 format_and_print_result_full(&mut data, info.returns, &output_format, &options);
452
453 Ok(Value::Null)
454 }
455
456 fn datastore_commands() -> CommandLineInterface {
457
458 let cmd_def = CliCommandMap::new()
459 .insert("list", CliCommand::new(&API_METHOD_LIST_DATASTORES))
460 .insert("create",
461 CliCommand::new(&api2::config::datastore::API_METHOD_CREATE_DATASTORE)
462 .arg_param(&["name", "path"])
463 )
464 .insert("update",
465 CliCommand::new(&api2::config::datastore::API_METHOD_UPDATE_DATASTORE)
466 .arg_param(&["name"])
467 .completion_cb("name", config::datastore::complete_datastore_name)
468 )
469 .insert("remove",
470 CliCommand::new(&api2::config::datastore::API_METHOD_DELETE_DATASTORE)
471 .arg_param(&["name"])
472 .completion_cb("name", config::datastore::complete_datastore_name)
473 );
474
475 cmd_def.into()
476 }
477
478
479 #[api(
480 input: {
481 properties: {
482 store: {
483 schema: DATASTORE_SCHEMA,
484 },
485 "output-format": {
486 schema: OUTPUT_FORMAT,
487 optional: true,
488 },
489 }
490 }
491 )]
492 /// Start garbage collection for a specific datastore.
493 async fn start_garbage_collection(param: Value) -> Result<Value, Error> {
494
495 let output_format = get_output_format(&param);
496
497 let store = tools::required_string_param(&param, "store")?;
498
499 let mut client = connect()?;
500
501 let path = format!("api2/json/admin/datastore/{}/gc", store);
502
503 let result = client.post(&path, None).await?;
504
505 view_task_result(client, result, &output_format).await?;
506
507 Ok(Value::Null)
508 }
509
510 #[api(
511 input: {
512 properties: {
513 store: {
514 schema: DATASTORE_SCHEMA,
515 },
516 "output-format": {
517 schema: OUTPUT_FORMAT,
518 optional: true,
519 },
520 }
521 }
522 )]
523 /// Show garbage collection status for a specific datastore.
524 async fn garbage_collection_status(param: Value) -> Result<Value, Error> {
525
526 let output_format = get_output_format(&param);
527
528 let store = tools::required_string_param(&param, "store")?;
529
530 let client = connect()?;
531
532 let path = format!("api2/json/admin/datastore/{}/gc", store);
533
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;
537
538 let options = default_table_format_options();
539
540 format_and_print_result_full(&mut data, schema, &output_format, &options);
541
542 Ok(Value::Null)
543 }
544
545 fn garbage_collection_commands() -> CommandLineInterface {
546
547 let cmd_def = CliCommandMap::new()
548 .insert("status",
549 CliCommand::new(&API_METHOD_GARBAGE_COLLECTION_STATUS)
550 .arg_param(&["store"])
551 .completion_cb("store", config::datastore::complete_datastore_name)
552 )
553 .insert("start",
554 CliCommand::new(&API_METHOD_START_GARBAGE_COLLECTION)
555 .arg_param(&["store"])
556 .completion_cb("store", config::datastore::complete_datastore_name)
557 );
558
559 cmd_def.into()
560 }
561
562 #[api(
563 input: {
564 properties: {
565 limit: {
566 description: "The maximal number of tasks to list.",
567 type: Integer,
568 optional: true,
569 minimum: 1,
570 maximum: 1000,
571 default: 50,
572 },
573 "output-format": {
574 schema: OUTPUT_FORMAT,
575 optional: true,
576 },
577 all: {
578 type: Boolean,
579 description: "Also list stopped tasks.",
580 optional: true,
581 }
582 }
583 }
584 )]
585 /// List running server tasks.
586 async fn task_list(param: Value) -> Result<Value, Error> {
587
588 let output_format = get_output_format(&param);
589
590 let client = connect()?;
591
592 let limit = param["limit"].as_u64().unwrap_or(50) as usize;
593 let running = !param["all"].as_bool().unwrap_or(false);
594 let args = json!({
595 "running": running,
596 "start": 0,
597 "limit": limit,
598 });
599 let mut result = client.get("api2/json/nodes/localhost/tasks", Some(args)).await?;
600
601 let mut data = result["data"].take();
602 let schema = api2::node::tasks::API_RETURN_SCHEMA_LIST_TASKS;
603
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));
609
610 format_and_print_result_full(&mut data, schema, &output_format, &options);
611
612 Ok(Value::Null)
613 }
614
615 #[api(
616 input: {
617 properties: {
618 upid: {
619 schema: UPID_SCHEMA,
620 },
621 }
622 }
623 )]
624 /// Display the task log.
625 async fn task_log(param: Value) -> Result<Value, Error> {
626
627 let upid = tools::required_string_param(&param, "upid")?;
628
629 let client = connect()?;
630
631 display_task_log(client, upid, true).await?;
632
633 Ok(Value::Null)
634 }
635
636 #[api(
637 input: {
638 properties: {
639 upid: {
640 schema: UPID_SCHEMA,
641 },
642 }
643 }
644 )]
645 /// Try to stop a specific task.
646 async fn task_stop(param: Value) -> Result<Value, Error> {
647
648 let upid_str = tools::required_string_param(&param, "upid")?;
649
650 let mut client = connect()?;
651
652 let path = format!("api2/json/nodes/localhost/tasks/{}", upid_str);
653 let _ = client.delete(&path, None).await?;
654
655 Ok(Value::Null)
656 }
657
658 fn task_mgmt_cli() -> CommandLineInterface {
659
660 let task_log_cmd_def = CliCommand::new(&API_METHOD_TASK_LOG)
661 .arg_param(&["upid"]);
662
663 let task_stop_cmd_def = CliCommand::new(&API_METHOD_TASK_STOP)
664 .arg_param(&["upid"]);
665
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);
670
671 cmd_def.into()
672 }
673
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()?));
678 }
679 Ok(parts.join(", "))
680 }
681
682 #[api]
683 /// Diplay node certificate information.
684 fn cert_info() -> Result<(), Error> {
685
686 let cert_path = PathBuf::from(configdir!("/proxy.pem"));
687
688 let cert_pem = proxmox::tools::fs::file_get_contents(&cert_path)?;
689
690 let cert = openssl::x509::X509::from_pem(&cert_pem)?;
691
692 println!("Subject: {}", x509name_to_string(cert.subject_name())?);
693
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);
704 }
705 }
706 }
707
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());
712
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(":");
717
718 println!("Fingerprint (sha256): {}", fp_string);
719
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());
723
724 Ok(())
725 }
726
727 #[api(
728 input: {
729 properties: {
730 force: {
731 description: "Force generation of new SSL certifate.",
732 type: Boolean,
733 optional:true,
734 },
735 }
736 },
737 )]
738 /// Update node certificates and generate all needed files/directories.
739 fn update_certs(force: Option<bool>) -> Result<(), Error> {
740
741 config::create_configdir()?;
742
743 if let Err(err) = generate_auth_key() {
744 bail!("unable to generate auth key - {}", err);
745 }
746
747 if let Err(err) = generate_csrf_key() {
748 bail!("unable to generate csrf key - {}", err);
749 }
750
751 config::update_self_signed_cert(force.unwrap_or(false))?;
752
753 Ok(())
754 }
755
756 fn cert_mgmt_cli() -> CommandLineInterface {
757
758 let cmd_def = CliCommandMap::new()
759 .insert("info", CliCommand::new(&API_METHOD_CERT_INFO))
760 .insert("update", CliCommand::new(&API_METHOD_UPDATE_CERTS));
761
762 cmd_def.into()
763 }
764
765 // fixme: avoid API redefinition
766 #[api(
767 input: {
768 properties: {
769 "local-store": {
770 schema: DATASTORE_SCHEMA,
771 },
772 remote: {
773 schema: REMOTE_ID_SCHEMA,
774 },
775 "remote-store": {
776 schema: DATASTORE_SCHEMA,
777 },
778 delete: {
779 description: "Delete vanished backups. This remove the local copy if the remote backup was deleted.",
780 type: Boolean,
781 optional: true,
782 default: true,
783 },
784 "output-format": {
785 schema: OUTPUT_FORMAT,
786 optional: true,
787 },
788 }
789 }
790 )]
791 /// Sync datastore from another repository
792 async fn pull_datastore(
793 remote: String,
794 remote_store: String,
795 local_store: String,
796 delete: Option<bool>,
797 param: Value,
798 ) -> Result<Value, Error> {
799
800 let output_format = get_output_format(&param);
801
802 let mut client = connect()?;
803
804 let mut args = json!({
805 "store": local_store,
806 "remote": remote,
807 "remote-store": remote_store,
808 });
809
810 if let Some(delete) = delete {
811 args["delete"] = delete.into();
812 }
813
814 let result = client.post("api2/json/pull", Some(args)).await?;
815
816 view_task_result(client, result, &output_format).await?;
817
818 Ok(Value::Null)
819 }
820
821 fn main() {
822
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())
833 .insert(
834 "pull",
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)
840 );
841
842 let mut rpcenv = CliEnvironment::new();
843 rpcenv.set_user(Some(String::from("root@pam")));
844
845 proxmox_backup::tools::runtime::main(run_async_cli_command(cmd_def, rpcenv));
846 }
847
848 // shell completion helper
849 pub fn complete_remote_datastore_name(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
850
851 let mut list = Vec::new();
852
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()?;
856
857 let remote: Remote = remote_config.lookup("remote", &remote)?;
858
859 let options = HttpClientOptions::new()
860 .password(Some(remote.password.clone()))
861 .fingerprint(remote.fingerprint.clone());
862
863 let client = HttpClient::new(
864 &remote.host,
865 &remote.userid,
866 options,
867 )?;
868
869 let result = crate::tools::runtime::block_on(client.get("api2/json/admin/datastore", None))?;
870
871 if let Some(data) = result["data"].as_array() {
872 for item in data {
873 if let Some(store) = item["store"].as_str() {
874 list.push(store.to_owned());
875 }
876 }
877 }
878
879 Ok(())
880 }).map_err(|_err: Error| { /* ignore */ });
881
882 list
883 }