]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/proxmox-backup-manager.rs
api: refactor remote client and add remote scan
[proxmox-backup.git] / src / bin / proxmox-backup-manager.rs
1 use std::collections::HashMap;
2 use std::io::{self, Write};
3
4 use anyhow::{format_err, Error};
5 use serde_json::{json, Value};
6
7 use proxmox::api::{api, cli::*, RpcEnvironment};
8
9 use proxmox_backup::tools;
10 use proxmox_backup::config;
11 use proxmox_backup::api2::{self, types::* };
12 use proxmox_backup::client::*;
13 use proxmox_backup::tools::ticket::Ticket;
14 use proxmox_backup::auth_helpers::*;
15
16 mod proxmox_backup_manager;
17 use proxmox_backup_manager::*;
18
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 }
35
36 // Note: local workers should print logs to stdout, so there is no need
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
54 fn connect() -> Result<HttpClient, Error> {
55
56 let uid = nix::unistd::Uid::current();
57
58 let mut options = HttpClientOptions::new()
59 .prefix(Some("proxmox-backup".to_string()))
60 .verify_cert(false); // not required for connection to localhost
61
62 let client = if uid.is_root() {
63 let ticket = Ticket::new("PBS", Userid::root_userid())?
64 .sign(private_auth_key(), None)?;
65 options = options.password(Some(ticket));
66 HttpClient::new("localhost", 8007, Authid::root_auth_id(), options)?
67 } else {
68 options = options.ticket_cache(true).interactive(true);
69 HttpClient::new("localhost", 8007, Authid::root_auth_id(), options)?
70 };
71
72 Ok(client)
73 }
74
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> {
90
91 let output_format = get_output_format(&param);
92
93 let store = tools::required_string_param(&param, "store")?;
94
95 let mut client = connect()?;
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
122 let output_format = get_output_format(&param);
123
124 let store = tools::required_string_param(&param, "store")?;
125
126 let client = connect()?;
127
128 let path = format!("api2/json/admin/datastore/{}/gc", store);
129
130 let mut result = client.get(&path, None).await?;
131 let mut data = result["data"].take();
132 let schema = &api2::admin::datastore::API_RETURN_SCHEMA_GARBAGE_COLLECTION_STATUS;
133
134 let options = default_table_format_options();
135
136 format_and_print_result_full(&mut data, schema, &output_format, &options);
137
138 Ok(Value::Null)
139 }
140
141 fn garbage_collection_commands() -> CommandLineInterface {
142
143 let cmd_def = CliCommandMap::new()
144 .insert("status",
145 CliCommand::new(&API_METHOD_GARBAGE_COLLECTION_STATUS)
146 .arg_param(&["store"])
147 .completion_cb("store", config::datastore::complete_datastore_name)
148 )
149 .insert("start",
150 CliCommand::new(&API_METHOD_START_GARBAGE_COLLECTION)
151 .arg_param(&["store"])
152 .completion_cb("store", config::datastore::complete_datastore_name)
153 );
154
155 cmd_def.into()
156 }
157
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
184 let output_format = get_output_format(&param);
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 });
195 let mut result = client.get("api2/json/nodes/localhost/tasks", Some(args)).await?;
196
197 let mut data = result["data"].take();
198 let schema = &api2::node::tasks::API_RETURN_SCHEMA_LIST_TASKS;
199
200 let options = default_table_format_options()
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))
203 .column(ColumnConfig::new("upid"))
204 .column(ColumnConfig::new("status").renderer(tools::format::render_task_status));
205
206 format_and_print_result_full(&mut data, schema, &output_format, &options);
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(&param, "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(&param, "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
270 // fixme: avoid API redefinition
271 #[api(
272 input: {
273 properties: {
274 "local-store": {
275 schema: DATASTORE_SCHEMA,
276 },
277 remote: {
278 schema: REMOTE_ID_SCHEMA,
279 },
280 "remote-store": {
281 schema: DATASTORE_SCHEMA,
282 },
283 "remove-vanished": {
284 schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
285 optional: true,
286 },
287 "output-format": {
288 schema: OUTPUT_FORMAT,
289 optional: true,
290 },
291 }
292 }
293 )]
294 /// Sync datastore from another repository
295 async fn pull_datastore(
296 remote: String,
297 remote_store: String,
298 local_store: String,
299 remove_vanished: Option<bool>,
300 param: Value,
301 ) -> Result<Value, Error> {
302
303 let output_format = get_output_format(&param);
304
305 let mut client = connect()?;
306
307 let mut args = json!({
308 "store": local_store,
309 "remote": remote,
310 "remote-store": remote_store,
311 });
312
313 if let Some(remove_vanished) = remove_vanished {
314 args["remove-vanished"] = Value::from(remove_vanished);
315 }
316
317 let result = client.post("api2/json/pull", Some(args)).await?;
318
319 view_task_result(client, result, &output_format).await?;
320
321 Ok(Value::Null)
322 }
323
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(&param);
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
358 #[api()]
359 /// System report
360 async fn report() -> Result<Value, Error> {
361 let report = proxmox_backup::server::generate_report();
362 io::stdout().write_all(report.as_bytes())?;
363 Ok(Value::Null)
364 }
365
366 fn main() {
367
368 proxmox_backup::tools::setup_safe_path_env();
369
370 let cmd_def = CliCommandMap::new()
371 .insert("acl", acl_commands())
372 .insert("datastore", datastore_commands())
373 .insert("disk", disk_commands())
374 .insert("dns", dns_commands())
375 .insert("network", network_commands())
376 .insert("user", user_commands())
377 .insert("remote", remote_commands())
378 .insert("garbage-collection", garbage_collection_commands())
379 .insert("cert", cert_mgmt_cli())
380 .insert("subscription", subscription_commands())
381 .insert("sync-job", sync_job_commands())
382 .insert("task", task_mgmt_cli())
383 .insert(
384 "pull",
385 CliCommand::new(&API_METHOD_PULL_DATASTORE)
386 .arg_param(&["remote", "remote-store", "local-store"])
387 .completion_cb("local-store", config::datastore::complete_datastore_name)
388 .completion_cb("remote", config::remote::complete_remote_name)
389 .completion_cb("remote-store", complete_remote_datastore_name)
390 )
391 .insert(
392 "verify",
393 CliCommand::new(&API_METHOD_VERIFY)
394 .arg_param(&["store"])
395 .completion_cb("store", config::datastore::complete_datastore_name)
396 )
397 .insert("report",
398 CliCommand::new(&API_METHOD_REPORT)
399 );
400
401
402
403 let mut rpcenv = CliEnvironment::new();
404 rpcenv.set_auth_id(Some(String::from("root@pam")));
405
406 proxmox_backup::tools::runtime::main(run_async_cli_command(cmd_def, rpcenv));
407 }
408
409 // shell completion helper
410 pub fn complete_remote_datastore_name(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
411
412 let mut list = Vec::new();
413
414 let _ = proxmox::try_block!({
415 let remote = param.get("remote").ok_or_else(|| format_err!("no remote"))?;
416
417 let data = crate::tools::runtime::block_on(async move {
418 crate::api2::config::remote::scan_remote_datastores(remote.clone()).await
419 })?;
420
421 for item in data {
422 list.push(item.store);
423 }
424
425 Ok(())
426 }).map_err(|_err: Error| { /* ignore */ });
427
428 list
429 }