]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/proxmox-backup-manager.rs
proxmox-backup-manager: split out network.rs
[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;
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 mod proxmox_backup_manager;
18 use proxmox_backup_manager::*;
19
20 async fn view_task_result(
21 client: HttpClient,
22 result: Value,
23 output_format: &str,
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?;
29 }
30 } else {
31 format_and_print_result(&data, &output_format);
32 }
33
34 Ok(())
35 }
36
37 fn connect() -> Result<HttpClient, Error> {
38
39 let uid = nix::unistd::Uid::current();
40
41 let mut options = HttpClientOptions::new()
42 .prefix(Some("proxmox-backup".to_string()))
43 .verify_cert(false); // not required for connection to localhost
44
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)?
49 } else {
50 options = options.ticket_cache(true).interactive(true);
51 HttpClient::new("localhost", "root@pam", options)?
52 };
53
54 Ok(client)
55 }
56
57 #[api(
58 input: {
59 properties: {
60 "output-format": {
61 schema: OUTPUT_FORMAT,
62 optional: true,
63 },
64 }
65 }
66 )]
67 /// Read DNS settings
68 fn get_dns(mut param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
69
70 let output_format = get_output_format(&param);
71
72 param["node"] = "localhost".into();
73
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)?,
77 _ => unreachable!(),
78 };
79
80
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"));
86
87 format_and_print_result_full(&mut data, info.returns, &output_format, &options);
88
89 Ok(Value::Null)
90 }
91
92 fn dns_commands() -> CommandLineInterface {
93
94 let cmd_def = CliCommandMap::new()
95 .insert(
96 "get",
97 CliCommand::new(&API_METHOD_GET_DNS)
98 )
99 .insert(
100 "set",
101 CliCommand::new(&api2::node::dns::API_METHOD_UPDATE_DNS)
102 .fixed_param("node", String::from("localhost"))
103 );
104
105 cmd_def.into()
106 }
107
108 #[api(
109 input: {
110 properties: {
111 "output-format": {
112 schema: OUTPUT_FORMAT,
113 optional: true,
114 },
115 }
116 }
117 )]
118 /// Datastore list.
119 fn list_datastores(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
120
121 let output_format = get_output_format(&param);
122
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)?,
126 _ => unreachable!(),
127 };
128
129 let options = default_table_format_options()
130 .column(ColumnConfig::new("name"))
131 .column(ColumnConfig::new("path"))
132 .column(ColumnConfig::new("comment"));
133
134 format_and_print_result_full(&mut data, info.returns, &output_format, &options);
135
136 Ok(Value::Null)
137 }
138
139 #[api(
140 input: {
141 properties: {
142 name: {
143 schema: DATASTORE_SCHEMA,
144 },
145 "output-format": {
146 schema: OUTPUT_FORMAT,
147 optional: true,
148 },
149 }
150 }
151 )]
152 /// Show datastore configuration
153 fn show_datastore(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
154
155 let output_format = get_output_format(&param);
156
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)?,
160 _ => unreachable!(),
161 };
162
163 let options = default_table_format_options();
164 format_and_print_result_full(&mut data, info.returns, &output_format, &options);
165
166 Ok(Value::Null)
167 }
168
169 fn datastore_commands() -> CommandLineInterface {
170
171 let cmd_def = CliCommandMap::new()
172 .insert("list", CliCommand::new(&API_METHOD_LIST_DATASTORES))
173 .insert("show",
174 CliCommand::new(&API_METHOD_SHOW_DATASTORE)
175 .arg_param(&["name"])
176 .completion_cb("name", config::datastore::complete_datastore_name)
177 )
178 .insert("create",
179 CliCommand::new(&api2::config::datastore::API_METHOD_CREATE_DATASTORE)
180 .arg_param(&["name", "path"])
181 )
182 .insert("update",
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)
188 )
189 .insert("remove",
190 CliCommand::new(&api2::config::datastore::API_METHOD_DELETE_DATASTORE)
191 .arg_param(&["name"])
192 .completion_cb("name", config::datastore::complete_datastore_name)
193 );
194
195 cmd_def.into()
196 }
197
198
199 #[api(
200 input: {
201 properties: {
202 store: {
203 schema: DATASTORE_SCHEMA,
204 },
205 "output-format": {
206 schema: OUTPUT_FORMAT,
207 optional: true,
208 },
209 }
210 }
211 )]
212 /// Start garbage collection for a specific datastore.
213 async fn start_garbage_collection(param: Value) -> Result<Value, Error> {
214
215 let output_format = get_output_format(&param);
216
217 let store = tools::required_string_param(&param, "store")?;
218
219 let mut client = connect()?;
220
221 let path = format!("api2/json/admin/datastore/{}/gc", store);
222
223 let result = client.post(&path, None).await?;
224
225 view_task_result(client, result, &output_format).await?;
226
227 Ok(Value::Null)
228 }
229
230 #[api(
231 input: {
232 properties: {
233 store: {
234 schema: DATASTORE_SCHEMA,
235 },
236 "output-format": {
237 schema: OUTPUT_FORMAT,
238 optional: true,
239 },
240 }
241 }
242 )]
243 /// Show garbage collection status for a specific datastore.
244 async fn garbage_collection_status(param: Value) -> Result<Value, Error> {
245
246 let output_format = get_output_format(&param);
247
248 let store = tools::required_string_param(&param, "store")?;
249
250 let client = connect()?;
251
252 let path = format!("api2/json/admin/datastore/{}/gc", store);
253
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;
257
258 let options = default_table_format_options();
259
260 format_and_print_result_full(&mut data, schema, &output_format, &options);
261
262 Ok(Value::Null)
263 }
264
265 fn garbage_collection_commands() -> CommandLineInterface {
266
267 let cmd_def = CliCommandMap::new()
268 .insert("status",
269 CliCommand::new(&API_METHOD_GARBAGE_COLLECTION_STATUS)
270 .arg_param(&["store"])
271 .completion_cb("store", config::datastore::complete_datastore_name)
272 )
273 .insert("start",
274 CliCommand::new(&API_METHOD_START_GARBAGE_COLLECTION)
275 .arg_param(&["store"])
276 .completion_cb("store", config::datastore::complete_datastore_name)
277 );
278
279 cmd_def.into()
280 }
281
282 #[api(
283 input: {
284 properties: {
285 limit: {
286 description: "The maximal number of tasks to list.",
287 type: Integer,
288 optional: true,
289 minimum: 1,
290 maximum: 1000,
291 default: 50,
292 },
293 "output-format": {
294 schema: OUTPUT_FORMAT,
295 optional: true,
296 },
297 all: {
298 type: Boolean,
299 description: "Also list stopped tasks.",
300 optional: true,
301 }
302 }
303 }
304 )]
305 /// List running server tasks.
306 async fn task_list(param: Value) -> Result<Value, Error> {
307
308 let output_format = get_output_format(&param);
309
310 let client = connect()?;
311
312 let limit = param["limit"].as_u64().unwrap_or(50) as usize;
313 let running = !param["all"].as_bool().unwrap_or(false);
314 let args = json!({
315 "running": running,
316 "start": 0,
317 "limit": limit,
318 });
319 let mut result = client.get("api2/json/nodes/localhost/tasks", Some(args)).await?;
320
321 let mut data = result["data"].take();
322 let schema = api2::node::tasks::API_RETURN_SCHEMA_LIST_TASKS;
323
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));
329
330 format_and_print_result_full(&mut data, schema, &output_format, &options);
331
332 Ok(Value::Null)
333 }
334
335 #[api(
336 input: {
337 properties: {
338 upid: {
339 schema: UPID_SCHEMA,
340 },
341 }
342 }
343 )]
344 /// Display the task log.
345 async fn task_log(param: Value) -> Result<Value, Error> {
346
347 let upid = tools::required_string_param(&param, "upid")?;
348
349 let client = connect()?;
350
351 display_task_log(client, upid, true).await?;
352
353 Ok(Value::Null)
354 }
355
356 #[api(
357 input: {
358 properties: {
359 upid: {
360 schema: UPID_SCHEMA,
361 },
362 }
363 }
364 )]
365 /// Try to stop a specific task.
366 async fn task_stop(param: Value) -> Result<Value, Error> {
367
368 let upid_str = tools::required_string_param(&param, "upid")?;
369
370 let mut client = connect()?;
371
372 let path = format!("api2/json/nodes/localhost/tasks/{}", upid_str);
373 let _ = client.delete(&path, None).await?;
374
375 Ok(Value::Null)
376 }
377
378 fn task_mgmt_cli() -> CommandLineInterface {
379
380 let task_log_cmd_def = CliCommand::new(&API_METHOD_TASK_LOG)
381 .arg_param(&["upid"]);
382
383 let task_stop_cmd_def = CliCommand::new(&API_METHOD_TASK_STOP)
384 .arg_param(&["upid"]);
385
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);
390
391 cmd_def.into()
392 }
393
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()?));
398 }
399 Ok(parts.join(", "))
400 }
401
402 #[api]
403 /// Diplay node certificate information.
404 fn cert_info() -> Result<(), Error> {
405
406 let cert_path = PathBuf::from(configdir!("/proxy.pem"));
407
408 let cert_pem = proxmox::tools::fs::file_get_contents(&cert_path)?;
409
410 let cert = openssl::x509::X509::from_pem(&cert_pem)?;
411
412 println!("Subject: {}", x509name_to_string(cert.subject_name())?);
413
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);
424 }
425 }
426 }
427
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());
432
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(":");
437
438 println!("Fingerprint (sha256): {}", fp_string);
439
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());
443
444 Ok(())
445 }
446
447 #[api(
448 input: {
449 properties: {
450 force: {
451 description: "Force generation of new SSL certifate.",
452 type: Boolean,
453 optional:true,
454 },
455 }
456 },
457 )]
458 /// Update node certificates and generate all needed files/directories.
459 fn update_certs(force: Option<bool>) -> Result<(), Error> {
460
461 config::create_configdir()?;
462
463 if let Err(err) = generate_auth_key() {
464 bail!("unable to generate auth key - {}", err);
465 }
466
467 if let Err(err) = generate_csrf_key() {
468 bail!("unable to generate csrf key - {}", err);
469 }
470
471 config::update_self_signed_cert(force.unwrap_or(false))?;
472
473 Ok(())
474 }
475
476 fn cert_mgmt_cli() -> CommandLineInterface {
477
478 let cmd_def = CliCommandMap::new()
479 .insert("info", CliCommand::new(&API_METHOD_CERT_INFO))
480 .insert("update", CliCommand::new(&API_METHOD_UPDATE_CERTS));
481
482 cmd_def.into()
483 }
484
485 // fixme: avoid API redefinition
486 #[api(
487 input: {
488 properties: {
489 "local-store": {
490 schema: DATASTORE_SCHEMA,
491 },
492 remote: {
493 schema: REMOTE_ID_SCHEMA,
494 },
495 "remote-store": {
496 schema: DATASTORE_SCHEMA,
497 },
498 delete: {
499 description: "Delete vanished backups. This remove the local copy if the remote backup was deleted.",
500 type: Boolean,
501 optional: true,
502 default: true,
503 },
504 "output-format": {
505 schema: OUTPUT_FORMAT,
506 optional: true,
507 },
508 }
509 }
510 )]
511 /// Sync datastore from another repository
512 async fn pull_datastore(
513 remote: String,
514 remote_store: String,
515 local_store: String,
516 delete: Option<bool>,
517 param: Value,
518 ) -> Result<Value, Error> {
519
520 let output_format = get_output_format(&param);
521
522 let mut client = connect()?;
523
524 let mut args = json!({
525 "store": local_store,
526 "remote": remote,
527 "remote-store": remote_store,
528 });
529
530 if let Some(delete) = delete {
531 args["delete"] = delete.into();
532 }
533
534 let result = client.post("api2/json/pull", Some(args)).await?;
535
536 view_task_result(client, result, &output_format).await?;
537
538 Ok(Value::Null)
539 }
540
541 fn main() {
542
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())
553 .insert(
554 "pull",
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)
560 );
561
562 let mut rpcenv = CliEnvironment::new();
563 rpcenv.set_user(Some(String::from("root@pam")));
564
565 proxmox_backup::tools::runtime::main(run_async_cli_command(cmd_def, rpcenv));
566 }
567
568 // shell completion helper
569 pub fn complete_remote_datastore_name(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
570
571 let mut list = Vec::new();
572
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()?;
576
577 let remote: config::remote::Remote = remote_config.lookup("remote", &remote)?;
578
579 let options = HttpClientOptions::new()
580 .password(Some(remote.password.clone()))
581 .fingerprint(remote.fingerprint.clone());
582
583 let client = HttpClient::new(
584 &remote.host,
585 &remote.userid,
586 options,
587 )?;
588
589 let result = crate::tools::runtime::block_on(client.get("api2/json/admin/datastore", None))?;
590
591 if let Some(data) = result["data"].as_array() {
592 for item in data {
593 if let Some(store) = item["store"].as_str() {
594 list.push(store.to_owned());
595 }
596 }
597 }
598
599 Ok(())
600 }).map_err(|_err: Error| { /* ignore */ });
601
602 list
603 }