]>
Commit | Line | Data |
---|---|---|
331b869d | 1 | use std::collections::HashMap; |
941342f7 | 2 | use std::io::{self, Write}; |
ea0b8b6e | 3 | |
b29d046e | 4 | use anyhow::{format_err, Error}; |
9894469e | 5 | use serde_json::{json, Value}; |
9894469e | 6 | |
380bd7df | 7 | use proxmox::api::{api, cli::*, RpcEnvironment}; |
769f8c99 DM |
8 | |
9 | use proxmox_backup::tools; | |
a220a456 | 10 | use proxmox_backup::config; |
9894469e | 11 | use proxmox_backup::api2::{self, types::* }; |
769f8c99 | 12 | use proxmox_backup::client::*; |
72dc6832 | 13 | use proxmox_backup::tools::ticket::Ticket; |
769f8c99 DM |
14 | use proxmox_backup::auth_helpers::*; |
15 | ||
a220a456 DM |
16 | mod proxmox_backup_manager; |
17 | use proxmox_backup_manager::*; | |
18 | ||
769f8c99 DM |
19 | async fn view_task_result( |
20 | client: HttpClient, | |
21 | result: Value, | |
22 | output_format: &str, | |
23 | ) -> Result<(), Error> { | |
24 | let data = &result["data"]; | |
25 | if output_format == "text" { | |
26 | if let Some(upid) = data.as_str() { | |
27 | display_task_log(client, upid, true).await?; | |
28 | } | |
29 | } else { | |
30 | format_and_print_result(&data, &output_format); | |
31 | } | |
32 | ||
33 | Ok(()) | |
34 | } | |
211fabd7 | 35 | |
dfb31de8 | 36 | // Note: local workers should print logs to stdout, so there is no need |
707974fd DM |
37 | // to fetch/display logs. We just wait for the worker to finish. |
38 | pub async fn wait_for_local_worker(upid_str: &str) -> Result<(), Error> { | |
39 | ||
40 | let upid: proxmox_backup::server::UPID = upid_str.parse()?; | |
41 | ||
42 | let sleep_duration = core::time::Duration::new(0, 100_000_000); | |
43 | ||
44 | loop { | |
45 | if proxmox_backup::server::worker_is_active_local(&upid) { | |
46 | tokio::time::delay_for(sleep_duration).await; | |
47 | } else { | |
48 | break; | |
49 | } | |
50 | } | |
51 | Ok(()) | |
52 | } | |
53 | ||
47d47121 DM |
54 | fn connect() -> Result<HttpClient, Error> { |
55 | ||
56 | let uid = nix::unistd::Uid::current(); | |
57 | ||
d59dbeca | 58 | let mut options = HttpClientOptions::new() |
5030b7ce | 59 | .prefix(Some("proxmox-backup".to_string())) |
d59dbeca DM |
60 | .verify_cert(false); // not required for connection to localhost |
61 | ||
47d47121 | 62 | let client = if uid.is_root() { |
72dc6832 WB |
63 | let ticket = Ticket::new("PBS", Userid::root_userid())? |
64 | .sign(private_auth_key(), None)?; | |
d59dbeca | 65 | options = options.password(Some(ticket)); |
34aa8e13 | 66 | HttpClient::new("localhost", 8007, Authid::root_auth_id(), options)? |
47d47121 | 67 | } else { |
d59dbeca | 68 | options = options.ticket_cache(true).interactive(true); |
34aa8e13 | 69 | HttpClient::new("localhost", 8007, Authid::root_auth_id(), options)? |
47d47121 DM |
70 | }; |
71 | ||
72 | Ok(client) | |
73 | } | |
74 | ||
769f8c99 DM |
75 | #[api( |
76 | input: { | |
77 | properties: { | |
78 | store: { | |
79 | schema: DATASTORE_SCHEMA, | |
80 | }, | |
81 | "output-format": { | |
82 | schema: OUTPUT_FORMAT, | |
83 | optional: true, | |
84 | }, | |
85 | } | |
86 | } | |
87 | )] | |
88 | /// Start garbage collection for a specific datastore. | |
89 | async fn start_garbage_collection(param: Value) -> Result<Value, Error> { | |
691c89a0 | 90 | |
ac3faaf5 | 91 | let output_format = get_output_format(¶m); |
691c89a0 | 92 | |
769f8c99 DM |
93 | let store = tools::required_string_param(¶m, "store")?; |
94 | ||
47d47121 | 95 | let mut client = connect()?; |
769f8c99 DM |
96 | |
97 | let path = format!("api2/json/admin/datastore/{}/gc", store); | |
98 | ||
99 | let result = client.post(&path, None).await?; | |
100 | ||
101 | view_task_result(client, result, &output_format).await?; | |
102 | ||
103 | Ok(Value::Null) | |
104 | } | |
105 | ||
106 | #[api( | |
107 | input: { | |
108 | properties: { | |
109 | store: { | |
110 | schema: DATASTORE_SCHEMA, | |
111 | }, | |
112 | "output-format": { | |
113 | schema: OUTPUT_FORMAT, | |
114 | optional: true, | |
115 | }, | |
116 | } | |
117 | } | |
118 | )] | |
119 | /// Show garbage collection status for a specific datastore. | |
120 | async fn garbage_collection_status(param: Value) -> Result<Value, Error> { | |
121 | ||
ac3faaf5 | 122 | let output_format = get_output_format(¶m); |
769f8c99 DM |
123 | |
124 | let store = tools::required_string_param(¶m, "store")?; | |
125 | ||
47d47121 | 126 | let client = connect()?; |
769f8c99 DM |
127 | |
128 | let path = format!("api2/json/admin/datastore/{}/gc", store); | |
129 | ||
9894469e DM |
130 | let mut result = client.get(&path, None).await?; |
131 | let mut data = result["data"].take(); | |
660a3489 | 132 | let schema = &api2::admin::datastore::API_RETURN_SCHEMA_GARBAGE_COLLECTION_STATUS; |
9894469e | 133 | |
ac3faaf5 | 134 | let options = default_table_format_options(); |
9894469e DM |
135 | |
136 | format_and_print_result_full(&mut data, schema, &output_format, &options); | |
769f8c99 DM |
137 | |
138 | Ok(Value::Null) | |
139 | } | |
140 | ||
141 | fn garbage_collection_commands() -> CommandLineInterface { | |
691c89a0 DM |
142 | |
143 | let cmd_def = CliCommandMap::new() | |
144 | .insert("status", | |
769f8c99 | 145 | CliCommand::new(&API_METHOD_GARBAGE_COLLECTION_STATUS) |
49fddd98 | 146 | .arg_param(&["store"]) |
9ac1045c | 147 | .completion_cb("store", config::datastore::complete_datastore_name) |
48ef3c33 | 148 | ) |
691c89a0 | 149 | .insert("start", |
769f8c99 | 150 | CliCommand::new(&API_METHOD_START_GARBAGE_COLLECTION) |
49fddd98 | 151 | .arg_param(&["store"]) |
9ac1045c | 152 | .completion_cb("store", config::datastore::complete_datastore_name) |
48ef3c33 | 153 | ); |
691c89a0 DM |
154 | |
155 | cmd_def.into() | |
156 | } | |
157 | ||
47d47121 DM |
158 | #[api( |
159 | input: { | |
160 | properties: { | |
161 | limit: { | |
162 | description: "The maximal number of tasks to list.", | |
163 | type: Integer, | |
164 | optional: true, | |
165 | minimum: 1, | |
166 | maximum: 1000, | |
167 | default: 50, | |
168 | }, | |
169 | "output-format": { | |
170 | schema: OUTPUT_FORMAT, | |
171 | optional: true, | |
172 | }, | |
173 | all: { | |
174 | type: Boolean, | |
175 | description: "Also list stopped tasks.", | |
176 | optional: true, | |
177 | } | |
178 | } | |
179 | } | |
180 | )] | |
181 | /// List running server tasks. | |
182 | async fn task_list(param: Value) -> Result<Value, Error> { | |
183 | ||
ac3faaf5 | 184 | let output_format = get_output_format(¶m); |
47d47121 DM |
185 | |
186 | let client = connect()?; | |
187 | ||
188 | let limit = param["limit"].as_u64().unwrap_or(50) as usize; | |
189 | let running = !param["all"].as_bool().unwrap_or(false); | |
190 | let args = json!({ | |
191 | "running": running, | |
192 | "start": 0, | |
193 | "limit": limit, | |
194 | }); | |
9894469e | 195 | let mut result = client.get("api2/json/nodes/localhost/tasks", Some(args)).await?; |
47d47121 | 196 | |
9894469e | 197 | let mut data = result["data"].take(); |
660a3489 | 198 | let schema = &api2::node::tasks::API_RETURN_SCHEMA_LIST_TASKS; |
47d47121 | 199 | |
ac3faaf5 | 200 | let options = default_table_format_options() |
4939255f DM |
201 | .column(ColumnConfig::new("starttime").right_align(false).renderer(tools::format::render_epoch)) |
202 | .column(ColumnConfig::new("endtime").right_align(false).renderer(tools::format::render_epoch)) | |
93fbb4ef | 203 | .column(ColumnConfig::new("upid")) |
4939255f | 204 | .column(ColumnConfig::new("status").renderer(tools::format::render_task_status)); |
9894469e DM |
205 | |
206 | format_and_print_result_full(&mut data, schema, &output_format, &options); | |
47d47121 DM |
207 | |
208 | Ok(Value::Null) | |
209 | } | |
210 | ||
211 | #[api( | |
212 | input: { | |
213 | properties: { | |
214 | upid: { | |
215 | schema: UPID_SCHEMA, | |
216 | }, | |
217 | } | |
218 | } | |
219 | )] | |
220 | /// Display the task log. | |
221 | async fn task_log(param: Value) -> Result<Value, Error> { | |
222 | ||
223 | let upid = tools::required_string_param(¶m, "upid")?; | |
224 | ||
225 | let client = connect()?; | |
226 | ||
227 | display_task_log(client, upid, true).await?; | |
228 | ||
229 | Ok(Value::Null) | |
230 | } | |
231 | ||
232 | #[api( | |
233 | input: { | |
234 | properties: { | |
235 | upid: { | |
236 | schema: UPID_SCHEMA, | |
237 | }, | |
238 | } | |
239 | } | |
240 | )] | |
241 | /// Try to stop a specific task. | |
242 | async fn task_stop(param: Value) -> Result<Value, Error> { | |
243 | ||
244 | let upid_str = tools::required_string_param(¶m, "upid")?; | |
245 | ||
246 | let mut client = connect()?; | |
247 | ||
248 | let path = format!("api2/json/nodes/localhost/tasks/{}", upid_str); | |
249 | let _ = client.delete(&path, None).await?; | |
250 | ||
251 | Ok(Value::Null) | |
252 | } | |
253 | ||
254 | fn task_mgmt_cli() -> CommandLineInterface { | |
255 | ||
256 | let task_log_cmd_def = CliCommand::new(&API_METHOD_TASK_LOG) | |
257 | .arg_param(&["upid"]); | |
258 | ||
259 | let task_stop_cmd_def = CliCommand::new(&API_METHOD_TASK_STOP) | |
260 | .arg_param(&["upid"]); | |
261 | ||
262 | let cmd_def = CliCommandMap::new() | |
263 | .insert("list", CliCommand::new(&API_METHOD_TASK_LIST)) | |
264 | .insert("log", task_log_cmd_def) | |
265 | .insert("stop", task_stop_cmd_def); | |
266 | ||
267 | cmd_def.into() | |
268 | } | |
269 | ||
4b4eba0b | 270 | // fixme: avoid API redefinition |
0eb0e024 DM |
271 | #[api( |
272 | input: { | |
273 | properties: { | |
eb506c83 | 274 | "local-store": { |
0eb0e024 DM |
275 | schema: DATASTORE_SCHEMA, |
276 | }, | |
277 | remote: { | |
167971ed | 278 | schema: REMOTE_ID_SCHEMA, |
0eb0e024 DM |
279 | }, |
280 | "remote-store": { | |
281 | schema: DATASTORE_SCHEMA, | |
282 | }, | |
40dc1031 DC |
283 | "remove-vanished": { |
284 | schema: REMOVE_VANISHED_BACKUPS_SCHEMA, | |
4b4eba0b | 285 | optional: true, |
4b4eba0b | 286 | }, |
0eb0e024 DM |
287 | "output-format": { |
288 | schema: OUTPUT_FORMAT, | |
289 | optional: true, | |
290 | }, | |
291 | } | |
292 | } | |
293 | )] | |
eb506c83 DM |
294 | /// Sync datastore from another repository |
295 | async fn pull_datastore( | |
0eb0e024 DM |
296 | remote: String, |
297 | remote_store: String, | |
eb506c83 | 298 | local_store: String, |
40dc1031 | 299 | remove_vanished: Option<bool>, |
ac3faaf5 | 300 | param: Value, |
0eb0e024 DM |
301 | ) -> Result<Value, Error> { |
302 | ||
ac3faaf5 | 303 | let output_format = get_output_format(¶m); |
0eb0e024 DM |
304 | |
305 | let mut client = connect()?; | |
306 | ||
8c877436 | 307 | let mut args = json!({ |
eb506c83 | 308 | "store": local_store, |
94609e23 | 309 | "remote": remote, |
0eb0e024 | 310 | "remote-store": remote_store, |
0eb0e024 DM |
311 | }); |
312 | ||
8c877436 DC |
313 | if let Some(remove_vanished) = remove_vanished { |
314 | args["remove-vanished"] = Value::from(remove_vanished); | |
315 | } | |
316 | ||
eb506c83 | 317 | let result = client.post("api2/json/pull", Some(args)).await?; |
0eb0e024 DM |
318 | |
319 | view_task_result(client, result, &output_format).await?; | |
320 | ||
321 | Ok(Value::Null) | |
322 | } | |
323 | ||
355c055e DM |
324 | #[api( |
325 | input: { | |
326 | properties: { | |
327 | "store": { | |
328 | schema: DATASTORE_SCHEMA, | |
329 | }, | |
330 | "output-format": { | |
331 | schema: OUTPUT_FORMAT, | |
332 | optional: true, | |
333 | }, | |
334 | } | |
335 | } | |
336 | )] | |
337 | /// Verify backups | |
338 | async fn verify( | |
339 | store: String, | |
340 | param: Value, | |
341 | ) -> Result<Value, Error> { | |
342 | ||
343 | let output_format = get_output_format(¶m); | |
344 | ||
345 | let mut client = connect()?; | |
346 | ||
347 | let args = json!({}); | |
348 | ||
349 | let path = format!("api2/json/admin/datastore/{}/verify", store); | |
350 | ||
351 | let result = client.post(&path, Some(args)).await?; | |
352 | ||
353 | view_task_result(client, result, &output_format).await?; | |
354 | ||
355 | Ok(Value::Null) | |
356 | } | |
357 | ||
9a556c8a HL |
358 | #[api()] |
359 | /// System report | |
360 | async fn report() -> Result<Value, Error> { | |
941342f7 TL |
361 | let report = proxmox_backup::server::generate_report(); |
362 | io::stdout().write_all(report.as_bytes())?; | |
9a556c8a HL |
363 | Ok(Value::Null) |
364 | } | |
365 | ||
c100fe91 ML |
366 | #[api( |
367 | input: { | |
368 | properties: { | |
369 | verbose: { | |
370 | type: Boolean, | |
371 | optional: true, | |
372 | default: false, | |
373 | description: "Output verbose package information. It is ignored if output-format is specified.", | |
374 | }, | |
375 | "output-format": { | |
376 | schema: OUTPUT_FORMAT, | |
377 | optional: true, | |
378 | } | |
379 | } | |
380 | } | |
381 | )] | |
382 | /// List package versions for important Proxmox Backup Server packages. | |
383 | async fn get_versions(verbose: bool, param: Value) -> Result<Value, Error> { | |
294466ee | 384 | let output_format = get_output_format(¶m); |
c100fe91 | 385 | |
294466ee TL |
386 | let mut packages = if verbose { |
387 | crate::api2::node::apt::get_versions()? | |
c100fe91 | 388 | } else { |
294466ee TL |
389 | // TODO: slice first element out in a nicer way? |
390 | let packages = crate::api2::node::apt::get_versions()?; | |
391 | let packages = packages.as_array().unwrap(); | |
392 | Value::Array(vec![packages[0].to_owned()]) | |
393 | }; | |
c100fe91 | 394 | |
294466ee TL |
395 | let options = default_table_format_options() |
396 | .disable_sort() | |
397 | .noborder(true) // just not helpfull for version info which gets copy pasted often | |
398 | .column(ColumnConfig::new("Package")) | |
399 | .column(ColumnConfig::new("Version")) | |
400 | .column(ColumnConfig::new("ExtraInfo").header("Extra Info")) | |
401 | ; | |
402 | let schema = &crate::api2::node::apt::API_RETURN_SCHEMA_GET_VERSIONS; | |
403 | ||
404 | format_and_print_result_full(&mut packages, schema, &output_format, &options); | |
c100fe91 ML |
405 | |
406 | Ok(Value::Null) | |
407 | } | |
408 | ||
211fabd7 DM |
409 | fn main() { |
410 | ||
ac7513e3 DM |
411 | proxmox_backup::tools::setup_safe_path_env(); |
412 | ||
6460764d | 413 | let cmd_def = CliCommandMap::new() |
ed3e60ae | 414 | .insert("acl", acl_commands()) |
48ef3c33 | 415 | .insert("datastore", datastore_commands()) |
8e40aa63 | 416 | .insert("disk", disk_commands()) |
14627d67 | 417 | .insert("dns", dns_commands()) |
ca0e5347 | 418 | .insert("network", network_commands()) |
579728c6 | 419 | .insert("user", user_commands()) |
f357390c | 420 | .insert("remote", remote_commands()) |
47d47121 | 421 | .insert("garbage-collection", garbage_collection_commands()) |
550e0d88 | 422 | .insert("cert", cert_mgmt_cli()) |
2762481c | 423 | .insert("subscription", subscription_commands()) |
a3016d65 | 424 | .insert("sync-job", sync_job_commands()) |
0eb0e024 DM |
425 | .insert("task", task_mgmt_cli()) |
426 | .insert( | |
eb506c83 DM |
427 | "pull", |
428 | CliCommand::new(&API_METHOD_PULL_DATASTORE) | |
429 | .arg_param(&["remote", "remote-store", "local-store"]) | |
430 | .completion_cb("local-store", config::datastore::complete_datastore_name) | |
f357390c | 431 | .completion_cb("remote", config::remote::complete_remote_name) |
331b869d | 432 | .completion_cb("remote-store", complete_remote_datastore_name) |
355c055e DM |
433 | ) |
434 | .insert( | |
435 | "verify", | |
436 | CliCommand::new(&API_METHOD_VERIFY) | |
437 | .arg_param(&["store"]) | |
438 | .completion_cb("store", config::datastore::complete_datastore_name) | |
9a556c8a HL |
439 | ) |
440 | .insert("report", | |
441 | CliCommand::new(&API_METHOD_REPORT) | |
c100fe91 ML |
442 | ) |
443 | .insert("versions", | |
444 | CliCommand::new(&API_METHOD_GET_VERSIONS) | |
0eb0e024 | 445 | ); |
34d3ba52 | 446 | |
355c055e DM |
447 | |
448 | ||
7b22acd0 | 449 | let mut rpcenv = CliEnvironment::new(); |
e6dc35ac | 450 | rpcenv.set_auth_id(Some(String::from("root@pam"))); |
525008f7 | 451 | |
7b22acd0 | 452 | proxmox_backup::tools::runtime::main(run_async_cli_command(cmd_def, rpcenv)); |
ea0b8b6e | 453 | } |
331b869d DM |
454 | |
455 | // shell completion helper | |
456 | pub fn complete_remote_datastore_name(_arg: &str, param: &HashMap<String, String>) -> Vec<String> { | |
457 | ||
458 | let mut list = Vec::new(); | |
459 | ||
9ea4bce4 | 460 | let _ = proxmox::try_block!({ |
331b869d | 461 | let remote = param.get("remote").ok_or_else(|| format_err!("no remote"))?; |
331b869d | 462 | |
e0100d61 FG |
463 | let data = crate::tools::runtime::block_on(async move { |
464 | crate::api2::config::remote::scan_remote_datastores(remote.clone()).await | |
465 | })?; | |
331b869d | 466 | |
e0100d61 FG |
467 | for item in data { |
468 | list.push(item.store); | |
331b869d DM |
469 | } |
470 | ||
471 | Ok(()) | |
472 | }).map_err(|_err: Error| { /* ignore */ }); | |
473 | ||
474 | list | |
475 | } |