]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/proxmox-backup-manager.rs
eea262c56d7a06314f5403adbd998c5bf88190f0
[proxmox-backup.git] / src / bin / proxmox-backup-manager.rs
1 use std::collections::HashMap;
2 use std::io::{self, Write};
3
4 use anyhow::Error;
5 use serde_json::{json, Value};
6
7 use proxmox::tools::fs::CreateOptions;
8 use proxmox_router::{cli::*, RpcEnvironment};
9 use proxmox_schema::api;
10
11 use pbs_client::{display_task_log, view_task_result};
12 use pbs_config::sync;
13 use pbs_tools::percent_encoding::percent_encode_component;
14 use pbs_tools::json::required_string_param;
15 use pbs_api_types::{
16 GroupFilter, SyncJobConfig,
17 DATASTORE_SCHEMA, GROUP_FILTER_LIST_SCHEMA, IGNORE_VERIFIED_BACKUPS_SCHEMA, REMOTE_ID_SCHEMA,
18 REMOVE_VANISHED_BACKUPS_SCHEMA, UPID_SCHEMA, VERIFICATION_OUTDATED_AFTER_SCHEMA,
19 };
20
21 use proxmox_rest_server::wait_for_local_worker;
22
23 use proxmox_backup::api2;
24 use proxmox_backup::client_helpers::connect_to_localhost;
25 use proxmox_backup::config;
26
27 mod proxmox_backup_manager;
28 use proxmox_backup_manager::*;
29
30 #[api(
31 input: {
32 properties: {
33 store: {
34 schema: DATASTORE_SCHEMA,
35 },
36 "output-format": {
37 schema: OUTPUT_FORMAT,
38 optional: true,
39 },
40 }
41 }
42 )]
43 /// Start garbage collection for a specific datastore.
44 async fn start_garbage_collection(param: Value) -> Result<Value, Error> {
45
46 let output_format = get_output_format(&param);
47
48 let store = required_string_param(&param, "store")?;
49
50 let mut client = connect_to_localhost()?;
51
52 let path = format!("api2/json/admin/datastore/{}/gc", store);
53
54 let result = client.post(&path, None).await?;
55
56 view_task_result(&mut client, result, &output_format).await?;
57
58 Ok(Value::Null)
59 }
60
61 #[api(
62 input: {
63 properties: {
64 store: {
65 schema: DATASTORE_SCHEMA,
66 },
67 "output-format": {
68 schema: OUTPUT_FORMAT,
69 optional: true,
70 },
71 }
72 }
73 )]
74 /// Show garbage collection status for a specific datastore.
75 async fn garbage_collection_status(param: Value) -> Result<Value, Error> {
76
77 let output_format = get_output_format(&param);
78
79 let store = required_string_param(&param, "store")?;
80
81 let client = connect_to_localhost()?;
82
83 let path = format!("api2/json/admin/datastore/{}/gc", store);
84
85 let mut result = client.get(&path, None).await?;
86 let mut data = result["data"].take();
87 let return_type = &api2::admin::datastore::API_METHOD_GARBAGE_COLLECTION_STATUS.returns;
88
89 let options = default_table_format_options();
90
91 format_and_print_result_full(&mut data, return_type, &output_format, &options);
92
93 Ok(Value::Null)
94 }
95
96 fn garbage_collection_commands() -> CommandLineInterface {
97
98 let cmd_def = CliCommandMap::new()
99 .insert("status",
100 CliCommand::new(&API_METHOD_GARBAGE_COLLECTION_STATUS)
101 .arg_param(&["store"])
102 .completion_cb("store", pbs_config::datastore::complete_datastore_name)
103 )
104 .insert("start",
105 CliCommand::new(&API_METHOD_START_GARBAGE_COLLECTION)
106 .arg_param(&["store"])
107 .completion_cb("store", pbs_config::datastore::complete_datastore_name)
108 );
109
110 cmd_def.into()
111 }
112
113 #[api(
114 input: {
115 properties: {
116 limit: {
117 description: "The maximal number of tasks to list.",
118 type: Integer,
119 optional: true,
120 minimum: 1,
121 maximum: 1000,
122 default: 50,
123 },
124 "output-format": {
125 schema: OUTPUT_FORMAT,
126 optional: true,
127 },
128 all: {
129 type: Boolean,
130 description: "Also list stopped tasks.",
131 optional: true,
132 }
133 }
134 }
135 )]
136 /// List running server tasks.
137 async fn task_list(param: Value) -> Result<Value, Error> {
138
139 let output_format = get_output_format(&param);
140
141 let client = connect_to_localhost()?;
142
143 let limit = param["limit"].as_u64().unwrap_or(50) as usize;
144 let running = !param["all"].as_bool().unwrap_or(false);
145 let args = json!({
146 "running": running,
147 "start": 0,
148 "limit": limit,
149 });
150 let mut result = client.get("api2/json/nodes/localhost/tasks", Some(args)).await?;
151
152 let mut data = result["data"].take();
153 let return_type = &api2::node::tasks::API_METHOD_LIST_TASKS.returns;
154
155 use pbs_tools::format::{render_epoch, render_task_status};
156 let options = default_table_format_options()
157 .column(ColumnConfig::new("starttime").right_align(false).renderer(render_epoch))
158 .column(ColumnConfig::new("endtime").right_align(false).renderer(render_epoch))
159 .column(ColumnConfig::new("upid"))
160 .column(ColumnConfig::new("status").renderer(render_task_status));
161
162 format_and_print_result_full(&mut data, return_type, &output_format, &options);
163
164 Ok(Value::Null)
165 }
166
167 #[api(
168 input: {
169 properties: {
170 upid: {
171 schema: UPID_SCHEMA,
172 },
173 }
174 }
175 )]
176 /// Display the task log.
177 async fn task_log(param: Value) -> Result<Value, Error> {
178
179 let upid = required_string_param(&param, "upid")?;
180
181 let mut client = connect_to_localhost()?;
182
183 display_task_log(&mut client, upid, true).await?;
184
185 Ok(Value::Null)
186 }
187
188 #[api(
189 input: {
190 properties: {
191 upid: {
192 schema: UPID_SCHEMA,
193 },
194 }
195 }
196 )]
197 /// Try to stop a specific task.
198 async fn task_stop(param: Value) -> Result<Value, Error> {
199
200 let upid_str = required_string_param(&param, "upid")?;
201
202 let mut client = connect_to_localhost()?;
203
204 let path = format!("api2/json/nodes/localhost/tasks/{}", percent_encode_component(upid_str));
205 let _ = client.delete(&path, None).await?;
206
207 Ok(Value::Null)
208 }
209
210 fn task_mgmt_cli() -> CommandLineInterface {
211
212 let task_log_cmd_def = CliCommand::new(&API_METHOD_TASK_LOG)
213 .arg_param(&["upid"]);
214
215 let task_stop_cmd_def = CliCommand::new(&API_METHOD_TASK_STOP)
216 .arg_param(&["upid"]);
217
218 let cmd_def = CliCommandMap::new()
219 .insert("list", CliCommand::new(&API_METHOD_TASK_LIST))
220 .insert("log", task_log_cmd_def)
221 .insert("stop", task_stop_cmd_def);
222
223 cmd_def.into()
224 }
225
226 // fixme: avoid API redefinition
227 #[api(
228 input: {
229 properties: {
230 "local-store": {
231 schema: DATASTORE_SCHEMA,
232 },
233 remote: {
234 schema: REMOTE_ID_SCHEMA,
235 },
236 "remote-store": {
237 schema: DATASTORE_SCHEMA,
238 },
239 "remove-vanished": {
240 schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
241 optional: true,
242 },
243 "group-filter": {
244 schema: GROUP_FILTER_LIST_SCHEMA,
245 optional: true,
246 },
247 "output-format": {
248 schema: OUTPUT_FORMAT,
249 optional: true,
250 },
251 }
252 }
253 )]
254 /// Sync datastore from another repository
255 async fn pull_datastore(
256 remote: String,
257 remote_store: String,
258 local_store: String,
259 remove_vanished: Option<bool>,
260 group_filter: Option<Vec<GroupFilter>>,
261 param: Value,
262 ) -> Result<Value, Error> {
263
264 let output_format = get_output_format(&param);
265
266 let mut client = connect_to_localhost()?;
267
268 let mut args = json!({
269 "store": local_store,
270 "remote": remote,
271 "remote-store": remote_store,
272 });
273
274 if group_filter.is_some() {
275 args["group-filter"] = json!(group_filter);
276 }
277
278 if let Some(remove_vanished) = remove_vanished {
279 args["remove-vanished"] = Value::from(remove_vanished);
280 }
281
282 let result = client.post("api2/json/pull", Some(args)).await?;
283
284 view_task_result(&mut client, result, &output_format).await?;
285
286 Ok(Value::Null)
287 }
288
289 #[api(
290 input: {
291 properties: {
292 "store": {
293 schema: DATASTORE_SCHEMA,
294 },
295 "ignore-verified": {
296 schema: IGNORE_VERIFIED_BACKUPS_SCHEMA,
297 optional: true,
298 },
299 "outdated-after": {
300 schema: VERIFICATION_OUTDATED_AFTER_SCHEMA,
301 optional: true,
302 },
303 "output-format": {
304 schema: OUTPUT_FORMAT,
305 optional: true,
306 },
307 }
308 }
309 )]
310 /// Verify backups
311 async fn verify(
312 store: String,
313 mut param: Value,
314 ) -> Result<Value, Error> {
315
316 let output_format = extract_output_format(&mut param);
317
318 let mut client = connect_to_localhost()?;
319
320 let args = json!(param);
321
322 let path = format!("api2/json/admin/datastore/{}/verify", store);
323
324 let result = client.post(&path, Some(args)).await?;
325
326 view_task_result(&mut client, result, &output_format).await?;
327
328 Ok(Value::Null)
329 }
330
331 #[api()]
332 /// System report
333 async fn report() -> Result<Value, Error> {
334 let report = proxmox_backup::server::generate_report();
335 io::stdout().write_all(report.as_bytes())?;
336 Ok(Value::Null)
337 }
338
339 #[api(
340 input: {
341 properties: {
342 verbose: {
343 type: Boolean,
344 optional: true,
345 default: false,
346 description: "Output verbose package information. It is ignored if output-format is specified.",
347 },
348 "output-format": {
349 schema: OUTPUT_FORMAT,
350 optional: true,
351 }
352 }
353 }
354 )]
355 /// List package versions for important Proxmox Backup Server packages.
356 async fn get_versions(verbose: bool, param: Value) -> Result<Value, Error> {
357 let output_format = get_output_format(&param);
358
359 let packages = crate::api2::node::apt::get_versions()?;
360 let mut packages = json!(if verbose { &packages[..] } else { &packages[1..2] });
361
362 let options = default_table_format_options()
363 .disable_sort()
364 .noborder(true) // just not helpful for version info which gets copy pasted often
365 .column(ColumnConfig::new("Package"))
366 .column(ColumnConfig::new("Version"))
367 .column(ColumnConfig::new("ExtraInfo").header("Extra Info"))
368 ;
369 let return_type = &crate::api2::node::apt::API_METHOD_GET_VERSIONS.returns;
370
371 format_and_print_result_full(&mut packages, return_type, &output_format, &options);
372
373 Ok(Value::Null)
374 }
375
376 async fn run() -> Result<(), Error> {
377
378 let cmd_def = CliCommandMap::new()
379 .insert("acl", acl_commands())
380 .insert("datastore", datastore_commands())
381 .insert("disk", disk_commands())
382 .insert("dns", dns_commands())
383 .insert("network", network_commands())
384 .insert("node", node_commands())
385 .insert("user", user_commands())
386 .insert("openid", openid_commands())
387 .insert("remote", remote_commands())
388 .insert("traffic-control", traffic_control_commands())
389 .insert("garbage-collection", garbage_collection_commands())
390 .insert("acme", acme_mgmt_cli())
391 .insert("cert", cert_mgmt_cli())
392 .insert("subscription", subscription_commands())
393 .insert("sync-job", sync_job_commands())
394 .insert("verify-job", verify_job_commands())
395 .insert("task", task_mgmt_cli())
396 .insert(
397 "pull",
398 CliCommand::new(&API_METHOD_PULL_DATASTORE)
399 .arg_param(&["remote", "remote-store", "local-store"])
400 .completion_cb("local-store", pbs_config::datastore::complete_datastore_name)
401 .completion_cb("remote", pbs_config::remote::complete_remote_name)
402 .completion_cb("remote-store", complete_remote_datastore_name)
403 .completion_cb("group_filter", complete_remote_datastore_group_filter)
404 )
405 .insert(
406 "verify",
407 CliCommand::new(&API_METHOD_VERIFY)
408 .arg_param(&["store"])
409 .completion_cb("store", pbs_config::datastore::complete_datastore_name)
410 )
411 .insert("report",
412 CliCommand::new(&API_METHOD_REPORT)
413 )
414 .insert("versions",
415 CliCommand::new(&API_METHOD_GET_VERSIONS)
416 );
417
418 let args: Vec<String> = std::env::args().take(2).collect();
419 let avoid_init = args.len() >= 2 && (args[1] == "bashcomplete" || args[1] == "printdoc");
420
421 if !avoid_init {
422 let backup_user = pbs_config::backup_user()?;
423 let file_opts = CreateOptions::new().owner(backup_user.uid).group(backup_user.gid);
424 proxmox_rest_server::init_worker_tasks(pbs_buildcfg::PROXMOX_BACKUP_LOG_DIR_M!().into(), file_opts.clone())?;
425
426 let mut commando_sock = proxmox_rest_server::CommandSocket::new(proxmox_rest_server::our_ctrl_sock(), backup_user.gid);
427 proxmox_rest_server::register_task_control_commands(&mut commando_sock)?;
428 commando_sock.spawn()?;
429 }
430
431 let mut rpcenv = CliEnvironment::new();
432 rpcenv.set_auth_id(Some(String::from("root@pam")));
433
434 run_async_cli_command(cmd_def, rpcenv).await; // this call exit(-1) on error
435
436 Ok(())
437 }
438
439 fn main() -> Result<(), Error> {
440
441 proxmox_backup::tools::setup_safe_path_env();
442
443 pbs_runtime::main(run())
444 }
445
446 fn get_sync_job(id: &String) -> Result<SyncJobConfig, Error> {
447 let (config, _digest) = sync::config()?;
448
449 config.lookup("sync", id)
450 }
451
452 fn get_remote(param: &HashMap<String, String>) -> Option<String> {
453 param
454 .get("remote")
455 .map(|r| r.to_owned())
456 .or_else(|| {
457 if let Some(id) = param.get("id") {
458 if let Ok(job) = get_sync_job(id) {
459 return Some(job.remote.clone());
460 }
461 }
462 None
463 })
464 }
465
466 fn get_remote_store(param: &HashMap<String, String>) -> Option<(String, String)> {
467 let mut job: Option<SyncJobConfig> = None;
468
469 let remote = param
470 .get("remote")
471 .map(|r| r.to_owned())
472 .or_else(|| {
473 if let Some(id) = param.get("id") {
474 job = get_sync_job(id).ok();
475 if let Some(ref job) = job {
476 return Some(job.remote.clone());
477 }
478 }
479 None
480 });
481
482 if let Some(remote) = remote {
483 let store = param
484 .get("remote-store")
485 .map(|r| r.to_owned())
486 .or_else(|| job.map(|job| job.remote_store.clone()));
487
488 if let Some(store) = store {
489 return Some((remote, store))
490 }
491 }
492
493 None
494 }
495
496 // shell completion helper
497 pub fn complete_remote_datastore_name(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
498
499 let mut list = Vec::new();
500
501 if let Some(remote) = get_remote(param) {
502 if let Ok(data) = pbs_runtime::block_on(async move {
503 crate::api2::config::remote::scan_remote_datastores(remote).await
504 }) {
505
506 for item in data {
507 list.push(item.store);
508 }
509 }
510 }
511
512 list
513 }
514
515 // shell completion helper
516 pub fn complete_remote_datastore_group(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
517
518 let mut list = Vec::new();
519
520 if let Some((remote, remote_store)) = get_remote_store(param) {
521 if let Ok(data) = pbs_runtime::block_on(async move {
522 crate::api2::config::remote::scan_remote_groups(remote.clone(), remote_store.clone()).await
523 }) {
524
525 for item in data {
526 list.push(format!("{}/{}", item.backup_type, item.backup_id));
527 }
528 }
529 }
530
531 list
532 }
533
534 // shell completion helper
535 pub fn complete_remote_datastore_group_filter(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
536
537 let mut list = Vec::new();
538
539 list.push("regex:".to_string());
540 list.push("type:ct".to_string());
541 list.push("type:host".to_string());
542 list.push("type:vm".to_string());
543
544 list.extend(complete_remote_datastore_group(_arg, param).iter().map(|group| format!("group:{}", group)));
545
546 list
547 }