xref: /cloud-hypervisor/src/bin/ch-remote.rs (revision 19dc733267fecd378e28bff3a0946839e1c15a90)
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