]>
Commit | Line | Data |
---|---|---|
550e0d88 | 1 | use std::path::PathBuf; |
331b869d | 2 | use std::collections::HashMap; |
ea0b8b6e | 3 | |
f7d4e4b5 | 4 | use anyhow::{bail, format_err, Error}; |
9894469e | 5 | use serde_json::{json, Value}; |
9894469e | 6 | |
9e165b5c | 7 | use proxmox::api::{api, cli::*, RpcEnvironment, ApiHandler}; |
769f8c99 | 8 | |
550e0d88 | 9 | use proxmox_backup::configdir; |
769f8c99 | 10 | use proxmox_backup::tools; |
f357390c | 11 | use proxmox_backup::config::{self, remote::{self, Remote}}; |
9894469e | 12 | use proxmox_backup::api2::{self, types::* }; |
769f8c99 DM |
13 | use proxmox_backup::client::*; |
14 | use proxmox_backup::tools::ticket::*; | |
15 | use proxmox_backup::auth_helpers::*; | |
16 | ||
769f8c99 DM |
17 | async fn view_task_result( |
18 | client: HttpClient, | |
19 | result: Value, | |
20 | output_format: &str, | |
21 | ) -> Result<(), Error> { | |
22 | let data = &result["data"]; | |
23 | if output_format == "text" { | |
24 | if let Some(upid) = data.as_str() { | |
25 | display_task_log(client, upid, true).await?; | |
26 | } | |
27 | } else { | |
28 | format_and_print_result(&data, &output_format); | |
29 | } | |
30 | ||
31 | Ok(()) | |
32 | } | |
211fabd7 | 33 | |
47d47121 DM |
34 | fn connect() -> Result<HttpClient, Error> { |
35 | ||
36 | let uid = nix::unistd::Uid::current(); | |
37 | ||
d59dbeca | 38 | let mut options = HttpClientOptions::new() |
5030b7ce | 39 | .prefix(Some("proxmox-backup".to_string())) |
d59dbeca DM |
40 | .verify_cert(false); // not required for connection to localhost |
41 | ||
47d47121 DM |
42 | let client = if uid.is_root() { |
43 | let ticket = assemble_rsa_ticket(private_auth_key(), "PBS", Some("root@pam"), None)?; | |
d59dbeca DM |
44 | options = options.password(Some(ticket)); |
45 | HttpClient::new("localhost", "root@pam", options)? | |
47d47121 | 46 | } else { |
d59dbeca DM |
47 | options = options.ticket_cache(true).interactive(true); |
48 | HttpClient::new("localhost", "root@pam", options)? | |
47d47121 DM |
49 | }; |
50 | ||
51 | Ok(client) | |
52 | } | |
53 | ||
9894469e DM |
54 | #[api( |
55 | input: { | |
56 | properties: { | |
57 | "output-format": { | |
58 | schema: OUTPUT_FORMAT, | |
59 | optional: true, | |
60 | }, | |
61 | } | |
62 | } | |
63 | )] | |
64 | /// List configured remotes. | |
9e165b5c | 65 | fn list_remotes(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> { |
688fbe07 | 66 | |
ac3faaf5 | 67 | let output_format = get_output_format(¶m); |
9894469e | 68 | |
9e165b5c DM |
69 | let info = &api2::config::remote::API_METHOD_LIST_REMOTES; |
70 | let mut data = match info.handler { | |
71 | ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?, | |
72 | _ => unreachable!(), | |
73 | }; | |
9894469e | 74 | |
ac3faaf5 | 75 | let options = default_table_format_options() |
93fbb4ef DM |
76 | .column(ColumnConfig::new("name")) |
77 | .column(ColumnConfig::new("host")) | |
78 | .column(ColumnConfig::new("userid")) | |
79 | .column(ColumnConfig::new("fingerprint")) | |
80 | .column(ColumnConfig::new("comment")); | |
9894469e | 81 | |
9e165b5c | 82 | format_and_print_result_full(&mut data, info.returns, &output_format, &options); |
9894469e DM |
83 | |
84 | Ok(Value::Null) | |
85 | } | |
86 | ||
87 | fn remote_commands() -> CommandLineInterface { | |
688fbe07 DM |
88 | |
89 | let cmd_def = CliCommandMap::new() | |
9894469e | 90 | .insert("list", CliCommand::new(&&API_METHOD_LIST_REMOTES)) |
688fbe07 DM |
91 | .insert( |
92 | "create", | |
93 | // fixme: howto handle password parameter? | |
f357390c | 94 | CliCommand::new(&api2::config::remote::API_METHOD_CREATE_REMOTE) |
688fbe07 DM |
95 | .arg_param(&["name"]) |
96 | ) | |
08195ac8 DM |
97 | .insert( |
98 | "update", | |
f357390c | 99 | CliCommand::new(&api2::config::remote::API_METHOD_UPDATE_REMOTE) |
08195ac8 | 100 | .arg_param(&["name"]) |
f357390c | 101 | .completion_cb("name", config::remote::complete_remote_name) |
08195ac8 | 102 | ) |
688fbe07 DM |
103 | .insert( |
104 | "remove", | |
f357390c | 105 | CliCommand::new(&api2::config::remote::API_METHOD_DELETE_REMOTE) |
688fbe07 | 106 | .arg_param(&["name"]) |
f357390c | 107 | .completion_cb("name", config::remote::complete_remote_name) |
688fbe07 DM |
108 | ); |
109 | ||
110 | cmd_def.into() | |
111 | } | |
112 | ||
579728c6 DM |
113 | #[api( |
114 | input: { | |
115 | properties: { | |
116 | "output-format": { | |
117 | schema: OUTPUT_FORMAT, | |
118 | optional: true, | |
119 | }, | |
120 | } | |
121 | } | |
122 | )] | |
123 | /// List configured users. | |
124 | fn list_users(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> { | |
125 | ||
126 | let output_format = get_output_format(¶m); | |
127 | ||
685e1334 | 128 | let info = &api2::access::user::API_METHOD_LIST_USERS; |
579728c6 DM |
129 | let mut data = match info.handler { |
130 | ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?, | |
131 | _ => unreachable!(), | |
132 | }; | |
133 | ||
134 | let options = default_table_format_options() | |
135 | .column(ColumnConfig::new("userid")) | |
b080583b DM |
136 | .column( |
137 | ColumnConfig::new("enable") | |
138 | .renderer(tools::format::render_bool_with_default_true) | |
139 | ) | |
140 | .column( | |
141 | ColumnConfig::new("expire") | |
142 | .renderer(tools::format::render_epoch) | |
143 | ) | |
579728c6 DM |
144 | .column(ColumnConfig::new("firstname")) |
145 | .column(ColumnConfig::new("lastname")) | |
146 | .column(ColumnConfig::new("email")) | |
147 | .column(ColumnConfig::new("comment")); | |
148 | ||
149 | format_and_print_result_full(&mut data, info.returns, &output_format, &options); | |
150 | ||
151 | Ok(Value::Null) | |
152 | } | |
153 | ||
154 | fn user_commands() -> CommandLineInterface { | |
155 | ||
156 | let cmd_def = CliCommandMap::new() | |
157 | .insert("list", CliCommand::new(&&API_METHOD_LIST_USERS)) | |
158 | .insert( | |
159 | "create", | |
160 | // fixme: howto handle password parameter? | |
685e1334 | 161 | CliCommand::new(&api2::access::user::API_METHOD_CREATE_USER) |
579728c6 DM |
162 | .arg_param(&["userid"]) |
163 | ) | |
164 | .insert( | |
165 | "update", | |
685e1334 | 166 | CliCommand::new(&api2::access::user::API_METHOD_UPDATE_USER) |
579728c6 DM |
167 | .arg_param(&["userid"]) |
168 | .completion_cb("userid", config::user::complete_user_name) | |
169 | ) | |
170 | .insert( | |
171 | "remove", | |
685e1334 | 172 | CliCommand::new(&api2::access::user::API_METHOD_DELETE_USER) |
579728c6 DM |
173 | .arg_param(&["userid"]) |
174 | .completion_cb("userid", config::user::complete_user_name) | |
175 | ); | |
176 | ||
177 | cmd_def.into() | |
178 | } | |
179 | ||
ed3e60ae DM |
180 | #[api( |
181 | input: { | |
182 | properties: { | |
183 | "output-format": { | |
184 | schema: OUTPUT_FORMAT, | |
185 | optional: true, | |
186 | }, | |
187 | } | |
188 | } | |
189 | )] | |
190 | /// Access Control list. | |
191 | fn list_acls(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> { | |
192 | ||
193 | let output_format = get_output_format(¶m); | |
194 | ||
195 | let info = &api2::access::acl::API_METHOD_READ_ACL; | |
196 | let mut data = match info.handler { | |
197 | ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?, | |
198 | _ => unreachable!(), | |
199 | }; | |
200 | ||
201 | fn render_ugid(value: &Value, record: &Value) -> Result<String, Error> { | |
202 | if value.is_null() { return Ok(String::new()); } | |
203 | let ugid = value.as_str().unwrap(); | |
204 | let ugid_type = record["ugid_type"].as_str().unwrap(); | |
205 | ||
206 | if ugid_type == "user" { | |
207 | Ok(ugid.to_string()) | |
208 | } else if ugid_type == "group" { | |
209 | Ok(format!("@{}", ugid)) | |
210 | } else { | |
211 | bail!("render_ugid: got unknown ugid_type"); | |
212 | } | |
213 | } | |
214 | ||
215 | let options = default_table_format_options() | |
216 | .column(ColumnConfig::new("ugid").renderer(render_ugid)) | |
217 | .column(ColumnConfig::new("path")) | |
218 | .column(ColumnConfig::new("propagate")) | |
219 | .column(ColumnConfig::new("roleid")); | |
220 | ||
221 | format_and_print_result_full(&mut data, info.returns, &output_format, &options); | |
222 | ||
223 | Ok(Value::Null) | |
224 | } | |
225 | ||
226 | fn acl_commands() -> CommandLineInterface { | |
227 | ||
228 | let cmd_def = CliCommandMap::new() | |
9765092e DM |
229 | .insert("list", CliCommand::new(&&API_METHOD_LIST_ACLS)) |
230 | .insert( | |
231 | "update", | |
232 | CliCommand::new(&api2::access::acl::API_METHOD_UPDATE_ACL) | |
233 | .arg_param(&["path", "role"]) | |
234 | .completion_cb("userid", config::user::complete_user_name) | |
235 | .completion_cb("path", config::datastore::complete_acl_path) | |
236 | ||
237 | ); | |
ed3e60ae DM |
238 | |
239 | cmd_def.into() | |
240 | } | |
241 | ||
bf004ecd DM |
242 | #[api( |
243 | input: { | |
244 | properties: { | |
245 | "output-format": { | |
246 | schema: OUTPUT_FORMAT, | |
247 | optional: true, | |
248 | }, | |
249 | } | |
250 | } | |
251 | )] | |
252 | /// Network device list. | |
76227a6a | 253 | fn list_network_devices(mut param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> { |
bf004ecd DM |
254 | |
255 | let output_format = get_output_format(¶m); | |
256 | ||
76227a6a DM |
257 | param["node"] = "localhost".into(); |
258 | ||
26d9aebc | 259 | let info = &api2::node::network::API_METHOD_LIST_NETWORK_DEVICES; |
bf004ecd DM |
260 | let mut data = match info.handler { |
261 | ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?, | |
262 | _ => unreachable!(), | |
263 | }; | |
264 | ||
2eefd9ae DM |
265 | if let Some(changes) = rpcenv.get_result_attrib("changes") { |
266 | if let Some(diff) = changes.as_str() { | |
3181f9b6 DM |
267 | if output_format == "text" { |
268 | eprintln!("pending changes:\n{}\n", diff); | |
269 | } | |
2eefd9ae DM |
270 | } |
271 | } | |
272 | ||
4cb6bd89 DM |
273 | fn render_address(_value: &Value, record: &Value) -> Result<String, Error> { |
274 | let mut text = String::new(); | |
275 | ||
7b22acd0 | 276 | if let Some(cidr) = record["cidr"].as_str() { |
4cb6bd89 DM |
277 | text.push_str(cidr); |
278 | } | |
7b22acd0 | 279 | if let Some(cidr) = record["cidr6"].as_str() { |
4cb6bd89 DM |
280 | if !text.is_empty() { text.push('\n'); } |
281 | text.push_str(cidr); | |
282 | } | |
283 | ||
284 | Ok(text) | |
285 | } | |
286 | ||
287 | fn render_gateway(_value: &Value, record: &Value) -> Result<String, Error> { | |
288 | let mut text = String::new(); | |
289 | ||
7b22acd0 | 290 | if let Some(gateway) = record["gateway"].as_str() { |
4cb6bd89 DM |
291 | text.push_str(gateway); |
292 | } | |
7b22acd0 | 293 | if let Some(gateway) = record["gateway6"].as_str() { |
4cb6bd89 DM |
294 | if !text.is_empty() { text.push('\n'); } |
295 | text.push_str(gateway); | |
296 | } | |
297 | ||
298 | Ok(text) | |
299 | } | |
300 | ||
bf004ecd | 301 | let options = default_table_format_options() |
7b22acd0 | 302 | .column(ColumnConfig::new("type").header("type")) |
bf004ecd | 303 | .column(ColumnConfig::new("name")) |
7b22acd0 DM |
304 | .column(ColumnConfig::new("autostart")) |
305 | .column(ColumnConfig::new("method")) | |
306 | .column(ColumnConfig::new("method6")) | |
307 | .column(ColumnConfig::new("cidr").header("address").renderer(render_address)) | |
308 | .column(ColumnConfig::new("gateway").header("gateway").renderer(render_gateway)); | |
bf004ecd DM |
309 | |
310 | format_and_print_result_full(&mut data, info.returns, &output_format, &options); | |
311 | ||
312 | Ok(Value::Null) | |
313 | } | |
314 | ||
c67bc9c3 DM |
315 | #[api()] |
316 | /// Show pending configuration changes (diff) | |
76227a6a DM |
317 | fn pending_network_changes(mut param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> { |
318 | param["node"] = "localhost".into(); | |
c67bc9c3 | 319 | |
26d9aebc | 320 | let info = &api2::node::network::API_METHOD_LIST_NETWORK_DEVICES; |
c67bc9c3 DM |
321 | let _data = match info.handler { |
322 | ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?, | |
323 | _ => unreachable!(), | |
324 | }; | |
325 | ||
326 | if let Some(changes) = rpcenv.get_result_attrib("changes") { | |
327 | if let Some(diff) = changes.as_str() { | |
328 | println!("{}", diff); | |
329 | } | |
330 | } | |
331 | ||
332 | Ok(Value::Null) | |
333 | } | |
334 | ||
ca0e5347 DM |
335 | fn network_commands() -> CommandLineInterface { |
336 | ||
337 | let cmd_def = CliCommandMap::new() | |
26d9aebc DM |
338 | .insert( |
339 | "list", | |
340 | CliCommand::new(&API_METHOD_LIST_NETWORK_DEVICES) | |
26d9aebc DM |
341 | ) |
342 | .insert( | |
343 | "changes", | |
344 | CliCommand::new(&API_METHOD_PENDING_NETWORK_CHANGES) | |
26d9aebc | 345 | ) |
86a5d56c DM |
346 | .insert( |
347 | "create", | |
348 | CliCommand::new(&api2::node::network::API_METHOD_CREATE_INTERFACE) | |
349 | .fixed_param("node", String::from("localhost")) | |
350 | .arg_param(&["iface"]) | |
351 | .completion_cb("iface", config::network::complete_interface_name) | |
352 | ) | |
2eefd9ae DM |
353 | .insert( |
354 | "update", | |
26d9aebc DM |
355 | CliCommand::new(&api2::node::network::API_METHOD_UPDATE_INTERFACE) |
356 | .fixed_param("node", String::from("localhost")) | |
7b22acd0 DM |
357 | .arg_param(&["iface"]) |
358 | .completion_cb("iface", config::network::complete_interface_name) | |
df6bb03d | 359 | ) |
2eefd9ae DM |
360 | .insert( |
361 | "remove", | |
26d9aebc DM |
362 | CliCommand::new(&api2::node::network::API_METHOD_DELETE_INTERFACE) |
363 | .fixed_param("node", String::from("localhost")) | |
7b22acd0 DM |
364 | .arg_param(&["iface"]) |
365 | .completion_cb("iface", config::network::complete_interface_name) | |
2eefd9ae | 366 | ) |
26d9aebc DM |
367 | .insert( |
368 | "revert", | |
369 | CliCommand::new(&api2::node::network::API_METHOD_REVERT_NETWORK_CONFIG) | |
370 | .fixed_param("node", String::from("localhost")) | |
371 | ) | |
2eefd9ae DM |
372 | .insert( |
373 | "reload", | |
26d9aebc DM |
374 | CliCommand::new(&api2::node::network::API_METHOD_RELOAD_NETWORK_CONFIG) |
375 | .fixed_param("node", String::from("localhost")) | |
df6bb03d | 376 | ); |
ca0e5347 DM |
377 | |
378 | cmd_def.into() | |
379 | } | |
380 | ||
14627d67 DM |
381 | #[api( |
382 | input: { | |
383 | properties: { | |
384 | "output-format": { | |
385 | schema: OUTPUT_FORMAT, | |
386 | optional: true, | |
387 | }, | |
388 | } | |
389 | } | |
390 | )] | |
391 | /// Read DNS settings | |
392 | fn get_dns(mut param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> { | |
393 | ||
394 | let output_format = get_output_format(¶m); | |
395 | ||
396 | param["node"] = "localhost".into(); | |
397 | ||
398 | let info = &api2::node::dns::API_METHOD_GET_DNS; | |
399 | let mut data = match info.handler { | |
400 | ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?, | |
401 | _ => unreachable!(), | |
402 | }; | |
403 | ||
404 | ||
405 | let options = default_table_format_options() | |
406 | .column(ColumnConfig::new("search")) | |
407 | .column(ColumnConfig::new("dns1")) | |
408 | .column(ColumnConfig::new("dns2")) | |
409 | .column(ColumnConfig::new("dns3")); | |
410 | ||
411 | format_and_print_result_full(&mut data, info.returns, &output_format, &options); | |
412 | ||
413 | Ok(Value::Null) | |
414 | } | |
415 | ||
416 | fn dns_commands() -> CommandLineInterface { | |
417 | ||
418 | let cmd_def = CliCommandMap::new() | |
419 | .insert( | |
420 | "get", | |
421 | CliCommand::new(&API_METHOD_GET_DNS) | |
422 | ) | |
423 | .insert( | |
424 | "set", | |
425 | CliCommand::new(&api2::node::dns::API_METHOD_UPDATE_DNS) | |
426 | .fixed_param("node", String::from("localhost")) | |
427 | ); | |
428 | ||
429 | cmd_def.into() | |
430 | } | |
431 | ||
b1564af2 DM |
432 | #[api( |
433 | input: { | |
434 | properties: { | |
435 | "output-format": { | |
436 | schema: OUTPUT_FORMAT, | |
437 | optional: true, | |
438 | }, | |
439 | } | |
440 | } | |
441 | )] | |
442 | /// Datastore list. | |
443 | fn list_datastores(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> { | |
444 | ||
445 | let output_format = get_output_format(¶m); | |
446 | ||
447 | let info = &api2::config::datastore::API_METHOD_LIST_DATASTORES; | |
448 | let mut data = match info.handler { | |
449 | ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?, | |
450 | _ => unreachable!(), | |
451 | }; | |
452 | ||
453 | let options = default_table_format_options() | |
454 | .column(ColumnConfig::new("name")) | |
455 | .column(ColumnConfig::new("path")) | |
456 | .column(ColumnConfig::new("comment")); | |
457 | ||
458 | format_and_print_result_full(&mut data, info.returns, &output_format, &options); | |
459 | ||
460 | Ok(Value::Null) | |
461 | } | |
462 | ||
9f6ab1fc | 463 | fn datastore_commands() -> CommandLineInterface { |
ea0b8b6e | 464 | |
6460764d | 465 | let cmd_def = CliCommandMap::new() |
b1564af2 | 466 | .insert("list", CliCommand::new(&API_METHOD_LIST_DATASTORES)) |
6460764d | 467 | .insert("create", |
688fbe07 | 468 | CliCommand::new(&api2::config::datastore::API_METHOD_CREATE_DATASTORE) |
49fddd98 | 469 | .arg_param(&["name", "path"]) |
48ef3c33 | 470 | ) |
ddc52662 DM |
471 | .insert("update", |
472 | CliCommand::new(&api2::config::datastore::API_METHOD_UPDATE_DATASTORE) | |
473 | .arg_param(&["name"]) | |
3be839c6 | 474 | .completion_cb("name", config::datastore::complete_datastore_name) |
ddc52662 | 475 | ) |
6460764d | 476 | .insert("remove", |
688fbe07 | 477 | CliCommand::new(&api2::config::datastore::API_METHOD_DELETE_DATASTORE) |
49fddd98 | 478 | .arg_param(&["name"]) |
07b4694a | 479 | .completion_cb("name", config::datastore::complete_datastore_name) |
48ef3c33 | 480 | ); |
211fabd7 | 481 | |
8f62336b | 482 | cmd_def.into() |
211fabd7 DM |
483 | } |
484 | ||
691c89a0 | 485 | |
769f8c99 DM |
486 | #[api( |
487 | input: { | |
488 | properties: { | |
489 | store: { | |
490 | schema: DATASTORE_SCHEMA, | |
491 | }, | |
492 | "output-format": { | |
493 | schema: OUTPUT_FORMAT, | |
494 | optional: true, | |
495 | }, | |
496 | } | |
497 | } | |
498 | )] | |
499 | /// Start garbage collection for a specific datastore. | |
500 | async fn start_garbage_collection(param: Value) -> Result<Value, Error> { | |
691c89a0 | 501 | |
ac3faaf5 | 502 | let output_format = get_output_format(¶m); |
691c89a0 | 503 | |
769f8c99 DM |
504 | let store = tools::required_string_param(¶m, "store")?; |
505 | ||
47d47121 | 506 | let mut client = connect()?; |
769f8c99 DM |
507 | |
508 | let path = format!("api2/json/admin/datastore/{}/gc", store); | |
509 | ||
510 | let result = client.post(&path, None).await?; | |
511 | ||
512 | view_task_result(client, result, &output_format).await?; | |
513 | ||
514 | Ok(Value::Null) | |
515 | } | |
516 | ||
517 | #[api( | |
518 | input: { | |
519 | properties: { | |
520 | store: { | |
521 | schema: DATASTORE_SCHEMA, | |
522 | }, | |
523 | "output-format": { | |
524 | schema: OUTPUT_FORMAT, | |
525 | optional: true, | |
526 | }, | |
527 | } | |
528 | } | |
529 | )] | |
530 | /// Show garbage collection status for a specific datastore. | |
531 | async fn garbage_collection_status(param: Value) -> Result<Value, Error> { | |
532 | ||
ac3faaf5 | 533 | let output_format = get_output_format(¶m); |
769f8c99 DM |
534 | |
535 | let store = tools::required_string_param(¶m, "store")?; | |
536 | ||
47d47121 | 537 | let client = connect()?; |
769f8c99 DM |
538 | |
539 | let path = format!("api2/json/admin/datastore/{}/gc", store); | |
540 | ||
9894469e DM |
541 | let mut result = client.get(&path, None).await?; |
542 | let mut data = result["data"].take(); | |
543 | let schema = api2::admin::datastore::API_RETURN_SCHEMA_GARBAGE_COLLECTION_STATUS; | |
544 | ||
ac3faaf5 | 545 | let options = default_table_format_options(); |
9894469e DM |
546 | |
547 | format_and_print_result_full(&mut data, schema, &output_format, &options); | |
769f8c99 DM |
548 | |
549 | Ok(Value::Null) | |
550 | } | |
551 | ||
552 | fn garbage_collection_commands() -> CommandLineInterface { | |
691c89a0 DM |
553 | |
554 | let cmd_def = CliCommandMap::new() | |
555 | .insert("status", | |
769f8c99 | 556 | CliCommand::new(&API_METHOD_GARBAGE_COLLECTION_STATUS) |
49fddd98 | 557 | .arg_param(&["store"]) |
9ac1045c | 558 | .completion_cb("store", config::datastore::complete_datastore_name) |
48ef3c33 | 559 | ) |
691c89a0 | 560 | .insert("start", |
769f8c99 | 561 | CliCommand::new(&API_METHOD_START_GARBAGE_COLLECTION) |
49fddd98 | 562 | .arg_param(&["store"]) |
9ac1045c | 563 | .completion_cb("store", config::datastore::complete_datastore_name) |
48ef3c33 | 564 | ); |
691c89a0 DM |
565 | |
566 | cmd_def.into() | |
567 | } | |
568 | ||
47d47121 DM |
569 | #[api( |
570 | input: { | |
571 | properties: { | |
572 | limit: { | |
573 | description: "The maximal number of tasks to list.", | |
574 | type: Integer, | |
575 | optional: true, | |
576 | minimum: 1, | |
577 | maximum: 1000, | |
578 | default: 50, | |
579 | }, | |
580 | "output-format": { | |
581 | schema: OUTPUT_FORMAT, | |
582 | optional: true, | |
583 | }, | |
584 | all: { | |
585 | type: Boolean, | |
586 | description: "Also list stopped tasks.", | |
587 | optional: true, | |
588 | } | |
589 | } | |
590 | } | |
591 | )] | |
592 | /// List running server tasks. | |
593 | async fn task_list(param: Value) -> Result<Value, Error> { | |
594 | ||
ac3faaf5 | 595 | let output_format = get_output_format(¶m); |
47d47121 DM |
596 | |
597 | let client = connect()?; | |
598 | ||
599 | let limit = param["limit"].as_u64().unwrap_or(50) as usize; | |
600 | let running = !param["all"].as_bool().unwrap_or(false); | |
601 | let args = json!({ | |
602 | "running": running, | |
603 | "start": 0, | |
604 | "limit": limit, | |
605 | }); | |
9894469e | 606 | let mut result = client.get("api2/json/nodes/localhost/tasks", Some(args)).await?; |
47d47121 | 607 | |
9894469e DM |
608 | let mut data = result["data"].take(); |
609 | let schema = api2::node::tasks::API_RETURN_SCHEMA_LIST_TASKS; | |
47d47121 | 610 | |
ac3faaf5 | 611 | let options = default_table_format_options() |
4939255f DM |
612 | .column(ColumnConfig::new("starttime").right_align(false).renderer(tools::format::render_epoch)) |
613 | .column(ColumnConfig::new("endtime").right_align(false).renderer(tools::format::render_epoch)) | |
93fbb4ef | 614 | .column(ColumnConfig::new("upid")) |
4939255f | 615 | .column(ColumnConfig::new("status").renderer(tools::format::render_task_status)); |
9894469e DM |
616 | |
617 | format_and_print_result_full(&mut data, schema, &output_format, &options); | |
47d47121 DM |
618 | |
619 | Ok(Value::Null) | |
620 | } | |
621 | ||
622 | #[api( | |
623 | input: { | |
624 | properties: { | |
625 | upid: { | |
626 | schema: UPID_SCHEMA, | |
627 | }, | |
628 | } | |
629 | } | |
630 | )] | |
631 | /// Display the task log. | |
632 | async fn task_log(param: Value) -> Result<Value, Error> { | |
633 | ||
634 | let upid = tools::required_string_param(¶m, "upid")?; | |
635 | ||
636 | let client = connect()?; | |
637 | ||
638 | display_task_log(client, upid, true).await?; | |
639 | ||
640 | Ok(Value::Null) | |
641 | } | |
642 | ||
643 | #[api( | |
644 | input: { | |
645 | properties: { | |
646 | upid: { | |
647 | schema: UPID_SCHEMA, | |
648 | }, | |
649 | } | |
650 | } | |
651 | )] | |
652 | /// Try to stop a specific task. | |
653 | async fn task_stop(param: Value) -> Result<Value, Error> { | |
654 | ||
655 | let upid_str = tools::required_string_param(¶m, "upid")?; | |
656 | ||
657 | let mut client = connect()?; | |
658 | ||
659 | let path = format!("api2/json/nodes/localhost/tasks/{}", upid_str); | |
660 | let _ = client.delete(&path, None).await?; | |
661 | ||
662 | Ok(Value::Null) | |
663 | } | |
664 | ||
665 | fn task_mgmt_cli() -> CommandLineInterface { | |
666 | ||
667 | let task_log_cmd_def = CliCommand::new(&API_METHOD_TASK_LOG) | |
668 | .arg_param(&["upid"]); | |
669 | ||
670 | let task_stop_cmd_def = CliCommand::new(&API_METHOD_TASK_STOP) | |
671 | .arg_param(&["upid"]); | |
672 | ||
673 | let cmd_def = CliCommandMap::new() | |
674 | .insert("list", CliCommand::new(&API_METHOD_TASK_LIST)) | |
675 | .insert("log", task_log_cmd_def) | |
676 | .insert("stop", task_stop_cmd_def); | |
677 | ||
678 | cmd_def.into() | |
679 | } | |
680 | ||
e739a8d8 DM |
681 | fn x509name_to_string(name: &openssl::x509::X509NameRef) -> Result<String, Error> { |
682 | let mut parts = Vec::new(); | |
683 | for entry in name.entries() { | |
684 | parts.push(format!("{} = {}", entry.object().nid().short_name()?, entry.data().as_utf8()?)); | |
685 | } | |
686 | Ok(parts.join(", ")) | |
687 | } | |
688 | ||
689 | #[api] | |
690 | /// Diplay node certificate information. | |
691 | fn cert_info() -> Result<(), Error> { | |
692 | ||
693 | let cert_path = PathBuf::from(configdir!("/proxy.pem")); | |
694 | ||
695 | let cert_pem = proxmox::tools::fs::file_get_contents(&cert_path)?; | |
696 | ||
697 | let cert = openssl::x509::X509::from_pem(&cert_pem)?; | |
698 | ||
699 | println!("Subject: {}", x509name_to_string(cert.subject_name())?); | |
700 | ||
701 | if let Some(san) = cert.subject_alt_names() { | |
702 | for name in san.iter() { | |
703 | if let Some(v) = name.dnsname() { | |
704 | println!(" DNS:{}", v); | |
705 | } else if let Some(v) = name.ipaddress() { | |
706 | println!(" IP:{:?}", v); | |
707 | } else if let Some(v) = name.email() { | |
708 | println!(" EMAIL:{}", v); | |
709 | } else if let Some(v) = name.uri() { | |
710 | println!(" URI:{}", v); | |
711 | } | |
712 | } | |
713 | } | |
714 | ||
715 | println!("Issuer: {}", x509name_to_string(cert.issuer_name())?); | |
716 | println!("Validity:"); | |
717 | println!(" Not Before: {}", cert.not_before()); | |
718 | println!(" Not After : {}", cert.not_after()); | |
719 | ||
720 | let fp = cert.digest(openssl::hash::MessageDigest::sha256())?; | |
721 | let fp_string = proxmox::tools::digest_to_hex(&fp); | |
722 | let fp_string = fp_string.as_bytes().chunks(2).map(|v| std::str::from_utf8(v).unwrap()) | |
723 | .collect::<Vec<&str>>().join(":"); | |
724 | ||
725 | println!("Fingerprint (sha256): {}", fp_string); | |
726 | ||
727 | let pubkey = cert.public_key()?; | |
728 | println!("Public key type: {}", openssl::nid::Nid::from_raw(pubkey.id().as_raw()).long_name()?); | |
729 | println!("Public key bits: {}", pubkey.bits()); | |
730 | ||
731 | Ok(()) | |
732 | } | |
733 | ||
550e0d88 DM |
734 | #[api( |
735 | input: { | |
736 | properties: { | |
737 | force: { | |
738 | description: "Force generation of new SSL certifate.", | |
739 | type: Boolean, | |
740 | optional:true, | |
741 | }, | |
742 | } | |
743 | }, | |
744 | )] | |
745 | /// Update node certificates and generate all needed files/directories. | |
746 | fn update_certs(force: Option<bool>) -> Result<(), Error> { | |
747 | ||
550e0d88 DM |
748 | config::create_configdir()?; |
749 | ||
750 | if let Err(err) = generate_auth_key() { | |
751 | bail!("unable to generate auth key - {}", err); | |
752 | } | |
753 | ||
754 | if let Err(err) = generate_csrf_key() { | |
755 | bail!("unable to generate csrf key - {}", err); | |
756 | } | |
757 | ||
f8fd5095 | 758 | config::update_self_signed_cert(force.unwrap_or(false))?; |
550e0d88 DM |
759 | |
760 | Ok(()) | |
761 | } | |
762 | ||
763 | fn cert_mgmt_cli() -> CommandLineInterface { | |
764 | ||
550e0d88 | 765 | let cmd_def = CliCommandMap::new() |
e739a8d8 DM |
766 | .insert("info", CliCommand::new(&API_METHOD_CERT_INFO)) |
767 | .insert("update", CliCommand::new(&API_METHOD_UPDATE_CERTS)); | |
550e0d88 DM |
768 | |
769 | cmd_def.into() | |
770 | } | |
771 | ||
4b4eba0b | 772 | // fixme: avoid API redefinition |
0eb0e024 DM |
773 | #[api( |
774 | input: { | |
775 | properties: { | |
eb506c83 | 776 | "local-store": { |
0eb0e024 DM |
777 | schema: DATASTORE_SCHEMA, |
778 | }, | |
779 | remote: { | |
167971ed | 780 | schema: REMOTE_ID_SCHEMA, |
0eb0e024 DM |
781 | }, |
782 | "remote-store": { | |
783 | schema: DATASTORE_SCHEMA, | |
784 | }, | |
4b4eba0b DM |
785 | delete: { |
786 | description: "Delete vanished backups. This remove the local copy if the remote backup was deleted.", | |
787 | type: Boolean, | |
788 | optional: true, | |
789 | default: true, | |
790 | }, | |
0eb0e024 DM |
791 | "output-format": { |
792 | schema: OUTPUT_FORMAT, | |
793 | optional: true, | |
794 | }, | |
795 | } | |
796 | } | |
797 | )] | |
eb506c83 DM |
798 | /// Sync datastore from another repository |
799 | async fn pull_datastore( | |
0eb0e024 DM |
800 | remote: String, |
801 | remote_store: String, | |
eb506c83 | 802 | local_store: String, |
4b4eba0b | 803 | delete: Option<bool>, |
ac3faaf5 | 804 | param: Value, |
0eb0e024 DM |
805 | ) -> Result<Value, Error> { |
806 | ||
ac3faaf5 | 807 | let output_format = get_output_format(¶m); |
0eb0e024 DM |
808 | |
809 | let mut client = connect()?; | |
810 | ||
4b4eba0b | 811 | let mut args = json!({ |
eb506c83 | 812 | "store": local_store, |
94609e23 | 813 | "remote": remote, |
0eb0e024 | 814 | "remote-store": remote_store, |
0eb0e024 DM |
815 | }); |
816 | ||
4b4eba0b DM |
817 | if let Some(delete) = delete { |
818 | args["delete"] = delete.into(); | |
819 | } | |
820 | ||
eb506c83 | 821 | let result = client.post("api2/json/pull", Some(args)).await?; |
0eb0e024 DM |
822 | |
823 | view_task_result(client, result, &output_format).await?; | |
824 | ||
825 | Ok(Value::Null) | |
826 | } | |
827 | ||
211fabd7 DM |
828 | fn main() { |
829 | ||
6460764d | 830 | let cmd_def = CliCommandMap::new() |
ed3e60ae | 831 | .insert("acl", acl_commands()) |
48ef3c33 | 832 | .insert("datastore", datastore_commands()) |
14627d67 | 833 | .insert("dns", dns_commands()) |
ca0e5347 | 834 | .insert("network", network_commands()) |
579728c6 | 835 | .insert("user", user_commands()) |
f357390c | 836 | .insert("remote", remote_commands()) |
47d47121 | 837 | .insert("garbage-collection", garbage_collection_commands()) |
550e0d88 | 838 | .insert("cert", cert_mgmt_cli()) |
0eb0e024 DM |
839 | .insert("task", task_mgmt_cli()) |
840 | .insert( | |
eb506c83 DM |
841 | "pull", |
842 | CliCommand::new(&API_METHOD_PULL_DATASTORE) | |
843 | .arg_param(&["remote", "remote-store", "local-store"]) | |
844 | .completion_cb("local-store", config::datastore::complete_datastore_name) | |
f357390c | 845 | .completion_cb("remote", config::remote::complete_remote_name) |
331b869d | 846 | .completion_cb("remote-store", complete_remote_datastore_name) |
0eb0e024 | 847 | ); |
34d3ba52 | 848 | |
7b22acd0 DM |
849 | let mut rpcenv = CliEnvironment::new(); |
850 | rpcenv.set_user(Some(String::from("root@pam"))); | |
851 | ||
852 | proxmox_backup::tools::runtime::main(run_async_cli_command(cmd_def, rpcenv)); | |
ea0b8b6e | 853 | } |
331b869d DM |
854 | |
855 | // shell completion helper | |
856 | pub fn complete_remote_datastore_name(_arg: &str, param: &HashMap<String, String>) -> Vec<String> { | |
857 | ||
858 | let mut list = Vec::new(); | |
859 | ||
9ea4bce4 | 860 | let _ = proxmox::try_block!({ |
331b869d | 861 | let remote = param.get("remote").ok_or_else(|| format_err!("no remote"))?; |
f357390c | 862 | let (remote_config, _digest) = remote::config()?; |
331b869d DM |
863 | |
864 | let remote: Remote = remote_config.lookup("remote", &remote)?; | |
865 | ||
d59dbeca DM |
866 | let options = HttpClientOptions::new() |
867 | .password(Some(remote.password.clone())) | |
868 | .fingerprint(remote.fingerprint.clone()); | |
869 | ||
331b869d DM |
870 | let client = HttpClient::new( |
871 | &remote.host, | |
872 | &remote.userid, | |
d59dbeca | 873 | options, |
331b869d DM |
874 | )?; |
875 | ||
03ac286c | 876 | let result = crate::tools::runtime::block_on(client.get("api2/json/admin/datastore", None))?; |
331b869d DM |
877 | |
878 | if let Some(data) = result["data"].as_array() { | |
879 | for item in data { | |
880 | if let Some(store) = item["store"].as_str() { | |
881 | list.push(store.to_owned()); | |
882 | } | |
883 | } | |
884 | } | |
885 | ||
886 | Ok(()) | |
887 | }).map_err(|_err: Error| { /* ignore */ }); | |
888 | ||
889 | list | |
890 | } |