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