1#![expect(missing_docs)]
8#![cfg_attr(not(test), forbid(unsafe_code))]
9
10mod cli_args;
11mod crash_dump;
12mod kvp;
13mod meshworker;
14mod serial_io;
15mod storage_builder;
16mod tracing_init;
17mod ttrpc;
18
19pub use cli_args::Options;
22use console_relay::ConsoleLaunchOptions;
23
24use crate::cli_args::SecureBootTemplateCli;
25use anyhow::Context;
26use anyhow::bail;
27use chipset_resources::battery::HostBatteryUpdate;
28use clap::CommandFactory;
29use clap::FromArgMatches;
30use clap::Parser;
31use cli_args::DiskCliKind;
32use cli_args::EfiDiagnosticsLogLevelCli;
33use cli_args::EndpointConfigCli;
34use cli_args::NicConfigCli;
35use cli_args::ProvisionVmgs;
36use cli_args::SerialConfigCli;
37use cli_args::UefiConsoleModeCli;
38use cli_args::VirtioBusCli;
39use cli_args::VmgsCli;
40use crash_dump::spawn_dump_handler;
41use disk_backend_resources::DelayDiskHandle;
42use disk_backend_resources::DiskLayerDescription;
43use disk_backend_resources::layer::DiskLayerHandle;
44use disk_backend_resources::layer::RamDiskLayerHandle;
45use disk_backend_resources::layer::SqliteAutoCacheDiskLayerHandle;
46use disk_backend_resources::layer::SqliteDiskLayerHandle;
47use floppy_resources::FloppyDiskConfig;
48use framebuffer::FRAMEBUFFER_SIZE;
49use framebuffer::FramebufferAccess;
50use futures::AsyncReadExt;
51use futures::AsyncWrite;
52use futures::AsyncWriteExt;
53use futures::FutureExt;
54use futures::StreamExt;
55use futures::executor::block_on;
56use futures::io::AllowStdIo;
57use futures_concurrency::stream::Merge;
58use gdma_resources::GdmaDeviceHandle;
59use gdma_resources::VportDefinition;
60use get_resources::ged::GuestServicingFlags;
61use guid::Guid;
62use input_core::MultiplexedInputHandle;
63use inspect::InspectMut;
64use inspect::InspectionBuilder;
65use io::Read;
66use mesh::CancelContext;
67use mesh::CellUpdater;
68use mesh::error::RemoteError;
69use mesh::rpc::Rpc;
70use mesh::rpc::RpcError;
71use mesh::rpc::RpcSend;
72use mesh_worker::WorkerEvent;
73use mesh_worker::WorkerHandle;
74use meshworker::VmmMesh;
75use net_backend_resources::mac_address::MacAddress;
76use nvme_resources::NamespaceDefinition;
77use nvme_resources::NvmeControllerRequest;
78use openvmm_defs::config::Config;
79use openvmm_defs::config::DEFAULT_MMIO_GAPS_AARCH64;
80use openvmm_defs::config::DEFAULT_MMIO_GAPS_AARCH64_WITH_VTL2;
81use openvmm_defs::config::DEFAULT_MMIO_GAPS_X86;
82use openvmm_defs::config::DEFAULT_MMIO_GAPS_X86_WITH_VTL2;
83use openvmm_defs::config::DEFAULT_PCAT_BOOT_ORDER;
84use openvmm_defs::config::DEFAULT_PCIE_ECAM_BASE;
85use openvmm_defs::config::DeviceVtl;
86use openvmm_defs::config::EfiDiagnosticsLogLevelType;
87use openvmm_defs::config::HypervisorConfig;
88use openvmm_defs::config::LateMapVtl0MemoryPolicy;
89use openvmm_defs::config::LoadMode;
90use openvmm_defs::config::MemoryConfig;
91use openvmm_defs::config::PcieDeviceConfig;
92use openvmm_defs::config::PcieRootComplexConfig;
93use openvmm_defs::config::PcieRootPortConfig;
94use openvmm_defs::config::PcieSwitchConfig;
95use openvmm_defs::config::ProcessorTopologyConfig;
96use openvmm_defs::config::SerialInformation;
97use openvmm_defs::config::VirtioBus;
98use openvmm_defs::config::VmbusConfig;
99use openvmm_defs::config::VpciDeviceConfig;
100use openvmm_defs::config::Vtl2BaseAddressType;
101use openvmm_defs::config::Vtl2Config;
102use openvmm_defs::rpc::PulseSaveRestoreError;
103use openvmm_defs::rpc::VmRpc;
104use openvmm_defs::worker::VM_WORKER;
105use openvmm_defs::worker::VmWorkerParameters;
106use openvmm_helpers::disk::create_disk_type;
107use openvmm_helpers::disk::open_disk_type;
108use pal_async::DefaultDriver;
109use pal_async::DefaultPool;
110use pal_async::socket::PolledSocket;
111use pal_async::task::Spawn;
112use pal_async::task::Task;
113use pal_async::timer::PolledTimer;
114use scsidisk_resources::SimpleScsiDiskHandle;
115use scsidisk_resources::SimpleScsiDvdHandle;
116use serial_16550_resources::ComPort;
117use serial_core::resources::DisconnectedSerialBackendHandle;
118use sparse_mmap::alloc_shared_memory;
119use std::cell::RefCell;
120use std::fmt::Write as _;
121use std::future::pending;
122use std::io;
123#[cfg(unix)]
124use std::io::IsTerminal;
125use std::io::Write;
126use std::net::TcpListener;
127use std::path::Path;
128use std::path::PathBuf;
129use std::pin::pin;
130use std::sync::Arc;
131use std::thread;
132use std::time::Duration;
133use std::time::Instant;
134use storvsp_resources::ScsiControllerRequest;
135use storvsp_resources::ScsiDeviceAndPath;
136use storvsp_resources::ScsiPath;
137use tpm_resources::TpmDeviceHandle;
138use tpm_resources::TpmRegisterLayout;
139use tracing_helpers::AnyhowValueExt;
140use uidevices_resources::SynthKeyboardHandle;
141use uidevices_resources::SynthMouseHandle;
142use uidevices_resources::SynthVideoHandle;
143use video_core::SharedFramebufferHandle;
144use virtio_resources::VirtioPciDeviceHandle;
145use vm_manifest_builder::BaseChipsetType;
146use vm_manifest_builder::MachineArch;
147use vm_manifest_builder::VmChipsetResult;
148use vm_manifest_builder::VmManifestBuilder;
149use vm_resource::IntoResource;
150use vm_resource::Resource;
151use vm_resource::kind::DiskHandleKind;
152use vm_resource::kind::DiskLayerHandleKind;
153use vm_resource::kind::NetEndpointHandleKind;
154use vm_resource::kind::VirtioDeviceHandle;
155use vm_resource::kind::VmbusDeviceHandleKind;
156use vmbus_serial_resources::VmbusSerialDeviceHandle;
157use vmbus_serial_resources::VmbusSerialPort;
158use vmcore::non_volatile_store::resources::EphemeralNonVolatileStoreHandle;
159use vmgs_resources::GuestStateEncryptionPolicy;
160use vmgs_resources::VmgsDisk;
161use vmgs_resources::VmgsFileHandle;
162use vmgs_resources::VmgsResource;
163use vmotherboard::ChipsetDeviceHandle;
164use vnc_worker_defs::VncParameters;
165
166pub fn openvmm_main() {
167 #[cfg(unix)]
170 let orig_termios = io::stderr().is_terminal().then(term::get_termios);
171
172 let exit_code = match do_main() {
173 Ok(_) => 0,
174 Err(err) => {
175 eprintln!("fatal error: {:?}", err);
176 1
177 }
178 };
179
180 #[cfg(unix)]
182 if let Some(orig_termios) = orig_termios {
183 term::set_termios(orig_termios);
184 }
185
186 let _ = io::stdout().flush();
192 pal::process::terminate(exit_code);
193}
194
195#[derive(Default)]
196struct VmResources {
197 console_in: Option<Box<dyn AsyncWrite + Send + Unpin>>,
198 framebuffer_access: Option<FramebufferAccess>,
199 shutdown_ic: Option<mesh::Sender<hyperv_ic_resources::shutdown::ShutdownRpc>>,
200 kvp_ic: Option<mesh::Sender<hyperv_ic_resources::kvp::KvpConnectRpc>>,
201 scsi_rpc: Option<mesh::Sender<ScsiControllerRequest>>,
202 nvme_vtl2_rpc: Option<mesh::Sender<NvmeControllerRequest>>,
203 ged_rpc: Option<mesh::Sender<get_resources::ged::GuestEmulationRequest>>,
204 vtl2_settings: Option<vtl2_settings_proto::Vtl2Settings>,
205 #[cfg(windows)]
206 switch_ports: Vec<vmswitch::kernel::SwitchPort>,
207}
208
209impl VmResources {
210 async fn modify_vtl2_settings(
215 &mut self,
216 f: impl FnOnce(&mut vtl2_settings_proto::Vtl2Settings),
217 ) -> anyhow::Result<()> {
218 let mut settings_copy = self
219 .vtl2_settings
220 .clone()
221 .context("vtl2 settings not configured")?;
222
223 f(&mut settings_copy);
224
225 let ged_rpc = self.ged_rpc.as_ref().context("no GED configured")?;
226
227 ged_rpc
228 .call_failable(
229 get_resources::ged::GuestEmulationRequest::ModifyVtl2Settings,
230 prost::Message::encode_to_vec(&settings_copy),
231 )
232 .await?;
233
234 self.vtl2_settings = Some(settings_copy);
236 Ok(())
237 }
238
239 async fn add_vtl0_scsi_disk(
244 &mut self,
245 controller_guid: Guid,
246 lun: u32,
247 device_type: vtl2_settings_proto::physical_device::DeviceType,
248 device_path: Guid,
249 sub_device_path: u32,
250 ) -> anyhow::Result<()> {
251 let mut not_found = false;
252 self.modify_vtl2_settings(|settings| {
253 let dynamic = settings.dynamic.get_or_insert_with(Default::default);
254
255 let scsi_controller = dynamic.storage_controllers.iter_mut().find(|c| {
257 c.instance_id == controller_guid.to_string()
258 && c.protocol
259 == vtl2_settings_proto::storage_controller::StorageProtocol::Scsi as i32
260 });
261
262 let Some(scsi_controller) = scsi_controller else {
263 not_found = true;
264 return;
265 };
266
267 scsi_controller.luns.push(vtl2_settings_proto::Lun {
269 location: lun,
270 device_id: Guid::new_random().to_string(),
271 vendor_id: "OpenVMM".to_string(),
272 product_id: "Disk".to_string(),
273 product_revision_level: "1.0".to_string(),
274 serial_number: "0".to_string(),
275 model_number: "1".to_string(),
276 physical_devices: Some(vtl2_settings_proto::PhysicalDevices {
277 r#type: vtl2_settings_proto::physical_devices::BackingType::Single.into(),
278 device: Some(vtl2_settings_proto::PhysicalDevice {
279 device_type: device_type.into(),
280 device_path: device_path.to_string(),
281 sub_device_path,
282 }),
283 devices: Vec::new(),
284 }),
285 is_dvd: false,
286 ..Default::default()
287 });
288 })
289 .await?;
290
291 if not_found {
292 anyhow::bail!("SCSI controller {} not found", controller_guid);
293 }
294 Ok(())
295 }
296
297 async fn remove_vtl0_scsi_disk(
301 &mut self,
302 controller_guid: Guid,
303 lun: u32,
304 ) -> anyhow::Result<()> {
305 self.modify_vtl2_settings(|settings| {
306 let dynamic = settings.dynamic.as_mut();
307 if let Some(dynamic) = dynamic {
308 if let Some(scsi_controller) = dynamic.storage_controllers.iter_mut().find(|c| {
310 c.instance_id == controller_guid.to_string()
311 && c.protocol
312 == vtl2_settings_proto::storage_controller::StorageProtocol::Scsi as i32
313 }) {
314 scsi_controller.luns.retain(|l| l.location != lun);
316 }
317 }
318 })
319 .await
320 }
321
322 async fn remove_vtl0_scsi_disk_by_nvme_nsid(
326 &mut self,
327 controller_guid: Guid,
328 nvme_controller_guid: Guid,
329 nsid: u32,
330 ) -> anyhow::Result<Option<u32>> {
331 let mut removed_lun = None;
332 self.modify_vtl2_settings(|settings| {
333 let dynamic = settings.dynamic.as_mut();
334 if let Some(dynamic) = dynamic {
335 if let Some(scsi_controller) = dynamic.storage_controllers.iter_mut().find(|c| {
337 c.instance_id == controller_guid.to_string()
338 && c.protocol
339 == vtl2_settings_proto::storage_controller::StorageProtocol::Scsi as i32
340 }) {
341 let nvme_controller_str = nvme_controller_guid.to_string();
343 scsi_controller.luns.retain(|l| {
344 let dominated_by_nsid = l.physical_devices.as_ref().is_some_and(|pd| {
345 pd.device.as_ref().is_some_and(|d| {
346 d.device_type
347 == vtl2_settings_proto::physical_device::DeviceType::Nvme as i32
348 && d.device_path == nvme_controller_str
349 && d.sub_device_path == nsid
350 })
351 });
352 if dominated_by_nsid {
353 removed_lun = Some(l.location);
354 false } else {
356 true }
358 });
359 }
360 }
361 })
362 .await?;
363 Ok(removed_lun)
364 }
365}
366
367struct ConsoleState<'a> {
368 device: &'a str,
369 input: Box<dyn AsyncWrite + Unpin + Send>,
370}
371
372fn build_switch_list(all_switches: &[cli_args::GenericPcieSwitchCli]) -> Vec<PcieSwitchConfig> {
377 all_switches
378 .iter()
379 .map(|switch_cli| PcieSwitchConfig {
380 name: switch_cli.name.clone(),
381 num_downstream_ports: switch_cli.num_downstream_ports,
382 parent_port: switch_cli.port_name.clone(),
383 hotplug: switch_cli.hotplug,
384 })
385 .collect()
386}
387
388async fn vm_config_from_command_line(
389 spawner: impl Spawn,
390 mesh: &VmmMesh,
391 opt: &Options,
392) -> anyhow::Result<(Config, VmResources)> {
393 let (_, serial_driver) = DefaultPool::spawn_on_thread("serial");
394 serial_driver.spawn("leak", pending::<()>()).detach();
396
397 let openhcl_vtl = if opt.vtl2 {
398 DeviceVtl::Vtl2
399 } else {
400 DeviceVtl::Vtl0
401 };
402
403 let console_state: RefCell<Option<ConsoleState<'_>>> = RefCell::new(None);
404 let setup_serial = |name: &str, cli_cfg, device| -> anyhow::Result<_> {
405 Ok(match cli_cfg {
406 SerialConfigCli::Console => {
407 if let Some(console_state) = console_state.borrow().as_ref() {
408 bail!("console already set by {}", console_state.device);
409 }
410 let (config, serial) = serial_io::anonymous_serial_pair(&serial_driver)?;
411 let (serial_read, serial_write) = AsyncReadExt::split(serial);
412 *console_state.borrow_mut() = Some(ConsoleState {
413 device,
414 input: Box::new(serial_write),
415 });
416 thread::Builder::new()
417 .name(name.to_owned())
418 .spawn(move || {
419 let _ = block_on(futures::io::copy(
420 serial_read,
421 &mut AllowStdIo::new(term::raw_stdout()),
422 ));
423 })
424 .unwrap();
425 Some(config)
426 }
427 SerialConfigCli::Stderr => {
428 let (config, serial) = serial_io::anonymous_serial_pair(&serial_driver)?;
429 thread::Builder::new()
430 .name(name.to_owned())
431 .spawn(move || {
432 let _ = block_on(futures::io::copy(
433 serial,
434 &mut AllowStdIo::new(term::raw_stderr()),
435 ));
436 })
437 .unwrap();
438 Some(config)
439 }
440 SerialConfigCli::File(path) => {
441 let (config, serial) = serial_io::anonymous_serial_pair(&serial_driver)?;
442 let file = fs_err::File::create(path).context("failed to create file")?;
443
444 thread::Builder::new()
445 .name(name.to_owned())
446 .spawn(move || {
447 let _ = block_on(futures::io::copy(serial, &mut AllowStdIo::new(file)));
448 })
449 .unwrap();
450 Some(config)
451 }
452 SerialConfigCli::None => None,
453 SerialConfigCli::Pipe(path) => {
454 Some(serial_io::bind_serial(&path).context("failed to bind serial")?)
455 }
456 SerialConfigCli::Tcp(addr) => {
457 Some(serial_io::bind_tcp_serial(&addr).context("failed to bind serial")?)
458 }
459 SerialConfigCli::NewConsole(app, window_title) => {
460 let path = console_relay::random_console_path();
461 let config =
462 serial_io::bind_serial(&path).context("failed to bind console serial")?;
463 let window_title =
464 window_title.unwrap_or_else(|| name.to_uppercase() + " [OpenVMM]");
465
466 console_relay::launch_console(
467 app.or_else(openvmm_terminal_app).as_deref(),
468 &path,
469 ConsoleLaunchOptions {
470 window_title: Some(window_title),
471 },
472 )
473 .context("failed to launch console")?;
474
475 Some(config)
476 }
477 })
478 };
479
480 let mut vmbus_devices = Vec::new();
481
482 let serial0_cfg = setup_serial(
483 "com1",
484 opt.com1.clone().unwrap_or(SerialConfigCli::Console),
485 if cfg!(guest_arch = "x86_64") {
486 "ttyS0"
487 } else {
488 "ttyAMA0"
489 },
490 )?;
491 let serial1_cfg = setup_serial(
492 "com2",
493 opt.com2.clone().unwrap_or(SerialConfigCli::None),
494 if cfg!(guest_arch = "x86_64") {
495 "ttyS1"
496 } else {
497 "ttyAMA1"
498 },
499 )?;
500 let serial2_cfg = setup_serial(
501 "com3",
502 opt.com3.clone().unwrap_or(SerialConfigCli::None),
503 if cfg!(guest_arch = "x86_64") {
504 "ttyS2"
505 } else {
506 "ttyAMA2"
507 },
508 )?;
509 let serial3_cfg = setup_serial(
510 "com4",
511 opt.com4.clone().unwrap_or(SerialConfigCli::None),
512 if cfg!(guest_arch = "x86_64") {
513 "ttyS3"
514 } else {
515 "ttyAMA3"
516 },
517 )?;
518 let with_vmbus_com1_serial = if let Some(vmbus_com1_cfg) = setup_serial(
519 "vmbus_com1",
520 opt.vmbus_com1_serial
521 .clone()
522 .unwrap_or(SerialConfigCli::None),
523 "vmbus_com1",
524 )? {
525 vmbus_devices.push((
526 openhcl_vtl,
527 VmbusSerialDeviceHandle {
528 port: VmbusSerialPort::Com1,
529 backend: vmbus_com1_cfg,
530 }
531 .into_resource(),
532 ));
533 true
534 } else {
535 false
536 };
537 let with_vmbus_com2_serial = if let Some(vmbus_com2_cfg) = setup_serial(
538 "vmbus_com2",
539 opt.vmbus_com2_serial
540 .clone()
541 .unwrap_or(SerialConfigCli::None),
542 "vmbus_com2",
543 )? {
544 vmbus_devices.push((
545 openhcl_vtl,
546 VmbusSerialDeviceHandle {
547 port: VmbusSerialPort::Com2,
548 backend: vmbus_com2_cfg,
549 }
550 .into_resource(),
551 ));
552 true
553 } else {
554 false
555 };
556 let debugcon_cfg = setup_serial(
557 "debugcon",
558 opt.debugcon
559 .clone()
560 .map(|cfg| cfg.serial)
561 .unwrap_or(SerialConfigCli::None),
562 "debugcon",
563 )?;
564
565 let mut resources = VmResources::default();
566 let mut console_str = "";
567 if let Some(ConsoleState { device, input }) = console_state.into_inner() {
568 resources.console_in = Some(input);
569 console_str = device;
570 }
571
572 if opt.shared_memory {
573 tracing::warn!("--shared-memory/-M flag has no effect and will be removed");
574 }
575
576 const MAX_PROCESSOR_COUNT: u32 = 1024;
577
578 if opt.processors == 0 || opt.processors > MAX_PROCESSOR_COUNT {
579 bail!("invalid proc count: {}", opt.processors);
580 }
581
582 if opt.scsi_sub_channels > (MAX_PROCESSOR_COUNT - 1) as u16 {
585 bail!(
586 "invalid SCSI sub-channel count: requested {}, max {}",
587 opt.scsi_sub_channels,
588 MAX_PROCESSOR_COUNT - 1
589 );
590 }
591
592 let with_get = opt.get || (opt.vtl2 && !opt.no_get);
593
594 let mut storage = storage_builder::StorageBuilder::new(with_get.then_some(openhcl_vtl));
595 for &cli_args::DiskCli {
596 vtl,
597 ref kind,
598 read_only,
599 is_dvd,
600 underhill,
601 ref pcie_port,
602 } in &opt.disk
603 {
604 if pcie_port.is_some() {
605 anyhow::bail!("`--disk` is incompatible with PCIe");
606 }
607
608 storage.add(
609 vtl,
610 underhill,
611 storage_builder::DiskLocation::Scsi(None),
612 kind,
613 is_dvd,
614 read_only,
615 )?;
616 }
617
618 for &cli_args::IdeDiskCli {
619 ref kind,
620 read_only,
621 channel,
622 device,
623 is_dvd,
624 } in &opt.ide
625 {
626 storage.add(
627 DeviceVtl::Vtl0,
628 None,
629 storage_builder::DiskLocation::Ide(channel, device),
630 kind,
631 is_dvd,
632 read_only,
633 )?;
634 }
635
636 for &cli_args::DiskCli {
637 vtl,
638 ref kind,
639 read_only,
640 is_dvd,
641 underhill,
642 ref pcie_port,
643 } in &opt.nvme
644 {
645 storage.add(
646 vtl,
647 underhill,
648 storage_builder::DiskLocation::Nvme(None, pcie_port.clone()),
649 kind,
650 is_dvd,
651 read_only,
652 )?;
653 }
654
655 let floppy_disks: Vec<_> = opt
656 .floppy
657 .iter()
658 .map(|disk| -> anyhow::Result<_> {
659 let &cli_args::FloppyDiskCli {
660 ref kind,
661 read_only,
662 } = disk;
663 Ok(FloppyDiskConfig {
664 disk_type: disk_open(kind, read_only)?,
665 read_only,
666 })
667 })
668 .collect::<Result<Vec<_>, _>>()?;
669
670 let mut mana_nics = [(); 3].map(|()| None);
671 let mut underhill_nics = Vec::new();
672 let mut vpci_devices = Vec::new();
673
674 let mut nic_index = 0;
675 for cli_cfg in &opt.net {
676 let vport = parse_endpoint(cli_cfg, &mut nic_index, &mut resources)?;
677 if cli_cfg.underhill {
678 if !opt.no_alias_map {
679 anyhow::bail!("must specify --no-alias-map to offer NICs to VTL2");
680 }
681 let mana = mana_nics[openhcl_vtl as usize].get_or_insert_with(|| {
682 let vpci_instance_id = Guid::new_random();
683 underhill_nics.push(vtl2_settings_proto::NicDeviceLegacy {
684 instance_id: vpci_instance_id.to_string(),
685 subordinate_instance_id: None,
686 max_sub_channels: None,
687 });
688 (vpci_instance_id, GdmaDeviceHandle { vports: Vec::new() })
689 });
690 mana.1.vports.push(VportDefinition {
691 mac_address: vport.mac_address,
692 endpoint: vport.endpoint,
693 });
694 } else {
695 vmbus_devices.push(vport.into_netvsp_handle());
696 }
697 }
698
699 if opt.nic {
700 let nic_config = parse_endpoint(
701 &NicConfigCli {
702 vtl: DeviceVtl::Vtl0,
703 endpoint: EndpointConfigCli::Consomme { cidr: None },
704 max_queues: None,
705 underhill: false,
706 },
707 &mut nic_index,
708 &mut resources,
709 )?;
710 vmbus_devices.push(nic_config.into_netvsp_handle());
711 }
712
713 if opt.mcr {
714 tracing::info!("Instantiating MCR controller");
715
716 const MCR_INSTANCE_ID: Guid = guid::guid!("07effd8f-7501-426c-a947-d8345f39113d");
718
719 vpci_devices.push(VpciDeviceConfig {
720 vtl: DeviceVtl::Vtl0,
721 instance_id: MCR_INSTANCE_ID,
722 resource: mcr_resources::McrControllerHandle {
723 instance_id: MCR_INSTANCE_ID,
724 }
725 .into_resource(),
726 });
727 }
728
729 let mut pcie_devices = Vec::new();
732 for (index, cli_cfg) in opt.pcie_remote.iter().enumerate() {
733 tracing::info!(
734 port_name = %cli_cfg.port_name,
735 socket_path = ?cli_cfg.socket_path,
736 "instantiating PCIe remote device"
737 );
738
739 const PCIE_REMOTE_BASE_INSTANCE_ID: Guid =
741 guid::guid!("28ed784d-c059-429f-9d9a-46bea02562c0");
742 let instance_id = Guid {
743 data1: index as u32,
744 ..PCIE_REMOTE_BASE_INSTANCE_ID
745 };
746
747 pcie_devices.push(PcieDeviceConfig {
748 port_name: cli_cfg.port_name.clone(),
749 resource: pcie_remote_resources::PcieRemoteHandle {
750 instance_id,
751 socket_path: cli_cfg.socket_path.clone(),
752 hu: cli_cfg.hu,
753 controller: cli_cfg.controller,
754 }
755 .into_resource(),
756 });
757 }
758
759 #[cfg(windows)]
760 let mut kernel_vmnics = Vec::new();
761 #[cfg(windows)]
762 for (index, switch_id) in opt.kernel_vmnic.iter().enumerate() {
763 let mut mac_address = [0x00, 0x15, 0x5D, 0, 0, 0];
765 getrandom::fill(&mut mac_address[3..]).expect("rng failure");
766
767 const BASE_INSTANCE_ID: Guid = guid::guid!("00000000-435d-11ee-9f59-00155d5016fc");
769 let instance_id = Guid {
770 data1: index as u32,
771 ..BASE_INSTANCE_ID
772 };
773
774 let switch_id = if switch_id == "default" {
775 DEFAULT_SWITCH
776 } else {
777 switch_id
778 };
779 let (port_id, port) = new_switch_port(switch_id)?;
780 resources.switch_ports.push(port);
781
782 kernel_vmnics.push(openvmm_defs::config::KernelVmNicConfig {
783 instance_id,
784 mac_address: mac_address.into(),
785 switch_port_id: port_id,
786 });
787 }
788
789 for vport in &opt.mana {
790 let vport = parse_endpoint(vport, &mut nic_index, &mut resources)?;
791 mana_nics[vport.vtl as usize]
792 .get_or_insert_with(|| (Guid::new_random(), GdmaDeviceHandle { vports: Vec::new() }))
793 .1
794 .vports
795 .push(VportDefinition {
796 mac_address: vport.mac_address,
797 endpoint: vport.endpoint,
798 });
799 }
800
801 vpci_devices.extend(mana_nics.into_iter().enumerate().filter_map(|(vtl, nic)| {
802 nic.map(|(instance_id, handle)| VpciDeviceConfig {
803 vtl: match vtl {
804 0 => DeviceVtl::Vtl0,
805 1 => DeviceVtl::Vtl1,
806 2 => DeviceVtl::Vtl2,
807 _ => unreachable!(),
808 },
809 instance_id,
810 resource: handle.into_resource(),
811 })
812 }));
813
814 let pcie_switches = build_switch_list(&opt.pcie_switch);
815
816 let pcie_root_complexes = opt
817 .pcie_root_complex
818 .iter()
819 .enumerate()
820 .map(|(i, cli)| {
821 let ports = opt
822 .pcie_root_port
823 .iter()
824 .filter(|port_cli| port_cli.root_complex_name == cli.name)
825 .map(|port_cli| PcieRootPortConfig {
826 name: port_cli.name.clone(),
827 hotplug: port_cli.hotplug,
828 })
829 .collect();
830
831 PcieRootComplexConfig {
832 index: i as u32,
833 name: cli.name.clone(),
834 segment: cli.segment,
835 start_bus: cli.start_bus,
836 end_bus: cli.end_bus,
837 low_mmio_size: cli.low_mmio,
838 high_mmio_size: cli.high_mmio,
839 ports,
840 }
841 })
842 .collect();
843
844 #[cfg(windows)]
845 let vpci_resources: Vec<_> = opt
846 .device
847 .iter()
848 .map(|path| -> anyhow::Result<_> {
849 Ok(virt_whp::device::DeviceHandle(
850 whp::VpciResource::new(
851 None,
852 Default::default(),
853 &whp::VpciResourceDescriptor::Sriov(path, 0, 0),
854 )
855 .with_context(|| format!("opening PCI device {}", path))?,
856 ))
857 })
858 .collect::<Result<_, _>>()?;
859
860 #[cfg(windows)]
862 let vmbusproxy_handle = if !kernel_vmnics.is_empty() {
863 Some(vmbus_proxy::ProxyHandle::new().context("failed to open vmbusproxy handle")?)
864 } else {
865 None
866 };
867
868 let framebuffer = if opt.gfx || opt.vtl2_gfx || opt.vnc || opt.pcat {
869 let vram = alloc_shared_memory(FRAMEBUFFER_SIZE)?;
870 let (fb, fba) =
871 framebuffer::framebuffer(vram, FRAMEBUFFER_SIZE, 0).context("creating framebuffer")?;
872 resources.framebuffer_access = Some(fba);
873 Some(fb)
874 } else {
875 None
876 };
877
878 let is_arm = cfg!(guest_arch = "aarch64");
879 let is_x86 = cfg!(guest_arch = "x86_64");
880
881 let load_mode;
882 let with_hv;
883
884 let any_serial_configured = serial0_cfg.is_some()
885 || serial1_cfg.is_some()
886 || serial2_cfg.is_some()
887 || serial3_cfg.is_some();
888
889 let has_com3 = serial2_cfg.is_some();
890
891 let mut chipset = VmManifestBuilder::new(
892 if opt.igvm.is_some() {
893 BaseChipsetType::HclHost
894 } else if opt.pcat {
895 BaseChipsetType::HypervGen1
896 } else if opt.uefi {
897 BaseChipsetType::HypervGen2Uefi
898 } else if opt.hv {
899 BaseChipsetType::HyperVGen2LinuxDirect
900 } else {
901 BaseChipsetType::UnenlightenedLinuxDirect
902 },
903 if is_x86 {
904 MachineArch::X86_64
905 } else {
906 MachineArch::Aarch64
907 },
908 );
909
910 if framebuffer.is_some() {
911 chipset = chipset.with_framebuffer();
912 }
913 if opt.guest_watchdog {
914 chipset = chipset.with_guest_watchdog();
915 }
916 if any_serial_configured {
917 chipset = chipset.with_serial([serial0_cfg, serial1_cfg, serial2_cfg, serial3_cfg]);
918 }
919 if opt.battery {
920 let (tx, rx) = mesh::channel();
921 tx.send(HostBatteryUpdate::default_present());
922 chipset = chipset.with_battery(rx);
923 }
924 if let Some(cfg) = &opt.debugcon {
925 chipset = chipset.with_debugcon(
926 debugcon_cfg.unwrap_or_else(|| DisconnectedSerialBackendHandle.into_resource()),
927 cfg.port,
928 );
929 }
930
931 let bios_guid = Guid::new_random();
933
934 let VmChipsetResult {
935 chipset,
936 mut chipset_devices,
937 } = chipset
938 .build()
939 .context("failed to build chipset configuration")?;
940
941 if let Some(path) = &opt.igvm {
942 let file = fs_err::File::open(path)
943 .context("failed to open igvm file")?
944 .into();
945 let cmdline = opt.cmdline.join(" ");
946 with_hv = true;
947
948 load_mode = LoadMode::Igvm {
949 file,
950 cmdline,
951 vtl2_base_address: opt.igvm_vtl2_relocation_type,
952 com_serial: has_com3.then(|| SerialInformation {
953 io_port: ComPort::Com3.io_port(),
954 irq: ComPort::Com3.irq().into(),
955 }),
956 };
957 } else if opt.pcat {
958 if !is_x86 {
960 anyhow::bail!("pcat not supported on this architecture");
961 }
962 with_hv = true;
963
964 let firmware = openvmm_pcat_locator::find_pcat_bios(opt.pcat_firmware.as_deref())?;
965 load_mode = LoadMode::Pcat {
966 firmware,
967 boot_order: opt
968 .pcat_boot_order
969 .map(|x| x.0)
970 .unwrap_or(DEFAULT_PCAT_BOOT_ORDER),
971 };
972 } else if opt.uefi {
973 use openvmm_defs::config::UefiConsoleMode;
974
975 with_hv = true;
976
977 let firmware = fs_err::File::open(
978 (opt.uefi_firmware.0)
979 .as_ref()
980 .context("must provide uefi firmware when booting with uefi")?,
981 )
982 .context("failed to open uefi firmware")?;
983
984 load_mode = LoadMode::Uefi {
987 firmware: firmware.into(),
988 enable_debugging: opt.uefi_debug,
989 enable_memory_protections: opt.uefi_enable_memory_protections,
990 disable_frontpage: opt.disable_frontpage,
991 enable_tpm: opt.tpm,
992 enable_battery: opt.battery,
993 enable_serial: any_serial_configured,
994 enable_vpci_boot: false,
995 uefi_console_mode: opt.uefi_console_mode.map(|m| match m {
996 UefiConsoleModeCli::Default => UefiConsoleMode::Default,
997 UefiConsoleModeCli::Com1 => UefiConsoleMode::Com1,
998 UefiConsoleModeCli::Com2 => UefiConsoleMode::Com2,
999 UefiConsoleModeCli::None => UefiConsoleMode::None,
1000 }),
1001 default_boot_always_attempt: opt.default_boot_always_attempt,
1002 bios_guid,
1003 };
1004 } else {
1005 let mut cmdline = "panic=-1 debug".to_string();
1007
1008 with_hv = opt.hv;
1009 if with_hv {
1010 cmdline += " pci=off";
1011 }
1012
1013 if !console_str.is_empty() {
1014 let _ = write!(&mut cmdline, " console={}", console_str);
1015 }
1016 if opt.gfx {
1017 cmdline += " console=tty";
1018 }
1019 for extra in &opt.cmdline {
1020 let _ = write!(&mut cmdline, " {}", extra);
1021 }
1022
1023 let kernel = fs_err::File::open(
1024 (opt.kernel.0)
1025 .as_ref()
1026 .context("must provide kernel when booting with linux direct")?,
1027 )
1028 .context("failed to open kernel")?;
1029 let initrd = (opt.initrd.0)
1030 .as_ref()
1031 .map(fs_err::File::open)
1032 .transpose()
1033 .context("failed to open initrd")?;
1034
1035 let custom_dsdt = match &opt.custom_dsdt {
1036 Some(path) => {
1037 let mut v = Vec::new();
1038 fs_err::File::open(path)
1039 .context("failed to open custom dsdt")?
1040 .read_to_end(&mut v)
1041 .context("failed to read custom dsdt")?;
1042 Some(v)
1043 }
1044 None => None,
1045 };
1046
1047 load_mode = LoadMode::Linux {
1048 kernel: kernel.into(),
1049 initrd: initrd.map(Into::into),
1050 cmdline,
1051 custom_dsdt,
1052 enable_serial: any_serial_configured,
1053 };
1054 }
1055
1056 let mut vmgs = Some(if let Some(VmgsCli { kind, provision }) = &opt.vmgs {
1057 let disk = VmgsDisk {
1058 disk: disk_open(kind, false).context("failed to open vmgs disk")?,
1059 encryption_policy: if opt.test_gsp_by_id {
1060 GuestStateEncryptionPolicy::GspById(true)
1061 } else {
1062 GuestStateEncryptionPolicy::None(true)
1063 },
1064 };
1065 match provision {
1066 ProvisionVmgs::OnEmpty => VmgsResource::Disk(disk),
1067 ProvisionVmgs::OnFailure => VmgsResource::ReprovisionOnFailure(disk),
1068 ProvisionVmgs::True => VmgsResource::Reprovision(disk),
1069 }
1070 } else {
1071 VmgsResource::Ephemeral
1072 });
1073
1074 if with_get && with_hv {
1075 let vtl2_settings = vtl2_settings_proto::Vtl2Settings {
1076 version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
1077 fixed: Some(Default::default()),
1078 dynamic: Some(vtl2_settings_proto::Vtl2SettingsDynamic {
1079 storage_controllers: storage.build_underhill(opt.vmbus_redirect),
1080 nic_devices: underhill_nics,
1081 }),
1082 namespace_settings: Vec::default(),
1083 };
1084
1085 resources.vtl2_settings = Some(vtl2_settings.clone());
1087
1088 let (send, guest_request_recv) = mesh::channel();
1089 resources.ged_rpc = Some(send);
1090
1091 let vmgs = vmgs.take().unwrap();
1092
1093 vmbus_devices.extend([
1094 (
1095 openhcl_vtl,
1096 get_resources::gel::GuestEmulationLogHandle.into_resource(),
1097 ),
1098 (
1099 openhcl_vtl,
1100 get_resources::ged::GuestEmulationDeviceHandle {
1101 firmware: if opt.pcat {
1102 get_resources::ged::GuestFirmwareConfig::Pcat {
1103 boot_order: opt
1104 .pcat_boot_order
1105 .map_or(DEFAULT_PCAT_BOOT_ORDER, |x| x.0)
1106 .map(|x| match x {
1107 openvmm_defs::config::PcatBootDevice::Floppy => {
1108 get_resources::ged::PcatBootDevice::Floppy
1109 }
1110 openvmm_defs::config::PcatBootDevice::HardDrive => {
1111 get_resources::ged::PcatBootDevice::HardDrive
1112 }
1113 openvmm_defs::config::PcatBootDevice::Optical => {
1114 get_resources::ged::PcatBootDevice::Optical
1115 }
1116 openvmm_defs::config::PcatBootDevice::Network => {
1117 get_resources::ged::PcatBootDevice::Network
1118 }
1119 }),
1120 }
1121 } else {
1122 use get_resources::ged::UefiConsoleMode;
1123
1124 get_resources::ged::GuestFirmwareConfig::Uefi {
1125 enable_vpci_boot: storage.has_vtl0_nvme(),
1126 firmware_debug: opt.uefi_debug,
1127 disable_frontpage: opt.disable_frontpage,
1128 console_mode: match opt.uefi_console_mode.unwrap_or(UefiConsoleModeCli::Default) {
1129 UefiConsoleModeCli::Default => UefiConsoleMode::Default,
1130 UefiConsoleModeCli::Com1 => UefiConsoleMode::COM1,
1131 UefiConsoleModeCli::Com2 => UefiConsoleMode::COM2,
1132 UefiConsoleModeCli::None => UefiConsoleMode::None,
1133 },
1134 default_boot_always_attempt: opt.default_boot_always_attempt,
1135 }
1136 },
1137 com1: with_vmbus_com1_serial,
1138 com2: with_vmbus_com2_serial,
1139 serial_tx_only: opt.serial_tx_only,
1140 vtl2_settings: Some(prost::Message::encode_to_vec(&vtl2_settings)),
1141 vmbus_redirection: opt.vmbus_redirect,
1142 vmgs,
1143 framebuffer: opt
1144 .vtl2_gfx
1145 .then(|| SharedFramebufferHandle.into_resource()),
1146 guest_request_recv,
1147 enable_tpm: opt.tpm,
1148 firmware_event_send: None,
1149 secure_boot_enabled: opt.secure_boot,
1150 secure_boot_template: match opt.secure_boot_template {
1151 Some(SecureBootTemplateCli::Windows) => {
1152 get_resources::ged::GuestSecureBootTemplateType::MicrosoftWindows
1153 },
1154 Some(SecureBootTemplateCli::UefiCa) => {
1155 get_resources::ged::GuestSecureBootTemplateType::MicrosoftUefiCertificateAuthority
1156 }
1157 None => {
1158 get_resources::ged::GuestSecureBootTemplateType::None
1159 },
1160 },
1161 enable_battery: opt.battery,
1162 no_persistent_secrets: true,
1163 igvm_attest_test_config: None,
1164 test_gsp_by_id: opt.test_gsp_by_id,
1165 efi_diagnostics_log_level: {
1166 match opt.efi_diagnostics_log_level.unwrap_or_default() {
1167 EfiDiagnosticsLogLevelCli::Default => get_resources::ged::EfiDiagnosticsLogLevelType::Default,
1168 EfiDiagnosticsLogLevelCli::Info => get_resources::ged::EfiDiagnosticsLogLevelType::Info,
1169 EfiDiagnosticsLogLevelCli::Full => get_resources::ged::EfiDiagnosticsLogLevelType::Full,
1170 }
1171 },
1172 hv_sint_enabled: false,
1173 }
1174 .into_resource(),
1175 ),
1176 ]);
1177 }
1178
1179 if opt.tpm && !opt.vtl2 {
1180 let register_layout = if cfg!(guest_arch = "x86_64") {
1181 TpmRegisterLayout::IoPort
1182 } else {
1183 TpmRegisterLayout::Mmio
1184 };
1185
1186 let (ppi_store, nvram_store) = if opt.vmgs.is_some() {
1187 (
1188 VmgsFileHandle::new(vmgs_format::FileId::TPM_PPI, true).into_resource(),
1189 VmgsFileHandle::new(vmgs_format::FileId::TPM_NVRAM, true).into_resource(),
1190 )
1191 } else {
1192 (
1193 EphemeralNonVolatileStoreHandle.into_resource(),
1194 EphemeralNonVolatileStoreHandle.into_resource(),
1195 )
1196 };
1197
1198 chipset_devices.push(ChipsetDeviceHandle {
1199 name: "tpm".to_string(),
1200 resource: chipset_device_worker_defs::RemoteChipsetDeviceHandle {
1201 device: TpmDeviceHandle {
1202 ppi_store,
1203 nvram_store,
1204 nvram_size: None,
1205 refresh_tpm_seeds: false,
1206 ak_cert_type: tpm_resources::TpmAkCertTypeResource::None,
1207 register_layout,
1208 guest_secret_key: None,
1209 logger: None,
1210 is_confidential_vm: false,
1211 bios_guid,
1212 }
1213 .into_resource(),
1214 worker_host: mesh.make_host("tpm", None).await?,
1215 }
1216 .into_resource(),
1217 });
1218 }
1219
1220 let custom_uefi_vars = {
1221 use firmware_uefi_custom_vars::CustomVars;
1222
1223 let base_vars = match opt.secure_boot_template {
1226 Some(template) => {
1227 if is_x86 {
1228 match template {
1229 SecureBootTemplateCli::Windows => {
1230 hyperv_secure_boot_templates::x64::microsoft_windows()
1231 }
1232 SecureBootTemplateCli::UefiCa => {
1233 hyperv_secure_boot_templates::x64::microsoft_uefi_ca()
1234 }
1235 }
1236 } else if is_arm {
1237 match template {
1238 SecureBootTemplateCli::Windows => {
1239 hyperv_secure_boot_templates::aarch64::microsoft_windows()
1240 }
1241 SecureBootTemplateCli::UefiCa => {
1242 hyperv_secure_boot_templates::aarch64::microsoft_uefi_ca()
1243 }
1244 }
1245 } else {
1246 anyhow::bail!("no secure boot template for current guest_arch")
1247 }
1248 }
1249 None => CustomVars::default(),
1250 };
1251
1252 let custom_uefi_json_data = match &opt.custom_uefi_json {
1255 Some(file) => Some(fs_err::read(file).context("opening custom uefi json file")?),
1256 None => None,
1257 };
1258
1259 match custom_uefi_json_data {
1261 Some(data) => {
1262 let delta = hyperv_uefi_custom_vars_json::load_delta_from_json(&data)?;
1263 base_vars.apply_delta(delta)?
1264 }
1265 None => base_vars,
1266 }
1267 };
1268
1269 let vga_firmware = if opt.pcat {
1270 Some(openvmm_pcat_locator::find_svga_bios(
1271 opt.vga_firmware.as_deref(),
1272 )?)
1273 } else {
1274 None
1275 };
1276
1277 if opt.gfx {
1278 vmbus_devices.extend([
1279 (
1280 DeviceVtl::Vtl0,
1281 SynthVideoHandle {
1282 framebuffer: SharedFramebufferHandle.into_resource(),
1283 }
1284 .into_resource(),
1285 ),
1286 (
1287 DeviceVtl::Vtl0,
1288 SynthKeyboardHandle {
1289 source: MultiplexedInputHandle {
1290 elevation: 1,
1292 }
1293 .into_resource(),
1294 }
1295 .into_resource(),
1296 ),
1297 (
1298 DeviceVtl::Vtl0,
1299 SynthMouseHandle {
1300 source: MultiplexedInputHandle {
1301 elevation: 1,
1303 }
1304 .into_resource(),
1305 }
1306 .into_resource(),
1307 ),
1308 ]);
1309 }
1310
1311 let vsock_listener = |path: Option<&str>| -> anyhow::Result<_> {
1312 if let Some(path) = path {
1313 cleanup_socket(path.as_ref());
1314 let listener = unix_socket::UnixListener::bind(path)
1315 .with_context(|| format!("failed to bind to hybrid vsock path: {}", path))?;
1316 Ok(Some(listener))
1317 } else {
1318 Ok(None)
1319 }
1320 };
1321
1322 let vtl0_vsock_listener = vsock_listener(opt.vsock_path.as_deref())?;
1323 let vtl2_vsock_listener = vsock_listener(opt.vtl2_vsock_path.as_deref())?;
1324
1325 let mmio_gaps = if opt.vtl2
1328 && !matches!(
1329 opt.igvm_vtl2_relocation_type,
1330 Vtl2BaseAddressType::Vtl2Allocate { .. },
1331 ) {
1332 if is_x86 {
1333 DEFAULT_MMIO_GAPS_X86_WITH_VTL2.into()
1334 } else {
1335 DEFAULT_MMIO_GAPS_AARCH64_WITH_VTL2.into()
1336 }
1337 } else if is_x86 {
1338 DEFAULT_MMIO_GAPS_X86.into()
1339 } else {
1340 DEFAULT_MMIO_GAPS_AARCH64.into()
1341 };
1342
1343 if let Some(path) = &opt.openhcl_dump_path {
1344 let (resource, task) = spawn_dump_handler(&spawner, path.clone(), None);
1345 task.detach();
1346 vmbus_devices.push((openhcl_vtl, resource));
1347 }
1348
1349 #[cfg(guest_arch = "aarch64")]
1350 let topology_arch = openvmm_defs::config::ArchTopologyConfig::Aarch64(
1351 openvmm_defs::config::Aarch64TopologyConfig {
1352 gic_config: None,
1354 pmu_gsiv: openvmm_defs::config::PmuGsivConfig::Platform,
1355 },
1356 );
1357 #[cfg(guest_arch = "x86_64")]
1358 let topology_arch =
1359 openvmm_defs::config::ArchTopologyConfig::X86(openvmm_defs::config::X86TopologyConfig {
1360 apic_id_offset: opt.apic_id_offset,
1361 x2apic: opt.x2apic,
1362 });
1363
1364 let with_isolation = if let Some(isolation) = &opt.isolation {
1365 if !opt.vtl2 {
1367 anyhow::bail!("isolation is only currently supported with vtl2");
1368 }
1369
1370 if !opt.no_alias_map {
1372 anyhow::bail!("alias map not supported with isolation");
1373 }
1374
1375 match isolation {
1376 cli_args::IsolationCli::Vbs => Some(openvmm_defs::config::IsolationType::Vbs),
1377 }
1378 } else {
1379 None
1380 };
1381
1382 if with_hv {
1383 let (shutdown_send, shutdown_recv) = mesh::channel();
1384 resources.shutdown_ic = Some(shutdown_send);
1385 let (kvp_send, kvp_recv) = mesh::channel();
1386 resources.kvp_ic = Some(kvp_send);
1387 vmbus_devices.extend(
1388 [
1389 hyperv_ic_resources::shutdown::ShutdownIcHandle {
1390 recv: shutdown_recv,
1391 }
1392 .into_resource(),
1393 hyperv_ic_resources::kvp::KvpIcHandle { recv: kvp_recv }.into_resource(),
1394 hyperv_ic_resources::timesync::TimesyncIcHandle.into_resource(),
1395 ]
1396 .map(|r| (DeviceVtl::Vtl0, r)),
1397 );
1398 }
1399
1400 if let Some(hive_path) = &opt.imc {
1401 let file = fs_err::File::open(hive_path).context("failed to open imc hive")?;
1402 vmbus_devices.push((
1403 DeviceVtl::Vtl0,
1404 vmbfs_resources::VmbfsImcDeviceHandle { file: file.into() }.into_resource(),
1405 ));
1406 }
1407
1408 let mut virtio_devices = Vec::new();
1409 let mut add_virtio_device = |bus, resource: Resource<VirtioDeviceHandle>| {
1410 let bus = match bus {
1411 VirtioBusCli::Auto => {
1412 if with_hv && (cfg!(windows) || cfg!(target_os = "macos")) {
1415 None
1416 } else {
1417 Some(VirtioBus::Pci)
1418 }
1419 }
1420 VirtioBusCli::Mmio => Some(VirtioBus::Mmio),
1421 VirtioBusCli::Pci => Some(VirtioBus::Pci),
1422 VirtioBusCli::Vpci => None,
1423 };
1424 if let Some(bus) = bus {
1425 virtio_devices.push((bus, resource));
1426 } else {
1427 vpci_devices.push(VpciDeviceConfig {
1428 vtl: DeviceVtl::Vtl0,
1429 instance_id: Guid::new_random(),
1430 resource: VirtioPciDeviceHandle(resource).into_resource(),
1431 });
1432 }
1433 };
1434
1435 for cli_cfg in &opt.virtio_net {
1436 if cli_cfg.underhill {
1437 anyhow::bail!("use --net uh:[...] to add underhill NICs")
1438 }
1439 let vport = parse_endpoint(cli_cfg, &mut nic_index, &mut resources)?;
1440 add_virtio_device(
1441 VirtioBusCli::Auto,
1442 virtio_resources::net::VirtioNetHandle {
1443 max_queues: vport.max_queues,
1444 mac_address: vport.mac_address,
1445 endpoint: vport.endpoint,
1446 }
1447 .into_resource(),
1448 );
1449 }
1450
1451 for args in &opt.virtio_fs {
1452 add_virtio_device(
1453 opt.virtio_fs_bus,
1454 virtio_resources::fs::VirtioFsHandle {
1455 tag: args.tag.clone(),
1456 fs: virtio_resources::fs::VirtioFsBackend::HostFs {
1457 root_path: args.path.clone(),
1458 mount_options: args.options.clone(),
1459 },
1460 }
1461 .into_resource(),
1462 );
1463 }
1464
1465 for args in &opt.virtio_fs_shmem {
1466 add_virtio_device(
1467 opt.virtio_fs_bus,
1468 virtio_resources::fs::VirtioFsHandle {
1469 tag: args.tag.clone(),
1470 fs: virtio_resources::fs::VirtioFsBackend::SectionFs {
1471 root_path: args.path.clone(),
1472 },
1473 }
1474 .into_resource(),
1475 );
1476 }
1477
1478 for args in &opt.virtio_9p {
1479 add_virtio_device(
1480 VirtioBusCli::Auto,
1481 virtio_resources::p9::VirtioPlan9Handle {
1482 tag: args.tag.clone(),
1483 root_path: args.path.clone(),
1484 debug: opt.virtio_9p_debug,
1485 }
1486 .into_resource(),
1487 );
1488 }
1489
1490 if let Some(path) = &opt.virtio_pmem {
1491 add_virtio_device(
1492 VirtioBusCli::Auto,
1493 virtio_resources::pmem::VirtioPmemHandle { path: path.clone() }.into_resource(),
1494 );
1495 }
1496
1497 let mut cfg = Config {
1498 chipset,
1499 load_mode,
1500 floppy_disks,
1501 pcie_root_complexes,
1502 pcie_devices,
1503 pcie_switches,
1504 vpci_devices,
1505 ide_disks: Vec::new(),
1506 memory: MemoryConfig {
1507 mem_size: opt.memory,
1508 mmio_gaps,
1509 prefetch_memory: opt.prefetch,
1510 pcie_ecam_base: DEFAULT_PCIE_ECAM_BASE,
1511 },
1512 processor_topology: ProcessorTopologyConfig {
1513 proc_count: opt.processors,
1514 vps_per_socket: opt.vps_per_socket,
1515 enable_smt: match opt.smt {
1516 cli_args::SmtConfigCli::Auto => None,
1517 cli_args::SmtConfigCli::Force => Some(true),
1518 cli_args::SmtConfigCli::Off => Some(false),
1519 },
1520 arch: Some(topology_arch),
1521 },
1522 hypervisor: HypervisorConfig {
1523 with_hv,
1524 with_vtl2: opt.vtl2.then_some(Vtl2Config {
1525 vtl0_alias_map: !opt.no_alias_map,
1526 late_map_vtl0_memory: match opt.late_map_vtl0_policy {
1527 cli_args::Vtl0LateMapPolicyCli::Off => None,
1528 cli_args::Vtl0LateMapPolicyCli::Log => Some(LateMapVtl0MemoryPolicy::Log),
1529 cli_args::Vtl0LateMapPolicyCli::Halt => Some(LateMapVtl0MemoryPolicy::Halt),
1530 cli_args::Vtl0LateMapPolicyCli::Exception => {
1531 Some(LateMapVtl0MemoryPolicy::InjectException)
1532 }
1533 },
1534 }),
1535 with_isolation,
1536 user_mode_hv_enlightenments: opt.no_enlightenments,
1537 user_mode_apic: opt.user_mode_apic,
1538 },
1539 #[cfg(windows)]
1540 kernel_vmnics,
1541 input: mesh::Receiver::new(),
1542 framebuffer,
1543 vga_firmware,
1544 vtl2_gfx: opt.vtl2_gfx,
1545 virtio_devices,
1546 vmbus: with_hv.then_some(VmbusConfig {
1547 vsock_listener: vtl0_vsock_listener,
1548 vsock_path: opt.vsock_path.clone(),
1549 vtl2_redirect: opt.vmbus_redirect,
1550 vmbus_max_version: opt.vmbus_max_version,
1551 #[cfg(windows)]
1552 vmbusproxy_handle,
1553 }),
1554 vtl2_vmbus: (with_hv && opt.vtl2).then_some(VmbusConfig {
1555 vsock_listener: vtl2_vsock_listener,
1556 vsock_path: opt.vtl2_vsock_path.clone(),
1557 ..Default::default()
1558 }),
1559 vmbus_devices,
1560 chipset_devices,
1561 #[cfg(windows)]
1562 vpci_resources,
1563 vmgs,
1564 secure_boot_enabled: opt.secure_boot,
1565 custom_uefi_vars,
1566 firmware_event_send: None,
1567 debugger_rpc: None,
1568 generation_id_recv: None,
1569 rtc_delta_milliseconds: 0,
1570 automatic_guest_reset: !opt.halt_on_reset,
1571 efi_diagnostics_log_level: {
1572 match opt.efi_diagnostics_log_level.unwrap_or_default() {
1573 EfiDiagnosticsLogLevelCli::Default => EfiDiagnosticsLogLevelType::Default,
1574 EfiDiagnosticsLogLevelCli::Info => EfiDiagnosticsLogLevelType::Info,
1575 EfiDiagnosticsLogLevelCli::Full => EfiDiagnosticsLogLevelType::Full,
1576 }
1577 },
1578 };
1579
1580 storage.build_config(&mut cfg, &mut resources, opt.scsi_sub_channels)?;
1581 Ok((cfg, resources))
1582}
1583
1584fn openvmm_terminal_app() -> Option<PathBuf> {
1586 std::env::var_os("OPENVMM_TERM")
1587 .or_else(|| std::env::var_os("HVLITE_TERM"))
1588 .map(Into::into)
1589}
1590
1591fn cleanup_socket(path: &Path) {
1593 #[cfg(windows)]
1594 let is_socket = pal::windows::fs::is_unix_socket(path).unwrap_or(false);
1595 #[cfg(not(windows))]
1596 let is_socket = path
1597 .metadata()
1598 .is_ok_and(|meta| std::os::unix::fs::FileTypeExt::is_socket(&meta.file_type()));
1599
1600 if is_socket {
1601 let _ = std::fs::remove_file(path);
1602 }
1603}
1604
1605#[cfg(windows)]
1606const DEFAULT_SWITCH: &str = "C08CB7B8-9B3C-408E-8E30-5E16A3AEB444";
1607
1608#[cfg(windows)]
1609fn new_switch_port(
1610 switch_id: &str,
1611) -> anyhow::Result<(
1612 openvmm_defs::config::SwitchPortId,
1613 vmswitch::kernel::SwitchPort,
1614)> {
1615 let id = vmswitch::kernel::SwitchPortId {
1616 switch: switch_id.parse().context("invalid switch id")?,
1617 port: Guid::new_random(),
1618 };
1619 let _ = vmswitch::hcn::Network::open(&id.switch)
1620 .with_context(|| format!("could not find switch {}", id.switch))?;
1621
1622 let port = vmswitch::kernel::SwitchPort::new(&id).context("failed to create switch port")?;
1623
1624 let id = openvmm_defs::config::SwitchPortId {
1625 switch: id.switch,
1626 port: id.port,
1627 };
1628 Ok((id, port))
1629}
1630
1631fn parse_endpoint(
1632 cli_cfg: &NicConfigCli,
1633 index: &mut usize,
1634 resources: &mut VmResources,
1635) -> anyhow::Result<NicConfig> {
1636 let _ = resources;
1637 let endpoint = match &cli_cfg.endpoint {
1638 EndpointConfigCli::Consomme { cidr } => {
1639 net_backend_resources::consomme::ConsommeHandle { cidr: cidr.clone() }.into_resource()
1640 }
1641 EndpointConfigCli::None => net_backend_resources::null::NullHandle.into_resource(),
1642 EndpointConfigCli::Dio { id } => {
1643 #[cfg(windows)]
1644 {
1645 let (port_id, port) = new_switch_port(id.as_deref().unwrap_or(DEFAULT_SWITCH))?;
1646 resources.switch_ports.push(port);
1647 net_backend_resources::dio::WindowsDirectIoHandle {
1648 switch_port_id: net_backend_resources::dio::SwitchPortId {
1649 switch: port_id.switch,
1650 port: port_id.port,
1651 },
1652 }
1653 .into_resource()
1654 }
1655
1656 #[cfg(not(windows))]
1657 {
1658 let _ = id;
1659 bail!("cannot use dio on non-windows platforms")
1660 }
1661 }
1662 EndpointConfigCli::Tap { name } => {
1663 net_backend_resources::tap::TapHandle { name: name.clone() }.into_resource()
1664 }
1665 };
1666
1667 let mut mac_address = [0x00, 0x15, 0x5D, 0, 0, 0];
1669 getrandom::fill(&mut mac_address[3..]).expect("rng failure");
1670
1671 const BASE_INSTANCE_ID: Guid = guid::guid!("00000000-da43-11ed-936a-00155d6db52f");
1673 let instance_id = Guid {
1674 data1: *index as u32,
1675 ..BASE_INSTANCE_ID
1676 };
1677 *index += 1;
1678
1679 Ok(NicConfig {
1680 vtl: cli_cfg.vtl,
1681 instance_id,
1682 endpoint,
1683 mac_address: mac_address.into(),
1684 max_queues: cli_cfg.max_queues,
1685 })
1686}
1687
1688#[derive(Debug)]
1689struct NicConfig {
1690 vtl: DeviceVtl,
1691 instance_id: Guid,
1692 mac_address: MacAddress,
1693 endpoint: Resource<NetEndpointHandleKind>,
1694 max_queues: Option<u16>,
1695}
1696
1697impl NicConfig {
1698 fn into_netvsp_handle(self) -> (DeviceVtl, Resource<VmbusDeviceHandleKind>) {
1699 (
1700 self.vtl,
1701 netvsp_resources::NetvspHandle {
1702 instance_id: self.instance_id,
1703 mac_address: self.mac_address,
1704 endpoint: self.endpoint,
1705 max_queues: self.max_queues,
1706 }
1707 .into_resource(),
1708 )
1709 }
1710}
1711
1712enum LayerOrDisk {
1713 Layer(DiskLayerDescription),
1714 Disk(Resource<DiskHandleKind>),
1715}
1716
1717fn disk_open(disk_cli: &DiskCliKind, read_only: bool) -> anyhow::Result<Resource<DiskHandleKind>> {
1718 let mut layers = Vec::new();
1719 disk_open_inner(disk_cli, read_only, &mut layers)?;
1720 if layers.len() == 1 && matches!(layers[0], LayerOrDisk::Disk(_)) {
1721 let LayerOrDisk::Disk(disk) = layers.pop().unwrap() else {
1722 unreachable!()
1723 };
1724 Ok(disk)
1725 } else {
1726 Ok(Resource::new(disk_backend_resources::LayeredDiskHandle {
1727 layers: layers
1728 .into_iter()
1729 .map(|layer| match layer {
1730 LayerOrDisk::Layer(layer) => layer,
1731 LayerOrDisk::Disk(disk) => DiskLayerDescription {
1732 layer: DiskLayerHandle(disk).into_resource(),
1733 read_cache: false,
1734 write_through: false,
1735 },
1736 })
1737 .collect(),
1738 }))
1739 }
1740}
1741
1742fn disk_open_inner(
1743 disk_cli: &DiskCliKind,
1744 read_only: bool,
1745 layers: &mut Vec<LayerOrDisk>,
1746) -> anyhow::Result<()> {
1747 fn layer<T: IntoResource<DiskLayerHandleKind>>(layer: T) -> LayerOrDisk {
1748 LayerOrDisk::Layer(layer.into_resource().into())
1749 }
1750 fn disk<T: IntoResource<DiskHandleKind>>(disk: T) -> LayerOrDisk {
1751 LayerOrDisk::Disk(disk.into_resource())
1752 }
1753 match disk_cli {
1754 &DiskCliKind::Memory(len) => {
1755 layers.push(layer(RamDiskLayerHandle { len: Some(len) }));
1756 }
1757 DiskCliKind::File {
1758 path,
1759 create_with_len,
1760 } => layers.push(LayerOrDisk::Disk(if let Some(size) = create_with_len {
1761 create_disk_type(path, *size)
1762 .with_context(|| format!("failed to create {}", path.display()))?
1763 } else {
1764 open_disk_type(path, read_only)
1765 .with_context(|| format!("failed to open {}", path.display()))?
1766 })),
1767 DiskCliKind::Blob { kind, url } => {
1768 layers.push(disk(disk_backend_resources::BlobDiskHandle {
1769 url: url.to_owned(),
1770 format: match kind {
1771 cli_args::BlobKind::Flat => disk_backend_resources::BlobDiskFormat::Flat,
1772 cli_args::BlobKind::Vhd1 => disk_backend_resources::BlobDiskFormat::FixedVhd1,
1773 },
1774 }))
1775 }
1776 DiskCliKind::MemoryDiff(inner) => {
1777 layers.push(layer(RamDiskLayerHandle { len: None }));
1778 disk_open_inner(inner, true, layers)?;
1779 }
1780 DiskCliKind::PersistentReservationsWrapper(inner) => layers.push(disk(
1781 disk_backend_resources::DiskWithReservationsHandle(disk_open(inner, read_only)?),
1782 )),
1783 DiskCliKind::DelayDiskWrapper {
1784 delay_ms,
1785 disk: inner,
1786 } => layers.push(disk(DelayDiskHandle {
1787 delay: CellUpdater::new(Duration::from_millis(*delay_ms)).cell(),
1788 disk: disk_open(inner, read_only)?,
1789 })),
1790 DiskCliKind::Crypt {
1791 disk: inner,
1792 cipher,
1793 key_file,
1794 } => layers.push(disk(disk_crypt_resources::DiskCryptHandle {
1795 disk: disk_open(inner, read_only)?,
1796 cipher: match cipher {
1797 cli_args::DiskCipher::XtsAes256 => disk_crypt_resources::Cipher::XtsAes256,
1798 },
1799 key: fs_err::read(key_file).context("failed to read key file")?,
1800 })),
1801 DiskCliKind::Sqlite {
1802 path,
1803 create_with_len,
1804 } => {
1805 match (create_with_len.is_some(), path.exists()) {
1810 (true, true) => anyhow::bail!(
1811 "cannot create new sqlite disk at {} - file already exists",
1812 path.display()
1813 ),
1814 (false, false) => anyhow::bail!(
1815 "cannot open sqlite disk at {} - file not found",
1816 path.display()
1817 ),
1818 _ => {}
1819 }
1820
1821 layers.push(layer(SqliteDiskLayerHandle {
1822 dbhd_path: path.display().to_string(),
1823 format_dbhd: create_with_len.map(|len| {
1824 disk_backend_resources::layer::SqliteDiskLayerFormatParams {
1825 logically_read_only: false,
1826 len: Some(len),
1827 }
1828 }),
1829 }));
1830 }
1831 DiskCliKind::SqliteDiff { path, create, disk } => {
1832 match (create, path.exists()) {
1837 (true, true) => anyhow::bail!(
1838 "cannot create new sqlite disk at {} - file already exists",
1839 path.display()
1840 ),
1841 (false, false) => anyhow::bail!(
1842 "cannot open sqlite disk at {} - file not found",
1843 path.display()
1844 ),
1845 _ => {}
1846 }
1847
1848 layers.push(layer(SqliteDiskLayerHandle {
1849 dbhd_path: path.display().to_string(),
1850 format_dbhd: create.then_some(
1851 disk_backend_resources::layer::SqliteDiskLayerFormatParams {
1852 logically_read_only: false,
1853 len: None,
1854 },
1855 ),
1856 }));
1857 disk_open_inner(disk, true, layers)?;
1858 }
1859 DiskCliKind::AutoCacheSqlite {
1860 cache_path,
1861 key,
1862 disk,
1863 } => {
1864 layers.push(LayerOrDisk::Layer(DiskLayerDescription {
1865 read_cache: true,
1866 write_through: false,
1867 layer: SqliteAutoCacheDiskLayerHandle {
1868 cache_path: cache_path.clone(),
1869 cache_key: key.clone(),
1870 }
1871 .into_resource(),
1872 }));
1873 disk_open_inner(disk, read_only, layers)?;
1874 }
1875 }
1876 Ok(())
1877}
1878
1879fn do_main() -> anyhow::Result<()> {
1880 #[cfg(windows)]
1881 pal::windows::disable_hard_error_dialog();
1882
1883 tracing_init::enable_tracing()?;
1884
1885 meshworker::run_vmm_mesh_host()?;
1889
1890 let opt = Options::parse();
1891 if let Some(path) = &opt.write_saved_state_proto {
1892 mesh::payload::protofile::DescriptorWriter::new(vmcore::save_restore::saved_state_roots())
1893 .write_to_path(path)
1894 .context("failed to write protobuf descriptors")?;
1895 return Ok(());
1896 }
1897
1898 if let Some(path) = opt.relay_console_path {
1899 let console_title = opt.relay_console_title.unwrap_or_default();
1900 return console_relay::relay_console(&path, console_title.as_str());
1901 }
1902
1903 #[cfg(any(feature = "grpc", feature = "ttrpc"))]
1904 if let Some(path) = opt.ttrpc.as_ref().or(opt.grpc.as_ref()) {
1905 return block_on(async {
1906 let _ = std::fs::remove_file(path);
1907 let listener =
1908 unix_socket::UnixListener::bind(path).context("failed to bind to socket")?;
1909
1910 let transport = if opt.ttrpc.is_some() {
1911 ttrpc::RpcTransport::Ttrpc
1912 } else {
1913 ttrpc::RpcTransport::Grpc
1914 };
1915
1916 let mut handle =
1918 mesh_worker::launch_local_worker::<ttrpc::TtrpcWorker>(ttrpc::Parameters {
1919 listener,
1920 transport,
1921 })
1922 .await?;
1923
1924 tracing::info!(%transport, path = %path.display(), "listening");
1925
1926 pal::close_stdout().context("failed to close stdout")?;
1928
1929 handle.join().await?;
1930
1931 Ok(())
1932 });
1933 }
1934
1935 DefaultPool::run_with(async |driver| {
1936 let mesh = VmmMesh::new(&driver, opt.single_process)?;
1937 let result = run_control(&driver, &mesh, opt).await;
1938 mesh.shutdown().await;
1939 result
1940 })
1941}
1942
1943fn maybe_with_radix_u64(s: &str) -> Result<u64, String> {
1944 let (radix, prefix_len) = if s.starts_with("0x") || s.starts_with("0X") {
1945 (16, 2)
1946 } else if s.starts_with("0o") || s.starts_with("0O") {
1947 (8, 2)
1948 } else if s.starts_with("0b") || s.starts_with("0B") {
1949 (2, 2)
1950 } else {
1951 (10, 0)
1952 };
1953
1954 u64::from_str_radix(&s[prefix_len..], radix).map_err(|e| format!("{e}"))
1955}
1956
1957#[derive(Parser)]
1958#[clap(
1959 name = "openvmm",
1960 disable_help_flag = true,
1961 disable_version_flag = true,
1962 no_binary_name = true,
1963 help_template("{subcommands}")
1964)]
1965enum InteractiveCommand {
1966 #[clap(visible_alias = "R")]
1970 Restart,
1971
1972 #[clap(visible_alias = "n")]
1974 Nmi,
1975
1976 #[clap(visible_alias = "p")]
1978 Pause,
1979
1980 #[clap(visible_alias = "r")]
1982 Resume,
1983
1984 #[clap(visible_alias = "psr")]
1986 PulseSaveRestore,
1987
1988 #[clap(visible_alias = "spsr")]
1990 SchedulePulseSaveRestore {
1991 interval: Option<u64>,
1994 },
1995
1996 #[clap(visible_alias = "d")]
1998 AddDisk {
1999 #[clap(long = "ro")]
2000 read_only: bool,
2001 #[clap(long = "dvd")]
2002 is_dvd: bool,
2003 #[clap(long, default_value_t)]
2004 target: u8,
2005 #[clap(long, default_value_t)]
2006 path: u8,
2007 #[clap(long, default_value_t)]
2008 lun: u8,
2009 #[clap(long)]
2010 ram: Option<u64>,
2011 file_path: Option<PathBuf>,
2012 },
2013
2014 #[clap(visible_alias = "D")]
2016 RmDisk {
2017 #[clap(long)]
2018 target: u8,
2019 #[clap(long)]
2020 path: u8,
2021 #[clap(long)]
2022 lun: u8,
2023 },
2024
2025 #[clap(subcommand)]
2027 Vtl2Settings(Vtl2SettingsCommand),
2028
2029 AddNvmeNs {
2031 #[clap(long = "ro")]
2032 read_only: bool,
2033 #[clap(long)]
2035 nsid: u32,
2036 #[clap(long)]
2038 ram: Option<u64>,
2039 file_path: Option<PathBuf>,
2041 #[clap(long)]
2044 vtl0_lun: Option<u32>,
2045 },
2046
2047 RmNvmeNs {
2049 #[clap(long)]
2051 nsid: u32,
2052 #[clap(long)]
2054 vtl0: bool,
2055 },
2056
2057 #[clap(visible_alias = "x")]
2059 Inspect {
2060 #[clap(short, long)]
2062 recursive: bool,
2063 #[clap(short, long, requires("recursive"))]
2065 limit: Option<usize>,
2066 #[clap(short = 'v', long)]
2068 paravisor: bool,
2069 element: Option<String>,
2071 #[clap(short, long, conflicts_with("recursive"))]
2073 update: Option<String>,
2074 },
2075
2076 #[clap(visible_alias = "V")]
2078 RestartVnc,
2079
2080 #[clap(visible_alias = "v")]
2082 Hvsock {
2083 #[clap(short, long)]
2085 term: Option<PathBuf>,
2086 port: u32,
2088 },
2089
2090 #[clap(visible_alias = "q")]
2092 Quit,
2093
2094 #[clap(visible_alias = "i")]
2099 Input { data: Vec<String> },
2100
2101 #[clap(visible_alias = "I")]
2105 InputMode,
2106
2107 Reset,
2109
2110 Shutdown {
2112 #[clap(long, short = 'r')]
2114 reboot: bool,
2115 #[clap(long, short = 'h', conflicts_with = "reboot")]
2117 hibernate: bool,
2118 #[clap(long, short = 'f')]
2120 force: bool,
2121 },
2122
2123 #[clap(visible_alias = "ch")]
2126 ClearHalt,
2127
2128 ServiceVtl2 {
2130 #[clap(long, short = 'u')]
2133 user_mode_only: bool,
2134 #[clap(long, conflicts_with("user_mode_only"))]
2137 igvm: Option<PathBuf>,
2138 #[clap(long, short = 'n', default_missing_value = "true")]
2141 nvme_keepalive: bool,
2142 #[clap(long)]
2145 mana_keepalive: bool,
2146 },
2147
2148 ReadMemory {
2150 #[clap(value_parser=maybe_with_radix_u64)]
2152 gpa: u64,
2153 #[clap(value_parser=maybe_with_radix_u64)]
2155 size: u64,
2156 #[clap(long, short = 'f')]
2159 file: Option<PathBuf>,
2160 },
2161
2162 WriteMemory {
2164 #[clap(value_parser=maybe_with_radix_u64)]
2166 gpa: u64,
2167 hex: Option<String>,
2170 #[clap(long, short = 'f')]
2172 file: Option<PathBuf>,
2173 },
2174
2175 Panic,
2177
2178 Kvp(kvp::KvpCommand),
2180}
2181
2182#[derive(clap::Subcommand)]
2184enum Vtl2SettingsCommand {
2185 Show,
2187
2188 AddScsiDisk {
2192 #[clap(long)]
2195 controller: Option<String>,
2196 #[clap(long)]
2198 lun: u32,
2199 #[clap(
2201 long,
2202 conflicts_with = "backing_scsi_lun",
2203 required_unless_present = "backing_scsi_lun"
2204 )]
2205 backing_nvme_nsid: Option<u32>,
2206 #[clap(
2208 long,
2209 conflicts_with = "backing_nvme_nsid",
2210 required_unless_present = "backing_nvme_nsid"
2211 )]
2212 backing_scsi_lun: Option<u32>,
2213 },
2214
2215 RmScsiDisk {
2217 #[clap(long)]
2220 controller: Option<String>,
2221 #[clap(long)]
2223 lun: u32,
2224 },
2225}
2226
2227struct CommandParser {
2228 app: clap::Command,
2229}
2230
2231impl CommandParser {
2232 fn new() -> Self {
2233 let mut app = InteractiveCommand::command();
2235 for sc in app.get_subcommands_mut() {
2236 *sc = sc
2237 .clone()
2238 .help_template("{about-with-newline}\n{usage-heading}\n {usage}\n\n{all-args}");
2239 }
2240 Self { app }
2241 }
2242
2243 fn parse(&mut self, line: &str) -> clap::error::Result<InteractiveCommand> {
2244 let args = shell_words::split(line)
2245 .map_err(|err| self.app.error(clap::error::ErrorKind::ValueValidation, err))?;
2246 let matches = self.app.try_get_matches_from_mut(args)?;
2247 InteractiveCommand::from_arg_matches(&matches).map_err(|err| err.format(&mut self.app))
2248 }
2249}
2250
2251fn new_hvsock_service_id(port: u32) -> Guid {
2252 Guid {
2255 data1: port,
2256 .."00000000-facb-11e6-bd58-64006a7986d3".parse().unwrap()
2257 }
2258}
2259
2260async fn run_control(driver: &DefaultDriver, mesh: &VmmMesh, opt: Options) -> anyhow::Result<()> {
2261 let (mut vm_config, mut resources) = vm_config_from_command_line(driver, mesh, &opt).await?;
2262
2263 let mut vnc_worker = None;
2264 if opt.gfx || opt.vnc {
2265 let listener = TcpListener::bind(format!("127.0.0.1:{}", opt.vnc_port))
2266 .with_context(|| format!("binding to VNC port {}", opt.vnc_port))?;
2267
2268 let input_send = vm_config.input.sender();
2269 let framebuffer = resources
2270 .framebuffer_access
2271 .take()
2272 .expect("synth video enabled");
2273
2274 let vnc_host = mesh
2275 .make_host("vnc", None)
2276 .await
2277 .context("spawning vnc process failed")?;
2278
2279 vnc_worker = Some(
2280 vnc_host
2281 .launch_worker(
2282 vnc_worker_defs::VNC_WORKER_TCP,
2283 VncParameters {
2284 listener,
2285 framebuffer,
2286 input_send,
2287 },
2288 )
2289 .await?,
2290 )
2291 }
2292
2293 let gdb_worker = if let Some(port) = opt.gdb {
2295 let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
2296 .with_context(|| format!("binding to gdb port {}", port))?;
2297
2298 let (req_tx, req_rx) = mesh::channel();
2299 vm_config.debugger_rpc = Some(req_rx);
2300
2301 let gdb_host = mesh
2302 .make_host("gdb", None)
2303 .await
2304 .context("spawning gdbstub process failed")?;
2305
2306 Some(
2307 gdb_host
2308 .launch_worker(
2309 debug_worker_defs::DEBUGGER_WORKER,
2310 debug_worker_defs::DebuggerParameters {
2311 listener,
2312 req_chan: req_tx,
2313 vp_count: vm_config.processor_topology.proc_count,
2314 target_arch: if cfg!(guest_arch = "x86_64") {
2315 debug_worker_defs::TargetArch::X86_64
2316 } else {
2317 debug_worker_defs::TargetArch::Aarch64
2318 },
2319 },
2320 )
2321 .await
2322 .context("failed to launch gdbstub worker")?,
2323 )
2324 } else {
2325 None
2326 };
2327
2328 let (vm_rpc, rpc_recv) = mesh::channel();
2330 let (notify_send, notify_recv) = mesh::channel();
2331 let mut vm_worker = {
2332 let vm_host = mesh.make_host("vm", opt.log_file.clone()).await?;
2333
2334 let params = VmWorkerParameters {
2335 hypervisor: opt.hypervisor,
2336 cfg: vm_config,
2337 saved_state: None,
2338 rpc: rpc_recv,
2339 notify: notify_send,
2340 };
2341 vm_host
2342 .launch_worker(VM_WORKER, params)
2343 .await
2344 .context("failed to launch vm worker")?
2345 };
2346
2347 if !opt.paused {
2348 vm_rpc.call(VmRpc::Resume, ()).await?;
2349 }
2350
2351 let paravisor_diag = Arc::new(diag_client::DiagClient::from_dialer(
2352 driver.clone(),
2353 DiagDialer {
2354 driver: driver.clone(),
2355 vm_rpc: vm_rpc.clone(),
2356 openhcl_vtl: if opt.vtl2 {
2357 DeviceVtl::Vtl2
2358 } else {
2359 DeviceVtl::Vtl0
2360 },
2361 },
2362 ));
2363
2364 let mut diag_inspector = DiagInspector::new(driver.clone(), paravisor_diag.clone());
2365
2366 let (console_command_send, console_command_recv) = mesh::channel();
2367 let (inspect_completion_engine_send, inspect_completion_engine_recv) = mesh::channel();
2368
2369 let mut console_in = resources.console_in.take();
2370 thread::Builder::new()
2371 .name("stdio-thread".to_string())
2372 .spawn(move || {
2373 #[cfg(unix)]
2375 if io::stderr().is_terminal() {
2376 term::revert_terminal_on_panic()
2377 }
2378
2379 let mut rl = rustyline::Editor::<
2380 interactive_console::OpenvmmRustylineEditor,
2381 rustyline::history::FileHistory,
2382 >::with_config(
2383 rustyline::Config::builder()
2384 .completion_type(rustyline::CompletionType::List)
2385 .build(),
2386 )
2387 .unwrap();
2388
2389 rl.set_helper(Some(interactive_console::OpenvmmRustylineEditor {
2390 openvmm_inspect_req: Arc::new(inspect_completion_engine_send),
2391 }));
2392
2393 let history_file = {
2394 const HISTORY_FILE: &str = ".openvmm_history";
2395
2396 let history_folder = None
2399 .or_else(dirs::state_dir)
2400 .or_else(dirs::data_local_dir)
2401 .map(|path| path.join("openvmm"));
2402
2403 if let Some(history_folder) = history_folder {
2404 if let Err(err) = std::fs::create_dir_all(&history_folder) {
2405 tracing::warn!(
2406 error = &err as &dyn std::error::Error,
2407 "could not create directory: {}",
2408 history_folder.display()
2409 )
2410 }
2411
2412 Some(history_folder.join(HISTORY_FILE))
2413 } else {
2414 None
2415 }
2416 };
2417
2418 if let Some(history_file) = &history_file {
2419 tracing::info!("restoring history from {}", history_file.display());
2420 if rl.load_history(history_file).is_err() {
2421 tracing::info!("could not find existing {}", history_file.display());
2422 }
2423 }
2424
2425 rl.bind_sequence(
2427 rustyline::KeyEvent::new('\x08', rustyline::Modifiers::CTRL),
2428 rustyline::Cmd::Kill(rustyline::Movement::BackwardWord(1, rustyline::Word::Emacs)),
2429 );
2430
2431 let mut parser = CommandParser::new();
2432
2433 let mut stdin = io::stdin();
2434 loop {
2435 term::set_raw_console(true).expect("failed to set raw console mode");
2437
2438 if let Some(input) = console_in.as_mut() {
2439 let mut buf = [0; 32];
2440 loop {
2441 let n = stdin.read(&mut buf).unwrap();
2442 let mut b = &buf[..n];
2443 let stop = if let Some(ctrlq) = b.iter().position(|x| *x == 0x11) {
2444 b = &b[..ctrlq];
2445 true
2446 } else {
2447 false
2448 };
2449 block_on(input.as_mut().write_all(b)).expect("BUGBUG");
2450 if stop {
2451 break;
2452 }
2453 }
2454 }
2455
2456 term::set_raw_console(false).expect("failed to set raw console mode");
2457
2458 loop {
2459 let line = rl.readline("openvmm> ");
2460 if line.is_err() {
2461 break;
2462 }
2463 let line = line.unwrap();
2464 let trimmed = line.trim();
2465 if trimmed.is_empty() {
2466 continue;
2467 }
2468 if let Err(err) = rl.add_history_entry(&line) {
2469 tracing::warn!(
2470 err = &err as &dyn std::error::Error,
2471 "error adding to .openvmm_history"
2472 )
2473 }
2474
2475 match parser.parse(trimmed) {
2476 Ok(cmd) => match cmd {
2477 InteractiveCommand::Input { data } => {
2478 let mut data = data.join(" ");
2479 data.push('\n');
2480 if let Some(input) = console_in.as_mut() {
2481 block_on(input.write_all(data.as_bytes())).expect("BUGBUG");
2482 }
2483 }
2484 InteractiveCommand::InputMode => break,
2485 cmd => {
2486 let (processing_done_send, processing_done_recv) =
2488 mesh::oneshot::<()>();
2489 console_command_send.send((cmd, processing_done_send));
2490 let _ = block_on(processing_done_recv);
2491 }
2492 },
2493 Err(err) => {
2494 err.print().unwrap();
2495 }
2496 }
2497
2498 if let Some(history_file) = &history_file {
2499 rl.append_history(history_file).unwrap();
2500 }
2501 }
2502 }
2503 })
2504 .unwrap();
2505
2506 let mut state_change_task = None::<Task<Result<StateChange, RpcError>>>;
2507 let mut pulse_save_restore_interval: Option<Duration> = None;
2508 let mut pending_shutdown = None;
2509
2510 enum StateChange {
2511 Pause(bool),
2512 Resume(bool),
2513 Reset(Result<(), RemoteError>),
2514 PulseSaveRestore(Result<(), PulseSaveRestoreError>),
2515 ServiceVtl2(anyhow::Result<Duration>),
2516 }
2517
2518 enum Event {
2519 Command((InteractiveCommand, mesh::OneshotSender<()>)),
2520 InspectRequestFromCompletionEngine(
2521 (InspectTarget, String, mesh::OneshotSender<inspect::Node>),
2522 ),
2523 Quit,
2524 Halt(vmm_core_defs::HaltReason),
2525 PulseSaveRestore,
2526 Worker(WorkerEvent),
2527 VncWorker(WorkerEvent),
2528 StateChange(Result<StateChange, RpcError>),
2529 ShutdownResult(Result<hyperv_ic_resources::shutdown::ShutdownResult, RpcError>),
2530 }
2531
2532 let mut console_command_recv = console_command_recv
2533 .map(Event::Command)
2534 .chain(futures::stream::repeat_with(|| Event::Quit));
2535
2536 let mut notify_recv = notify_recv.map(Event::Halt);
2537
2538 let mut inspect_completion_engine_recv =
2539 inspect_completion_engine_recv.map(Event::InspectRequestFromCompletionEngine);
2540
2541 let mut quit = false;
2542 loop {
2543 let event = {
2544 let pulse_save_restore = pin!(async {
2545 match pulse_save_restore_interval {
2546 Some(wait) => {
2547 PolledTimer::new(driver).sleep(wait).await;
2548 Event::PulseSaveRestore
2549 }
2550 None => pending().await,
2551 }
2552 });
2553
2554 let vm = (&mut vm_worker).map(Event::Worker);
2555 let vnc = futures::stream::iter(vnc_worker.as_mut())
2556 .flatten()
2557 .map(Event::VncWorker);
2558 let change = futures::stream::iter(state_change_task.as_mut().map(|x| x.into_stream()))
2559 .flatten()
2560 .map(Event::StateChange);
2561 let shutdown = pin!(async {
2562 if let Some(s) = &mut pending_shutdown {
2563 Event::ShutdownResult(s.await)
2564 } else {
2565 pending().await
2566 }
2567 });
2568
2569 (
2570 &mut console_command_recv,
2571 &mut inspect_completion_engine_recv,
2572 &mut notify_recv,
2573 pulse_save_restore.into_stream(),
2574 vm,
2575 vnc,
2576 change,
2577 shutdown.into_stream(),
2578 )
2579 .merge()
2580 .next()
2581 .await
2582 .unwrap()
2583 };
2584
2585 let (cmd, _processing_done_send) = match event {
2586 Event::Command(message) => message,
2587 Event::InspectRequestFromCompletionEngine((vtl, path, res)) => {
2588 let mut inspection =
2589 InspectionBuilder::new(&path)
2590 .depth(Some(1))
2591 .inspect(inspect_obj(
2592 vtl,
2593 mesh,
2594 &vm_worker,
2595 vnc_worker.as_ref(),
2596 gdb_worker.as_ref(),
2597 &mut diag_inspector,
2598 ));
2599 let _ = CancelContext::new()
2600 .with_timeout(Duration::from_secs(1))
2601 .until_cancelled(inspection.resolve())
2602 .await;
2603
2604 let node = inspection.results();
2605 res.send(node);
2606 continue;
2607 }
2608 Event::Quit => break,
2609 Event::Halt(reason) => {
2610 tracing::info!(?reason, "guest halted");
2611 continue;
2612 }
2613 Event::PulseSaveRestore => {
2614 vm_rpc.call(VmRpc::PulseSaveRestore, ()).await??;
2615 continue;
2616 }
2617 Event::Worker(event) => {
2618 match event {
2619 WorkerEvent::Stopped => {
2620 if quit {
2621 tracing::info!("vm stopped");
2622 } else {
2623 tracing::error!("vm worker unexpectedly stopped");
2624 }
2625 break;
2626 }
2627 WorkerEvent::Failed(err) => {
2628 tracing::error!(error = &err as &dyn std::error::Error, "vm worker failed");
2629 break;
2630 }
2631 WorkerEvent::RestartFailed(err) => {
2632 tracing::error!(
2633 error = &err as &dyn std::error::Error,
2634 "vm worker restart failed"
2635 );
2636 }
2637 WorkerEvent::Started => {
2638 tracing::info!("vm worker restarted");
2639 }
2640 }
2641 continue;
2642 }
2643 Event::VncWorker(event) => {
2644 match event {
2645 WorkerEvent::Stopped => tracing::error!("vnc unexpectedly stopped"),
2646 WorkerEvent::Failed(err) => {
2647 tracing::error!(
2648 error = &err as &dyn std::error::Error,
2649 "vnc worker failed"
2650 );
2651 }
2652 WorkerEvent::RestartFailed(err) => {
2653 tracing::error!(
2654 error = &err as &dyn std::error::Error,
2655 "vnc worker restart failed"
2656 );
2657 }
2658 WorkerEvent::Started => {
2659 tracing::info!("vnc worker restarted");
2660 }
2661 }
2662 continue;
2663 }
2664 Event::StateChange(r) => {
2665 match r {
2666 Ok(sc) => match sc {
2667 StateChange::Pause(success) => {
2668 if success {
2669 tracing::info!("pause complete");
2670 } else {
2671 tracing::warn!("already paused");
2672 }
2673 }
2674 StateChange::Resume(success) => {
2675 if success {
2676 tracing::info!("resumed complete");
2677 } else {
2678 tracing::warn!("already running");
2679 }
2680 }
2681 StateChange::Reset(r) => match r {
2682 Ok(()) => tracing::info!("reset complete"),
2683 Err(err) => tracing::error!(
2684 error = &err as &dyn std::error::Error,
2685 "reset failed"
2686 ),
2687 },
2688 StateChange::PulseSaveRestore(r) => match r {
2689 Ok(()) => tracing::info!("pulse save/restore complete"),
2690 Err(err) => tracing::error!(
2691 error = &err as &dyn std::error::Error,
2692 "pulse save/restore failed"
2693 ),
2694 },
2695 StateChange::ServiceVtl2(r) => match r {
2696 Ok(dur) => {
2697 tracing::info!(
2698 duration = dur.as_millis() as i64,
2699 "vtl2 servicing complete"
2700 )
2701 }
2702 Err(err) => tracing::error!(
2703 error = err.as_ref() as &dyn std::error::Error,
2704 "vtl2 servicing failed"
2705 ),
2706 },
2707 },
2708 Err(err) => {
2709 tracing::error!(
2710 error = &err as &dyn std::error::Error,
2711 "communication failure during state change"
2712 );
2713 }
2714 }
2715 state_change_task = None;
2716 continue;
2717 }
2718 Event::ShutdownResult(r) => {
2719 match r {
2720 Ok(r) => match r {
2721 hyperv_ic_resources::shutdown::ShutdownResult::Ok => {
2722 tracing::info!("shutdown initiated");
2723 }
2724 hyperv_ic_resources::shutdown::ShutdownResult::NotReady => {
2725 tracing::error!("shutdown ic not ready");
2726 }
2727 hyperv_ic_resources::shutdown::ShutdownResult::AlreadyInProgress => {
2728 tracing::error!("shutdown already in progress");
2729 }
2730 hyperv_ic_resources::shutdown::ShutdownResult::Failed(hr) => {
2731 tracing::error!("shutdown failed with error code {hr:#x}");
2732 }
2733 },
2734 Err(err) => {
2735 tracing::error!(
2736 error = &err as &dyn std::error::Error,
2737 "communication failure during shutdown"
2738 );
2739 }
2740 }
2741 pending_shutdown = None;
2742 continue;
2743 }
2744 };
2745
2746 fn inspect_obj<'a>(
2747 target: InspectTarget,
2748 mesh: &'a VmmMesh,
2749 vm_worker: &'a WorkerHandle,
2750 vnc_worker: Option<&'a WorkerHandle>,
2751 gdb_worker: Option<&'a WorkerHandle>,
2752 diag_inspector: &'a mut DiagInspector,
2753 ) -> impl 'a + InspectMut {
2754 inspect::adhoc_mut(move |req| match target {
2755 InspectTarget::Host => {
2756 let mut resp = req.respond();
2757 resp.field("mesh", mesh)
2758 .field("vm", vm_worker)
2759 .field("vnc", vnc_worker)
2760 .field("gdb", gdb_worker);
2761 }
2762 InspectTarget::Paravisor => {
2763 diag_inspector.inspect_mut(req);
2764 }
2765 })
2766 }
2767
2768 fn state_change<U: 'static + Send>(
2769 driver: impl Spawn,
2770 vm_rpc: &mesh::Sender<VmRpc>,
2771 state_change_task: &mut Option<Task<Result<StateChange, RpcError>>>,
2772 f: impl FnOnce(Rpc<(), U>) -> VmRpc,
2773 g: impl FnOnce(U) -> StateChange + 'static + Send,
2774 ) {
2775 if state_change_task.is_some() {
2776 tracing::error!("state change already in progress");
2777 } else {
2778 let rpc = vm_rpc.call(f, ());
2779 *state_change_task =
2780 Some(driver.spawn("state-change", async move { Ok(g(rpc.await?)) }));
2781 }
2782 }
2783
2784 match cmd {
2785 InteractiveCommand::Panic => {
2786 panic!("injected panic")
2787 }
2788 InteractiveCommand::Restart => {
2789 let vm_host = mesh.make_host("vm", opt.log_file.clone()).await?;
2791
2792 vm_worker.restart(&vm_host);
2793 }
2794 InteractiveCommand::Pause => {
2795 state_change(
2796 driver,
2797 &vm_rpc,
2798 &mut state_change_task,
2799 VmRpc::Pause,
2800 StateChange::Pause,
2801 );
2802 }
2803 InteractiveCommand::Resume => {
2804 state_change(
2805 driver,
2806 &vm_rpc,
2807 &mut state_change_task,
2808 VmRpc::Resume,
2809 StateChange::Resume,
2810 );
2811 }
2812 InteractiveCommand::Reset => {
2813 state_change(
2814 driver,
2815 &vm_rpc,
2816 &mut state_change_task,
2817 VmRpc::Reset,
2818 StateChange::Reset,
2819 );
2820 }
2821 InteractiveCommand::PulseSaveRestore => {
2822 state_change(
2823 driver,
2824 &vm_rpc,
2825 &mut state_change_task,
2826 VmRpc::PulseSaveRestore,
2827 StateChange::PulseSaveRestore,
2828 );
2829 }
2830 InteractiveCommand::SchedulePulseSaveRestore { interval } => {
2831 pulse_save_restore_interval = match interval {
2832 Some(seconds) if seconds != 0 => Some(Duration::from_secs(seconds)),
2833 _ => {
2834 None
2836 }
2837 }
2838 }
2839 InteractiveCommand::Shutdown {
2840 reboot,
2841 hibernate,
2842 force,
2843 } => {
2844 if pending_shutdown.is_some() {
2845 println!("shutdown already in progress");
2846 } else if let Some(ic) = &resources.shutdown_ic {
2847 let params = hyperv_ic_resources::shutdown::ShutdownParams {
2848 shutdown_type: if hibernate {
2849 hyperv_ic_resources::shutdown::ShutdownType::Hibernate
2850 } else if reboot {
2851 hyperv_ic_resources::shutdown::ShutdownType::Reboot
2852 } else {
2853 hyperv_ic_resources::shutdown::ShutdownType::PowerOff
2854 },
2855 force,
2856 };
2857 pending_shutdown =
2858 Some(ic.call(hyperv_ic_resources::shutdown::ShutdownRpc::Shutdown, params));
2859 } else {
2860 println!("no shutdown ic configured");
2861 }
2862 }
2863 InteractiveCommand::Nmi => {
2864 let _ = vm_rpc.call(VmRpc::Nmi, 0).await;
2865 }
2866 InteractiveCommand::ClearHalt => {
2867 vm_rpc.call(VmRpc::ClearHalt, ()).await.ok();
2868 }
2869 InteractiveCommand::AddDisk {
2870 read_only,
2871 target,
2872 path,
2873 lun,
2874 ram,
2875 file_path,
2876 is_dvd,
2877 } => {
2878 let action = async {
2879 let scsi = resources.scsi_rpc.as_ref().context("no scsi controller")?;
2880 let disk_type = match ram {
2881 None => {
2882 let path = file_path.context("no filename passed")?;
2883 open_disk_type(path.as_ref(), read_only)
2884 .with_context(|| format!("failed to open {}", path.display()))?
2885 }
2886 Some(size) => {
2887 Resource::new(disk_backend_resources::LayeredDiskHandle::single_layer(
2888 RamDiskLayerHandle { len: Some(size) },
2889 ))
2890 }
2891 };
2892
2893 let device = if is_dvd {
2894 SimpleScsiDvdHandle {
2895 media: Some(disk_type),
2896 requests: None,
2897 }
2898 .into_resource()
2899 } else {
2900 SimpleScsiDiskHandle {
2901 disk: disk_type,
2902 read_only,
2903 parameters: Default::default(),
2904 }
2905 .into_resource()
2906 };
2907
2908 let cfg = ScsiDeviceAndPath {
2909 path: ScsiPath { path, target, lun },
2910 device,
2911 };
2912
2913 scsi.call_failable(ScsiControllerRequest::AddDevice, cfg)
2914 .await?;
2915
2916 anyhow::Result::<_>::Ok(())
2917 };
2918
2919 if let Err(error) = action.await {
2920 tracing::error!(error = error.as_error(), "error adding disk")
2921 }
2922 }
2923 InteractiveCommand::RmDisk { target, path, lun } => {
2924 let action = async {
2925 let scsi = resources.scsi_rpc.as_ref().context("no scsi controller")?;
2926 scsi.call_failable(
2927 ScsiControllerRequest::RemoveDevice,
2928 ScsiPath { target, path, lun },
2929 )
2930 .await?;
2931 anyhow::Ok(())
2932 };
2933
2934 if let Err(error) = action.await {
2935 tracing::error!(error = error.as_error(), "error removing disk")
2936 }
2937 }
2938 InteractiveCommand::Vtl2Settings(cmd) => {
2939 if resources.vtl2_settings.is_none() {
2940 eprintln!("error: no VTL2 settings (not running with VTL2?)");
2941 continue;
2942 }
2943 let action = async {
2944 match cmd {
2945 Vtl2SettingsCommand::Show => {
2946 let settings = resources.vtl2_settings.as_ref().unwrap();
2947 println!("{:#?}", settings);
2948 }
2949 Vtl2SettingsCommand::AddScsiDisk {
2950 controller,
2951 lun,
2952 backing_nvme_nsid,
2953 backing_scsi_lun,
2954 } => {
2955 let (device_type, device_path, sub_device_path) = match (
2957 backing_nvme_nsid,
2958 backing_scsi_lun,
2959 ) {
2960 (Some(nsid), None) => (
2961 vtl2_settings_proto::physical_device::DeviceType::Nvme,
2962 storage_builder::NVME_VTL2_INSTANCE_ID,
2963 nsid,
2964 ),
2965 (None, Some(scsi_lun)) => (
2966 vtl2_settings_proto::physical_device::DeviceType::Vscsi,
2967 storage_builder::SCSI_VTL2_INSTANCE_ID,
2968 scsi_lun,
2969 ),
2970 (Some(_), Some(_)) => {
2971 anyhow::bail!(
2972 "can't specify both --backing-nvme-nsid and --backing-scsi-lun"
2973 );
2974 }
2975 (None, None) => {
2976 anyhow::bail!(
2977 "must specify either --backing-nvme-nsid or --backing-scsi-lun"
2978 );
2979 }
2980 };
2981
2982 let controller_guid = controller
2984 .map(|s| s.parse())
2985 .transpose()
2986 .context("invalid controller GUID")?
2987 .unwrap_or(storage_builder::UNDERHILL_VTL0_SCSI_INSTANCE);
2988
2989 resources
2990 .add_vtl0_scsi_disk(
2991 controller_guid,
2992 lun,
2993 device_type,
2994 device_path,
2995 sub_device_path,
2996 )
2997 .await?;
2998
2999 let backing_desc = if backing_nvme_nsid.is_some() {
3000 format!("nvme_nsid={}", sub_device_path)
3001 } else {
3002 format!("scsi_lun={}", sub_device_path)
3003 };
3004 println!(
3005 "Added VTL0 SCSI disk: controller={}, lun={}, backing={}",
3006 controller_guid, lun, backing_desc
3007 );
3008 }
3009 Vtl2SettingsCommand::RmScsiDisk { controller, lun } => {
3010 let controller_guid = controller
3012 .map(|s| s.parse())
3013 .transpose()
3014 .context("invalid controller GUID")?
3015 .unwrap_or(storage_builder::UNDERHILL_VTL0_SCSI_INSTANCE);
3016
3017 resources
3018 .remove_vtl0_scsi_disk(controller_guid, lun)
3019 .await?;
3020
3021 println!(
3022 "Removed VTL0 SCSI disk: controller={}, lun={}",
3023 controller_guid, lun
3024 );
3025 }
3026 }
3027 anyhow::Ok(())
3028 };
3029
3030 if let Err(error) = action.await {
3031 eprintln!("error: {}", error);
3032 }
3033 }
3034 InteractiveCommand::AddNvmeNs {
3035 read_only,
3036 nsid,
3037 ram,
3038 file_path,
3039 vtl0_lun,
3040 } => {
3041 if resources.vtl2_settings.is_none() {
3042 eprintln!("error: add-nvme-ns requires --vtl2 mode");
3043 continue;
3044 }
3045 let action = async {
3046 let nvme = resources
3047 .nvme_vtl2_rpc
3048 .as_ref()
3049 .context("no vtl2 nvme controller")?;
3050 let disk_type = match (ram, file_path) {
3051 (None, Some(path)) => open_disk_type(path.as_ref(), read_only)
3052 .with_context(|| format!("failed to open {}", path.display()))?,
3053 (Some(size), None) => {
3054 Resource::new(disk_backend_resources::LayeredDiskHandle::single_layer(
3055 RamDiskLayerHandle { len: Some(size) },
3056 ))
3057 }
3058 (None, None) => {
3059 anyhow::bail!("must specify either file path or --ram");
3060 }
3061 (Some(_), Some(_)) => {
3062 anyhow::bail!("cannot specify both file path and --ram");
3063 }
3064 };
3065
3066 let ns = NamespaceDefinition {
3067 nsid,
3068 read_only,
3069 disk: disk_type,
3070 };
3071
3072 nvme.call_failable(NvmeControllerRequest::AddNamespace, ns)
3073 .await?;
3074 println!("Added namespace {}", nsid);
3075
3076 if let Some(lun) = vtl0_lun {
3078 resources
3079 .add_vtl0_scsi_disk(
3080 storage_builder::UNDERHILL_VTL0_SCSI_INSTANCE,
3081 lun,
3082 vtl2_settings_proto::physical_device::DeviceType::Nvme,
3083 storage_builder::NVME_VTL2_INSTANCE_ID,
3084 nsid,
3085 )
3086 .await?;
3087 println!("Exposed namespace {} to VTL0 as SCSI lun={}", nsid, lun);
3088 }
3089
3090 Ok(())
3091 };
3092
3093 if let Err(error) = action.await {
3094 eprintln!("error adding nvme namespace: {}", error);
3095 }
3096 }
3097 InteractiveCommand::RmNvmeNs { nsid, vtl0 } => {
3098 if resources.vtl2_settings.is_none() {
3099 eprintln!("error: rm-nvme-ns requires --vtl2 mode");
3100 continue;
3101 }
3102 let action = async {
3103 if vtl0 {
3105 let removed_lun = resources
3106 .remove_vtl0_scsi_disk_by_nvme_nsid(
3107 storage_builder::UNDERHILL_VTL0_SCSI_INSTANCE,
3108 storage_builder::NVME_VTL2_INSTANCE_ID,
3109 nsid,
3110 )
3111 .await?;
3112 if let Some(lun) = removed_lun {
3113 println!("Removed VTL0 SCSI lun={}", lun);
3114 } else {
3115 println!("No VTL0 SCSI disk found backed by NVMe nsid={}", nsid);
3116 }
3117 }
3118
3119 let nvme = resources
3120 .nvme_vtl2_rpc
3121 .as_ref()
3122 .context("no vtl2 nvme controller")?;
3123 nvme.call_failable(NvmeControllerRequest::RemoveNamespace, nsid)
3124 .await?;
3125 println!("Removed NVMe namespace {}", nsid);
3126 anyhow::Ok(())
3127 };
3128
3129 if let Err(error) = action.await {
3130 eprintln!("error removing nvme namespace: {}", error);
3131 }
3132 }
3133 InteractiveCommand::Inspect {
3134 recursive,
3135 limit,
3136 paravisor,
3137 element,
3138 update,
3139 } => {
3140 let obj = inspect_obj(
3141 if paravisor {
3142 InspectTarget::Paravisor
3143 } else {
3144 InspectTarget::Host
3145 },
3146 mesh,
3147 &vm_worker,
3148 vnc_worker.as_ref(),
3149 gdb_worker.as_ref(),
3150 &mut diag_inspector,
3151 );
3152
3153 if let Some(value) = update {
3154 let Some(element) = element else {
3155 anyhow::bail!("must provide element for update")
3156 };
3157
3158 let value = async {
3159 let update = inspect::update(&element, &value, obj);
3160 let value = CancelContext::new()
3161 .with_timeout(Duration::from_secs(1))
3162 .until_cancelled(update)
3163 .await??;
3164 anyhow::Ok(value)
3165 }
3166 .await;
3167 match value {
3168 Ok(node) => match &node.kind {
3169 inspect::ValueKind::String(s) => println!("{s}"),
3170 _ => println!("{:#}", node),
3171 },
3172 Err(err) => println!("error: {:#}", err),
3173 }
3174 } else {
3175 let element = element.unwrap_or_default();
3176 let depth = if recursive { limit } else { Some(0) };
3177 let node = async {
3178 let mut inspection =
3179 InspectionBuilder::new(&element).depth(depth).inspect(obj);
3180 let _ = CancelContext::new()
3181 .with_timeout(Duration::from_secs(1))
3182 .until_cancelled(inspection.resolve())
3183 .await;
3184 inspection.results()
3185 }
3186 .await;
3187
3188 println!("{:#}", node);
3189 }
3190 }
3191 InteractiveCommand::RestartVnc => {
3192 if let Some(vnc) = &mut vnc_worker {
3193 let action = async {
3194 let vnc_host = mesh
3195 .make_host("vnc", None)
3196 .await
3197 .context("spawning vnc process failed")?;
3198
3199 vnc.restart(&vnc_host);
3200 anyhow::Result::<_>::Ok(())
3201 };
3202
3203 if let Err(error) = action.await {
3204 eprintln!("error: {}", error);
3205 }
3206 } else {
3207 eprintln!("ERROR: no VNC server running");
3208 }
3209 }
3210 InteractiveCommand::Hvsock { term, port } => {
3211 let vm_rpc = &vm_rpc;
3212 let action = async || {
3213 let service_id = new_hvsock_service_id(port);
3214 let socket = vm_rpc
3215 .call_failable(
3216 VmRpc::ConnectHvsock,
3217 (
3218 CancelContext::new().with_timeout(Duration::from_secs(2)),
3219 service_id,
3220 DeviceVtl::Vtl0,
3221 ),
3222 )
3223 .await?;
3224 let socket = PolledSocket::new(driver, socket)?;
3225 let mut console = console_relay::Console::new(
3226 driver.clone(),
3227 term.or_else(openvmm_terminal_app).as_deref(),
3228 Some(ConsoleLaunchOptions {
3229 window_title: Some(format!("HVSock{} [OpenVMM]", port)),
3230 }),
3231 )?;
3232 driver
3233 .spawn("console-relay", async move { console.relay(socket).await })
3234 .detach();
3235 anyhow::Result::<_>::Ok(())
3236 };
3237
3238 if let Err(error) = (action)().await {
3239 eprintln!("error: {}", error);
3240 }
3241 }
3242 InteractiveCommand::ServiceVtl2 {
3243 user_mode_only,
3244 igvm,
3245 mana_keepalive,
3246 nvme_keepalive,
3247 } => {
3248 let paravisor_diag = paravisor_diag.clone();
3249 let vm_rpc = vm_rpc.clone();
3250 let igvm = igvm.or_else(|| opt.igvm.clone());
3251 let ged_rpc = resources.ged_rpc.clone();
3252 let r = async move {
3253 let start;
3254 if user_mode_only {
3255 start = Instant::now();
3256 paravisor_diag.restart().await?;
3257 } else {
3258 let path = igvm.context("no igvm file loaded")?;
3259 let file = fs_err::File::open(path)?;
3260 start = Instant::now();
3261 openvmm_helpers::underhill::save_underhill(
3262 &vm_rpc,
3263 ged_rpc.as_ref().context("no GED")?,
3264 GuestServicingFlags {
3265 nvme_keepalive,
3266 mana_keepalive,
3267 },
3268 file.into(),
3269 )
3270 .await?;
3271 openvmm_helpers::underhill::restore_underhill(
3272 &vm_rpc,
3273 ged_rpc.as_ref().context("no GED")?,
3274 )
3275 .await?;
3276 }
3277 let end = Instant::now();
3278 Ok(end - start)
3279 }
3280 .map(|r| Ok(StateChange::ServiceVtl2(r)));
3281 if state_change_task.is_some() {
3282 tracing::error!("state change already in progress");
3283 } else {
3284 state_change_task = Some(driver.spawn("state-change", r));
3285 }
3286 }
3287 InteractiveCommand::Quit => {
3288 tracing::info!("quitting");
3289 resources.scsi_rpc = None;
3292 resources.nvme_vtl2_rpc = None;
3293
3294 vm_worker.stop();
3295 quit = true;
3296 }
3297 InteractiveCommand::ReadMemory { gpa, size, file } => {
3298 let size = size as usize;
3299 let data = vm_rpc.call(VmRpc::ReadMemory, (gpa, size)).await?;
3300
3301 match data {
3302 Ok(bytes) => {
3303 if let Some(file) = file {
3304 if let Err(err) = fs_err::write(file, bytes) {
3305 eprintln!("error: {err:?}");
3306 }
3307 } else {
3308 let width = 16;
3309 let show_ascii = true;
3310
3311 let mut dump = String::new();
3312 for (i, chunk) in bytes.chunks(width).enumerate() {
3313 let hex_part: Vec<String> =
3314 chunk.iter().map(|byte| format!("{:02x}", byte)).collect();
3315 let hex_line = hex_part.join(" ");
3316
3317 if show_ascii {
3318 let ascii_part: String = chunk
3319 .iter()
3320 .map(|&byte| {
3321 if byte.is_ascii_graphic() || byte == b' ' {
3322 byte as char
3323 } else {
3324 '.'
3325 }
3326 })
3327 .collect();
3328 dump.push_str(&format!(
3329 "{:04x}: {:<width$} {}\n",
3330 i * width,
3331 hex_line,
3332 ascii_part,
3333 width = width * 3 - 1
3334 ));
3335 } else {
3336 dump.push_str(&format!("{:04x}: {}\n", i * width, hex_line));
3337 }
3338 }
3339
3340 println!("{dump}");
3341 }
3342 }
3343 Err(err) => {
3344 eprintln!("error: {err:?}");
3345 }
3346 }
3347 }
3348 InteractiveCommand::WriteMemory { gpa, hex, file } => {
3349 if hex.is_some() == file.is_some() {
3350 eprintln!("error: either path to the file or the hex string must be specified");
3351 continue;
3352 }
3353
3354 let data = if let Some(file) = file {
3355 let data = fs_err::read(file);
3356 match data {
3357 Ok(data) => data,
3358 Err(err) => {
3359 eprintln!("error: {err:?}");
3360 continue;
3361 }
3362 }
3363 } else if let Some(hex) = hex {
3364 if hex.len() & 1 != 0 {
3365 eprintln!(
3366 "error: expected even number of hex digits (2 hex digits per byte)"
3367 );
3368 continue;
3369 }
3370 let data: Result<Vec<u8>, String> = (0..hex.len())
3371 .step_by(2)
3372 .map(|i| {
3373 u8::from_str_radix(&hex[i..i + 2], 16).map_err(|e| {
3374 format!("invalid hex character at position {}: {}", i, e)
3375 })
3376 })
3377 .collect();
3378
3379 match data {
3380 Ok(data) => data,
3381 Err(err) => {
3382 eprintln!("error: {err}");
3383 continue;
3384 }
3385 }
3386 } else {
3387 unreachable!();
3388 };
3389
3390 if data.is_empty() {
3391 eprintln!("error: no data to write");
3392 continue;
3393 }
3394
3395 if let Err(err) = vm_rpc.call(VmRpc::WriteMemory, (gpa, data)).await? {
3396 eprintln!("error: {err:?}");
3397 }
3398 }
3399 InteractiveCommand::Kvp(command) => {
3400 let Some(kvp) = &resources.kvp_ic else {
3401 eprintln!("error: no kvp ic configured");
3402 continue;
3403 };
3404 if let Err(err) = kvp::handle_kvp(kvp, command).await {
3405 eprintln!("error: {err:#}");
3406 }
3407 }
3408 InteractiveCommand::Input { .. } | InteractiveCommand::InputMode => unreachable!(),
3409 }
3410 }
3411
3412 vm_worker.stop();
3413 vm_worker.join().await?;
3414 Ok(())
3415}
3416
3417struct DiagDialer {
3418 driver: DefaultDriver,
3419 vm_rpc: mesh::Sender<VmRpc>,
3420 openhcl_vtl: DeviceVtl,
3421}
3422
3423impl mesh_rpc::client::Dial for DiagDialer {
3424 type Stream = PolledSocket<unix_socket::UnixStream>;
3425
3426 async fn dial(&mut self) -> io::Result<Self::Stream> {
3427 let service_id = new_hvsock_service_id(1);
3428 let socket = self
3429 .vm_rpc
3430 .call_failable(
3431 VmRpc::ConnectHvsock,
3432 (
3433 CancelContext::new().with_timeout(Duration::from_secs(2)),
3434 service_id,
3435 self.openhcl_vtl,
3436 ),
3437 )
3438 .await
3439 .map_err(io::Error::other)?;
3440
3441 PolledSocket::new(&self.driver, socket)
3442 }
3443}
3444
3445pub struct DiagInspector(DiagInspectorInner);
3452
3453enum DiagInspectorInner {
3454 NotStarted(DefaultDriver, Arc<diag_client::DiagClient>),
3455 Started {
3456 send: mesh::Sender<inspect::Deferred>,
3457 _task: Task<()>,
3458 },
3459 Invalid,
3460}
3461
3462impl DiagInspector {
3463 pub fn new(driver: DefaultDriver, diag_client: Arc<diag_client::DiagClient>) -> Self {
3464 Self(DiagInspectorInner::NotStarted(driver, diag_client))
3465 }
3466
3467 fn start(&mut self) -> &mesh::Sender<inspect::Deferred> {
3468 loop {
3469 match self.0 {
3470 DiagInspectorInner::NotStarted { .. } => {
3471 let DiagInspectorInner::NotStarted(driver, client) =
3472 std::mem::replace(&mut self.0, DiagInspectorInner::Invalid)
3473 else {
3474 unreachable!()
3475 };
3476 let (send, recv) = mesh::channel();
3477 let task = driver.clone().spawn("diag-inspect", async move {
3478 Self::run(&client, recv).await
3479 });
3480
3481 self.0 = DiagInspectorInner::Started { send, _task: task };
3482 }
3483 DiagInspectorInner::Started { ref send, .. } => break send,
3484 DiagInspectorInner::Invalid => unreachable!(),
3485 }
3486 }
3487 }
3488
3489 async fn run(
3490 diag_client: &diag_client::DiagClient,
3491 mut recv: mesh::Receiver<inspect::Deferred>,
3492 ) {
3493 while let Some(deferred) = recv.next().await {
3494 let info = deferred.external_request();
3495 let result = match info.request_type {
3496 inspect::ExternalRequestType::Inspect { depth } => {
3497 if depth == 0 {
3498 Ok(inspect::Node::Unevaluated)
3499 } else {
3500 diag_client
3502 .inspect(info.path, Some(depth - 1), Some(Duration::from_secs(1)))
3503 .await
3504 }
3505 }
3506 inspect::ExternalRequestType::Update { value } => {
3507 (diag_client.update(info.path, value).await).map(inspect::Node::Value)
3508 }
3509 };
3510 deferred.complete_external(
3511 result.unwrap_or_else(|err| {
3512 inspect::Node::Failed(inspect::Error::Mesh(format!("{err:#}")))
3513 }),
3514 inspect::SensitivityLevel::Unspecified,
3515 )
3516 }
3517 }
3518}
3519
3520impl InspectMut for DiagInspector {
3521 fn inspect_mut(&mut self, req: inspect::Request<'_>) {
3522 self.start().send(req.defer());
3523 }
3524}
3525
3526enum InspectTarget {
3527 Host,
3528 Paravisor,
3529}
3530
3531mod interactive_console {
3532 use super::InteractiveCommand;
3533 use rustyline::Helper;
3534 use rustyline::Highlighter;
3535 use rustyline::Hinter;
3536 use rustyline::Validator;
3537
3538 #[derive(Helper, Highlighter, Hinter, Validator)]
3539 pub(crate) struct OpenvmmRustylineEditor {
3540 pub openvmm_inspect_req: std::sync::Arc<
3541 mesh::Sender<(
3542 super::InspectTarget,
3543 String,
3544 mesh::OneshotSender<inspect::Node>,
3545 )>,
3546 >,
3547 }
3548
3549 impl rustyline::completion::Completer for OpenvmmRustylineEditor {
3550 type Candidate = String;
3551
3552 fn complete(
3553 &self,
3554 line: &str,
3555 pos: usize,
3556 _ctx: &rustyline::Context<'_>,
3557 ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
3558 let Ok(cmd) = shell_words::split(line) else {
3559 return Ok((0, Vec::with_capacity(0)));
3560 };
3561
3562 let completions = futures::executor::block_on(
3563 clap_dyn_complete::Complete {
3564 cmd,
3565 raw: Some(line.into()),
3566 position: Some(pos),
3567 }
3568 .generate_completions::<InteractiveCommand>(None, self),
3569 );
3570
3571 let pos_from_end = {
3572 let line = line.chars().take(pos).collect::<String>();
3573
3574 let trailing_ws = line.len() - line.trim_end().len();
3575
3576 if trailing_ws > 0 {
3577 line.len() - trailing_ws + 1 } else {
3579 let last_word = shell_words::split(&line)
3580 .unwrap_or_default()
3581 .last()
3582 .cloned()
3583 .unwrap_or_default();
3584
3585 line.len() - last_word.len()
3586 }
3587 };
3588
3589 Ok((pos_from_end, completions))
3590 }
3591 }
3592
3593 impl clap_dyn_complete::CustomCompleterFactory for &OpenvmmRustylineEditor {
3594 type CustomCompleter = OpenvmmComplete;
3595 async fn build(&self, _ctx: &clap_dyn_complete::RootCtx<'_>) -> Self::CustomCompleter {
3596 OpenvmmComplete {
3597 openvmm_inspect_req: self.openvmm_inspect_req.clone(),
3598 }
3599 }
3600 }
3601
3602 pub struct OpenvmmComplete {
3603 openvmm_inspect_req: std::sync::Arc<
3604 mesh::Sender<(
3605 super::InspectTarget,
3606 String,
3607 mesh::OneshotSender<inspect::Node>,
3608 )>,
3609 >,
3610 }
3611
3612 impl clap_dyn_complete::CustomCompleter for OpenvmmComplete {
3613 async fn complete(
3614 &self,
3615 ctx: &clap_dyn_complete::RootCtx<'_>,
3616 subcommand_path: &[&str],
3617 arg_id: &str,
3618 ) -> Vec<String> {
3619 match (subcommand_path, arg_id) {
3620 (["openvmm", "inspect"], "element") => {
3621 let on_error = vec!["failed/to/connect".into()];
3622
3623 let (parent_path, to_complete) = (ctx.to_complete)
3624 .rsplit_once('/')
3625 .unwrap_or(("", ctx.to_complete));
3626
3627 let node = {
3628 let paravisor = {
3629 let raw_arg = ctx
3630 .matches
3631 .subcommand()
3632 .unwrap()
3633 .1
3634 .get_one::<String>("paravisor")
3635 .map(|x| x.as_str())
3636 .unwrap_or_default();
3637 raw_arg == "true"
3638 };
3639
3640 let (tx, rx) = mesh::oneshot();
3641 self.openvmm_inspect_req.send((
3642 if paravisor {
3643 super::InspectTarget::Paravisor
3644 } else {
3645 super::InspectTarget::Host
3646 },
3647 parent_path.to_owned(),
3648 tx,
3649 ));
3650 let Ok(node) = rx.await else {
3651 return on_error;
3652 };
3653
3654 node
3655 };
3656
3657 let mut completions = Vec::new();
3658
3659 if let inspect::Node::Dir(dir) = node {
3660 for entry in dir {
3661 if entry.name.starts_with(to_complete) {
3662 if parent_path.is_empty() {
3663 completions.push(format!("{}/", entry.name))
3664 } else {
3665 completions.push(format!(
3666 "{}/{}{}",
3667 parent_path,
3668 entry.name,
3669 if matches!(entry.node, inspect::Node::Dir(..)) {
3670 "/"
3671 } else {
3672 ""
3673 }
3674 ))
3675 }
3676 }
3677 }
3678 } else {
3679 return on_error;
3680 }
3681
3682 completions
3683 }
3684 _ => Vec::new(),
3685 }
3686 }
3687 }
3688}