]> git.proxmox.com Git - proxmox-backup.git/blame - src/bin/proxmox-backup-manager.rs
src/config/network.rs - check_bridge_ports: correctly check vlan ports
[proxmox-backup.git] / src / bin / proxmox-backup-manager.rs
CommitLineData
550e0d88 1use std::path::PathBuf;
331b869d 2use std::collections::HashMap;
ea0b8b6e 3
f7d4e4b5 4use anyhow::{bail, format_err, Error};
9894469e 5use serde_json::{json, Value};
9894469e 6
9e165b5c 7use proxmox::api::{api, cli::*, RpcEnvironment, ApiHandler};
769f8c99 8
550e0d88 9use proxmox_backup::configdir;
769f8c99 10use proxmox_backup::tools;
f357390c 11use proxmox_backup::config::{self, remote::{self, Remote}};
9894469e 12use proxmox_backup::api2::{self, types::* };
769f8c99
DM
13use proxmox_backup::client::*;
14use proxmox_backup::tools::ticket::*;
15use proxmox_backup::auth_helpers::*;
16
769f8c99
DM
17async 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}
211fabd7 33
47d47121
DM
34fn connect() -> Result<HttpClient, Error> {
35
36 let uid = nix::unistd::Uid::current();
37
d59dbeca 38 let mut options = HttpClientOptions::new()
5030b7ce 39 .prefix(Some("proxmox-backup".to_string()))
d59dbeca
DM
40 .verify_cert(false); // not required for connection to localhost
41
47d47121
DM
42 let client = if uid.is_root() {
43 let ticket = assemble_rsa_ticket(private_auth_key(), "PBS", Some("root@pam"), None)?;
d59dbeca
DM
44 options = options.password(Some(ticket));
45 HttpClient::new("localhost", "root@pam", options)?
47d47121 46 } else {
d59dbeca
DM
47 options = options.ticket_cache(true).interactive(true);
48 HttpClient::new("localhost", "root@pam", options)?
47d47121
DM
49 };
50
51 Ok(client)
52}
53
9894469e
DM
54#[api(
55 input: {
56 properties: {
57 "output-format": {
58 schema: OUTPUT_FORMAT,
59 optional: true,
60 },
61 }
62 }
63)]
64/// List configured remotes.
9e165b5c 65fn list_remotes(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
688fbe07 66
ac3faaf5 67 let output_format = get_output_format(&param);
9894469e 68
9e165b5c
DM
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 };
9894469e 74
ac3faaf5 75 let options = default_table_format_options()
93fbb4ef
DM
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"));
9894469e 81
9e165b5c 82 format_and_print_result_full(&mut data, info.returns, &output_format, &options);
9894469e
DM
83
84 Ok(Value::Null)
85}
86
87fn remote_commands() -> CommandLineInterface {
688fbe07
DM
88
89 let cmd_def = CliCommandMap::new()
9894469e 90 .insert("list", CliCommand::new(&&API_METHOD_LIST_REMOTES))
688fbe07
DM
91 .insert(
92 "create",
93 // fixme: howto handle password parameter?
f357390c 94 CliCommand::new(&api2::config::remote::API_METHOD_CREATE_REMOTE)
688fbe07
DM
95 .arg_param(&["name"])
96 )
08195ac8
DM
97 .insert(
98 "update",
f357390c 99 CliCommand::new(&api2::config::remote::API_METHOD_UPDATE_REMOTE)
08195ac8 100 .arg_param(&["name"])
f357390c 101 .completion_cb("name", config::remote::complete_remote_name)
08195ac8 102 )
688fbe07
DM
103 .insert(
104 "remove",
f357390c 105 CliCommand::new(&api2::config::remote::API_METHOD_DELETE_REMOTE)
688fbe07 106 .arg_param(&["name"])
f357390c 107 .completion_cb("name", config::remote::complete_remote_name)
688fbe07
DM
108 );
109
110 cmd_def.into()
111}
112
579728c6
DM
113#[api(
114 input: {
115 properties: {
116 "output-format": {
117 schema: OUTPUT_FORMAT,
118 optional: true,
119 },
120 }
121 }
122)]
123/// List configured users.
124fn list_users(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
125
126 let output_format = get_output_format(&param);
127
685e1334 128 let info = &api2::access::user::API_METHOD_LIST_USERS;
579728c6
DM
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"))
b080583b
DM
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 )
579728c6
DM
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
154fn 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?
685e1334 161 CliCommand::new(&api2::access::user::API_METHOD_CREATE_USER)
579728c6
DM
162 .arg_param(&["userid"])
163 )
164 .insert(
165 "update",
685e1334 166 CliCommand::new(&api2::access::user::API_METHOD_UPDATE_USER)
579728c6
DM
167 .arg_param(&["userid"])
168 .completion_cb("userid", config::user::complete_user_name)
169 )
170 .insert(
171 "remove",
685e1334 172 CliCommand::new(&api2::access::user::API_METHOD_DELETE_USER)
579728c6
DM
173 .arg_param(&["userid"])
174 .completion_cb("userid", config::user::complete_user_name)
175 );
176
177 cmd_def.into()
178}
179
ed3e60ae
DM
180#[api(
181 input: {
182 properties: {
183 "output-format": {
184 schema: OUTPUT_FORMAT,
185 optional: true,
186 },
187 }
188 }
189)]
190/// Access Control list.
191fn 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
226fn acl_commands() -> CommandLineInterface {
227
228 let cmd_def = CliCommandMap::new()
9765092e
DM
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 );
ed3e60ae
DM
238
239 cmd_def.into()
240}
241
bf004ecd
DM
242#[api(
243 input: {
244 properties: {
245 "output-format": {
246 schema: OUTPUT_FORMAT,
247 optional: true,
248 },
249 }
250 }
251)]
252/// Network device list.
76227a6a 253fn list_network_devices(mut param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
bf004ecd
DM
254
255 let output_format = get_output_format(&param);
256
76227a6a
DM
257 param["node"] = "localhost".into();
258
26d9aebc 259 let info = &api2::node::network::API_METHOD_LIST_NETWORK_DEVICES;
bf004ecd
DM
260 let mut data = match info.handler {
261 ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
262 _ => unreachable!(),
263 };
264
2eefd9ae
DM
265 if let Some(changes) = rpcenv.get_result_attrib("changes") {
266 if let Some(diff) = changes.as_str() {
3181f9b6
DM
267 if output_format == "text" {
268 eprintln!("pending changes:\n{}\n", diff);
269 }
2eefd9ae
DM
270 }
271 }
272
4cb6bd89
DM
273 fn render_address(_value: &Value, record: &Value) -> Result<String, Error> {
274 let mut text = String::new();
275
7b22acd0 276 if let Some(cidr) = record["cidr"].as_str() {
4cb6bd89
DM
277 text.push_str(cidr);
278 }
7b22acd0 279 if let Some(cidr) = record["cidr6"].as_str() {
4cb6bd89
DM
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
7b22acd0 290 if let Some(gateway) = record["gateway"].as_str() {
4cb6bd89
DM
291 text.push_str(gateway);
292 }
7b22acd0 293 if let Some(gateway) = record["gateway6"].as_str() {
4cb6bd89
DM
294 if !text.is_empty() { text.push('\n'); }
295 text.push_str(gateway);
296 }
297
298 Ok(text)
299 }
300
bf004ecd 301 let options = default_table_format_options()
7b22acd0 302 .column(ColumnConfig::new("type").header("type"))
bf004ecd 303 .column(ColumnConfig::new("name"))
7b22acd0
DM
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));
bf004ecd
DM
309
310 format_and_print_result_full(&mut data, info.returns, &output_format, &options);
311
312 Ok(Value::Null)
313}
314
c67bc9c3
DM
315#[api()]
316/// Show pending configuration changes (diff)
76227a6a
DM
317fn pending_network_changes(mut param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
318 param["node"] = "localhost".into();
c67bc9c3 319
26d9aebc 320 let info = &api2::node::network::API_METHOD_LIST_NETWORK_DEVICES;
c67bc9c3
DM
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
ca0e5347
DM
335fn network_commands() -> CommandLineInterface {
336
337 let cmd_def = CliCommandMap::new()
26d9aebc
DM
338 .insert(
339 "list",
340 CliCommand::new(&API_METHOD_LIST_NETWORK_DEVICES)
26d9aebc
DM
341 )
342 .insert(
343 "changes",
344 CliCommand::new(&API_METHOD_PENDING_NETWORK_CHANGES)
26d9aebc 345 )
86a5d56c
DM
346 .insert(
347 "create",
348 CliCommand::new(&api2::node::network::API_METHOD_CREATE_INTERFACE)
349 .fixed_param("node", String::from("localhost"))
350 .arg_param(&["iface"])
351 .completion_cb("iface", config::network::complete_interface_name)
352 )
2eefd9ae
DM
353 .insert(
354 "update",
26d9aebc
DM
355 CliCommand::new(&api2::node::network::API_METHOD_UPDATE_INTERFACE)
356 .fixed_param("node", String::from("localhost"))
7b22acd0
DM
357 .arg_param(&["iface"])
358 .completion_cb("iface", config::network::complete_interface_name)
df6bb03d 359 )
2eefd9ae
DM
360 .insert(
361 "remove",
26d9aebc
DM
362 CliCommand::new(&api2::node::network::API_METHOD_DELETE_INTERFACE)
363 .fixed_param("node", String::from("localhost"))
7b22acd0
DM
364 .arg_param(&["iface"])
365 .completion_cb("iface", config::network::complete_interface_name)
2eefd9ae 366 )
26d9aebc
DM
367 .insert(
368 "revert",
369 CliCommand::new(&api2::node::network::API_METHOD_REVERT_NETWORK_CONFIG)
370 .fixed_param("node", String::from("localhost"))
371 )
2eefd9ae
DM
372 .insert(
373 "reload",
26d9aebc
DM
374 CliCommand::new(&api2::node::network::API_METHOD_RELOAD_NETWORK_CONFIG)
375 .fixed_param("node", String::from("localhost"))
df6bb03d 376 );
ca0e5347
DM
377
378 cmd_def.into()
379}
380
14627d67
DM
381#[api(
382 input: {
383 properties: {
384 "output-format": {
385 schema: OUTPUT_FORMAT,
386 optional: true,
387 },
388 }
389 }
390)]
391/// Read DNS settings
392fn get_dns(mut param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
393
394 let output_format = get_output_format(&param);
395
396 param["node"] = "localhost".into();
397
398 let info = &api2::node::dns::API_METHOD_GET_DNS;
399 let mut data = match info.handler {
400 ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
401 _ => unreachable!(),
402 };
403
404
405 let options = default_table_format_options()
406 .column(ColumnConfig::new("search"))
407 .column(ColumnConfig::new("dns1"))
408 .column(ColumnConfig::new("dns2"))
409 .column(ColumnConfig::new("dns3"));
410
411 format_and_print_result_full(&mut data, info.returns, &output_format, &options);
412
413 Ok(Value::Null)
414}
415
416fn dns_commands() -> CommandLineInterface {
417
418 let cmd_def = CliCommandMap::new()
419 .insert(
420 "get",
421 CliCommand::new(&API_METHOD_GET_DNS)
422 )
423 .insert(
424 "set",
425 CliCommand::new(&api2::node::dns::API_METHOD_UPDATE_DNS)
426 .fixed_param("node", String::from("localhost"))
427 );
428
429 cmd_def.into()
430}
431
b1564af2
DM
432#[api(
433 input: {
434 properties: {
435 "output-format": {
436 schema: OUTPUT_FORMAT,
437 optional: true,
438 },
439 }
440 }
441)]
442/// Datastore list.
443fn list_datastores(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
444
445 let output_format = get_output_format(&param);
446
447 let info = &api2::config::datastore::API_METHOD_LIST_DATASTORES;
448 let mut data = match info.handler {
449 ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
450 _ => unreachable!(),
451 };
452
453 let options = default_table_format_options()
454 .column(ColumnConfig::new("name"))
455 .column(ColumnConfig::new("path"))
456 .column(ColumnConfig::new("comment"));
457
458 format_and_print_result_full(&mut data, info.returns, &output_format, &options);
459
460 Ok(Value::Null)
461}
462
9f6ab1fc 463fn datastore_commands() -> CommandLineInterface {
ea0b8b6e 464
6460764d 465 let cmd_def = CliCommandMap::new()
b1564af2 466 .insert("list", CliCommand::new(&API_METHOD_LIST_DATASTORES))
6460764d 467 .insert("create",
688fbe07 468 CliCommand::new(&api2::config::datastore::API_METHOD_CREATE_DATASTORE)
49fddd98 469 .arg_param(&["name", "path"])
48ef3c33 470 )
ddc52662
DM
471 .insert("update",
472 CliCommand::new(&api2::config::datastore::API_METHOD_UPDATE_DATASTORE)
473 .arg_param(&["name"])
3be839c6 474 .completion_cb("name", config::datastore::complete_datastore_name)
ddc52662 475 )
6460764d 476 .insert("remove",
688fbe07 477 CliCommand::new(&api2::config::datastore::API_METHOD_DELETE_DATASTORE)
49fddd98 478 .arg_param(&["name"])
07b4694a 479 .completion_cb("name", config::datastore::complete_datastore_name)
48ef3c33 480 );
211fabd7 481
8f62336b 482 cmd_def.into()
211fabd7
DM
483}
484
691c89a0 485
769f8c99
DM
486#[api(
487 input: {
488 properties: {
489 store: {
490 schema: DATASTORE_SCHEMA,
491 },
492 "output-format": {
493 schema: OUTPUT_FORMAT,
494 optional: true,
495 },
496 }
497 }
498)]
499/// Start garbage collection for a specific datastore.
500async fn start_garbage_collection(param: Value) -> Result<Value, Error> {
691c89a0 501
ac3faaf5 502 let output_format = get_output_format(&param);
691c89a0 503
769f8c99
DM
504 let store = tools::required_string_param(&param, "store")?;
505
47d47121 506 let mut client = connect()?;
769f8c99
DM
507
508 let path = format!("api2/json/admin/datastore/{}/gc", store);
509
510 let result = client.post(&path, None).await?;
511
512 view_task_result(client, result, &output_format).await?;
513
514 Ok(Value::Null)
515}
516
517#[api(
518 input: {
519 properties: {
520 store: {
521 schema: DATASTORE_SCHEMA,
522 },
523 "output-format": {
524 schema: OUTPUT_FORMAT,
525 optional: true,
526 },
527 }
528 }
529)]
530/// Show garbage collection status for a specific datastore.
531async fn garbage_collection_status(param: Value) -> Result<Value, Error> {
532
ac3faaf5 533 let output_format = get_output_format(&param);
769f8c99
DM
534
535 let store = tools::required_string_param(&param, "store")?;
536
47d47121 537 let client = connect()?;
769f8c99
DM
538
539 let path = format!("api2/json/admin/datastore/{}/gc", store);
540
9894469e
DM
541 let mut result = client.get(&path, None).await?;
542 let mut data = result["data"].take();
543 let schema = api2::admin::datastore::API_RETURN_SCHEMA_GARBAGE_COLLECTION_STATUS;
544
ac3faaf5 545 let options = default_table_format_options();
9894469e
DM
546
547 format_and_print_result_full(&mut data, schema, &output_format, &options);
769f8c99
DM
548
549 Ok(Value::Null)
550}
551
552fn garbage_collection_commands() -> CommandLineInterface {
691c89a0
DM
553
554 let cmd_def = CliCommandMap::new()
555 .insert("status",
769f8c99 556 CliCommand::new(&API_METHOD_GARBAGE_COLLECTION_STATUS)
49fddd98 557 .arg_param(&["store"])
9ac1045c 558 .completion_cb("store", config::datastore::complete_datastore_name)
48ef3c33 559 )
691c89a0 560 .insert("start",
769f8c99 561 CliCommand::new(&API_METHOD_START_GARBAGE_COLLECTION)
49fddd98 562 .arg_param(&["store"])
9ac1045c 563 .completion_cb("store", config::datastore::complete_datastore_name)
48ef3c33 564 );
691c89a0
DM
565
566 cmd_def.into()
567}
568
47d47121
DM
569#[api(
570 input: {
571 properties: {
572 limit: {
573 description: "The maximal number of tasks to list.",
574 type: Integer,
575 optional: true,
576 minimum: 1,
577 maximum: 1000,
578 default: 50,
579 },
580 "output-format": {
581 schema: OUTPUT_FORMAT,
582 optional: true,
583 },
584 all: {
585 type: Boolean,
586 description: "Also list stopped tasks.",
587 optional: true,
588 }
589 }
590 }
591)]
592/// List running server tasks.
593async fn task_list(param: Value) -> Result<Value, Error> {
594
ac3faaf5 595 let output_format = get_output_format(&param);
47d47121
DM
596
597 let client = connect()?;
598
599 let limit = param["limit"].as_u64().unwrap_or(50) as usize;
600 let running = !param["all"].as_bool().unwrap_or(false);
601 let args = json!({
602 "running": running,
603 "start": 0,
604 "limit": limit,
605 });
9894469e 606 let mut result = client.get("api2/json/nodes/localhost/tasks", Some(args)).await?;
47d47121 607
9894469e
DM
608 let mut data = result["data"].take();
609 let schema = api2::node::tasks::API_RETURN_SCHEMA_LIST_TASKS;
47d47121 610
ac3faaf5 611 let options = default_table_format_options()
4939255f
DM
612 .column(ColumnConfig::new("starttime").right_align(false).renderer(tools::format::render_epoch))
613 .column(ColumnConfig::new("endtime").right_align(false).renderer(tools::format::render_epoch))
93fbb4ef 614 .column(ColumnConfig::new("upid"))
4939255f 615 .column(ColumnConfig::new("status").renderer(tools::format::render_task_status));
9894469e
DM
616
617 format_and_print_result_full(&mut data, schema, &output_format, &options);
47d47121
DM
618
619 Ok(Value::Null)
620}
621
622#[api(
623 input: {
624 properties: {
625 upid: {
626 schema: UPID_SCHEMA,
627 },
628 }
629 }
630)]
631/// Display the task log.
632async fn task_log(param: Value) -> Result<Value, Error> {
633
634 let upid = tools::required_string_param(&param, "upid")?;
635
636 let client = connect()?;
637
638 display_task_log(client, upid, true).await?;
639
640 Ok(Value::Null)
641}
642
643#[api(
644 input: {
645 properties: {
646 upid: {
647 schema: UPID_SCHEMA,
648 },
649 }
650 }
651)]
652/// Try to stop a specific task.
653async fn task_stop(param: Value) -> Result<Value, Error> {
654
655 let upid_str = tools::required_string_param(&param, "upid")?;
656
657 let mut client = connect()?;
658
659 let path = format!("api2/json/nodes/localhost/tasks/{}", upid_str);
660 let _ = client.delete(&path, None).await?;
661
662 Ok(Value::Null)
663}
664
665fn task_mgmt_cli() -> CommandLineInterface {
666
667 let task_log_cmd_def = CliCommand::new(&API_METHOD_TASK_LOG)
668 .arg_param(&["upid"]);
669
670 let task_stop_cmd_def = CliCommand::new(&API_METHOD_TASK_STOP)
671 .arg_param(&["upid"]);
672
673 let cmd_def = CliCommandMap::new()
674 .insert("list", CliCommand::new(&API_METHOD_TASK_LIST))
675 .insert("log", task_log_cmd_def)
676 .insert("stop", task_stop_cmd_def);
677
678 cmd_def.into()
679}
680
e739a8d8
DM
681fn x509name_to_string(name: &openssl::x509::X509NameRef) -> Result<String, Error> {
682 let mut parts = Vec::new();
683 for entry in name.entries() {
684 parts.push(format!("{} = {}", entry.object().nid().short_name()?, entry.data().as_utf8()?));
685 }
686 Ok(parts.join(", "))
687}
688
689#[api]
690/// Diplay node certificate information.
691fn cert_info() -> Result<(), Error> {
692
693 let cert_path = PathBuf::from(configdir!("/proxy.pem"));
694
695 let cert_pem = proxmox::tools::fs::file_get_contents(&cert_path)?;
696
697 let cert = openssl::x509::X509::from_pem(&cert_pem)?;
698
699 println!("Subject: {}", x509name_to_string(cert.subject_name())?);
700
701 if let Some(san) = cert.subject_alt_names() {
702 for name in san.iter() {
703 if let Some(v) = name.dnsname() {
704 println!(" DNS:{}", v);
705 } else if let Some(v) = name.ipaddress() {
706 println!(" IP:{:?}", v);
707 } else if let Some(v) = name.email() {
708 println!(" EMAIL:{}", v);
709 } else if let Some(v) = name.uri() {
710 println!(" URI:{}", v);
711 }
712 }
713 }
714
715 println!("Issuer: {}", x509name_to_string(cert.issuer_name())?);
716 println!("Validity:");
717 println!(" Not Before: {}", cert.not_before());
718 println!(" Not After : {}", cert.not_after());
719
720 let fp = cert.digest(openssl::hash::MessageDigest::sha256())?;
721 let fp_string = proxmox::tools::digest_to_hex(&fp);
722 let fp_string = fp_string.as_bytes().chunks(2).map(|v| std::str::from_utf8(v).unwrap())
723 .collect::<Vec<&str>>().join(":");
724
725 println!("Fingerprint (sha256): {}", fp_string);
726
727 let pubkey = cert.public_key()?;
728 println!("Public key type: {}", openssl::nid::Nid::from_raw(pubkey.id().as_raw()).long_name()?);
729 println!("Public key bits: {}", pubkey.bits());
730
731 Ok(())
732}
733
550e0d88
DM
734#[api(
735 input: {
736 properties: {
737 force: {
738 description: "Force generation of new SSL certifate.",
739 type: Boolean,
740 optional:true,
741 },
742 }
743 },
744)]
745/// Update node certificates and generate all needed files/directories.
746fn update_certs(force: Option<bool>) -> Result<(), Error> {
747
550e0d88
DM
748 config::create_configdir()?;
749
750 if let Err(err) = generate_auth_key() {
751 bail!("unable to generate auth key - {}", err);
752 }
753
754 if let Err(err) = generate_csrf_key() {
755 bail!("unable to generate csrf key - {}", err);
756 }
757
f8fd5095 758 config::update_self_signed_cert(force.unwrap_or(false))?;
550e0d88
DM
759
760 Ok(())
761}
762
763fn cert_mgmt_cli() -> CommandLineInterface {
764
550e0d88 765 let cmd_def = CliCommandMap::new()
e739a8d8
DM
766 .insert("info", CliCommand::new(&API_METHOD_CERT_INFO))
767 .insert("update", CliCommand::new(&API_METHOD_UPDATE_CERTS));
550e0d88
DM
768
769 cmd_def.into()
770}
771
4b4eba0b 772// fixme: avoid API redefinition
0eb0e024
DM
773#[api(
774 input: {
775 properties: {
eb506c83 776 "local-store": {
0eb0e024
DM
777 schema: DATASTORE_SCHEMA,
778 },
779 remote: {
167971ed 780 schema: REMOTE_ID_SCHEMA,
0eb0e024
DM
781 },
782 "remote-store": {
783 schema: DATASTORE_SCHEMA,
784 },
4b4eba0b
DM
785 delete: {
786 description: "Delete vanished backups. This remove the local copy if the remote backup was deleted.",
787 type: Boolean,
788 optional: true,
789 default: true,
790 },
0eb0e024
DM
791 "output-format": {
792 schema: OUTPUT_FORMAT,
793 optional: true,
794 },
795 }
796 }
797)]
eb506c83
DM
798/// Sync datastore from another repository
799async fn pull_datastore(
0eb0e024
DM
800 remote: String,
801 remote_store: String,
eb506c83 802 local_store: String,
4b4eba0b 803 delete: Option<bool>,
ac3faaf5 804 param: Value,
0eb0e024
DM
805) -> Result<Value, Error> {
806
ac3faaf5 807 let output_format = get_output_format(&param);
0eb0e024
DM
808
809 let mut client = connect()?;
810
4b4eba0b 811 let mut args = json!({
eb506c83 812 "store": local_store,
94609e23 813 "remote": remote,
0eb0e024 814 "remote-store": remote_store,
0eb0e024
DM
815 });
816
4b4eba0b
DM
817 if let Some(delete) = delete {
818 args["delete"] = delete.into();
819 }
820
eb506c83 821 let result = client.post("api2/json/pull", Some(args)).await?;
0eb0e024
DM
822
823 view_task_result(client, result, &output_format).await?;
824
825 Ok(Value::Null)
826}
827
211fabd7
DM
828fn main() {
829
6460764d 830 let cmd_def = CliCommandMap::new()
ed3e60ae 831 .insert("acl", acl_commands())
48ef3c33 832 .insert("datastore", datastore_commands())
14627d67 833 .insert("dns", dns_commands())
ca0e5347 834 .insert("network", network_commands())
579728c6 835 .insert("user", user_commands())
f357390c 836 .insert("remote", remote_commands())
47d47121 837 .insert("garbage-collection", garbage_collection_commands())
550e0d88 838 .insert("cert", cert_mgmt_cli())
0eb0e024
DM
839 .insert("task", task_mgmt_cli())
840 .insert(
eb506c83
DM
841 "pull",
842 CliCommand::new(&API_METHOD_PULL_DATASTORE)
843 .arg_param(&["remote", "remote-store", "local-store"])
844 .completion_cb("local-store", config::datastore::complete_datastore_name)
f357390c 845 .completion_cb("remote", config::remote::complete_remote_name)
331b869d 846 .completion_cb("remote-store", complete_remote_datastore_name)
0eb0e024 847 );
34d3ba52 848
7b22acd0
DM
849 let mut rpcenv = CliEnvironment::new();
850 rpcenv.set_user(Some(String::from("root@pam")));
851
852 proxmox_backup::tools::runtime::main(run_async_cli_command(cmd_def, rpcenv));
ea0b8b6e 853}
331b869d
DM
854
855// shell completion helper
856pub fn complete_remote_datastore_name(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
857
858 let mut list = Vec::new();
859
9ea4bce4 860 let _ = proxmox::try_block!({
331b869d 861 let remote = param.get("remote").ok_or_else(|| format_err!("no remote"))?;
f357390c 862 let (remote_config, _digest) = remote::config()?;
331b869d
DM
863
864 let remote: Remote = remote_config.lookup("remote", &remote)?;
865
d59dbeca
DM
866 let options = HttpClientOptions::new()
867 .password(Some(remote.password.clone()))
868 .fingerprint(remote.fingerprint.clone());
869
331b869d
DM
870 let client = HttpClient::new(
871 &remote.host,
872 &remote.userid,
d59dbeca 873 options,
331b869d
DM
874 )?;
875
03ac286c 876 let result = crate::tools::runtime::block_on(client.get("api2/json/admin/datastore", None))?;
331b869d
DM
877
878 if let Some(data) = result["data"].as_array() {
879 for item in data {
880 if let Some(store) = item["store"].as_str() {
881 list.push(store.to_owned());
882 }
883 }
884 }
885
886 Ok(())
887 }).map_err(|_err: Error| { /* ignore */ });
888
889 list
890}