]>
Commit | Line | Data |
---|---|---|
331b869d | 1 | use std::collections::HashMap; |
941342f7 | 2 | use std::io::{self, Write}; |
ea0b8b6e | 3 | |
1d9bc184 | 4 | use anyhow::Error; |
9894469e | 5 | use serde_json::{json, Value}; |
9894469e | 6 | |
25877d05 | 7 | use proxmox_sys::fs::CreateOptions; |
6ef1b649 WB |
8 | use proxmox_router::{cli::*, RpcEnvironment}; |
9 | use proxmox_schema::api; | |
769f8c99 | 10 | |
01a08021 | 11 | use pbs_client::{display_task_log, view_task_result}; |
1d9bc184 | 12 | use pbs_config::sync; |
3c8c2827 | 13 | use pbs_tools::json::required_string_param; |
577095e2 | 14 | use pbs_api_types::percent_encoding::percent_encode_component; |
6227654a | 15 | use pbs_api_types::{ |
1d9bc184 | 16 | GroupFilter, SyncJobConfig, |
71e53463 FG |
17 | DATASTORE_SCHEMA, GROUP_FILTER_LIST_SCHEMA, IGNORE_VERIFIED_BACKUPS_SCHEMA, REMOTE_ID_SCHEMA, |
18 | REMOVE_VANISHED_BACKUPS_SCHEMA, UPID_SCHEMA, VERIFICATION_OUTDATED_AFTER_SCHEMA, | |
6227654a | 19 | }; |
4805edc4 | 20 | |
b9700a9f DM |
21 | use proxmox_rest_server::wait_for_local_worker; |
22 | ||
6227654a | 23 | use proxmox_backup::api2; |
01a08021 WB |
24 | use proxmox_backup::client_helpers::connect_to_localhost; |
25 | use proxmox_backup::config; | |
769f8c99 | 26 | |
a220a456 DM |
27 | mod proxmox_backup_manager; |
28 | use proxmox_backup_manager::*; | |
29 | ||
769f8c99 DM |
30 | #[api( |
31 | input: { | |
32 | properties: { | |
33 | store: { | |
34 | schema: DATASTORE_SCHEMA, | |
35 | }, | |
36 | "output-format": { | |
37 | schema: OUTPUT_FORMAT, | |
38 | optional: true, | |
39 | }, | |
40 | } | |
41 | } | |
42 | )] | |
43 | /// Start garbage collection for a specific datastore. | |
44 | async fn start_garbage_collection(param: Value) -> Result<Value, Error> { | |
691c89a0 | 45 | |
ac3faaf5 | 46 | let output_format = get_output_format(¶m); |
691c89a0 | 47 | |
3c8c2827 | 48 | let store = required_string_param(¶m, "store")?; |
769f8c99 | 49 | |
d4877712 | 50 | let client = connect_to_localhost()?; |
769f8c99 DM |
51 | |
52 | let path = format!("api2/json/admin/datastore/{}/gc", store); | |
53 | ||
54 | let result = client.post(&path, None).await?; | |
55 | ||
d4877712 | 56 | view_task_result(&client, result, &output_format).await?; |
769f8c99 DM |
57 | |
58 | Ok(Value::Null) | |
59 | } | |
60 | ||
61 | #[api( | |
62 | input: { | |
63 | properties: { | |
64 | store: { | |
65 | schema: DATASTORE_SCHEMA, | |
66 | }, | |
67 | "output-format": { | |
68 | schema: OUTPUT_FORMAT, | |
69 | optional: true, | |
70 | }, | |
71 | } | |
72 | } | |
73 | )] | |
74 | /// Show garbage collection status for a specific datastore. | |
75 | async fn garbage_collection_status(param: Value) -> Result<Value, Error> { | |
76 | ||
ac3faaf5 | 77 | let output_format = get_output_format(¶m); |
769f8c99 | 78 | |
3c8c2827 | 79 | let store = required_string_param(¶m, "store")?; |
769f8c99 | 80 | |
6b68e5d5 | 81 | let client = connect_to_localhost()?; |
769f8c99 DM |
82 | |
83 | let path = format!("api2/json/admin/datastore/{}/gc", store); | |
84 | ||
9894469e DM |
85 | let mut result = client.get(&path, None).await?; |
86 | let mut data = result["data"].take(); | |
b2362a12 | 87 | let return_type = &api2::admin::datastore::API_METHOD_GARBAGE_COLLECTION_STATUS.returns; |
9894469e | 88 | |
ac3faaf5 | 89 | let options = default_table_format_options(); |
9894469e | 90 | |
b2362a12 | 91 | format_and_print_result_full(&mut data, return_type, &output_format, &options); |
769f8c99 DM |
92 | |
93 | Ok(Value::Null) | |
94 | } | |
95 | ||
96 | fn garbage_collection_commands() -> CommandLineInterface { | |
691c89a0 DM |
97 | |
98 | let cmd_def = CliCommandMap::new() | |
99 | .insert("status", | |
769f8c99 | 100 | CliCommand::new(&API_METHOD_GARBAGE_COLLECTION_STATUS) |
49fddd98 | 101 | .arg_param(&["store"]) |
e7d4be9d | 102 | .completion_cb("store", pbs_config::datastore::complete_datastore_name) |
48ef3c33 | 103 | ) |
691c89a0 | 104 | .insert("start", |
769f8c99 | 105 | CliCommand::new(&API_METHOD_START_GARBAGE_COLLECTION) |
49fddd98 | 106 | .arg_param(&["store"]) |
e7d4be9d | 107 | .completion_cb("store", pbs_config::datastore::complete_datastore_name) |
48ef3c33 | 108 | ); |
691c89a0 DM |
109 | |
110 | cmd_def.into() | |
111 | } | |
112 | ||
47d47121 DM |
113 | #[api( |
114 | input: { | |
115 | properties: { | |
116 | limit: { | |
117 | description: "The maximal number of tasks to list.", | |
118 | type: Integer, | |
119 | optional: true, | |
120 | minimum: 1, | |
121 | maximum: 1000, | |
122 | default: 50, | |
123 | }, | |
124 | "output-format": { | |
125 | schema: OUTPUT_FORMAT, | |
126 | optional: true, | |
127 | }, | |
128 | all: { | |
129 | type: Boolean, | |
130 | description: "Also list stopped tasks.", | |
131 | optional: true, | |
132 | } | |
133 | } | |
134 | } | |
135 | )] | |
136 | /// List running server tasks. | |
137 | async fn task_list(param: Value) -> Result<Value, Error> { | |
138 | ||
ac3faaf5 | 139 | let output_format = get_output_format(¶m); |
47d47121 | 140 | |
6b68e5d5 | 141 | let client = connect_to_localhost()?; |
47d47121 DM |
142 | |
143 | let limit = param["limit"].as_u64().unwrap_or(50) as usize; | |
144 | let running = !param["all"].as_bool().unwrap_or(false); | |
145 | let args = json!({ | |
146 | "running": running, | |
147 | "start": 0, | |
148 | "limit": limit, | |
149 | }); | |
9894469e | 150 | let mut result = client.get("api2/json/nodes/localhost/tasks", Some(args)).await?; |
47d47121 | 151 | |
9894469e | 152 | let mut data = result["data"].take(); |
b2362a12 | 153 | let return_type = &api2::node::tasks::API_METHOD_LIST_TASKS.returns; |
47d47121 | 154 | |
770a36e5 | 155 | use pbs_tools::format::{render_epoch, render_task_status}; |
ac3faaf5 | 156 | let options = default_table_format_options() |
770a36e5 WB |
157 | .column(ColumnConfig::new("starttime").right_align(false).renderer(render_epoch)) |
158 | .column(ColumnConfig::new("endtime").right_align(false).renderer(render_epoch)) | |
93fbb4ef | 159 | .column(ColumnConfig::new("upid")) |
770a36e5 | 160 | .column(ColumnConfig::new("status").renderer(render_task_status)); |
9894469e | 161 | |
b2362a12 | 162 | format_and_print_result_full(&mut data, return_type, &output_format, &options); |
47d47121 DM |
163 | |
164 | Ok(Value::Null) | |
165 | } | |
166 | ||
167 | #[api( | |
168 | input: { | |
169 | properties: { | |
170 | upid: { | |
171 | schema: UPID_SCHEMA, | |
172 | }, | |
173 | } | |
174 | } | |
175 | )] | |
176 | /// Display the task log. | |
177 | async fn task_log(param: Value) -> Result<Value, Error> { | |
178 | ||
3c8c2827 | 179 | let upid = required_string_param(¶m, "upid")?; |
47d47121 | 180 | |
d4877712 | 181 | let client = connect_to_localhost()?; |
47d47121 | 182 | |
d4877712 | 183 | display_task_log(&client, upid, true).await?; |
47d47121 DM |
184 | |
185 | Ok(Value::Null) | |
186 | } | |
187 | ||
188 | #[api( | |
189 | input: { | |
190 | properties: { | |
191 | upid: { | |
192 | schema: UPID_SCHEMA, | |
193 | }, | |
194 | } | |
195 | } | |
196 | )] | |
197 | /// Try to stop a specific task. | |
198 | async fn task_stop(param: Value) -> Result<Value, Error> { | |
199 | ||
3c8c2827 | 200 | let upid_str = required_string_param(¶m, "upid")?; |
47d47121 | 201 | |
d4877712 | 202 | let client = connect_to_localhost()?; |
47d47121 | 203 | |
4805edc4 | 204 | let path = format!("api2/json/nodes/localhost/tasks/{}", percent_encode_component(upid_str)); |
47d47121 DM |
205 | let _ = client.delete(&path, None).await?; |
206 | ||
207 | Ok(Value::Null) | |
208 | } | |
209 | ||
210 | fn task_mgmt_cli() -> CommandLineInterface { | |
211 | ||
212 | let task_log_cmd_def = CliCommand::new(&API_METHOD_TASK_LOG) | |
213 | .arg_param(&["upid"]); | |
214 | ||
215 | let task_stop_cmd_def = CliCommand::new(&API_METHOD_TASK_STOP) | |
216 | .arg_param(&["upid"]); | |
217 | ||
218 | let cmd_def = CliCommandMap::new() | |
219 | .insert("list", CliCommand::new(&API_METHOD_TASK_LIST)) | |
220 | .insert("log", task_log_cmd_def) | |
221 | .insert("stop", task_stop_cmd_def); | |
222 | ||
223 | cmd_def.into() | |
224 | } | |
225 | ||
4b4eba0b | 226 | // fixme: avoid API redefinition |
0eb0e024 DM |
227 | #[api( |
228 | input: { | |
229 | properties: { | |
eb506c83 | 230 | "local-store": { |
0eb0e024 DM |
231 | schema: DATASTORE_SCHEMA, |
232 | }, | |
233 | remote: { | |
167971ed | 234 | schema: REMOTE_ID_SCHEMA, |
0eb0e024 DM |
235 | }, |
236 | "remote-store": { | |
237 | schema: DATASTORE_SCHEMA, | |
238 | }, | |
40dc1031 DC |
239 | "remove-vanished": { |
240 | schema: REMOVE_VANISHED_BACKUPS_SCHEMA, | |
4b4eba0b | 241 | optional: true, |
4b4eba0b | 242 | }, |
062edce2 | 243 | "group-filter": { |
71e53463 FG |
244 | schema: GROUP_FILTER_LIST_SCHEMA, |
245 | optional: true, | |
246 | }, | |
0eb0e024 DM |
247 | "output-format": { |
248 | schema: OUTPUT_FORMAT, | |
249 | optional: true, | |
250 | }, | |
251 | } | |
252 | } | |
253 | )] | |
eb506c83 DM |
254 | /// Sync datastore from another repository |
255 | async fn pull_datastore( | |
0eb0e024 DM |
256 | remote: String, |
257 | remote_store: String, | |
eb506c83 | 258 | local_store: String, |
40dc1031 | 259 | remove_vanished: Option<bool>, |
062edce2 | 260 | group_filter: Option<Vec<GroupFilter>>, |
ac3faaf5 | 261 | param: Value, |
0eb0e024 DM |
262 | ) -> Result<Value, Error> { |
263 | ||
ac3faaf5 | 264 | let output_format = get_output_format(¶m); |
0eb0e024 | 265 | |
d4877712 | 266 | let client = connect_to_localhost()?; |
0eb0e024 | 267 | |
8c877436 | 268 | let mut args = json!({ |
eb506c83 | 269 | "store": local_store, |
94609e23 | 270 | "remote": remote, |
0eb0e024 | 271 | "remote-store": remote_store, |
0eb0e024 DM |
272 | }); |
273 | ||
062edce2 TL |
274 | if group_filter.is_some() { |
275 | args["group-filter"] = json!(group_filter); | |
71e53463 FG |
276 | } |
277 | ||
8c877436 DC |
278 | if let Some(remove_vanished) = remove_vanished { |
279 | args["remove-vanished"] = Value::from(remove_vanished); | |
280 | } | |
281 | ||
eb506c83 | 282 | let result = client.post("api2/json/pull", Some(args)).await?; |
0eb0e024 | 283 | |
d4877712 | 284 | view_task_result(&client, result, &output_format).await?; |
0eb0e024 DM |
285 | |
286 | Ok(Value::Null) | |
287 | } | |
288 | ||
355c055e DM |
289 | #[api( |
290 | input: { | |
291 | properties: { | |
292 | "store": { | |
293 | schema: DATASTORE_SCHEMA, | |
294 | }, | |
60abf03f HL |
295 | "ignore-verified": { |
296 | schema: IGNORE_VERIFIED_BACKUPS_SCHEMA, | |
297 | optional: true, | |
298 | }, | |
299 | "outdated-after": { | |
300 | schema: VERIFICATION_OUTDATED_AFTER_SCHEMA, | |
301 | optional: true, | |
302 | }, | |
355c055e DM |
303 | "output-format": { |
304 | schema: OUTPUT_FORMAT, | |
305 | optional: true, | |
306 | }, | |
307 | } | |
308 | } | |
309 | )] | |
310 | /// Verify backups | |
311 | async fn verify( | |
312 | store: String, | |
60abf03f | 313 | mut param: Value, |
355c055e DM |
314 | ) -> Result<Value, Error> { |
315 | ||
60abf03f | 316 | let output_format = extract_output_format(&mut param); |
355c055e | 317 | |
d4877712 | 318 | let client = connect_to_localhost()?; |
355c055e | 319 | |
60abf03f | 320 | let args = json!(param); |
355c055e DM |
321 | |
322 | let path = format!("api2/json/admin/datastore/{}/verify", store); | |
323 | ||
324 | let result = client.post(&path, Some(args)).await?; | |
325 | ||
d4877712 | 326 | view_task_result(&client, result, &output_format).await?; |
355c055e DM |
327 | |
328 | Ok(Value::Null) | |
329 | } | |
330 | ||
9a556c8a HL |
331 | #[api()] |
332 | /// System report | |
333 | async fn report() -> Result<Value, Error> { | |
941342f7 TL |
334 | let report = proxmox_backup::server::generate_report(); |
335 | io::stdout().write_all(report.as_bytes())?; | |
9a556c8a HL |
336 | Ok(Value::Null) |
337 | } | |
338 | ||
c100fe91 ML |
339 | #[api( |
340 | input: { | |
341 | properties: { | |
342 | verbose: { | |
343 | type: Boolean, | |
344 | optional: true, | |
345 | default: false, | |
346 | description: "Output verbose package information. It is ignored if output-format is specified.", | |
347 | }, | |
348 | "output-format": { | |
349 | schema: OUTPUT_FORMAT, | |
350 | optional: true, | |
351 | } | |
352 | } | |
353 | } | |
354 | )] | |
355 | /// List package versions for important Proxmox Backup Server packages. | |
356 | async fn get_versions(verbose: bool, param: Value) -> Result<Value, Error> { | |
294466ee | 357 | let output_format = get_output_format(¶m); |
c100fe91 | 358 | |
5e293f13 | 359 | let packages = crate::api2::node::apt::get_versions()?; |
fc5a0120 | 360 | let mut packages = json!(if verbose { &packages[..] } else { &packages[1..2] }); |
c100fe91 | 361 | |
294466ee TL |
362 | let options = default_table_format_options() |
363 | .disable_sort() | |
d1d74c43 | 364 | .noborder(true) // just not helpful for version info which gets copy pasted often |
294466ee TL |
365 | .column(ColumnConfig::new("Package")) |
366 | .column(ColumnConfig::new("Version")) | |
367 | .column(ColumnConfig::new("ExtraInfo").header("Extra Info")) | |
368 | ; | |
b2362a12 | 369 | let return_type = &crate::api2::node::apt::API_METHOD_GET_VERSIONS.returns; |
294466ee | 370 | |
b2362a12 | 371 | format_and_print_result_full(&mut packages, return_type, &output_format, &options); |
c100fe91 ML |
372 | |
373 | Ok(Value::Null) | |
374 | } | |
375 | ||
ae18c436 | 376 | async fn run() -> Result<(), Error> { |
ac7513e3 | 377 | |
6460764d | 378 | let cmd_def = CliCommandMap::new() |
ed3e60ae | 379 | .insert("acl", acl_commands()) |
48ef3c33 | 380 | .insert("datastore", datastore_commands()) |
8e40aa63 | 381 | .insert("disk", disk_commands()) |
14627d67 | 382 | .insert("dns", dns_commands()) |
ca0e5347 | 383 | .insert("network", network_commands()) |
72e311c6 | 384 | .insert("node", node_commands()) |
579728c6 | 385 | .insert("user", user_commands()) |
0decd11e | 386 | .insert("openid", openid_commands()) |
f357390c | 387 | .insert("remote", remote_commands()) |
bfd12e87 | 388 | .insert("traffic-control", traffic_control_commands()) |
47d47121 | 389 | .insert("garbage-collection", garbage_collection_commands()) |
72bd8293 | 390 | .insert("acme", acme_mgmt_cli()) |
550e0d88 | 391 | .insert("cert", cert_mgmt_cli()) |
2762481c | 392 | .insert("subscription", subscription_commands()) |
a3016d65 | 393 | .insert("sync-job", sync_job_commands()) |
4a874665 | 394 | .insert("verify-job", verify_job_commands()) |
0eb0e024 DM |
395 | .insert("task", task_mgmt_cli()) |
396 | .insert( | |
eb506c83 DM |
397 | "pull", |
398 | CliCommand::new(&API_METHOD_PULL_DATASTORE) | |
399 | .arg_param(&["remote", "remote-store", "local-store"]) | |
e7d4be9d | 400 | .completion_cb("local-store", pbs_config::datastore::complete_datastore_name) |
6afdda88 | 401 | .completion_cb("remote", pbs_config::remote::complete_remote_name) |
331b869d | 402 | .completion_cb("remote-store", complete_remote_datastore_name) |
062edce2 | 403 | .completion_cb("group_filter", complete_remote_datastore_group_filter) |
355c055e DM |
404 | ) |
405 | .insert( | |
406 | "verify", | |
407 | CliCommand::new(&API_METHOD_VERIFY) | |
408 | .arg_param(&["store"]) | |
e7d4be9d | 409 | .completion_cb("store", pbs_config::datastore::complete_datastore_name) |
9a556c8a HL |
410 | ) |
411 | .insert("report", | |
412 | CliCommand::new(&API_METHOD_REPORT) | |
c100fe91 ML |
413 | ) |
414 | .insert("versions", | |
415 | CliCommand::new(&API_METHOD_GET_VERSIONS) | |
0eb0e024 | 416 | ); |
34d3ba52 | 417 | |
bbd57396 DM |
418 | let args: Vec<String> = std::env::args().take(2).collect(); |
419 | let avoid_init = args.len() >= 2 && (args[1] == "bashcomplete" || args[1] == "printdoc"); | |
355c055e | 420 | |
bbd57396 DM |
421 | if !avoid_init { |
422 | let backup_user = pbs_config::backup_user()?; | |
423 | let file_opts = CreateOptions::new().owner(backup_user.uid).group(backup_user.gid); | |
aa174e8e | 424 | proxmox_rest_server::init_worker_tasks(pbs_buildcfg::PROXMOX_BACKUP_LOG_DIR_M!().into(), file_opts)?; |
bbd57396 | 425 | |
49e25688 | 426 | let mut commando_sock = proxmox_rest_server::CommandSocket::new(proxmox_rest_server::our_ctrl_sock(), backup_user.gid); |
bbd57396 DM |
427 | proxmox_rest_server::register_task_control_commands(&mut commando_sock)?; |
428 | commando_sock.spawn()?; | |
429 | } | |
355c055e | 430 | |
7b22acd0 | 431 | let mut rpcenv = CliEnvironment::new(); |
e6dc35ac | 432 | rpcenv.set_auth_id(Some(String::from("root@pam"))); |
525008f7 | 433 | |
ae18c436 DM |
434 | run_async_cli_command(cmd_def, rpcenv).await; // this call exit(-1) on error |
435 | ||
436 | Ok(()) | |
437 | } | |
438 | ||
439 | fn main() -> Result<(), Error> { | |
440 | ||
441 | proxmox_backup::tools::setup_safe_path_env(); | |
442 | ||
9a1b24b6 | 443 | proxmox_async::runtime::main(run()) |
ea0b8b6e | 444 | } |
331b869d | 445 | |
1d9bc184 FG |
446 | fn get_sync_job(id: &String) -> Result<SyncJobConfig, Error> { |
447 | let (config, _digest) = sync::config()?; | |
448 | ||
449 | config.lookup("sync", id) | |
450 | } | |
451 | ||
452 | fn get_remote(param: &HashMap<String, String>) -> Option<String> { | |
453 | param | |
454 | .get("remote") | |
455 | .map(|r| r.to_owned()) | |
456 | .or_else(|| { | |
457 | if let Some(id) = param.get("id") { | |
458 | if let Ok(job) = get_sync_job(id) { | |
aa174e8e | 459 | return Some(job.remote); |
1d9bc184 FG |
460 | } |
461 | } | |
462 | None | |
463 | }) | |
464 | } | |
465 | ||
466 | fn get_remote_store(param: &HashMap<String, String>) -> Option<(String, String)> { | |
467 | let mut job: Option<SyncJobConfig> = None; | |
468 | ||
469 | let remote = param | |
470 | .get("remote") | |
471 | .map(|r| r.to_owned()) | |
472 | .or_else(|| { | |
473 | if let Some(id) = param.get("id") { | |
474 | job = get_sync_job(id).ok(); | |
475 | if let Some(ref job) = job { | |
476 | return Some(job.remote.clone()); | |
477 | } | |
478 | } | |
479 | None | |
480 | }); | |
481 | ||
482 | if let Some(remote) = remote { | |
483 | let store = param | |
484 | .get("remote-store") | |
485 | .map(|r| r.to_owned()) | |
aa174e8e | 486 | .or_else(|| job.map(|job| job.remote_store)); |
1d9bc184 FG |
487 | |
488 | if let Some(store) = store { | |
489 | return Some((remote, store)) | |
490 | } | |
491 | } | |
492 | ||
493 | None | |
494 | } | |
495 | ||
331b869d DM |
496 | // shell completion helper |
497 | pub fn complete_remote_datastore_name(_arg: &str, param: &HashMap<String, String>) -> Vec<String> { | |
498 | ||
499 | let mut list = Vec::new(); | |
500 | ||
1d9bc184 | 501 | if let Some(remote) = get_remote(param) { |
9a1b24b6 | 502 | if let Ok(data) = proxmox_async::runtime::block_on(async move { |
1d9bc184 FG |
503 | crate::api2::config::remote::scan_remote_datastores(remote).await |
504 | }) { | |
331b869d | 505 | |
1d9bc184 FG |
506 | for item in data { |
507 | list.push(item.store); | |
508 | } | |
509 | } | |
510 | } | |
331b869d | 511 | |
1d9bc184 FG |
512 | list |
513 | } | |
514 | ||
515 | // shell completion helper | |
516 | pub fn complete_remote_datastore_group(_arg: &str, param: &HashMap<String, String>) -> Vec<String> { | |
517 | ||
518 | let mut list = Vec::new(); | |
519 | ||
520 | if let Some((remote, remote_store)) = get_remote_store(param) { | |
9a1b24b6 | 521 | if let Ok(data) = proxmox_async::runtime::block_on(async move { |
1d9bc184 FG |
522 | crate::api2::config::remote::scan_remote_groups(remote.clone(), remote_store.clone()).await |
523 | }) { | |
524 | ||
525 | for item in data { | |
526 | list.push(format!("{}/{}", item.backup_type, item.backup_id)); | |
527 | } | |
331b869d | 528 | } |
1d9bc184 FG |
529 | } |
530 | ||
531 | list | |
532 | } | |
533 | ||
534 | // shell completion helper | |
535 | pub fn complete_remote_datastore_group_filter(_arg: &str, param: &HashMap<String, String>) -> Vec<String> { | |
536 | ||
537 | let mut list = Vec::new(); | |
538 | ||
539 | list.push("regex:".to_string()); | |
540 | list.push("type:ct".to_string()); | |
541 | list.push("type:host".to_string()); | |
542 | list.push("type:vm".to_string()); | |
331b869d | 543 | |
1d9bc184 | 544 | list.extend(complete_remote_datastore_group(_arg, param).iter().map(|group| format!("group:{}", group))); |
331b869d DM |
545 | |
546 | list | |
547 | } |