]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/proxmox-backup-manager.rs
Cargo.toml: pathpatterns, pxar, proxmox-fuse
[proxmox-backup.git] / src / bin / proxmox-backup-manager.rs
1 use std::collections::HashMap;
2
3 use anyhow::{format_err, Error};
4 use serde_json::{json, Value};
5
6 use proxmox::api::{api, cli::*, RpcEnvironment};
7
8 use proxmox_backup::tools;
9 use proxmox_backup::config;
10 use proxmox_backup::api2::{self, types::* };
11 use proxmox_backup::client::*;
12 use proxmox_backup::tools::ticket::*;
13 use proxmox_backup::auth_helpers::*;
14
15 mod proxmox_backup_manager;
16 use proxmox_backup_manager::*;
17
18 async fn view_task_result(
19 client: HttpClient,
20 result: Value,
21 output_format: &str,
22 ) -> Result<(), Error> {
23 let data = &result["data"];
24 if output_format == "text" {
25 if let Some(upid) = data.as_str() {
26 display_task_log(client, upid, true).await?;
27 }
28 } else {
29 format_and_print_result(&data, &output_format);
30 }
31
32 Ok(())
33 }
34
35 // Note: local workers should print logs to stdout, so there is no need
36 // to fetch/display logs. We just wait for the worker to finish.
37 pub async fn wait_for_local_worker(upid_str: &str) -> Result<(), Error> {
38
39 let upid: proxmox_backup::server::UPID = upid_str.parse()?;
40
41 let sleep_duration = core::time::Duration::new(0, 100_000_000);
42
43 loop {
44 if proxmox_backup::server::worker_is_active_local(&upid) {
45 tokio::time::delay_for(sleep_duration).await;
46 } else {
47 break;
48 }
49 }
50 Ok(())
51 }
52
53 fn connect() -> Result<HttpClient, Error> {
54
55 let uid = nix::unistd::Uid::current();
56
57 let mut options = HttpClientOptions::new()
58 .prefix(Some("proxmox-backup".to_string()))
59 .verify_cert(false); // not required for connection to localhost
60
61 let client = if uid.is_root() {
62 let ticket = assemble_rsa_ticket(private_auth_key(), "PBS", Some("root@pam"), None)?;
63 options = options.password(Some(ticket));
64 HttpClient::new("localhost", "root@pam", options)?
65 } else {
66 options = options.ticket_cache(true).interactive(true);
67 HttpClient::new("localhost", "root@pam", options)?
68 };
69
70 Ok(client)
71 }
72
73 #[api(
74 input: {
75 properties: {
76 store: {
77 schema: DATASTORE_SCHEMA,
78 },
79 "output-format": {
80 schema: OUTPUT_FORMAT,
81 optional: true,
82 },
83 }
84 }
85 )]
86 /// Start garbage collection for a specific datastore.
87 async fn start_garbage_collection(param: Value) -> Result<Value, Error> {
88
89 let output_format = get_output_format(&param);
90
91 let store = tools::required_string_param(&param, "store")?;
92
93 let mut client = connect()?;
94
95 let path = format!("api2/json/admin/datastore/{}/gc", store);
96
97 let result = client.post(&path, None).await?;
98
99 view_task_result(client, result, &output_format).await?;
100
101 Ok(Value::Null)
102 }
103
104 #[api(
105 input: {
106 properties: {
107 store: {
108 schema: DATASTORE_SCHEMA,
109 },
110 "output-format": {
111 schema: OUTPUT_FORMAT,
112 optional: true,
113 },
114 }
115 }
116 )]
117 /// Show garbage collection status for a specific datastore.
118 async fn garbage_collection_status(param: Value) -> Result<Value, Error> {
119
120 let output_format = get_output_format(&param);
121
122 let store = tools::required_string_param(&param, "store")?;
123
124 let client = connect()?;
125
126 let path = format!("api2/json/admin/datastore/{}/gc", store);
127
128 let mut result = client.get(&path, None).await?;
129 let mut data = result["data"].take();
130 let schema = api2::admin::datastore::API_RETURN_SCHEMA_GARBAGE_COLLECTION_STATUS;
131
132 let options = default_table_format_options();
133
134 format_and_print_result_full(&mut data, schema, &output_format, &options);
135
136 Ok(Value::Null)
137 }
138
139 fn garbage_collection_commands() -> CommandLineInterface {
140
141 let cmd_def = CliCommandMap::new()
142 .insert("status",
143 CliCommand::new(&API_METHOD_GARBAGE_COLLECTION_STATUS)
144 .arg_param(&["store"])
145 .completion_cb("store", config::datastore::complete_datastore_name)
146 )
147 .insert("start",
148 CliCommand::new(&API_METHOD_START_GARBAGE_COLLECTION)
149 .arg_param(&["store"])
150 .completion_cb("store", config::datastore::complete_datastore_name)
151 );
152
153 cmd_def.into()
154 }
155
156 #[api(
157 input: {
158 properties: {
159 limit: {
160 description: "The maximal number of tasks to list.",
161 type: Integer,
162 optional: true,
163 minimum: 1,
164 maximum: 1000,
165 default: 50,
166 },
167 "output-format": {
168 schema: OUTPUT_FORMAT,
169 optional: true,
170 },
171 all: {
172 type: Boolean,
173 description: "Also list stopped tasks.",
174 optional: true,
175 }
176 }
177 }
178 )]
179 /// List running server tasks.
180 async fn task_list(param: Value) -> Result<Value, Error> {
181
182 let output_format = get_output_format(&param);
183
184 let client = connect()?;
185
186 let limit = param["limit"].as_u64().unwrap_or(50) as usize;
187 let running = !param["all"].as_bool().unwrap_or(false);
188 let args = json!({
189 "running": running,
190 "start": 0,
191 "limit": limit,
192 });
193 let mut result = client.get("api2/json/nodes/localhost/tasks", Some(args)).await?;
194
195 let mut data = result["data"].take();
196 let schema = api2::node::tasks::API_RETURN_SCHEMA_LIST_TASKS;
197
198 let options = default_table_format_options()
199 .column(ColumnConfig::new("starttime").right_align(false).renderer(tools::format::render_epoch))
200 .column(ColumnConfig::new("endtime").right_align(false).renderer(tools::format::render_epoch))
201 .column(ColumnConfig::new("upid"))
202 .column(ColumnConfig::new("status").renderer(tools::format::render_task_status));
203
204 format_and_print_result_full(&mut data, schema, &output_format, &options);
205
206 Ok(Value::Null)
207 }
208
209 #[api(
210 input: {
211 properties: {
212 upid: {
213 schema: UPID_SCHEMA,
214 },
215 }
216 }
217 )]
218 /// Display the task log.
219 async fn task_log(param: Value) -> Result<Value, Error> {
220
221 let upid = tools::required_string_param(&param, "upid")?;
222
223 let client = connect()?;
224
225 display_task_log(client, upid, true).await?;
226
227 Ok(Value::Null)
228 }
229
230 #[api(
231 input: {
232 properties: {
233 upid: {
234 schema: UPID_SCHEMA,
235 },
236 }
237 }
238 )]
239 /// Try to stop a specific task.
240 async fn task_stop(param: Value) -> Result<Value, Error> {
241
242 let upid_str = tools::required_string_param(&param, "upid")?;
243
244 let mut client = connect()?;
245
246 let path = format!("api2/json/nodes/localhost/tasks/{}", upid_str);
247 let _ = client.delete(&path, None).await?;
248
249 Ok(Value::Null)
250 }
251
252 fn task_mgmt_cli() -> CommandLineInterface {
253
254 let task_log_cmd_def = CliCommand::new(&API_METHOD_TASK_LOG)
255 .arg_param(&["upid"]);
256
257 let task_stop_cmd_def = CliCommand::new(&API_METHOD_TASK_STOP)
258 .arg_param(&["upid"]);
259
260 let cmd_def = CliCommandMap::new()
261 .insert("list", CliCommand::new(&API_METHOD_TASK_LIST))
262 .insert("log", task_log_cmd_def)
263 .insert("stop", task_stop_cmd_def);
264
265 cmd_def.into()
266 }
267
268 // fixme: avoid API redefinition
269 #[api(
270 input: {
271 properties: {
272 "local-store": {
273 schema: DATASTORE_SCHEMA,
274 },
275 remote: {
276 schema: REMOTE_ID_SCHEMA,
277 },
278 "remote-store": {
279 schema: DATASTORE_SCHEMA,
280 },
281 "remove-vanished": {
282 schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
283 optional: true,
284 },
285 "output-format": {
286 schema: OUTPUT_FORMAT,
287 optional: true,
288 },
289 }
290 }
291 )]
292 /// Sync datastore from another repository
293 async fn pull_datastore(
294 remote: String,
295 remote_store: String,
296 local_store: String,
297 remove_vanished: Option<bool>,
298 param: Value,
299 ) -> Result<Value, Error> {
300
301 let output_format = get_output_format(&param);
302
303 let mut client = connect()?;
304
305 let mut args = json!({
306 "store": local_store,
307 "remote": remote,
308 "remote-store": remote_store,
309 });
310
311 if let Some(remove_vanished) = remove_vanished {
312 args["remove-vanished"] = Value::from(remove_vanished);
313 }
314
315 let result = client.post("api2/json/pull", Some(args)).await?;
316
317 view_task_result(client, result, &output_format).await?;
318
319 Ok(Value::Null)
320 }
321
322 fn main() {
323
324 let cmd_def = CliCommandMap::new()
325 .insert("acl", acl_commands())
326 .insert("datastore", datastore_commands())
327 .insert("disk", disk_commands())
328 .insert("dns", dns_commands())
329 .insert("network", network_commands())
330 .insert("user", user_commands())
331 .insert("remote", remote_commands())
332 .insert("garbage-collection", garbage_collection_commands())
333 .insert("cert", cert_mgmt_cli())
334 .insert("sync-job", sync_job_commands())
335 .insert("task", task_mgmt_cli())
336 .insert(
337 "pull",
338 CliCommand::new(&API_METHOD_PULL_DATASTORE)
339 .arg_param(&["remote", "remote-store", "local-store"])
340 .completion_cb("local-store", config::datastore::complete_datastore_name)
341 .completion_cb("remote", config::remote::complete_remote_name)
342 .completion_cb("remote-store", complete_remote_datastore_name)
343 );
344
345 let mut rpcenv = CliEnvironment::new();
346 rpcenv.set_user(Some(String::from("root@pam")));
347
348 proxmox_backup::tools::runtime::main(run_async_cli_command(cmd_def, rpcenv));
349 }
350
351 // shell completion helper
352 pub fn complete_remote_datastore_name(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
353
354 let mut list = Vec::new();
355
356 let _ = proxmox::try_block!({
357 let remote = param.get("remote").ok_or_else(|| format_err!("no remote"))?;
358 let (remote_config, _digest) = config::remote::config()?;
359
360 let remote: config::remote::Remote = remote_config.lookup("remote", &remote)?;
361
362 let options = HttpClientOptions::new()
363 .password(Some(remote.password.clone()))
364 .fingerprint(remote.fingerprint.clone());
365
366 let client = HttpClient::new(
367 &remote.host,
368 &remote.userid,
369 options,
370 )?;
371
372 let result = crate::tools::runtime::block_on(client.get("api2/json/admin/datastore", None))?;
373
374 if let Some(data) = result["data"].as_array() {
375 for item in data {
376 if let Some(store) = item["store"].as_str() {
377 list.push(store.to_owned());
378 }
379 }
380 }
381
382 Ok(())
383 }).map_err(|_err: Error| { /* ignore */ });
384
385 list
386 }