1 // Copyright © 2020 Intel Corporation
2 //
3 // SPDX-License-Identifier: Apache-2.0
4 //
5
6 #[cfg(test)]
7 #[path = "../test_util.rs"]
8 mod test_util;
9
10 use std::io::Read;
11 use std::marker::PhantomData;
12 use std::os::unix::net::UnixStream;
13 use std::process;
14
15 use api_client::{
16 simple_api_command, simple_api_command_with_fds, simple_api_full_command,
17 Error as ApiClientError,
18 };
19 use clap::{Arg, ArgAction, ArgMatches, Command};
20 use log::error;
21 use option_parser::{ByteSized, ByteSizedParseError};
22 use thiserror::Error;
23 use vmm::config::RestoreConfig;
24 use vmm::vm_config::{
25 DeviceConfig, DiskConfig, FsConfig, NetConfig, PmemConfig, UserDeviceConfig, VdpaConfig,
26 VsockConfig,
27 };
28 #[cfg(feature = "dbus_api")]
29 use zbus::{proxy, zvariant::Optional};
30
31 type ApiResult = Result<(), Error>;
32
33 #[derive(Error, Debug)]
34 enum Error {
35 #[error("http client error")]
36 HttpApiClient(#[source] ApiClientError),
37 #[cfg(feature = "dbus_api")]
38 #[error("dbus api client error")]
39 DBusApiClient(#[source] zbus::Error),
40 #[error("Error parsing CPU count")]
41 InvalidCpuCount(#[source] std::num::ParseIntError),
42 #[error("Error parsing memory size")]
43 InvalidMemorySize(#[source] ByteSizedParseError),
44 #[error("Error parsing balloon size")]
45 InvalidBalloonSize(#[source] ByteSizedParseError),
46 #[error("Error parsing device syntax")]
47 AddDeviceConfig(#[source] vmm::config::Error),
48 #[error("Error parsing disk syntax")]
49 AddDiskConfig(#[source] vmm::config::Error),
50 #[error("Error parsing filesystem syntax")]
51 AddFsConfig(#[source] vmm::config::Error),
52 #[error("Error parsing persistent memory syntax")]
53 AddPmemConfig(#[source] vmm::config::Error),
54 #[error("Error parsing network syntax")]
55 AddNetConfig(#[source] vmm::config::Error),
56 #[error("Error parsing user device syntax")]
57 AddUserDeviceConfig(#[source] vmm::config::Error),
58 #[error("Error parsing vDPA device syntax")]
59 AddVdpaConfig(#[source] vmm::config::Error),
60 #[error("Error parsing vsock syntax")]
61 AddVsockConfig(#[source] vmm::config::Error),
62 #[error("Error parsing restore syntax")]
63 Restore(#[source] vmm::config::Error),
64 #[error("Error reading from stdin")]
65 ReadingStdin(#[source] std::io::Error),
66 #[error("Error reading from file")]
67 ReadingFile(#[source] std::io::Error),
68 }
69
70 enum TargetApi<'a> {
71 HttpApi(UnixStream, PhantomData<&'a ()>),
72 #[cfg(feature = "dbus_api")]
73 DBusApi(DBusApi1ProxyBlocking<'a>),
74 }
75
76 #[cfg(feature = "dbus_api")]
77 #[proxy(name = "org.cloudhypervisor.DBusApi1", assume_defaults = false)]
78 trait DBusApi1 {
vmm_ping(&self) -> zbus::Result<String>79 fn vmm_ping(&self) -> zbus::Result<String>;
vmm_shutdown(&self) -> zbus::Result<()>80 fn vmm_shutdown(&self) -> zbus::Result<()>;
vm_add_device(&self, device_config: &str) -> zbus::Result<Optional<String>>81 fn vm_add_device(&self, device_config: &str) -> zbus::Result<Optional<String>>;
vm_add_disk(&self, disk_config: &str) -> zbus::Result<Optional<String>>82 fn vm_add_disk(&self, disk_config: &str) -> zbus::Result<Optional<String>>;
vm_add_fs(&self, fs_config: &str) -> zbus::Result<Optional<String>>83 fn vm_add_fs(&self, fs_config: &str) -> zbus::Result<Optional<String>>;
vm_add_net(&self, net_config: &str) -> zbus::Result<Optional<String>>84 fn vm_add_net(&self, net_config: &str) -> zbus::Result<Optional<String>>;
vm_add_pmem(&self, pmem_config: &str) -> zbus::Result<Optional<String>>85 fn vm_add_pmem(&self, pmem_config: &str) -> zbus::Result<Optional<String>>;
vm_add_user_device(&self, vm_add_user_device: &str) -> zbus::Result<Optional<String>>86 fn vm_add_user_device(&self, vm_add_user_device: &str) -> zbus::Result<Optional<String>>;
vm_add_vdpa(&self, vdpa_config: &str) -> zbus::Result<Optional<String>>87 fn vm_add_vdpa(&self, vdpa_config: &str) -> zbus::Result<Optional<String>>;
vm_add_vsock(&self, vsock_config: &str) -> zbus::Result<Optional<String>>88 fn vm_add_vsock(&self, vsock_config: &str) -> zbus::Result<Optional<String>>;
vm_boot(&self) -> zbus::Result<()>89 fn vm_boot(&self) -> zbus::Result<()>;
vm_coredump(&self, vm_coredump_data: &str) -> zbus::Result<()>90 fn vm_coredump(&self, vm_coredump_data: &str) -> zbus::Result<()>;
vm_counters(&self) -> zbus::Result<Optional<String>>91 fn vm_counters(&self) -> zbus::Result<Optional<String>>;
vm_create(&self, vm_config: &str) -> zbus::Result<()>92 fn vm_create(&self, vm_config: &str) -> zbus::Result<()>;
vm_delete(&self) -> zbus::Result<()>93 fn vm_delete(&self) -> zbus::Result<()>;
vm_info(&self) -> zbus::Result<String>94 fn vm_info(&self) -> zbus::Result<String>;
vm_pause(&self) -> zbus::Result<()>95 fn vm_pause(&self) -> zbus::Result<()>;
vm_power_button(&self) -> zbus::Result<()>96 fn vm_power_button(&self) -> zbus::Result<()>;
vm_reboot(&self) -> zbus::Result<()>97 fn vm_reboot(&self) -> zbus::Result<()>;
vm_remove_device(&self, vm_remove_device: &str) -> zbus::Result<()>98 fn vm_remove_device(&self, vm_remove_device: &str) -> zbus::Result<()>;
vm_resize(&self, vm_resize: &str) -> zbus::Result<()>99 fn vm_resize(&self, vm_resize: &str) -> zbus::Result<()>;
vm_resize_zone(&self, vm_resize_zone: &str) -> zbus::Result<()>100 fn vm_resize_zone(&self, vm_resize_zone: &str) -> zbus::Result<()>;
vm_restore(&self, restore_config: &str) -> zbus::Result<()>101 fn vm_restore(&self, restore_config: &str) -> zbus::Result<()>;
vm_receive_migration(&self, receive_migration_data: &str) -> zbus::Result<()>102 fn vm_receive_migration(&self, receive_migration_data: &str) -> zbus::Result<()>;
vm_send_migration(&self, receive_migration_data: &str) -> zbus::Result<()>103 fn vm_send_migration(&self, receive_migration_data: &str) -> zbus::Result<()>;
vm_resume(&self) -> zbus::Result<()>104 fn vm_resume(&self) -> zbus::Result<()>;
vm_shutdown(&self) -> zbus::Result<()>105 fn vm_shutdown(&self) -> zbus::Result<()>;
vm_snapshot(&self, vm_snapshot_config: &str) -> zbus::Result<()>106 fn vm_snapshot(&self, vm_snapshot_config: &str) -> zbus::Result<()>;
107 }
108
109 #[cfg(feature = "dbus_api")]
110 impl<'a> DBusApi1ProxyBlocking<'a> {
new_connection(name: &'a str, path: &'a str, system_bus: bool) -> Result<Self, zbus::Error>111 fn new_connection(name: &'a str, path: &'a str, system_bus: bool) -> Result<Self, zbus::Error> {
112 let connection = if system_bus {
113 zbus::blocking::Connection::system()?
114 } else {
115 zbus::blocking::Connection::session()?
116 };
117
118 Self::builder(&connection)
119 .destination(name)?
120 .path(path)?
121 .build()
122 }
123
print_response(&self, result: zbus::Result<Optional<String>>) -> ApiResult124 fn print_response(&self, result: zbus::Result<Optional<String>>) -> ApiResult {
125 result
126 .map(|ret| {
127 if let Some(ref output) = *ret {
128 println!("{output}");
129 }
130 })
131 .map_err(Error::DBusApiClient)
132 }
133
api_vmm_ping(&self) -> ApiResult134 fn api_vmm_ping(&self) -> ApiResult {
135 self.vmm_ping()
136 .map(|ping| println!("{ping}"))
137 .map_err(Error::DBusApiClient)
138 }
139
api_vmm_shutdown(&self) -> ApiResult140 fn api_vmm_shutdown(&self) -> ApiResult {
141 self.vmm_shutdown().map_err(Error::DBusApiClient)
142 }
143
api_vm_add_device(&self, device_config: &str) -> ApiResult144 fn api_vm_add_device(&self, device_config: &str) -> ApiResult {
145 self.print_response(self.vm_add_device(device_config))
146 }
147
api_vm_add_disk(&self, disk_config: &str) -> ApiResult148 fn api_vm_add_disk(&self, disk_config: &str) -> ApiResult {
149 self.print_response(self.vm_add_disk(disk_config))
150 }
151
api_vm_add_fs(&self, fs_config: &str) -> ApiResult152 fn api_vm_add_fs(&self, fs_config: &str) -> ApiResult {
153 self.print_response(self.vm_add_fs(fs_config))
154 }
155
api_vm_add_net(&self, net_config: &str) -> ApiResult156 fn api_vm_add_net(&self, net_config: &str) -> ApiResult {
157 self.print_response(self.vm_add_net(net_config))
158 }
159
api_vm_add_pmem(&self, pmem_config: &str) -> ApiResult160 fn api_vm_add_pmem(&self, pmem_config: &str) -> ApiResult {
161 self.print_response(self.vm_add_pmem(pmem_config))
162 }
163
api_vm_add_user_device(&self, vm_add_user_device: &str) -> ApiResult164 fn api_vm_add_user_device(&self, vm_add_user_device: &str) -> ApiResult {
165 self.print_response(self.vm_add_user_device(vm_add_user_device))
166 }
167
api_vm_add_vdpa(&self, vdpa_config: &str) -> ApiResult168 fn api_vm_add_vdpa(&self, vdpa_config: &str) -> ApiResult {
169 self.print_response(self.vm_add_vdpa(vdpa_config))
170 }
171
api_vm_add_vsock(&self, vsock_config: &str) -> ApiResult172 fn api_vm_add_vsock(&self, vsock_config: &str) -> ApiResult {
173 self.print_response(self.vm_add_vsock(vsock_config))
174 }
175
api_vm_boot(&self) -> ApiResult176 fn api_vm_boot(&self) -> ApiResult {
177 self.vm_boot().map_err(Error::DBusApiClient)
178 }
179
api_vm_coredump(&self, vm_coredump_data: &str) -> ApiResult180 fn api_vm_coredump(&self, vm_coredump_data: &str) -> ApiResult {
181 self.vm_coredump(vm_coredump_data)
182 .map_err(Error::DBusApiClient)
183 }
184
api_vm_counters(&self) -> ApiResult185 fn api_vm_counters(&self) -> ApiResult {
186 self.print_response(self.vm_counters())
187 }
188
api_vm_create(&self, vm_config: &str) -> ApiResult189 fn api_vm_create(&self, vm_config: &str) -> ApiResult {
190 self.vm_create(vm_config).map_err(Error::DBusApiClient)
191 }
192
api_vm_delete(&self) -> ApiResult193 fn api_vm_delete(&self) -> ApiResult {
194 self.vm_delete().map_err(Error::DBusApiClient)
195 }
196
api_vm_info(&self) -> ApiResult197 fn api_vm_info(&self) -> ApiResult {
198 self.vm_info()
199 .map(|info| println!("{info}"))
200 .map_err(Error::DBusApiClient)
201 }
202
api_vm_pause(&self) -> ApiResult203 fn api_vm_pause(&self) -> ApiResult {
204 self.vm_pause().map_err(Error::DBusApiClient)
205 }
206
api_vm_power_button(&self) -> ApiResult207 fn api_vm_power_button(&self) -> ApiResult {
208 self.vm_power_button().map_err(Error::DBusApiClient)
209 }
210
api_vm_reboot(&self) -> ApiResult211 fn api_vm_reboot(&self) -> ApiResult {
212 self.vm_reboot().map_err(Error::DBusApiClient)
213 }
214
api_vm_remove_device(&self, vm_remove_device: &str) -> ApiResult215 fn api_vm_remove_device(&self, vm_remove_device: &str) -> ApiResult {
216 self.vm_remove_device(vm_remove_device)
217 .map_err(Error::DBusApiClient)
218 }
219
api_vm_resize(&self, vm_resize: &str) -> ApiResult220 fn api_vm_resize(&self, vm_resize: &str) -> ApiResult {
221 self.vm_resize(vm_resize).map_err(Error::DBusApiClient)
222 }
223
api_vm_resize_zone(&self, vm_resize_zone: &str) -> ApiResult224 fn api_vm_resize_zone(&self, vm_resize_zone: &str) -> ApiResult {
225 self.vm_resize_zone(vm_resize_zone)
226 .map_err(Error::DBusApiClient)
227 }
228
api_vm_restore(&self, restore_config: &str) -> ApiResult229 fn api_vm_restore(&self, restore_config: &str) -> ApiResult {
230 self.vm_restore(restore_config)
231 .map_err(Error::DBusApiClient)
232 }
233
api_vm_receive_migration(&self, receive_migration_data: &str) -> ApiResult234 fn api_vm_receive_migration(&self, receive_migration_data: &str) -> ApiResult {
235 self.vm_receive_migration(receive_migration_data)
236 .map_err(Error::DBusApiClient)
237 }
238
api_vm_send_migration(&self, send_migration_data: &str) -> ApiResult239 fn api_vm_send_migration(&self, send_migration_data: &str) -> ApiResult {
240 self.vm_send_migration(send_migration_data)
241 .map_err(Error::DBusApiClient)
242 }
243
api_vm_resume(&self) -> ApiResult244 fn api_vm_resume(&self) -> ApiResult {
245 self.vm_resume().map_err(Error::DBusApiClient)
246 }
247
api_vm_shutdown(&self) -> ApiResult248 fn api_vm_shutdown(&self) -> ApiResult {
249 self.vm_shutdown().map_err(Error::DBusApiClient)
250 }
251
api_vm_snapshot(&self, vm_snapshot_config: &str) -> ApiResult252 fn api_vm_snapshot(&self, vm_snapshot_config: &str) -> ApiResult {
253 self.vm_snapshot(vm_snapshot_config)
254 .map_err(Error::DBusApiClient)
255 }
256 }
257
258 impl TargetApi<'_> {
do_command(&mut self, matches: &ArgMatches) -> ApiResult259 fn do_command(&mut self, matches: &ArgMatches) -> ApiResult {
260 match self {
261 Self::HttpApi(api_socket, _) => rest_api_do_command(matches, api_socket),
262 #[cfg(feature = "dbus_api")]
263 Self::DBusApi(proxy) => dbus_api_do_command(matches, proxy),
264 }
265 }
266 }
267
rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResult268 fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResult {
269 match matches.subcommand_name() {
270 Some("boot") => {
271 simple_api_command(socket, "PUT", "boot", None).map_err(Error::HttpApiClient)
272 }
273 Some("delete") => {
274 simple_api_command(socket, "PUT", "delete", None).map_err(Error::HttpApiClient)
275 }
276 Some("shutdown-vmm") => simple_api_full_command(socket, "PUT", "vmm.shutdown", None)
277 .map_err(Error::HttpApiClient),
278 Some("resume") => {
279 simple_api_command(socket, "PUT", "resume", None).map_err(Error::HttpApiClient)
280 }
281 Some("power-button") => {
282 simple_api_command(socket, "PUT", "power-button", None).map_err(Error::HttpApiClient)
283 }
284 Some("reboot") => {
285 simple_api_command(socket, "PUT", "reboot", None).map_err(Error::HttpApiClient)
286 }
287 Some("pause") => {
288 simple_api_command(socket, "PUT", "pause", None).map_err(Error::HttpApiClient)
289 }
290 Some("info") => {
291 simple_api_command(socket, "GET", "info", None).map_err(Error::HttpApiClient)
292 }
293 Some("counters") => {
294 simple_api_command(socket, "GET", "counters", None).map_err(Error::HttpApiClient)
295 }
296 Some("ping") => {
297 simple_api_full_command(socket, "GET", "vmm.ping", None).map_err(Error::HttpApiClient)
298 }
299 Some("shutdown") => {
300 simple_api_command(socket, "PUT", "shutdown", None).map_err(Error::HttpApiClient)
301 }
302 Some("nmi") => simple_api_command(socket, "PUT", "nmi", None).map_err(Error::HttpApiClient),
303 Some("resize") => {
304 let resize = resize_config(
305 matches
306 .subcommand_matches("resize")
307 .unwrap()
308 .get_one::<String>("cpus")
309 .map(|x| x as &str),
310 matches
311 .subcommand_matches("resize")
312 .unwrap()
313 .get_one::<String>("memory")
314 .map(|x| x as &str),
315 matches
316 .subcommand_matches("resize")
317 .unwrap()
318 .get_one::<String>("balloon")
319 .map(|x| x as &str),
320 )?;
321 simple_api_command(socket, "PUT", "resize", Some(&resize)).map_err(Error::HttpApiClient)
322 }
323 Some("resize-zone") => {
324 let resize_zone = resize_zone_config(
325 matches
326 .subcommand_matches("resize-zone")
327 .unwrap()
328 .get_one::<String>("id")
329 .unwrap(),
330 matches
331 .subcommand_matches("resize-zone")
332 .unwrap()
333 .get_one::<String>("size")
334 .unwrap(),
335 )?;
336 simple_api_command(socket, "PUT", "resize-zone", Some(&resize_zone))
337 .map_err(Error::HttpApiClient)
338 }
339 Some("add-device") => {
340 let device_config = add_device_config(
341 matches
342 .subcommand_matches("add-device")
343 .unwrap()
344 .get_one::<String>("device_config")
345 .unwrap(),
346 )?;
347 simple_api_command(socket, "PUT", "add-device", Some(&device_config))
348 .map_err(Error::HttpApiClient)
349 }
350 Some("remove-device") => {
351 let remove_device_data = remove_device_config(
352 matches
353 .subcommand_matches("remove-device")
354 .unwrap()
355 .get_one::<String>("id")
356 .unwrap(),
357 );
358 simple_api_command(socket, "PUT", "remove-device", Some(&remove_device_data))
359 .map_err(Error::HttpApiClient)
360 }
361 Some("add-disk") => {
362 let disk_config = add_disk_config(
363 matches
364 .subcommand_matches("add-disk")
365 .unwrap()
366 .get_one::<String>("disk_config")
367 .unwrap(),
368 )?;
369 simple_api_command(socket, "PUT", "add-disk", Some(&disk_config))
370 .map_err(Error::HttpApiClient)
371 }
372 Some("add-fs") => {
373 let fs_config = add_fs_config(
374 matches
375 .subcommand_matches("add-fs")
376 .unwrap()
377 .get_one::<String>("fs_config")
378 .unwrap(),
379 )?;
380 simple_api_command(socket, "PUT", "add-fs", Some(&fs_config))
381 .map_err(Error::HttpApiClient)
382 }
383 Some("add-pmem") => {
384 let pmem_config = add_pmem_config(
385 matches
386 .subcommand_matches("add-pmem")
387 .unwrap()
388 .get_one::<String>("pmem_config")
389 .unwrap(),
390 )?;
391 simple_api_command(socket, "PUT", "add-pmem", Some(&pmem_config))
392 .map_err(Error::HttpApiClient)
393 }
394 Some("add-net") => {
395 let (net_config, fds) = add_net_config(
396 matches
397 .subcommand_matches("add-net")
398 .unwrap()
399 .get_one::<String>("net_config")
400 .unwrap(),
401 )?;
402 simple_api_command_with_fds(socket, "PUT", "add-net", Some(&net_config), fds)
403 .map_err(Error::HttpApiClient)
404 }
405 Some("add-user-device") => {
406 let device_config = add_user_device_config(
407 matches
408 .subcommand_matches("add-user-device")
409 .unwrap()
410 .get_one::<String>("device_config")
411 .unwrap(),
412 )?;
413 simple_api_command(socket, "PUT", "add-user-device", Some(&device_config))
414 .map_err(Error::HttpApiClient)
415 }
416 Some("add-vdpa") => {
417 let vdpa_config = add_vdpa_config(
418 matches
419 .subcommand_matches("add-vdpa")
420 .unwrap()
421 .get_one::<String>("vdpa_config")
422 .unwrap(),
423 )?;
424 simple_api_command(socket, "PUT", "add-vdpa", Some(&vdpa_config))
425 .map_err(Error::HttpApiClient)
426 }
427 Some("add-vsock") => {
428 let vsock_config = add_vsock_config(
429 matches
430 .subcommand_matches("add-vsock")
431 .unwrap()
432 .get_one::<String>("vsock_config")
433 .unwrap(),
434 )?;
435 simple_api_command(socket, "PUT", "add-vsock", Some(&vsock_config))
436 .map_err(Error::HttpApiClient)
437 }
438 Some("snapshot") => {
439 let snapshot_config = snapshot_config(
440 matches
441 .subcommand_matches("snapshot")
442 .unwrap()
443 .get_one::<String>("snapshot_config")
444 .unwrap(),
445 );
446 simple_api_command(socket, "PUT", "snapshot", Some(&snapshot_config))
447 .map_err(Error::HttpApiClient)
448 }
449 Some("restore") => {
450 let (restore_config, fds) = restore_config(
451 matches
452 .subcommand_matches("restore")
453 .unwrap()
454 .get_one::<String>("restore_config")
455 .unwrap(),
456 )?;
457 simple_api_command_with_fds(socket, "PUT", "restore", Some(&restore_config), fds)
458 .map_err(Error::HttpApiClient)
459 }
460 Some("coredump") => {
461 let coredump_config = coredump_config(
462 matches
463 .subcommand_matches("coredump")
464 .unwrap()
465 .get_one::<String>("coredump_config")
466 .unwrap(),
467 );
468 simple_api_command(socket, "PUT", "coredump", Some(&coredump_config))
469 .map_err(Error::HttpApiClient)
470 }
471 Some("send-migration") => {
472 let send_migration_data = send_migration_data(
473 matches
474 .subcommand_matches("send-migration")
475 .unwrap()
476 .get_one::<String>("send_migration_config")
477 .unwrap(),
478 matches
479 .subcommand_matches("send-migration")
480 .unwrap()
481 .get_flag("send_migration_local"),
482 );
483 simple_api_command(socket, "PUT", "send-migration", Some(&send_migration_data))
484 .map_err(Error::HttpApiClient)
485 }
486 Some("receive-migration") => {
487 let receive_migration_data = receive_migration_data(
488 matches
489 .subcommand_matches("receive-migration")
490 .unwrap()
491 .get_one::<String>("receive_migration_config")
492 .unwrap(),
493 );
494 simple_api_command(
495 socket,
496 "PUT",
497 "receive-migration",
498 Some(&receive_migration_data),
499 )
500 .map_err(Error::HttpApiClient)
501 }
502 Some("create") => {
503 let data = create_data(
504 matches
505 .subcommand_matches("create")
506 .unwrap()
507 .get_one::<String>("path")
508 .unwrap(),
509 )?;
510 simple_api_command(socket, "PUT", "create", Some(&data)).map_err(Error::HttpApiClient)
511 }
512 _ => unreachable!(),
513 }
514 }
515
516 #[cfg(feature = "dbus_api")]
dbus_api_do_command(matches: &ArgMatches, proxy: &DBusApi1ProxyBlocking<'_>) -> ApiResult517 fn dbus_api_do_command(matches: &ArgMatches, proxy: &DBusApi1ProxyBlocking<'_>) -> ApiResult {
518 match matches.subcommand_name() {
519 Some("boot") => proxy.api_vm_boot(),
520 Some("delete") => proxy.api_vm_delete(),
521 Some("shutdown-vmm") => proxy.api_vmm_shutdown(),
522 Some("resume") => proxy.api_vm_resume(),
523 Some("power-button") => proxy.api_vm_power_button(),
524 Some("reboot") => proxy.api_vm_reboot(),
525 Some("pause") => proxy.api_vm_pause(),
526 Some("info") => proxy.api_vm_info(),
527 Some("counters") => proxy.api_vm_counters(),
528 Some("ping") => proxy.api_vmm_ping(),
529 Some("shutdown") => proxy.api_vm_shutdown(),
530 Some("resize") => {
531 let resize = resize_config(
532 matches
533 .subcommand_matches("resize")
534 .unwrap()
535 .get_one::<String>("cpus")
536 .map(|x| x as &str),
537 matches
538 .subcommand_matches("resize")
539 .unwrap()
540 .get_one::<String>("memory")
541 .map(|x| x as &str),
542 matches
543 .subcommand_matches("resize")
544 .unwrap()
545 .get_one::<String>("balloon")
546 .map(|x| x as &str),
547 )?;
548 proxy.api_vm_resize(&resize)
549 }
550 Some("resize-zone") => {
551 let resize_zone = resize_zone_config(
552 matches
553 .subcommand_matches("resize-zone")
554 .unwrap()
555 .get_one::<String>("id")
556 .unwrap(),
557 matches
558 .subcommand_matches("resize-zone")
559 .unwrap()
560 .get_one::<String>("size")
561 .unwrap(),
562 )?;
563 proxy.api_vm_resize_zone(&resize_zone)
564 }
565 Some("add-device") => {
566 let device_config = add_device_config(
567 matches
568 .subcommand_matches("add-device")
569 .unwrap()
570 .get_one::<String>("device_config")
571 .unwrap(),
572 )?;
573 proxy.api_vm_add_device(&device_config)
574 }
575 Some("remove-device") => {
576 let remove_device_data = remove_device_config(
577 matches
578 .subcommand_matches("remove-device")
579 .unwrap()
580 .get_one::<String>("id")
581 .unwrap(),
582 );
583 proxy.api_vm_remove_device(&remove_device_data)
584 }
585 Some("add-disk") => {
586 let disk_config = add_disk_config(
587 matches
588 .subcommand_matches("add-disk")
589 .unwrap()
590 .get_one::<String>("disk_config")
591 .unwrap(),
592 )?;
593 proxy.api_vm_add_disk(&disk_config)
594 }
595 Some("add-fs") => {
596 let fs_config = add_fs_config(
597 matches
598 .subcommand_matches("add-fs")
599 .unwrap()
600 .get_one::<String>("fs_config")
601 .unwrap(),
602 )?;
603 proxy.api_vm_add_fs(&fs_config)
604 }
605 Some("add-pmem") => {
606 let pmem_config = add_pmem_config(
607 matches
608 .subcommand_matches("add-pmem")
609 .unwrap()
610 .get_one::<String>("pmem_config")
611 .unwrap(),
612 )?;
613 proxy.api_vm_add_pmem(&pmem_config)
614 }
615 Some("add-net") => {
616 let (net_config, _fds) = add_net_config(
617 matches
618 .subcommand_matches("add-net")
619 .unwrap()
620 .get_one::<String>("net_config")
621 .unwrap(),
622 )?;
623 proxy.api_vm_add_net(&net_config)
624 }
625 Some("add-user-device") => {
626 let device_config = add_user_device_config(
627 matches
628 .subcommand_matches("add-user-device")
629 .unwrap()
630 .get_one::<String>("device_config")
631 .unwrap(),
632 )?;
633 proxy.api_vm_add_user_device(&device_config)
634 }
635 Some("add-vdpa") => {
636 let vdpa_config = add_vdpa_config(
637 matches
638 .subcommand_matches("add-vdpa")
639 .unwrap()
640 .get_one::<String>("vdpa_config")
641 .unwrap(),
642 )?;
643 proxy.api_vm_add_vdpa(&vdpa_config)
644 }
645 Some("add-vsock") => {
646 let vsock_config = add_vsock_config(
647 matches
648 .subcommand_matches("add-vsock")
649 .unwrap()
650 .get_one::<String>("vsock_config")
651 .unwrap(),
652 )?;
653 proxy.api_vm_add_vsock(&vsock_config)
654 }
655 Some("snapshot") => {
656 let snapshot_config = snapshot_config(
657 matches
658 .subcommand_matches("snapshot")
659 .unwrap()
660 .get_one::<String>("snapshot_config")
661 .unwrap(),
662 );
663 proxy.api_vm_snapshot(&snapshot_config)
664 }
665 Some("restore") => {
666 let (restore_config, _fds) = restore_config(
667 matches
668 .subcommand_matches("restore")
669 .unwrap()
670 .get_one::<String>("restore_config")
671 .unwrap(),
672 )?;
673 proxy.api_vm_restore(&restore_config)
674 }
675 Some("coredump") => {
676 let coredump_config = coredump_config(
677 matches
678 .subcommand_matches("coredump")
679 .unwrap()
680 .get_one::<String>("coredump_config")
681 .unwrap(),
682 );
683 proxy.api_vm_coredump(&coredump_config)
684 }
685 Some("send-migration") => {
686 let send_migration_data = send_migration_data(
687 matches
688 .subcommand_matches("send-migration")
689 .unwrap()
690 .get_one::<String>("send_migration_config")
691 .unwrap(),
692 matches
693 .subcommand_matches("send-migration")
694 .unwrap()
695 .get_flag("send_migration_local"),
696 );
697 proxy.api_vm_send_migration(&send_migration_data)
698 }
699 Some("receive-migration") => {
700 let receive_migration_data = receive_migration_data(
701 matches
702 .subcommand_matches("receive-migration")
703 .unwrap()
704 .get_one::<String>("receive_migration_config")
705 .unwrap(),
706 );
707 proxy.api_vm_receive_migration(&receive_migration_data)
708 }
709 Some("create") => {
710 let data = create_data(
711 matches
712 .subcommand_matches("create")
713 .unwrap()
714 .get_one::<String>("path")
715 .unwrap(),
716 )?;
717 proxy.api_vm_create(&data)
718 }
719 _ => unreachable!(),
720 }
721 }
722
resize_config( cpus: Option<&str>, memory: Option<&str>, balloon: Option<&str>, ) -> Result<String, Error>723 fn resize_config(
724 cpus: Option<&str>,
725 memory: Option<&str>,
726 balloon: Option<&str>,
727 ) -> Result<String, Error> {
728 let desired_vcpus: Option<u8> = if let Some(cpus) = cpus {
729 Some(cpus.parse().map_err(Error::InvalidCpuCount)?)
730 } else {
731 None
732 };
733
734 let desired_ram: Option<u64> = if let Some(memory) = memory {
735 Some(
736 memory
737 .parse::<ByteSized>()
738 .map_err(Error::InvalidMemorySize)?
739 .0,
740 )
741 } else {
742 None
743 };
744
745 let desired_balloon: Option<u64> = if let Some(balloon) = balloon {
746 Some(
747 balloon
748 .parse::<ByteSized>()
749 .map_err(Error::InvalidBalloonSize)?
750 .0,
751 )
752 } else {
753 None
754 };
755
756 let resize = vmm::api::VmResizeData {
757 desired_vcpus,
758 desired_ram,
759 desired_balloon,
760 };
761
762 Ok(serde_json::to_string(&resize).unwrap())
763 }
764
resize_zone_config(id: &str, size: &str) -> Result<String, Error>765 fn resize_zone_config(id: &str, size: &str) -> Result<String, Error> {
766 let resize_zone = vmm::api::VmResizeZoneData {
767 id: id.to_owned(),
768 desired_ram: size
769 .parse::<ByteSized>()
770 .map_err(Error::InvalidMemorySize)?
771 .0,
772 };
773
774 Ok(serde_json::to_string(&resize_zone).unwrap())
775 }
776
add_device_config(config: &str) -> Result<String, Error>777 fn add_device_config(config: &str) -> Result<String, Error> {
778 let device_config = DeviceConfig::parse(config).map_err(Error::AddDeviceConfig)?;
779 let device_config = serde_json::to_string(&device_config).unwrap();
780
781 Ok(device_config)
782 }
783
add_user_device_config(config: &str) -> Result<String, Error>784 fn add_user_device_config(config: &str) -> Result<String, Error> {
785 let device_config = UserDeviceConfig::parse(config).map_err(Error::AddUserDeviceConfig)?;
786 let device_config = serde_json::to_string(&device_config).unwrap();
787
788 Ok(device_config)
789 }
790
remove_device_config(id: &str) -> String791 fn remove_device_config(id: &str) -> String {
792 let remove_device_data = vmm::api::VmRemoveDeviceData { id: id.to_owned() };
793
794 serde_json::to_string(&remove_device_data).unwrap()
795 }
796
add_disk_config(config: &str) -> Result<String, Error>797 fn add_disk_config(config: &str) -> Result<String, Error> {
798 let disk_config = DiskConfig::parse(config).map_err(Error::AddDiskConfig)?;
799 let disk_config = serde_json::to_string(&disk_config).unwrap();
800
801 Ok(disk_config)
802 }
803
add_fs_config(config: &str) -> Result<String, Error>804 fn add_fs_config(config: &str) -> Result<String, Error> {
805 let fs_config = FsConfig::parse(config).map_err(Error::AddFsConfig)?;
806 let fs_config = serde_json::to_string(&fs_config).unwrap();
807
808 Ok(fs_config)
809 }
810
add_pmem_config(config: &str) -> Result<String, Error>811 fn add_pmem_config(config: &str) -> Result<String, Error> {
812 let pmem_config = PmemConfig::parse(config).map_err(Error::AddPmemConfig)?;
813 let pmem_config = serde_json::to_string(&pmem_config).unwrap();
814
815 Ok(pmem_config)
816 }
817
add_net_config(config: &str) -> Result<(String, Vec<i32>), Error>818 fn add_net_config(config: &str) -> Result<(String, Vec<i32>), Error> {
819 let mut net_config = NetConfig::parse(config).map_err(Error::AddNetConfig)?;
820
821 // NetConfig is modified on purpose here by taking the list of file
822 // descriptors out. Keeping the list and send it to the server side
823 // process would not make any sense since the file descriptor may be
824 // represented with different values.
825 let fds = net_config.fds.take().unwrap_or_default();
826 let net_config = serde_json::to_string(&net_config).unwrap();
827
828 Ok((net_config, fds))
829 }
830
add_vdpa_config(config: &str) -> Result<String, Error>831 fn add_vdpa_config(config: &str) -> Result<String, Error> {
832 let vdpa_config = VdpaConfig::parse(config).map_err(Error::AddVdpaConfig)?;
833 let vdpa_config = serde_json::to_string(&vdpa_config).unwrap();
834
835 Ok(vdpa_config)
836 }
837
add_vsock_config(config: &str) -> Result<String, Error>838 fn add_vsock_config(config: &str) -> Result<String, Error> {
839 let vsock_config = VsockConfig::parse(config).map_err(Error::AddVsockConfig)?;
840 let vsock_config = serde_json::to_string(&vsock_config).unwrap();
841
842 Ok(vsock_config)
843 }
844
snapshot_config(url: &str) -> String845 fn snapshot_config(url: &str) -> String {
846 let snapshot_config = vmm::api::VmSnapshotConfig {
847 destination_url: String::from(url),
848 };
849
850 serde_json::to_string(&snapshot_config).unwrap()
851 }
852
restore_config(config: &str) -> Result<(String, Vec<i32>), Error>853 fn restore_config(config: &str) -> Result<(String, Vec<i32>), Error> {
854 let mut restore_config = RestoreConfig::parse(config).map_err(Error::Restore)?;
855 // RestoreConfig is modified on purpose to take out the file descriptors.
856 // These fds are passed to the server side process via SCM_RIGHTS
857 let fds = match &mut restore_config.net_fds {
858 Some(net_fds) => net_fds
859 .iter_mut()
860 .flat_map(|net| net.fds.take().unwrap_or_default())
861 .collect(),
862 None => Vec::new(),
863 };
864 let restore_config = serde_json::to_string(&restore_config).unwrap();
865
866 Ok((restore_config, fds))
867 }
868
coredump_config(destination_url: &str) -> String869 fn coredump_config(destination_url: &str) -> String {
870 let coredump_config = vmm::api::VmCoredumpData {
871 destination_url: String::from(destination_url),
872 };
873
874 serde_json::to_string(&coredump_config).unwrap()
875 }
876
receive_migration_data(url: &str) -> String877 fn receive_migration_data(url: &str) -> String {
878 let receive_migration_data = vmm::api::VmReceiveMigrationData {
879 receiver_url: url.to_owned(),
880 };
881
882 serde_json::to_string(&receive_migration_data).unwrap()
883 }
884
send_migration_data(url: &str, local: bool) -> String885 fn send_migration_data(url: &str, local: bool) -> String {
886 let send_migration_data = vmm::api::VmSendMigrationData {
887 destination_url: url.to_owned(),
888 local,
889 };
890
891 serde_json::to_string(&send_migration_data).unwrap()
892 }
893
create_data(path: &str) -> Result<String, Error>894 fn create_data(path: &str) -> Result<String, Error> {
895 let mut data = String::default();
896 if path == "-" {
897 std::io::stdin()
898 .read_to_string(&mut data)
899 .map_err(Error::ReadingStdin)?;
900 } else {
901 data = std::fs::read_to_string(path).map_err(Error::ReadingFile)?;
902 }
903
904 Ok(data)
905 }
906
907 /// Returns all [`Arg`]s in alphabetical order.
908 ///
909 /// This is the order used in the `--help` output.
get_cli_args() -> Box<[Arg]>910 fn get_cli_args() -> Box<[Arg]> {
911 [
912 Arg::new("api-socket")
913 .long("api-socket")
914 .help("HTTP API socket path (UNIX domain socket).")
915 .num_args(1),
916 #[cfg(feature = "dbus_api")]
917 Arg::new("dbus-object-path")
918 .long("dbus-object-path")
919 .help("Object path which the interface is being served at")
920 .num_args(1),
921 #[cfg(feature = "dbus_api")]
922 Arg::new("dbus-service-name")
923 .long("dbus-service-name")
924 .help("Well known name of the dbus service")
925 .num_args(1),
926 #[cfg(feature = "dbus_api")]
927 Arg::new("dbus-system-bus")
928 .long("dbus-system-bus")
929 .action(ArgAction::SetTrue)
930 .num_args(0)
931 .help("Use the system bus instead of a session bus"),
932 ]
933 .to_vec()
934 .into_boxed_slice()
935 }
936
937 /// Returns all [`Command`]s in alphabetical order.
938 ///
939 /// This is the order used in the `--help` output.
get_cli_commands_sorted() -> Box<[Command]>940 fn get_cli_commands_sorted() -> Box<[Command]> {
941 [
942 Command::new("add-device").about("Add VFIO device").arg(
943 Arg::new("device_config")
944 .index(1)
945 .help(DeviceConfig::SYNTAX),
946 ),
947 Command::new("add-disk")
948 .about("Add block device")
949 .arg(Arg::new("disk_config").index(1).help(DiskConfig::SYNTAX)),
950 Command::new("add-fs")
951 .about("Add virtio-fs backed fs device")
952 .arg(
953 Arg::new("fs_config")
954 .index(1)
955 .help(vmm::vm_config::FsConfig::SYNTAX),
956 ),
957 Command::new("add-net")
958 .about("Add network device")
959 .arg(Arg::new("net_config").index(1).help(NetConfig::SYNTAX)),
960 Command::new("add-pmem")
961 .about("Add persistent memory device")
962 .arg(
963 Arg::new("pmem_config")
964 .index(1)
965 .help(vmm::vm_config::PmemConfig::SYNTAX),
966 ),
967 Command::new("add-user-device")
968 .about("Add userspace device")
969 .arg(
970 Arg::new("device_config")
971 .index(1)
972 .help(UserDeviceConfig::SYNTAX),
973 ),
974 Command::new("add-vdpa")
975 .about("Add vDPA device")
976 .arg(Arg::new("vdpa_config").index(1).help(VdpaConfig::SYNTAX)),
977 Command::new("add-vsock")
978 .about("Add vsock device")
979 .arg(Arg::new("vsock_config").index(1).help(VsockConfig::SYNTAX)),
980 Command::new("boot").about("Boot a created VM"),
981 Command::new("coredump")
982 .about("Create a coredump from VM")
983 .arg(Arg::new("coredump_config").index(1).help("<file_path>")),
984 Command::new("counters").about("Counters from the VM"),
985 Command::new("create")
986 .about("Create VM from a JSON configuration")
987 .arg(Arg::new("path").index(1).default_value("-")),
988 Command::new("delete").about("Delete a VM"),
989 Command::new("info").about("Info on the VM"),
990 Command::new("nmi").about("Trigger NMI"),
991 Command::new("pause").about("Pause the VM"),
992 Command::new("ping").about("Ping the VMM to check for API server availability"),
993 Command::new("power-button").about("Trigger a power button in the VM"),
994 Command::new("reboot").about("Reboot the VM"),
995 Command::new("receive-migration")
996 .about("Receive a VM migration")
997 .arg(
998 Arg::new("receive_migration_config")
999 .index(1)
1000 .help("<receiver_url>"),
1001 ),
1002 Command::new("remove-device")
1003 .about("Remove VFIO and PCI device")
1004 .arg(Arg::new("id").index(1).help("<device_id>")),
1005 Command::new("resize")
1006 .about("Resize the VM")
1007 .arg(
1008 Arg::new("balloon")
1009 .long("balloon")
1010 .help("New balloon size in bytes (supports K/M/G suffix)")
1011 .num_args(1),
1012 )
1013 .arg(
1014 Arg::new("cpus")
1015 .long("cpus")
1016 .help("New vCPUs count")
1017 .num_args(1),
1018 )
1019 .arg(
1020 Arg::new("memory")
1021 .long("memory")
1022 .help("New memory size in bytes (supports K/M/G suffix)")
1023 .num_args(1),
1024 ),
1025 Command::new("resize-zone")
1026 .about("Resize a memory zone")
1027 .arg(
1028 Arg::new("id")
1029 .long("id")
1030 .help("Memory zone identifier")
1031 .num_args(1),
1032 )
1033 .arg(
1034 Arg::new("size")
1035 .long("size")
1036 .help("New memory zone size in bytes (supports K/M/G suffix)")
1037 .num_args(1),
1038 ),
1039 Command::new("restore")
1040 .about("Restore VM from a snapshot")
1041 .arg(
1042 Arg::new("restore_config")
1043 .index(1)
1044 .help(RestoreConfig::SYNTAX),
1045 ),
1046 Command::new("resume").about("Resume the VM"),
1047 Command::new("send-migration")
1048 .about("Initiate a VM migration")
1049 .arg(
1050 Arg::new("send_migration_config")
1051 .index(1)
1052 .help("<destination_url>"),
1053 )
1054 .arg(
1055 Arg::new("send_migration_local")
1056 .long("local")
1057 .num_args(0)
1058 .action(ArgAction::SetTrue),
1059 ),
1060 Command::new("shutdown").about("Shutdown the VM"),
1061 Command::new("shutdown-vmm").about("Shutdown the VMM"),
1062 Command::new("snapshot")
1063 .about("Create a snapshot from VM")
1064 .arg(
1065 Arg::new("snapshot_config")
1066 .index(1)
1067 .help("<destination_url>"),
1068 ),
1069 ]
1070 .to_vec()
1071 .into_boxed_slice()
1072 }
1073
main()1074 fn main() {
1075 env_logger::init();
1076 let app = Command::new("ch-remote")
1077 .author(env!("CARGO_PKG_AUTHORS"))
1078 .version(env!("BUILD_VERSION"))
1079 .about("Remotely control a cloud-hypervisor VMM.")
1080 .arg_required_else_help(true)
1081 .subcommand_required(true)
1082 .args(get_cli_args())
1083 .subcommands(get_cli_commands_sorted());
1084
1085 let matches = app.get_matches();
1086
1087 let mut target_api = match (
1088 matches.get_one::<String>("api-socket"),
1089 #[cfg(feature = "dbus_api")]
1090 matches.get_one::<String>("dbus-service-name"),
1091 #[cfg(feature = "dbus_api")]
1092 matches.get_one::<String>("dbus-object-path"),
1093 ) {
1094 #[cfg(not(feature = "dbus_api"))]
1095 (Some(api_sock),) => TargetApi::HttpApi(
1096 UnixStream::connect(api_sock).unwrap_or_else(|e| {
1097 error!("Error opening HTTP socket: {e}");
1098 process::exit(1)
1099 }),
1100 PhantomData,
1101 ),
1102 #[cfg(feature = "dbus_api")]
1103 (Some(api_sock), None, None) => TargetApi::HttpApi(
1104 UnixStream::connect(api_sock).unwrap_or_else(|e| {
1105 error!("Error opening HTTP socket: {e}");
1106 process::exit(1)
1107 }),
1108 PhantomData,
1109 ),
1110 #[cfg(feature = "dbus_api")]
1111 (None, Some(dbus_name), Some(dbus_path)) => TargetApi::DBusApi(
1112 DBusApi1ProxyBlocking::new_connection(
1113 dbus_name,
1114 dbus_path,
1115 matches.get_flag("dbus-system-bus"),
1116 )
1117 .map_err(Error::DBusApiClient)
1118 .unwrap_or_else(|e| {
1119 error!("Error creating D-Bus proxy: {e}");
1120 process::exit(1)
1121 }),
1122 ),
1123 #[cfg(feature = "dbus_api")]
1124 (Some(_), Some(_) | None, Some(_) | None) => {
1125 error!(
1126 "`api-socket` and (dbus-service-name or dbus-object-path) are mutually exclusive"
1127 );
1128 process::exit(1);
1129 }
1130 _ => {
1131 error!("Please either provide the api-socket option or dbus-service-name and dbus-object-path options");
1132 process::exit(1);
1133 }
1134 };
1135
1136 if let Err(top_error) = target_api.do_command(&matches) {
1137 // Helper to join strings with a newline.
1138 fn join_strs(mut acc: String, next: String) -> String {
1139 if !acc.is_empty() {
1140 acc.push('\n');
1141 }
1142 acc.push_str(&next);
1143 acc
1144 }
1145
1146 // This function helps to modify the Display representation of remote
1147 // API failures so that it aligns with the regular output of error
1148 // messages. As we transfer a deep/rich chain of errors as String via
1149 // the HTTP API, the nested error chain is lost. We retrieve it from
1150 // the error response.
1151 //
1152 // In case the repose itself is broken, the error is printed directly
1153 // by using the `X` level.
1154 fn server_api_error_display_modifier(
1155 level: usize,
1156 indention: usize,
1157 error: &(dyn std::error::Error + 'static),
1158 ) -> Option<String> {
1159 if let Some(api_client::Error::ServerResponse(status_code, body)) =
1160 error.downcast_ref::<api_client::Error>()
1161 {
1162 let body = body.as_ref().map(|body| body.as_str()).unwrap_or("");
1163
1164 // Retrieve the list of error messages back.
1165 let lines: Vec<&str> = match serde_json::from_str(body) {
1166 Ok(json) => json,
1167 Err(e) => {
1168 return Some(format!(
1169 "{idention}X: Can't get remote's error messages from JSON response: {e}: body='{body}'",
1170 idention = " ".repeat(indention)
1171 ));
1172 }
1173 };
1174
1175 let error_status = format!("Server responded with {status_code:?}");
1176 // Prepend the error status line to the lines iter.
1177 let lines = std::iter::once(error_status.as_str()).chain(lines);
1178 let error_msg_multiline = lines
1179 .enumerate()
1180 .map(|(index, error_msg)| (index + level, error_msg))
1181 .map(|(level, error_msg)| {
1182 format!(
1183 "{idention}{level}: {error_msg}",
1184 idention = " ".repeat(indention)
1185 )
1186 })
1187 .fold(String::new(), join_strs);
1188
1189 return Some(error_msg_multiline);
1190 }
1191
1192 None
1193 }
1194
1195 let top_error: &dyn std::error::Error = &top_error;
1196 cloud_hypervisor::cli_print_error_chain(
1197 top_error,
1198 "ch-remote",
1199 server_api_error_display_modifier,
1200 );
1201 process::exit(1)
1202 };
1203 }
1204
1205 #[cfg(test)]
1206 mod tests {
1207 use std::cmp::Ordering;
1208
1209 use super::*;
1210 use crate::test_util::assert_args_sorted;
1211
1212 #[test]
test_cli_args_sorted()1213 fn test_cli_args_sorted() {
1214 let args = get_cli_args();
1215 assert_args_sorted(|| args.iter());
1216 }
1217
1218 #[test]
test_cli_commands_sorted()1219 fn test_cli_commands_sorted() {
1220 let commands = get_cli_commands_sorted();
1221
1222 // check commands itself are sorted
1223 let iter = commands.iter().zip(commands.iter().skip(1));
1224 for (command, next) in iter {
1225 assert_ne!(
1226 command.get_name().cmp(next.get_name()),
1227 Ordering::Greater,
1228 "commands not alphabetically sorted: command={}, next={}",
1229 command.get_name(),
1230 next.get_name()
1231 );
1232 }
1233
1234 // check args of commands sorted
1235 for command in commands {
1236 assert_args_sorted(|| command.get_arguments());
1237 }
1238 }
1239 }
1240