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