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 memory_range::MemoryRange;
67use mesh::CancelContext;
68use mesh::CellUpdater;
69use mesh::error::RemoteError;
70use mesh::rpc::Rpc;
71use mesh::rpc::RpcError;
72use mesh::rpc::RpcSend;
73use mesh_worker::WorkerEvent;
74use mesh_worker::WorkerHandle;
75use meshworker::VmmMesh;
76use net_backend_resources::mac_address::MacAddress;
77use nvme_resources::NamespaceDefinition;
78use nvme_resources::NvmeControllerRequest;
79use openvmm_defs::config::Config;
80use openvmm_defs::config::DEFAULT_MMIO_GAPS_AARCH64;
81use openvmm_defs::config::DEFAULT_MMIO_GAPS_AARCH64_WITH_VTL2;
82use openvmm_defs::config::DEFAULT_MMIO_GAPS_X86;
83use openvmm_defs::config::DEFAULT_MMIO_GAPS_X86_WITH_VTL2;
84use openvmm_defs::config::DEFAULT_PCAT_BOOT_ORDER;
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_addr = ?cli_cfg.socket_addr,
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_addr: cli_cfg.socket_addr.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 use_vtl2_gap = opt.vtl2
817 && !matches!(
818 opt.igvm_vtl2_relocation_type,
819 Vtl2BaseAddressType::Vtl2Allocate { .. },
820 );
821
822 #[cfg(guest_arch = "aarch64")]
823 let arch = MachineArch::Aarch64;
824 #[cfg(guest_arch = "x86_64")]
825 let arch = MachineArch::X86_64;
826
827 let mmio_gaps: Vec<MemoryRange> = match (use_vtl2_gap, arch) {
828 (true, MachineArch::X86_64) => DEFAULT_MMIO_GAPS_X86_WITH_VTL2.into(),
829 (true, MachineArch::Aarch64) => DEFAULT_MMIO_GAPS_AARCH64_WITH_VTL2.into(),
830 (false, MachineArch::X86_64) => DEFAULT_MMIO_GAPS_X86.into(),
831 (false, MachineArch::Aarch64) => DEFAULT_MMIO_GAPS_AARCH64.into(),
832 };
833
834 let mut pci_ecam_gaps = Vec::new();
835 let mut pci_mmio_gaps = Vec::new();
836
837 let mut low_mmio_start = mmio_gaps.first().context("expected mmio gap")?.start();
838 let mut high_mmio_end = mmio_gaps.last().context("expected second mmio gap")?.end();
839
840 let mut pcie_root_complexes = Vec::new();
841 for (i, rc_cli) in opt.pcie_root_complex.iter().enumerate() {
842 let ports = opt
843 .pcie_root_port
844 .iter()
845 .filter(|port_cli| port_cli.root_complex_name == rc_cli.name)
846 .map(|port_cli| PcieRootPortConfig {
847 name: port_cli.name.clone(),
848 hotplug: port_cli.hotplug,
849 })
850 .collect();
851
852 const ONE_MB: u64 = 1024 * 1024;
853 let low_mmio_size = (rc_cli.low_mmio as u64).next_multiple_of(ONE_MB);
854 let high_mmio_size = rc_cli
855 .high_mmio
856 .checked_next_multiple_of(ONE_MB)
857 .context("high mmio rounding error")?;
858 let ecam_size = (((rc_cli.end_bus - rc_cli.start_bus) as u64) + 1) * 256 * 4096;
859
860 let low_pci_mmio_start = low_mmio_start
861 .checked_sub(low_mmio_size)
862 .context("pci low mmio underflow")?;
863 let ecam_start = low_pci_mmio_start
864 .checked_sub(ecam_size)
865 .context("pci ecam underflow")?;
866 low_mmio_start = ecam_start;
867 high_mmio_end = high_mmio_end
868 .checked_add(high_mmio_size)
869 .context("pci high mmio overflow")?;
870
871 let ecam_range = MemoryRange::new(ecam_start..ecam_start + ecam_size);
872 let low_mmio = MemoryRange::new(low_pci_mmio_start..low_pci_mmio_start + low_mmio_size);
873 let high_mmio = MemoryRange::new(high_mmio_end - high_mmio_size..high_mmio_end);
874
875 pci_ecam_gaps.push(ecam_range);
876 pci_mmio_gaps.push(low_mmio);
877 pci_mmio_gaps.push(high_mmio);
878
879 pcie_root_complexes.push(PcieRootComplexConfig {
880 index: i as u32,
881 name: rc_cli.name.clone(),
882 segment: rc_cli.segment,
883 start_bus: rc_cli.start_bus,
884 end_bus: rc_cli.end_bus,
885 ecam_range,
886 low_mmio,
887 high_mmio,
888 ports,
889 });
890 }
891
892 pci_ecam_gaps.sort();
893 pci_mmio_gaps.sort();
894
895 let pcie_switches = build_switch_list(&opt.pcie_switch);
896
897 #[cfg(windows)]
898 let vpci_resources: Vec<_> = opt
899 .device
900 .iter()
901 .map(|path| -> anyhow::Result<_> {
902 Ok(virt_whp::device::DeviceHandle(
903 whp::VpciResource::new(
904 None,
905 Default::default(),
906 &whp::VpciResourceDescriptor::Sriov(path, 0, 0),
907 )
908 .with_context(|| format!("opening PCI device {}", path))?,
909 ))
910 })
911 .collect::<Result<_, _>>()?;
912
913 #[cfg(windows)]
915 let vmbusproxy_handle = if !kernel_vmnics.is_empty() {
916 Some(vmbus_proxy::ProxyHandle::new().context("failed to open vmbusproxy handle")?)
917 } else {
918 None
919 };
920
921 let framebuffer = if opt.gfx || opt.vtl2_gfx || opt.vnc || opt.pcat {
922 let vram = alloc_shared_memory(FRAMEBUFFER_SIZE)?;
923 let (fb, fba) =
924 framebuffer::framebuffer(vram, FRAMEBUFFER_SIZE, 0).context("creating framebuffer")?;
925 resources.framebuffer_access = Some(fba);
926 Some(fb)
927 } else {
928 None
929 };
930
931 let load_mode;
932 let with_hv;
933
934 let any_serial_configured = serial0_cfg.is_some()
935 || serial1_cfg.is_some()
936 || serial2_cfg.is_some()
937 || serial3_cfg.is_some();
938
939 let has_com3 = serial2_cfg.is_some();
940
941 let mut chipset = VmManifestBuilder::new(
942 if opt.igvm.is_some() {
943 BaseChipsetType::HclHost
944 } else if opt.pcat {
945 BaseChipsetType::HypervGen1
946 } else if opt.uefi {
947 BaseChipsetType::HypervGen2Uefi
948 } else if opt.hv {
949 BaseChipsetType::HyperVGen2LinuxDirect
950 } else {
951 BaseChipsetType::UnenlightenedLinuxDirect
952 },
953 arch,
954 );
955
956 if framebuffer.is_some() {
957 chipset = chipset.with_framebuffer();
958 }
959 if opt.guest_watchdog {
960 chipset = chipset.with_guest_watchdog();
961 }
962 if any_serial_configured {
963 chipset = chipset.with_serial([serial0_cfg, serial1_cfg, serial2_cfg, serial3_cfg]);
964 }
965 if opt.battery {
966 let (tx, rx) = mesh::channel();
967 tx.send(HostBatteryUpdate::default_present());
968 chipset = chipset.with_battery(rx);
969 }
970 if let Some(cfg) = &opt.debugcon {
971 chipset = chipset.with_debugcon(
972 debugcon_cfg.unwrap_or_else(|| DisconnectedSerialBackendHandle.into_resource()),
973 cfg.port,
974 );
975 }
976
977 let bios_guid = Guid::new_random();
979
980 let VmChipsetResult {
981 chipset,
982 mut chipset_devices,
983 } = chipset
984 .build()
985 .context("failed to build chipset configuration")?;
986
987 if let Some(path) = &opt.igvm {
988 let file = fs_err::File::open(path)
989 .context("failed to open igvm file")?
990 .into();
991 let cmdline = opt.cmdline.join(" ");
992 with_hv = true;
993
994 load_mode = LoadMode::Igvm {
995 file,
996 cmdline,
997 vtl2_base_address: opt.igvm_vtl2_relocation_type,
998 com_serial: has_com3.then(|| SerialInformation {
999 io_port: ComPort::Com3.io_port(),
1000 irq: ComPort::Com3.irq().into(),
1001 }),
1002 };
1003 } else if opt.pcat {
1004 if arch != MachineArch::X86_64 {
1006 anyhow::bail!("pcat not supported on this architecture");
1007 }
1008 with_hv = true;
1009
1010 let firmware = openvmm_pcat_locator::find_pcat_bios(opt.pcat_firmware.as_deref())?;
1011 load_mode = LoadMode::Pcat {
1012 firmware,
1013 boot_order: opt
1014 .pcat_boot_order
1015 .map(|x| x.0)
1016 .unwrap_or(DEFAULT_PCAT_BOOT_ORDER),
1017 };
1018 } else if opt.uefi {
1019 use openvmm_defs::config::UefiConsoleMode;
1020
1021 with_hv = true;
1022
1023 let firmware = fs_err::File::open(
1024 (opt.uefi_firmware.0)
1025 .as_ref()
1026 .context("must provide uefi firmware when booting with uefi")?,
1027 )
1028 .context("failed to open uefi firmware")?;
1029
1030 load_mode = LoadMode::Uefi {
1033 firmware: firmware.into(),
1034 enable_debugging: opt.uefi_debug,
1035 enable_memory_protections: opt.uefi_enable_memory_protections,
1036 disable_frontpage: opt.disable_frontpage,
1037 enable_tpm: opt.tpm,
1038 enable_battery: opt.battery,
1039 enable_serial: any_serial_configured,
1040 enable_vpci_boot: false,
1041 uefi_console_mode: opt.uefi_console_mode.map(|m| match m {
1042 UefiConsoleModeCli::Default => UefiConsoleMode::Default,
1043 UefiConsoleModeCli::Com1 => UefiConsoleMode::Com1,
1044 UefiConsoleModeCli::Com2 => UefiConsoleMode::Com2,
1045 UefiConsoleModeCli::None => UefiConsoleMode::None,
1046 }),
1047 default_boot_always_attempt: opt.default_boot_always_attempt,
1048 bios_guid,
1049 };
1050 } else {
1051 let mut cmdline = "panic=-1 debug".to_string();
1053
1054 with_hv = opt.hv;
1055 if with_hv {
1056 cmdline += " pci=off";
1057 }
1058
1059 if !console_str.is_empty() {
1060 let _ = write!(&mut cmdline, " console={}", console_str);
1061 }
1062 if opt.gfx {
1063 cmdline += " console=tty";
1064 }
1065 for extra in &opt.cmdline {
1066 let _ = write!(&mut cmdline, " {}", extra);
1067 }
1068
1069 let kernel = fs_err::File::open(
1070 (opt.kernel.0)
1071 .as_ref()
1072 .context("must provide kernel when booting with linux direct")?,
1073 )
1074 .context("failed to open kernel")?;
1075 let initrd = (opt.initrd.0)
1076 .as_ref()
1077 .map(fs_err::File::open)
1078 .transpose()
1079 .context("failed to open initrd")?;
1080
1081 let custom_dsdt = match &opt.custom_dsdt {
1082 Some(path) => {
1083 let mut v = Vec::new();
1084 fs_err::File::open(path)
1085 .context("failed to open custom dsdt")?
1086 .read_to_end(&mut v)
1087 .context("failed to read custom dsdt")?;
1088 Some(v)
1089 }
1090 None => None,
1091 };
1092
1093 load_mode = LoadMode::Linux {
1094 kernel: kernel.into(),
1095 initrd: initrd.map(Into::into),
1096 cmdline,
1097 custom_dsdt,
1098 enable_serial: any_serial_configured,
1099 };
1100 }
1101
1102 let mut vmgs = Some(if let Some(VmgsCli { kind, provision }) = &opt.vmgs {
1103 let disk = VmgsDisk {
1104 disk: disk_open(kind, false).context("failed to open vmgs disk")?,
1105 encryption_policy: if opt.test_gsp_by_id {
1106 GuestStateEncryptionPolicy::GspById(true)
1107 } else {
1108 GuestStateEncryptionPolicy::None(true)
1109 },
1110 };
1111 match provision {
1112 ProvisionVmgs::OnEmpty => VmgsResource::Disk(disk),
1113 ProvisionVmgs::OnFailure => VmgsResource::ReprovisionOnFailure(disk),
1114 ProvisionVmgs::True => VmgsResource::Reprovision(disk),
1115 }
1116 } else {
1117 VmgsResource::Ephemeral
1118 });
1119
1120 if with_get && with_hv {
1121 let vtl2_settings = vtl2_settings_proto::Vtl2Settings {
1122 version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
1123 fixed: Some(Default::default()),
1124 dynamic: Some(vtl2_settings_proto::Vtl2SettingsDynamic {
1125 storage_controllers: storage.build_underhill(opt.vmbus_redirect),
1126 nic_devices: underhill_nics,
1127 }),
1128 namespace_settings: Vec::default(),
1129 };
1130
1131 resources.vtl2_settings = Some(vtl2_settings.clone());
1133
1134 let (send, guest_request_recv) = mesh::channel();
1135 resources.ged_rpc = Some(send);
1136
1137 let vmgs = vmgs.take().unwrap();
1138
1139 vmbus_devices.extend([
1140 (
1141 openhcl_vtl,
1142 get_resources::gel::GuestEmulationLogHandle.into_resource(),
1143 ),
1144 (
1145 openhcl_vtl,
1146 get_resources::ged::GuestEmulationDeviceHandle {
1147 firmware: if opt.pcat {
1148 get_resources::ged::GuestFirmwareConfig::Pcat {
1149 boot_order: opt
1150 .pcat_boot_order
1151 .map_or(DEFAULT_PCAT_BOOT_ORDER, |x| x.0)
1152 .map(|x| match x {
1153 openvmm_defs::config::PcatBootDevice::Floppy => {
1154 get_resources::ged::PcatBootDevice::Floppy
1155 }
1156 openvmm_defs::config::PcatBootDevice::HardDrive => {
1157 get_resources::ged::PcatBootDevice::HardDrive
1158 }
1159 openvmm_defs::config::PcatBootDevice::Optical => {
1160 get_resources::ged::PcatBootDevice::Optical
1161 }
1162 openvmm_defs::config::PcatBootDevice::Network => {
1163 get_resources::ged::PcatBootDevice::Network
1164 }
1165 }),
1166 }
1167 } else {
1168 use get_resources::ged::UefiConsoleMode;
1169
1170 get_resources::ged::GuestFirmwareConfig::Uefi {
1171 enable_vpci_boot: storage.has_vtl0_nvme(),
1172 firmware_debug: opt.uefi_debug,
1173 disable_frontpage: opt.disable_frontpage,
1174 console_mode: match opt.uefi_console_mode.unwrap_or(UefiConsoleModeCli::Default) {
1175 UefiConsoleModeCli::Default => UefiConsoleMode::Default,
1176 UefiConsoleModeCli::Com1 => UefiConsoleMode::COM1,
1177 UefiConsoleModeCli::Com2 => UefiConsoleMode::COM2,
1178 UefiConsoleModeCli::None => UefiConsoleMode::None,
1179 },
1180 default_boot_always_attempt: opt.default_boot_always_attempt,
1181 }
1182 },
1183 com1: with_vmbus_com1_serial,
1184 com2: with_vmbus_com2_serial,
1185 serial_tx_only: opt.serial_tx_only,
1186 vtl2_settings: Some(prost::Message::encode_to_vec(&vtl2_settings)),
1187 vmbus_redirection: opt.vmbus_redirect,
1188 vmgs,
1189 framebuffer: opt
1190 .vtl2_gfx
1191 .then(|| SharedFramebufferHandle.into_resource()),
1192 guest_request_recv,
1193 enable_tpm: opt.tpm,
1194 firmware_event_send: None,
1195 secure_boot_enabled: opt.secure_boot,
1196 secure_boot_template: match opt.secure_boot_template {
1197 Some(SecureBootTemplateCli::Windows) => {
1198 get_resources::ged::GuestSecureBootTemplateType::MicrosoftWindows
1199 },
1200 Some(SecureBootTemplateCli::UefiCa) => {
1201 get_resources::ged::GuestSecureBootTemplateType::MicrosoftUefiCertificateAuthority
1202 }
1203 None => {
1204 get_resources::ged::GuestSecureBootTemplateType::None
1205 },
1206 },
1207 enable_battery: opt.battery,
1208 no_persistent_secrets: true,
1209 igvm_attest_test_config: None,
1210 test_gsp_by_id: opt.test_gsp_by_id,
1211 efi_diagnostics_log_level: {
1212 match opt.efi_diagnostics_log_level.unwrap_or_default() {
1213 EfiDiagnosticsLogLevelCli::Default => get_resources::ged::EfiDiagnosticsLogLevelType::Default,
1214 EfiDiagnosticsLogLevelCli::Info => get_resources::ged::EfiDiagnosticsLogLevelType::Info,
1215 EfiDiagnosticsLogLevelCli::Full => get_resources::ged::EfiDiagnosticsLogLevelType::Full,
1216 }
1217 },
1218 hv_sint_enabled: false,
1219 }
1220 .into_resource(),
1221 ),
1222 ]);
1223 }
1224
1225 if opt.tpm && !opt.vtl2 {
1226 let register_layout = if cfg!(guest_arch = "x86_64") {
1227 TpmRegisterLayout::IoPort
1228 } else {
1229 TpmRegisterLayout::Mmio
1230 };
1231
1232 let (ppi_store, nvram_store) = if opt.vmgs.is_some() {
1233 (
1234 VmgsFileHandle::new(vmgs_format::FileId::TPM_PPI, true).into_resource(),
1235 VmgsFileHandle::new(vmgs_format::FileId::TPM_NVRAM, true).into_resource(),
1236 )
1237 } else {
1238 (
1239 EphemeralNonVolatileStoreHandle.into_resource(),
1240 EphemeralNonVolatileStoreHandle.into_resource(),
1241 )
1242 };
1243
1244 chipset_devices.push(ChipsetDeviceHandle {
1245 name: "tpm".to_string(),
1246 resource: chipset_device_worker_defs::RemoteChipsetDeviceHandle {
1247 device: TpmDeviceHandle {
1248 ppi_store,
1249 nvram_store,
1250 nvram_size: None,
1251 refresh_tpm_seeds: false,
1252 ak_cert_type: tpm_resources::TpmAkCertTypeResource::None,
1253 register_layout,
1254 guest_secret_key: None,
1255 logger: None,
1256 is_confidential_vm: false,
1257 bios_guid,
1258 }
1259 .into_resource(),
1260 worker_host: mesh.make_host("tpm", None).await?,
1261 }
1262 .into_resource(),
1263 });
1264 }
1265
1266 let custom_uefi_vars = {
1267 use firmware_uefi_custom_vars::CustomVars;
1268
1269 let base_vars = match opt.secure_boot_template {
1272 Some(template) => match (arch, template) {
1273 (MachineArch::X86_64, SecureBootTemplateCli::Windows) => {
1274 hyperv_secure_boot_templates::x64::microsoft_windows()
1275 }
1276 (MachineArch::X86_64, SecureBootTemplateCli::UefiCa) => {
1277 hyperv_secure_boot_templates::x64::microsoft_uefi_ca()
1278 }
1279 (MachineArch::Aarch64, SecureBootTemplateCli::Windows) => {
1280 hyperv_secure_boot_templates::aarch64::microsoft_windows()
1281 }
1282 (MachineArch::Aarch64, SecureBootTemplateCli::UefiCa) => {
1283 hyperv_secure_boot_templates::aarch64::microsoft_uefi_ca()
1284 }
1285 },
1286 None => CustomVars::default(),
1287 };
1288
1289 let custom_uefi_json_data = match &opt.custom_uefi_json {
1292 Some(file) => Some(fs_err::read(file).context("opening custom uefi json file")?),
1293 None => None,
1294 };
1295
1296 match custom_uefi_json_data {
1298 Some(data) => {
1299 let delta = hyperv_uefi_custom_vars_json::load_delta_from_json(&data)?;
1300 base_vars.apply_delta(delta)?
1301 }
1302 None => base_vars,
1303 }
1304 };
1305
1306 let vga_firmware = if opt.pcat {
1307 Some(openvmm_pcat_locator::find_svga_bios(
1308 opt.vga_firmware.as_deref(),
1309 )?)
1310 } else {
1311 None
1312 };
1313
1314 if opt.gfx {
1315 vmbus_devices.extend([
1316 (
1317 DeviceVtl::Vtl0,
1318 SynthVideoHandle {
1319 framebuffer: SharedFramebufferHandle.into_resource(),
1320 }
1321 .into_resource(),
1322 ),
1323 (
1324 DeviceVtl::Vtl0,
1325 SynthKeyboardHandle {
1326 source: MultiplexedInputHandle {
1327 elevation: 1,
1329 }
1330 .into_resource(),
1331 }
1332 .into_resource(),
1333 ),
1334 (
1335 DeviceVtl::Vtl0,
1336 SynthMouseHandle {
1337 source: MultiplexedInputHandle {
1338 elevation: 1,
1340 }
1341 .into_resource(),
1342 }
1343 .into_resource(),
1344 ),
1345 ]);
1346 }
1347
1348 let vsock_listener = |path: Option<&str>| -> anyhow::Result<_> {
1349 if let Some(path) = path {
1350 cleanup_socket(path.as_ref());
1351 let listener = unix_socket::UnixListener::bind(path)
1352 .with_context(|| format!("failed to bind to hybrid vsock path: {}", path))?;
1353 Ok(Some(listener))
1354 } else {
1355 Ok(None)
1356 }
1357 };
1358
1359 let vtl0_vsock_listener = vsock_listener(opt.vsock_path.as_deref())?;
1360 let vtl2_vsock_listener = vsock_listener(opt.vtl2_vsock_path.as_deref())?;
1361
1362 if let Some(path) = &opt.openhcl_dump_path {
1363 let (resource, task) = spawn_dump_handler(&spawner, path.clone(), None);
1364 task.detach();
1365 vmbus_devices.push((openhcl_vtl, resource));
1366 }
1367
1368 #[cfg(guest_arch = "aarch64")]
1369 let topology_arch = openvmm_defs::config::ArchTopologyConfig::Aarch64(
1370 openvmm_defs::config::Aarch64TopologyConfig {
1371 gic_config: None,
1373 pmu_gsiv: openvmm_defs::config::PmuGsivConfig::Platform,
1374 },
1375 );
1376 #[cfg(guest_arch = "x86_64")]
1377 let topology_arch =
1378 openvmm_defs::config::ArchTopologyConfig::X86(openvmm_defs::config::X86TopologyConfig {
1379 apic_id_offset: opt.apic_id_offset,
1380 x2apic: opt.x2apic,
1381 });
1382
1383 let with_isolation = if let Some(isolation) = &opt.isolation {
1384 if !opt.vtl2 {
1386 anyhow::bail!("isolation is only currently supported with vtl2");
1387 }
1388
1389 if !opt.no_alias_map {
1391 anyhow::bail!("alias map not supported with isolation");
1392 }
1393
1394 match isolation {
1395 cli_args::IsolationCli::Vbs => Some(openvmm_defs::config::IsolationType::Vbs),
1396 }
1397 } else {
1398 None
1399 };
1400
1401 if with_hv {
1402 let (shutdown_send, shutdown_recv) = mesh::channel();
1403 resources.shutdown_ic = Some(shutdown_send);
1404 let (kvp_send, kvp_recv) = mesh::channel();
1405 resources.kvp_ic = Some(kvp_send);
1406 vmbus_devices.extend(
1407 [
1408 hyperv_ic_resources::shutdown::ShutdownIcHandle {
1409 recv: shutdown_recv,
1410 }
1411 .into_resource(),
1412 hyperv_ic_resources::kvp::KvpIcHandle { recv: kvp_recv }.into_resource(),
1413 hyperv_ic_resources::timesync::TimesyncIcHandle.into_resource(),
1414 ]
1415 .map(|r| (DeviceVtl::Vtl0, r)),
1416 );
1417 }
1418
1419 if let Some(hive_path) = &opt.imc {
1420 let file = fs_err::File::open(hive_path).context("failed to open imc hive")?;
1421 vmbus_devices.push((
1422 DeviceVtl::Vtl0,
1423 vmbfs_resources::VmbfsImcDeviceHandle { file: file.into() }.into_resource(),
1424 ));
1425 }
1426
1427 let mut virtio_devices = Vec::new();
1428 let mut add_virtio_device = |bus, resource: Resource<VirtioDeviceHandle>| {
1429 let bus = match bus {
1430 VirtioBusCli::Auto => {
1431 if with_hv && (cfg!(windows) || cfg!(target_os = "macos")) {
1434 None
1435 } else {
1436 Some(VirtioBus::Pci)
1437 }
1438 }
1439 VirtioBusCli::Mmio => Some(VirtioBus::Mmio),
1440 VirtioBusCli::Pci => Some(VirtioBus::Pci),
1441 VirtioBusCli::Vpci => None,
1442 };
1443 if let Some(bus) = bus {
1444 virtio_devices.push((bus, resource));
1445 } else {
1446 vpci_devices.push(VpciDeviceConfig {
1447 vtl: DeviceVtl::Vtl0,
1448 instance_id: Guid::new_random(),
1449 resource: VirtioPciDeviceHandle(resource).into_resource(),
1450 });
1451 }
1452 };
1453
1454 for cli_cfg in &opt.virtio_net {
1455 if cli_cfg.underhill {
1456 anyhow::bail!("use --net uh:[...] to add underhill NICs")
1457 }
1458 let vport = parse_endpoint(cli_cfg, &mut nic_index, &mut resources)?;
1459 add_virtio_device(
1460 VirtioBusCli::Auto,
1461 virtio_resources::net::VirtioNetHandle {
1462 max_queues: vport.max_queues,
1463 mac_address: vport.mac_address,
1464 endpoint: vport.endpoint,
1465 }
1466 .into_resource(),
1467 );
1468 }
1469
1470 for args in &opt.virtio_fs {
1471 add_virtio_device(
1472 opt.virtio_fs_bus,
1473 virtio_resources::fs::VirtioFsHandle {
1474 tag: args.tag.clone(),
1475 fs: virtio_resources::fs::VirtioFsBackend::HostFs {
1476 root_path: args.path.clone(),
1477 mount_options: args.options.clone(),
1478 },
1479 }
1480 .into_resource(),
1481 );
1482 }
1483
1484 for args in &opt.virtio_fs_shmem {
1485 add_virtio_device(
1486 opt.virtio_fs_bus,
1487 virtio_resources::fs::VirtioFsHandle {
1488 tag: args.tag.clone(),
1489 fs: virtio_resources::fs::VirtioFsBackend::SectionFs {
1490 root_path: args.path.clone(),
1491 },
1492 }
1493 .into_resource(),
1494 );
1495 }
1496
1497 for args in &opt.virtio_9p {
1498 add_virtio_device(
1499 VirtioBusCli::Auto,
1500 virtio_resources::p9::VirtioPlan9Handle {
1501 tag: args.tag.clone(),
1502 root_path: args.path.clone(),
1503 debug: opt.virtio_9p_debug,
1504 }
1505 .into_resource(),
1506 );
1507 }
1508
1509 if let Some(path) = &opt.virtio_pmem {
1510 add_virtio_device(
1511 VirtioBusCli::Auto,
1512 virtio_resources::pmem::VirtioPmemHandle { path: path.clone() }.into_resource(),
1513 );
1514 }
1515
1516 let mut cfg = Config {
1517 chipset,
1518 load_mode,
1519 floppy_disks,
1520 pcie_root_complexes,
1521 pcie_devices,
1522 pcie_switches,
1523 vpci_devices,
1524 ide_disks: Vec::new(),
1525 memory: MemoryConfig {
1526 mem_size: opt.memory,
1527 mmio_gaps,
1528 pci_ecam_gaps,
1529 pci_mmio_gaps,
1530 prefetch_memory: opt.prefetch,
1531 },
1532 processor_topology: ProcessorTopologyConfig {
1533 proc_count: opt.processors,
1534 vps_per_socket: opt.vps_per_socket,
1535 enable_smt: match opt.smt {
1536 cli_args::SmtConfigCli::Auto => None,
1537 cli_args::SmtConfigCli::Force => Some(true),
1538 cli_args::SmtConfigCli::Off => Some(false),
1539 },
1540 arch: Some(topology_arch),
1541 },
1542 hypervisor: HypervisorConfig {
1543 with_hv,
1544 with_vtl2: opt.vtl2.then_some(Vtl2Config {
1545 vtl0_alias_map: !opt.no_alias_map,
1546 late_map_vtl0_memory: match opt.late_map_vtl0_policy {
1547 cli_args::Vtl0LateMapPolicyCli::Off => None,
1548 cli_args::Vtl0LateMapPolicyCli::Log => Some(LateMapVtl0MemoryPolicy::Log),
1549 cli_args::Vtl0LateMapPolicyCli::Halt => Some(LateMapVtl0MemoryPolicy::Halt),
1550 cli_args::Vtl0LateMapPolicyCli::Exception => {
1551 Some(LateMapVtl0MemoryPolicy::InjectException)
1552 }
1553 },
1554 }),
1555 with_isolation,
1556 user_mode_hv_enlightenments: opt.no_enlightenments,
1557 user_mode_apic: opt.user_mode_apic,
1558 },
1559 #[cfg(windows)]
1560 kernel_vmnics,
1561 input: mesh::Receiver::new(),
1562 framebuffer,
1563 vga_firmware,
1564 vtl2_gfx: opt.vtl2_gfx,
1565 virtio_devices,
1566 vmbus: with_hv.then_some(VmbusConfig {
1567 vsock_listener: vtl0_vsock_listener,
1568 vsock_path: opt.vsock_path.clone(),
1569 vtl2_redirect: opt.vmbus_redirect,
1570 vmbus_max_version: opt.vmbus_max_version,
1571 #[cfg(windows)]
1572 vmbusproxy_handle,
1573 }),
1574 vtl2_vmbus: (with_hv && opt.vtl2).then_some(VmbusConfig {
1575 vsock_listener: vtl2_vsock_listener,
1576 vsock_path: opt.vtl2_vsock_path.clone(),
1577 ..Default::default()
1578 }),
1579 vmbus_devices,
1580 chipset_devices,
1581 #[cfg(windows)]
1582 vpci_resources,
1583 vmgs,
1584 secure_boot_enabled: opt.secure_boot,
1585 custom_uefi_vars,
1586 firmware_event_send: None,
1587 debugger_rpc: None,
1588 generation_id_recv: None,
1589 rtc_delta_milliseconds: 0,
1590 automatic_guest_reset: !opt.halt_on_reset,
1591 efi_diagnostics_log_level: {
1592 match opt.efi_diagnostics_log_level.unwrap_or_default() {
1593 EfiDiagnosticsLogLevelCli::Default => EfiDiagnosticsLogLevelType::Default,
1594 EfiDiagnosticsLogLevelCli::Info => EfiDiagnosticsLogLevelType::Info,
1595 EfiDiagnosticsLogLevelCli::Full => EfiDiagnosticsLogLevelType::Full,
1596 }
1597 },
1598 };
1599
1600 storage.build_config(&mut cfg, &mut resources, opt.scsi_sub_channels)?;
1601 Ok((cfg, resources))
1602}
1603
1604fn openvmm_terminal_app() -> Option<PathBuf> {
1606 std::env::var_os("OPENVMM_TERM")
1607 .or_else(|| std::env::var_os("HVLITE_TERM"))
1608 .map(Into::into)
1609}
1610
1611fn cleanup_socket(path: &Path) {
1613 #[cfg(windows)]
1614 let is_socket = pal::windows::fs::is_unix_socket(path).unwrap_or(false);
1615 #[cfg(not(windows))]
1616 let is_socket = path
1617 .metadata()
1618 .is_ok_and(|meta| std::os::unix::fs::FileTypeExt::is_socket(&meta.file_type()));
1619
1620 if is_socket {
1621 let _ = std::fs::remove_file(path);
1622 }
1623}
1624
1625#[cfg(windows)]
1626const DEFAULT_SWITCH: &str = "C08CB7B8-9B3C-408E-8E30-5E16A3AEB444";
1627
1628#[cfg(windows)]
1629fn new_switch_port(
1630 switch_id: &str,
1631) -> anyhow::Result<(
1632 openvmm_defs::config::SwitchPortId,
1633 vmswitch::kernel::SwitchPort,
1634)> {
1635 let id = vmswitch::kernel::SwitchPortId {
1636 switch: switch_id.parse().context("invalid switch id")?,
1637 port: Guid::new_random(),
1638 };
1639 let _ = vmswitch::hcn::Network::open(&id.switch)
1640 .with_context(|| format!("could not find switch {}", id.switch))?;
1641
1642 let port = vmswitch::kernel::SwitchPort::new(&id).context("failed to create switch port")?;
1643
1644 let id = openvmm_defs::config::SwitchPortId {
1645 switch: id.switch,
1646 port: id.port,
1647 };
1648 Ok((id, port))
1649}
1650
1651fn parse_endpoint(
1652 cli_cfg: &NicConfigCli,
1653 index: &mut usize,
1654 resources: &mut VmResources,
1655) -> anyhow::Result<NicConfig> {
1656 let _ = resources;
1657 let endpoint = match &cli_cfg.endpoint {
1658 EndpointConfigCli::Consomme { cidr } => {
1659 net_backend_resources::consomme::ConsommeHandle { cidr: cidr.clone() }.into_resource()
1660 }
1661 EndpointConfigCli::None => net_backend_resources::null::NullHandle.into_resource(),
1662 EndpointConfigCli::Dio { id } => {
1663 #[cfg(windows)]
1664 {
1665 let (port_id, port) = new_switch_port(id.as_deref().unwrap_or(DEFAULT_SWITCH))?;
1666 resources.switch_ports.push(port);
1667 net_backend_resources::dio::WindowsDirectIoHandle {
1668 switch_port_id: net_backend_resources::dio::SwitchPortId {
1669 switch: port_id.switch,
1670 port: port_id.port,
1671 },
1672 }
1673 .into_resource()
1674 }
1675
1676 #[cfg(not(windows))]
1677 {
1678 let _ = id;
1679 bail!("cannot use dio on non-windows platforms")
1680 }
1681 }
1682 EndpointConfigCli::Tap { name } => {
1683 net_backend_resources::tap::TapHandle { name: name.clone() }.into_resource()
1684 }
1685 };
1686
1687 let mut mac_address = [0x00, 0x15, 0x5D, 0, 0, 0];
1689 getrandom::fill(&mut mac_address[3..]).expect("rng failure");
1690
1691 const BASE_INSTANCE_ID: Guid = guid::guid!("00000000-da43-11ed-936a-00155d6db52f");
1693 let instance_id = Guid {
1694 data1: *index as u32,
1695 ..BASE_INSTANCE_ID
1696 };
1697 *index += 1;
1698
1699 Ok(NicConfig {
1700 vtl: cli_cfg.vtl,
1701 instance_id,
1702 endpoint,
1703 mac_address: mac_address.into(),
1704 max_queues: cli_cfg.max_queues,
1705 })
1706}
1707
1708#[derive(Debug)]
1709struct NicConfig {
1710 vtl: DeviceVtl,
1711 instance_id: Guid,
1712 mac_address: MacAddress,
1713 endpoint: Resource<NetEndpointHandleKind>,
1714 max_queues: Option<u16>,
1715}
1716
1717impl NicConfig {
1718 fn into_netvsp_handle(self) -> (DeviceVtl, Resource<VmbusDeviceHandleKind>) {
1719 (
1720 self.vtl,
1721 netvsp_resources::NetvspHandle {
1722 instance_id: self.instance_id,
1723 mac_address: self.mac_address,
1724 endpoint: self.endpoint,
1725 max_queues: self.max_queues,
1726 }
1727 .into_resource(),
1728 )
1729 }
1730}
1731
1732enum LayerOrDisk {
1733 Layer(DiskLayerDescription),
1734 Disk(Resource<DiskHandleKind>),
1735}
1736
1737fn disk_open(disk_cli: &DiskCliKind, read_only: bool) -> anyhow::Result<Resource<DiskHandleKind>> {
1738 let mut layers = Vec::new();
1739 disk_open_inner(disk_cli, read_only, &mut layers)?;
1740 if layers.len() == 1 && matches!(layers[0], LayerOrDisk::Disk(_)) {
1741 let LayerOrDisk::Disk(disk) = layers.pop().unwrap() else {
1742 unreachable!()
1743 };
1744 Ok(disk)
1745 } else {
1746 Ok(Resource::new(disk_backend_resources::LayeredDiskHandle {
1747 layers: layers
1748 .into_iter()
1749 .map(|layer| match layer {
1750 LayerOrDisk::Layer(layer) => layer,
1751 LayerOrDisk::Disk(disk) => DiskLayerDescription {
1752 layer: DiskLayerHandle(disk).into_resource(),
1753 read_cache: false,
1754 write_through: false,
1755 },
1756 })
1757 .collect(),
1758 }))
1759 }
1760}
1761
1762fn disk_open_inner(
1763 disk_cli: &DiskCliKind,
1764 read_only: bool,
1765 layers: &mut Vec<LayerOrDisk>,
1766) -> anyhow::Result<()> {
1767 fn layer<T: IntoResource<DiskLayerHandleKind>>(layer: T) -> LayerOrDisk {
1768 LayerOrDisk::Layer(layer.into_resource().into())
1769 }
1770 fn disk<T: IntoResource<DiskHandleKind>>(disk: T) -> LayerOrDisk {
1771 LayerOrDisk::Disk(disk.into_resource())
1772 }
1773 match disk_cli {
1774 &DiskCliKind::Memory(len) => {
1775 layers.push(layer(RamDiskLayerHandle { len: Some(len) }));
1776 }
1777 DiskCliKind::File {
1778 path,
1779 create_with_len,
1780 } => layers.push(LayerOrDisk::Disk(if let Some(size) = create_with_len {
1781 create_disk_type(path, *size)
1782 .with_context(|| format!("failed to create {}", path.display()))?
1783 } else {
1784 open_disk_type(path, read_only)
1785 .with_context(|| format!("failed to open {}", path.display()))?
1786 })),
1787 DiskCliKind::Blob { kind, url } => {
1788 layers.push(disk(disk_backend_resources::BlobDiskHandle {
1789 url: url.to_owned(),
1790 format: match kind {
1791 cli_args::BlobKind::Flat => disk_backend_resources::BlobDiskFormat::Flat,
1792 cli_args::BlobKind::Vhd1 => disk_backend_resources::BlobDiskFormat::FixedVhd1,
1793 },
1794 }))
1795 }
1796 DiskCliKind::MemoryDiff(inner) => {
1797 layers.push(layer(RamDiskLayerHandle { len: None }));
1798 disk_open_inner(inner, true, layers)?;
1799 }
1800 DiskCliKind::PersistentReservationsWrapper(inner) => layers.push(disk(
1801 disk_backend_resources::DiskWithReservationsHandle(disk_open(inner, read_only)?),
1802 )),
1803 DiskCliKind::DelayDiskWrapper {
1804 delay_ms,
1805 disk: inner,
1806 } => layers.push(disk(DelayDiskHandle {
1807 delay: CellUpdater::new(Duration::from_millis(*delay_ms)).cell(),
1808 disk: disk_open(inner, read_only)?,
1809 })),
1810 DiskCliKind::Crypt {
1811 disk: inner,
1812 cipher,
1813 key_file,
1814 } => layers.push(disk(disk_crypt_resources::DiskCryptHandle {
1815 disk: disk_open(inner, read_only)?,
1816 cipher: match cipher {
1817 cli_args::DiskCipher::XtsAes256 => disk_crypt_resources::Cipher::XtsAes256,
1818 },
1819 key: fs_err::read(key_file).context("failed to read key file")?,
1820 })),
1821 DiskCliKind::Sqlite {
1822 path,
1823 create_with_len,
1824 } => {
1825 match (create_with_len.is_some(), path.exists()) {
1830 (true, true) => anyhow::bail!(
1831 "cannot create new sqlite disk at {} - file already exists",
1832 path.display()
1833 ),
1834 (false, false) => anyhow::bail!(
1835 "cannot open sqlite disk at {} - file not found",
1836 path.display()
1837 ),
1838 _ => {}
1839 }
1840
1841 layers.push(layer(SqliteDiskLayerHandle {
1842 dbhd_path: path.display().to_string(),
1843 format_dbhd: create_with_len.map(|len| {
1844 disk_backend_resources::layer::SqliteDiskLayerFormatParams {
1845 logically_read_only: false,
1846 len: Some(len),
1847 }
1848 }),
1849 }));
1850 }
1851 DiskCliKind::SqliteDiff { path, create, disk } => {
1852 match (create, path.exists()) {
1857 (true, true) => anyhow::bail!(
1858 "cannot create new sqlite disk at {} - file already exists",
1859 path.display()
1860 ),
1861 (false, false) => anyhow::bail!(
1862 "cannot open sqlite disk at {} - file not found",
1863 path.display()
1864 ),
1865 _ => {}
1866 }
1867
1868 layers.push(layer(SqliteDiskLayerHandle {
1869 dbhd_path: path.display().to_string(),
1870 format_dbhd: create.then_some(
1871 disk_backend_resources::layer::SqliteDiskLayerFormatParams {
1872 logically_read_only: false,
1873 len: None,
1874 },
1875 ),
1876 }));
1877 disk_open_inner(disk, true, layers)?;
1878 }
1879 DiskCliKind::AutoCacheSqlite {
1880 cache_path,
1881 key,
1882 disk,
1883 } => {
1884 layers.push(LayerOrDisk::Layer(DiskLayerDescription {
1885 read_cache: true,
1886 write_through: false,
1887 layer: SqliteAutoCacheDiskLayerHandle {
1888 cache_path: cache_path.clone(),
1889 cache_key: key.clone(),
1890 }
1891 .into_resource(),
1892 }));
1893 disk_open_inner(disk, read_only, layers)?;
1894 }
1895 }
1896 Ok(())
1897}
1898
1899fn do_main() -> anyhow::Result<()> {
1900 #[cfg(windows)]
1901 pal::windows::disable_hard_error_dialog();
1902
1903 tracing_init::enable_tracing()?;
1904
1905 meshworker::run_vmm_mesh_host()?;
1909
1910 let opt = Options::parse();
1911 if let Some(path) = &opt.write_saved_state_proto {
1912 mesh::payload::protofile::DescriptorWriter::new(vmcore::save_restore::saved_state_roots())
1913 .write_to_path(path)
1914 .context("failed to write protobuf descriptors")?;
1915 return Ok(());
1916 }
1917
1918 if let Some(path) = opt.relay_console_path {
1919 let console_title = opt.relay_console_title.unwrap_or_default();
1920 return console_relay::relay_console(&path, console_title.as_str());
1921 }
1922
1923 #[cfg(any(feature = "grpc", feature = "ttrpc"))]
1924 if let Some(path) = opt.ttrpc.as_ref().or(opt.grpc.as_ref()) {
1925 return block_on(async {
1926 let _ = std::fs::remove_file(path);
1927 let listener =
1928 unix_socket::UnixListener::bind(path).context("failed to bind to socket")?;
1929
1930 let transport = if opt.ttrpc.is_some() {
1931 ttrpc::RpcTransport::Ttrpc
1932 } else {
1933 ttrpc::RpcTransport::Grpc
1934 };
1935
1936 let mut handle =
1938 mesh_worker::launch_local_worker::<ttrpc::TtrpcWorker>(ttrpc::Parameters {
1939 listener,
1940 transport,
1941 })
1942 .await?;
1943
1944 tracing::info!(%transport, path = %path.display(), "listening");
1945
1946 pal::close_stdout().context("failed to close stdout")?;
1948
1949 handle.join().await?;
1950
1951 Ok(())
1952 });
1953 }
1954
1955 DefaultPool::run_with(async |driver| {
1956 let mesh = VmmMesh::new(&driver, opt.single_process)?;
1957 let result = run_control(&driver, &mesh, opt).await;
1958 mesh.shutdown().await;
1959 result
1960 })
1961}
1962
1963fn maybe_with_radix_u64(s: &str) -> Result<u64, String> {
1964 let (radix, prefix_len) = if s.starts_with("0x") || s.starts_with("0X") {
1965 (16, 2)
1966 } else if s.starts_with("0o") || s.starts_with("0O") {
1967 (8, 2)
1968 } else if s.starts_with("0b") || s.starts_with("0B") {
1969 (2, 2)
1970 } else {
1971 (10, 0)
1972 };
1973
1974 u64::from_str_radix(&s[prefix_len..], radix).map_err(|e| format!("{e}"))
1975}
1976
1977#[derive(Parser)]
1978#[clap(
1979 name = "openvmm",
1980 disable_help_flag = true,
1981 disable_version_flag = true,
1982 no_binary_name = true,
1983 help_template("{subcommands}")
1984)]
1985enum InteractiveCommand {
1986 #[clap(visible_alias = "R")]
1990 Restart,
1991
1992 #[clap(visible_alias = "n")]
1994 Nmi,
1995
1996 #[clap(visible_alias = "p")]
1998 Pause,
1999
2000 #[clap(visible_alias = "r")]
2002 Resume,
2003
2004 #[clap(visible_alias = "psr")]
2006 PulseSaveRestore,
2007
2008 #[clap(visible_alias = "spsr")]
2010 SchedulePulseSaveRestore {
2011 interval: Option<u64>,
2014 },
2015
2016 #[clap(visible_alias = "d")]
2018 AddDisk {
2019 #[clap(long = "ro")]
2020 read_only: bool,
2021 #[clap(long = "dvd")]
2022 is_dvd: bool,
2023 #[clap(long, default_value_t)]
2024 target: u8,
2025 #[clap(long, default_value_t)]
2026 path: u8,
2027 #[clap(long, default_value_t)]
2028 lun: u8,
2029 #[clap(long)]
2030 ram: Option<u64>,
2031 file_path: Option<PathBuf>,
2032 },
2033
2034 #[clap(visible_alias = "D")]
2036 RmDisk {
2037 #[clap(long)]
2038 target: u8,
2039 #[clap(long)]
2040 path: u8,
2041 #[clap(long)]
2042 lun: u8,
2043 },
2044
2045 #[clap(subcommand)]
2047 Vtl2Settings(Vtl2SettingsCommand),
2048
2049 AddNvmeNs {
2051 #[clap(long = "ro")]
2052 read_only: bool,
2053 #[clap(long)]
2055 nsid: u32,
2056 #[clap(long)]
2058 ram: Option<u64>,
2059 file_path: Option<PathBuf>,
2061 #[clap(long)]
2064 vtl0_lun: Option<u32>,
2065 },
2066
2067 RmNvmeNs {
2069 #[clap(long)]
2071 nsid: u32,
2072 #[clap(long)]
2074 vtl0: bool,
2075 },
2076
2077 #[clap(visible_alias = "x")]
2079 Inspect {
2080 #[clap(short, long)]
2082 recursive: bool,
2083 #[clap(short, long, requires("recursive"))]
2085 limit: Option<usize>,
2086 #[clap(short = 'v', long)]
2088 paravisor: bool,
2089 element: Option<String>,
2091 #[clap(short, long, conflicts_with("recursive"))]
2093 update: Option<String>,
2094 },
2095
2096 #[clap(visible_alias = "V")]
2098 RestartVnc,
2099
2100 #[clap(visible_alias = "v")]
2102 Hvsock {
2103 #[clap(short, long)]
2105 term: Option<PathBuf>,
2106 port: u32,
2108 },
2109
2110 #[clap(visible_alias = "q")]
2112 Quit,
2113
2114 #[clap(visible_alias = "i")]
2119 Input { data: Vec<String> },
2120
2121 #[clap(visible_alias = "I")]
2125 InputMode,
2126
2127 Reset,
2129
2130 Shutdown {
2132 #[clap(long, short = 'r')]
2134 reboot: bool,
2135 #[clap(long, short = 'h', conflicts_with = "reboot")]
2137 hibernate: bool,
2138 #[clap(long, short = 'f')]
2140 force: bool,
2141 },
2142
2143 #[clap(visible_alias = "ch")]
2146 ClearHalt,
2147
2148 ServiceVtl2 {
2150 #[clap(long, short = 'u')]
2153 user_mode_only: bool,
2154 #[clap(long, conflicts_with("user_mode_only"))]
2157 igvm: Option<PathBuf>,
2158 #[clap(long, short = 'n', default_missing_value = "true")]
2161 nvme_keepalive: bool,
2162 #[clap(long)]
2165 mana_keepalive: bool,
2166 },
2167
2168 ReadMemory {
2170 #[clap(value_parser=maybe_with_radix_u64)]
2172 gpa: u64,
2173 #[clap(value_parser=maybe_with_radix_u64)]
2175 size: u64,
2176 #[clap(long, short = 'f')]
2179 file: Option<PathBuf>,
2180 },
2181
2182 WriteMemory {
2184 #[clap(value_parser=maybe_with_radix_u64)]
2186 gpa: u64,
2187 hex: Option<String>,
2190 #[clap(long, short = 'f')]
2192 file: Option<PathBuf>,
2193 },
2194
2195 Panic,
2197
2198 Kvp(kvp::KvpCommand),
2200}
2201
2202#[derive(clap::Subcommand)]
2204enum Vtl2SettingsCommand {
2205 Show,
2207
2208 AddScsiDisk {
2212 #[clap(long)]
2215 controller: Option<String>,
2216 #[clap(long)]
2218 lun: u32,
2219 #[clap(
2221 long,
2222 conflicts_with = "backing_scsi_lun",
2223 required_unless_present = "backing_scsi_lun"
2224 )]
2225 backing_nvme_nsid: Option<u32>,
2226 #[clap(
2228 long,
2229 conflicts_with = "backing_nvme_nsid",
2230 required_unless_present = "backing_nvme_nsid"
2231 )]
2232 backing_scsi_lun: Option<u32>,
2233 },
2234
2235 RmScsiDisk {
2237 #[clap(long)]
2240 controller: Option<String>,
2241 #[clap(long)]
2243 lun: u32,
2244 },
2245}
2246
2247struct CommandParser {
2248 app: clap::Command,
2249}
2250
2251impl CommandParser {
2252 fn new() -> Self {
2253 let mut app = InteractiveCommand::command();
2255 for sc in app.get_subcommands_mut() {
2256 *sc = sc
2257 .clone()
2258 .help_template("{about-with-newline}\n{usage-heading}\n {usage}\n\n{all-args}");
2259 }
2260 Self { app }
2261 }
2262
2263 fn parse(&mut self, line: &str) -> clap::error::Result<InteractiveCommand> {
2264 let args = shell_words::split(line)
2265 .map_err(|err| self.app.error(clap::error::ErrorKind::ValueValidation, err))?;
2266 let matches = self.app.try_get_matches_from_mut(args)?;
2267 InteractiveCommand::from_arg_matches(&matches).map_err(|err| err.format(&mut self.app))
2268 }
2269}
2270
2271fn new_hvsock_service_id(port: u32) -> Guid {
2272 Guid {
2275 data1: port,
2276 .."00000000-facb-11e6-bd58-64006a7986d3".parse().unwrap()
2277 }
2278}
2279
2280async fn run_control(driver: &DefaultDriver, mesh: &VmmMesh, opt: Options) -> anyhow::Result<()> {
2281 let (mut vm_config, mut resources) = vm_config_from_command_line(driver, mesh, &opt).await?;
2282
2283 let mut vnc_worker = None;
2284 if opt.gfx || opt.vnc {
2285 let listener = TcpListener::bind(format!("127.0.0.1:{}", opt.vnc_port))
2286 .with_context(|| format!("binding to VNC port {}", opt.vnc_port))?;
2287
2288 let input_send = vm_config.input.sender();
2289 let framebuffer = resources
2290 .framebuffer_access
2291 .take()
2292 .expect("synth video enabled");
2293
2294 let vnc_host = mesh
2295 .make_host("vnc", None)
2296 .await
2297 .context("spawning vnc process failed")?;
2298
2299 vnc_worker = Some(
2300 vnc_host
2301 .launch_worker(
2302 vnc_worker_defs::VNC_WORKER_TCP,
2303 VncParameters {
2304 listener,
2305 framebuffer,
2306 input_send,
2307 },
2308 )
2309 .await?,
2310 )
2311 }
2312
2313 let gdb_worker = if let Some(port) = opt.gdb {
2315 let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
2316 .with_context(|| format!("binding to gdb port {}", port))?;
2317
2318 let (req_tx, req_rx) = mesh::channel();
2319 vm_config.debugger_rpc = Some(req_rx);
2320
2321 let gdb_host = mesh
2322 .make_host("gdb", None)
2323 .await
2324 .context("spawning gdbstub process failed")?;
2325
2326 Some(
2327 gdb_host
2328 .launch_worker(
2329 debug_worker_defs::DEBUGGER_WORKER,
2330 debug_worker_defs::DebuggerParameters {
2331 listener,
2332 req_chan: req_tx,
2333 vp_count: vm_config.processor_topology.proc_count,
2334 target_arch: if cfg!(guest_arch = "x86_64") {
2335 debug_worker_defs::TargetArch::X86_64
2336 } else {
2337 debug_worker_defs::TargetArch::Aarch64
2338 },
2339 },
2340 )
2341 .await
2342 .context("failed to launch gdbstub worker")?,
2343 )
2344 } else {
2345 None
2346 };
2347
2348 let (vm_rpc, rpc_recv) = mesh::channel();
2350 let (notify_send, notify_recv) = mesh::channel();
2351 let mut vm_worker = {
2352 let vm_host = mesh.make_host("vm", opt.log_file.clone()).await?;
2353
2354 let params = VmWorkerParameters {
2355 hypervisor: opt.hypervisor,
2356 cfg: vm_config,
2357 saved_state: None,
2358 rpc: rpc_recv,
2359 notify: notify_send,
2360 };
2361 vm_host
2362 .launch_worker(VM_WORKER, params)
2363 .await
2364 .context("failed to launch vm worker")?
2365 };
2366
2367 if !opt.paused {
2368 vm_rpc.call(VmRpc::Resume, ()).await?;
2369 }
2370
2371 let paravisor_diag = Arc::new(diag_client::DiagClient::from_dialer(
2372 driver.clone(),
2373 DiagDialer {
2374 driver: driver.clone(),
2375 vm_rpc: vm_rpc.clone(),
2376 openhcl_vtl: if opt.vtl2 {
2377 DeviceVtl::Vtl2
2378 } else {
2379 DeviceVtl::Vtl0
2380 },
2381 },
2382 ));
2383
2384 let mut diag_inspector = DiagInspector::new(driver.clone(), paravisor_diag.clone());
2385
2386 let (console_command_send, console_command_recv) = mesh::channel();
2387 let (inspect_completion_engine_send, inspect_completion_engine_recv) = mesh::channel();
2388
2389 let mut console_in = resources.console_in.take();
2390 thread::Builder::new()
2391 .name("stdio-thread".to_string())
2392 .spawn(move || {
2393 #[cfg(unix)]
2395 if io::stderr().is_terminal() {
2396 term::revert_terminal_on_panic()
2397 }
2398
2399 let mut rl = rustyline::Editor::<
2400 interactive_console::OpenvmmRustylineEditor,
2401 rustyline::history::FileHistory,
2402 >::with_config(
2403 rustyline::Config::builder()
2404 .completion_type(rustyline::CompletionType::List)
2405 .build(),
2406 )
2407 .unwrap();
2408
2409 rl.set_helper(Some(interactive_console::OpenvmmRustylineEditor {
2410 openvmm_inspect_req: Arc::new(inspect_completion_engine_send),
2411 }));
2412
2413 let history_file = {
2414 const HISTORY_FILE: &str = ".openvmm_history";
2415
2416 let history_folder = None
2419 .or_else(dirs::state_dir)
2420 .or_else(dirs::data_local_dir)
2421 .map(|path| path.join("openvmm"));
2422
2423 if let Some(history_folder) = history_folder {
2424 if let Err(err) = std::fs::create_dir_all(&history_folder) {
2425 tracing::warn!(
2426 error = &err as &dyn std::error::Error,
2427 "could not create directory: {}",
2428 history_folder.display()
2429 )
2430 }
2431
2432 Some(history_folder.join(HISTORY_FILE))
2433 } else {
2434 None
2435 }
2436 };
2437
2438 if let Some(history_file) = &history_file {
2439 tracing::info!("restoring history from {}", history_file.display());
2440 if rl.load_history(history_file).is_err() {
2441 tracing::info!("could not find existing {}", history_file.display());
2442 }
2443 }
2444
2445 rl.bind_sequence(
2447 rustyline::KeyEvent::new('\x08', rustyline::Modifiers::CTRL),
2448 rustyline::Cmd::Kill(rustyline::Movement::BackwardWord(1, rustyline::Word::Emacs)),
2449 );
2450
2451 let mut parser = CommandParser::new();
2452
2453 let mut stdin = io::stdin();
2454 loop {
2455 term::set_raw_console(true).expect("failed to set raw console mode");
2457
2458 if let Some(input) = console_in.as_mut() {
2459 let mut buf = [0; 32];
2460 loop {
2461 let n = stdin.read(&mut buf).unwrap();
2462 let mut b = &buf[..n];
2463 let stop = if let Some(ctrlq) = b.iter().position(|x| *x == 0x11) {
2464 b = &b[..ctrlq];
2465 true
2466 } else {
2467 false
2468 };
2469 block_on(input.as_mut().write_all(b)).expect("BUGBUG");
2470 if stop {
2471 break;
2472 }
2473 }
2474 }
2475
2476 term::set_raw_console(false).expect("failed to set raw console mode");
2477
2478 loop {
2479 let line = rl.readline("openvmm> ");
2480 if line.is_err() {
2481 break;
2482 }
2483 let line = line.unwrap();
2484 let trimmed = line.trim();
2485 if trimmed.is_empty() {
2486 continue;
2487 }
2488 if let Err(err) = rl.add_history_entry(&line) {
2489 tracing::warn!(
2490 err = &err as &dyn std::error::Error,
2491 "error adding to .openvmm_history"
2492 )
2493 }
2494
2495 match parser.parse(trimmed) {
2496 Ok(cmd) => match cmd {
2497 InteractiveCommand::Input { data } => {
2498 let mut data = data.join(" ");
2499 data.push('\n');
2500 if let Some(input) = console_in.as_mut() {
2501 block_on(input.write_all(data.as_bytes())).expect("BUGBUG");
2502 }
2503 }
2504 InteractiveCommand::InputMode => break,
2505 cmd => {
2506 let (processing_done_send, processing_done_recv) =
2508 mesh::oneshot::<()>();
2509 console_command_send.send((cmd, processing_done_send));
2510 let _ = block_on(processing_done_recv);
2511 }
2512 },
2513 Err(err) => {
2514 err.print().unwrap();
2515 }
2516 }
2517
2518 if let Some(history_file) = &history_file {
2519 rl.append_history(history_file).unwrap();
2520 }
2521 }
2522 }
2523 })
2524 .unwrap();
2525
2526 let mut state_change_task = None::<Task<Result<StateChange, RpcError>>>;
2527 let mut pulse_save_restore_interval: Option<Duration> = None;
2528 let mut pending_shutdown = None;
2529
2530 enum StateChange {
2531 Pause(bool),
2532 Resume(bool),
2533 Reset(Result<(), RemoteError>),
2534 PulseSaveRestore(Result<(), PulseSaveRestoreError>),
2535 ServiceVtl2(anyhow::Result<Duration>),
2536 }
2537
2538 enum Event {
2539 Command((InteractiveCommand, mesh::OneshotSender<()>)),
2540 InspectRequestFromCompletionEngine(
2541 (InspectTarget, String, mesh::OneshotSender<inspect::Node>),
2542 ),
2543 Quit,
2544 Halt(vmm_core_defs::HaltReason),
2545 PulseSaveRestore,
2546 Worker(WorkerEvent),
2547 VncWorker(WorkerEvent),
2548 StateChange(Result<StateChange, RpcError>),
2549 ShutdownResult(Result<hyperv_ic_resources::shutdown::ShutdownResult, RpcError>),
2550 }
2551
2552 let mut console_command_recv = console_command_recv
2553 .map(Event::Command)
2554 .chain(futures::stream::repeat_with(|| Event::Quit));
2555
2556 let mut notify_recv = notify_recv.map(Event::Halt);
2557
2558 let mut inspect_completion_engine_recv =
2559 inspect_completion_engine_recv.map(Event::InspectRequestFromCompletionEngine);
2560
2561 let mut quit = false;
2562 loop {
2563 let event = {
2564 let pulse_save_restore = pin!(async {
2565 match pulse_save_restore_interval {
2566 Some(wait) => {
2567 PolledTimer::new(driver).sleep(wait).await;
2568 Event::PulseSaveRestore
2569 }
2570 None => pending().await,
2571 }
2572 });
2573
2574 let vm = (&mut vm_worker).map(Event::Worker);
2575 let vnc = futures::stream::iter(vnc_worker.as_mut())
2576 .flatten()
2577 .map(Event::VncWorker);
2578 let change = futures::stream::iter(state_change_task.as_mut().map(|x| x.into_stream()))
2579 .flatten()
2580 .map(Event::StateChange);
2581 let shutdown = pin!(async {
2582 if let Some(s) = &mut pending_shutdown {
2583 Event::ShutdownResult(s.await)
2584 } else {
2585 pending().await
2586 }
2587 });
2588
2589 (
2590 &mut console_command_recv,
2591 &mut inspect_completion_engine_recv,
2592 &mut notify_recv,
2593 pulse_save_restore.into_stream(),
2594 vm,
2595 vnc,
2596 change,
2597 shutdown.into_stream(),
2598 )
2599 .merge()
2600 .next()
2601 .await
2602 .unwrap()
2603 };
2604
2605 let (cmd, _processing_done_send) = match event {
2606 Event::Command(message) => message,
2607 Event::InspectRequestFromCompletionEngine((vtl, path, res)) => {
2608 let mut inspection =
2609 InspectionBuilder::new(&path)
2610 .depth(Some(1))
2611 .inspect(inspect_obj(
2612 vtl,
2613 mesh,
2614 &vm_worker,
2615 vnc_worker.as_ref(),
2616 gdb_worker.as_ref(),
2617 &mut diag_inspector,
2618 ));
2619 let _ = CancelContext::new()
2620 .with_timeout(Duration::from_secs(1))
2621 .until_cancelled(inspection.resolve())
2622 .await;
2623
2624 let node = inspection.results();
2625 res.send(node);
2626 continue;
2627 }
2628 Event::Quit => break,
2629 Event::Halt(reason) => {
2630 tracing::info!(?reason, "guest halted");
2631 continue;
2632 }
2633 Event::PulseSaveRestore => {
2634 vm_rpc.call(VmRpc::PulseSaveRestore, ()).await??;
2635 continue;
2636 }
2637 Event::Worker(event) => {
2638 match event {
2639 WorkerEvent::Stopped => {
2640 if quit {
2641 tracing::info!("vm stopped");
2642 } else {
2643 tracing::error!("vm worker unexpectedly stopped");
2644 }
2645 break;
2646 }
2647 WorkerEvent::Failed(err) => {
2648 tracing::error!(error = &err as &dyn std::error::Error, "vm worker failed");
2649 break;
2650 }
2651 WorkerEvent::RestartFailed(err) => {
2652 tracing::error!(
2653 error = &err as &dyn std::error::Error,
2654 "vm worker restart failed"
2655 );
2656 }
2657 WorkerEvent::Started => {
2658 tracing::info!("vm worker restarted");
2659 }
2660 }
2661 continue;
2662 }
2663 Event::VncWorker(event) => {
2664 match event {
2665 WorkerEvent::Stopped => tracing::error!("vnc unexpectedly stopped"),
2666 WorkerEvent::Failed(err) => {
2667 tracing::error!(
2668 error = &err as &dyn std::error::Error,
2669 "vnc worker failed"
2670 );
2671 }
2672 WorkerEvent::RestartFailed(err) => {
2673 tracing::error!(
2674 error = &err as &dyn std::error::Error,
2675 "vnc worker restart failed"
2676 );
2677 }
2678 WorkerEvent::Started => {
2679 tracing::info!("vnc worker restarted");
2680 }
2681 }
2682 continue;
2683 }
2684 Event::StateChange(r) => {
2685 match r {
2686 Ok(sc) => match sc {
2687 StateChange::Pause(success) => {
2688 if success {
2689 tracing::info!("pause complete");
2690 } else {
2691 tracing::warn!("already paused");
2692 }
2693 }
2694 StateChange::Resume(success) => {
2695 if success {
2696 tracing::info!("resumed complete");
2697 } else {
2698 tracing::warn!("already running");
2699 }
2700 }
2701 StateChange::Reset(r) => match r {
2702 Ok(()) => tracing::info!("reset complete"),
2703 Err(err) => tracing::error!(
2704 error = &err as &dyn std::error::Error,
2705 "reset failed"
2706 ),
2707 },
2708 StateChange::PulseSaveRestore(r) => match r {
2709 Ok(()) => tracing::info!("pulse save/restore complete"),
2710 Err(err) => tracing::error!(
2711 error = &err as &dyn std::error::Error,
2712 "pulse save/restore failed"
2713 ),
2714 },
2715 StateChange::ServiceVtl2(r) => match r {
2716 Ok(dur) => {
2717 tracing::info!(
2718 duration = dur.as_millis() as i64,
2719 "vtl2 servicing complete"
2720 )
2721 }
2722 Err(err) => tracing::error!(
2723 error = err.as_ref() as &dyn std::error::Error,
2724 "vtl2 servicing failed"
2725 ),
2726 },
2727 },
2728 Err(err) => {
2729 tracing::error!(
2730 error = &err as &dyn std::error::Error,
2731 "communication failure during state change"
2732 );
2733 }
2734 }
2735 state_change_task = None;
2736 continue;
2737 }
2738 Event::ShutdownResult(r) => {
2739 match r {
2740 Ok(r) => match r {
2741 hyperv_ic_resources::shutdown::ShutdownResult::Ok => {
2742 tracing::info!("shutdown initiated");
2743 }
2744 hyperv_ic_resources::shutdown::ShutdownResult::NotReady => {
2745 tracing::error!("shutdown ic not ready");
2746 }
2747 hyperv_ic_resources::shutdown::ShutdownResult::AlreadyInProgress => {
2748 tracing::error!("shutdown already in progress");
2749 }
2750 hyperv_ic_resources::shutdown::ShutdownResult::Failed(hr) => {
2751 tracing::error!("shutdown failed with error code {hr:#x}");
2752 }
2753 },
2754 Err(err) => {
2755 tracing::error!(
2756 error = &err as &dyn std::error::Error,
2757 "communication failure during shutdown"
2758 );
2759 }
2760 }
2761 pending_shutdown = None;
2762 continue;
2763 }
2764 };
2765
2766 fn inspect_obj<'a>(
2767 target: InspectTarget,
2768 mesh: &'a VmmMesh,
2769 vm_worker: &'a WorkerHandle,
2770 vnc_worker: Option<&'a WorkerHandle>,
2771 gdb_worker: Option<&'a WorkerHandle>,
2772 diag_inspector: &'a mut DiagInspector,
2773 ) -> impl 'a + InspectMut {
2774 inspect::adhoc_mut(move |req| match target {
2775 InspectTarget::Host => {
2776 let mut resp = req.respond();
2777 resp.field("mesh", mesh)
2778 .field("vm", vm_worker)
2779 .field("vnc", vnc_worker)
2780 .field("gdb", gdb_worker);
2781 }
2782 InspectTarget::Paravisor => {
2783 diag_inspector.inspect_mut(req);
2784 }
2785 })
2786 }
2787
2788 fn state_change<U: 'static + Send>(
2789 driver: impl Spawn,
2790 vm_rpc: &mesh::Sender<VmRpc>,
2791 state_change_task: &mut Option<Task<Result<StateChange, RpcError>>>,
2792 f: impl FnOnce(Rpc<(), U>) -> VmRpc,
2793 g: impl FnOnce(U) -> StateChange + 'static + Send,
2794 ) {
2795 if state_change_task.is_some() {
2796 tracing::error!("state change already in progress");
2797 } else {
2798 let rpc = vm_rpc.call(f, ());
2799 *state_change_task =
2800 Some(driver.spawn("state-change", async move { Ok(g(rpc.await?)) }));
2801 }
2802 }
2803
2804 match cmd {
2805 InteractiveCommand::Panic => {
2806 panic!("injected panic")
2807 }
2808 InteractiveCommand::Restart => {
2809 let vm_host = mesh.make_host("vm", opt.log_file.clone()).await?;
2811
2812 vm_worker.restart(&vm_host);
2813 }
2814 InteractiveCommand::Pause => {
2815 state_change(
2816 driver,
2817 &vm_rpc,
2818 &mut state_change_task,
2819 VmRpc::Pause,
2820 StateChange::Pause,
2821 );
2822 }
2823 InteractiveCommand::Resume => {
2824 state_change(
2825 driver,
2826 &vm_rpc,
2827 &mut state_change_task,
2828 VmRpc::Resume,
2829 StateChange::Resume,
2830 );
2831 }
2832 InteractiveCommand::Reset => {
2833 state_change(
2834 driver,
2835 &vm_rpc,
2836 &mut state_change_task,
2837 VmRpc::Reset,
2838 StateChange::Reset,
2839 );
2840 }
2841 InteractiveCommand::PulseSaveRestore => {
2842 state_change(
2843 driver,
2844 &vm_rpc,
2845 &mut state_change_task,
2846 VmRpc::PulseSaveRestore,
2847 StateChange::PulseSaveRestore,
2848 );
2849 }
2850 InteractiveCommand::SchedulePulseSaveRestore { interval } => {
2851 pulse_save_restore_interval = match interval {
2852 Some(seconds) if seconds != 0 => Some(Duration::from_secs(seconds)),
2853 _ => {
2854 None
2856 }
2857 }
2858 }
2859 InteractiveCommand::Shutdown {
2860 reboot,
2861 hibernate,
2862 force,
2863 } => {
2864 if pending_shutdown.is_some() {
2865 println!("shutdown already in progress");
2866 } else if let Some(ic) = &resources.shutdown_ic {
2867 let params = hyperv_ic_resources::shutdown::ShutdownParams {
2868 shutdown_type: if hibernate {
2869 hyperv_ic_resources::shutdown::ShutdownType::Hibernate
2870 } else if reboot {
2871 hyperv_ic_resources::shutdown::ShutdownType::Reboot
2872 } else {
2873 hyperv_ic_resources::shutdown::ShutdownType::PowerOff
2874 },
2875 force,
2876 };
2877 pending_shutdown =
2878 Some(ic.call(hyperv_ic_resources::shutdown::ShutdownRpc::Shutdown, params));
2879 } else {
2880 println!("no shutdown ic configured");
2881 }
2882 }
2883 InteractiveCommand::Nmi => {
2884 let _ = vm_rpc.call(VmRpc::Nmi, 0).await;
2885 }
2886 InteractiveCommand::ClearHalt => {
2887 vm_rpc.call(VmRpc::ClearHalt, ()).await.ok();
2888 }
2889 InteractiveCommand::AddDisk {
2890 read_only,
2891 target,
2892 path,
2893 lun,
2894 ram,
2895 file_path,
2896 is_dvd,
2897 } => {
2898 let action = async {
2899 let scsi = resources.scsi_rpc.as_ref().context("no scsi controller")?;
2900 let disk_type = match ram {
2901 None => {
2902 let path = file_path.context("no filename passed")?;
2903 open_disk_type(path.as_ref(), read_only)
2904 .with_context(|| format!("failed to open {}", path.display()))?
2905 }
2906 Some(size) => {
2907 Resource::new(disk_backend_resources::LayeredDiskHandle::single_layer(
2908 RamDiskLayerHandle { len: Some(size) },
2909 ))
2910 }
2911 };
2912
2913 let device = if is_dvd {
2914 SimpleScsiDvdHandle {
2915 media: Some(disk_type),
2916 requests: None,
2917 }
2918 .into_resource()
2919 } else {
2920 SimpleScsiDiskHandle {
2921 disk: disk_type,
2922 read_only,
2923 parameters: Default::default(),
2924 }
2925 .into_resource()
2926 };
2927
2928 let cfg = ScsiDeviceAndPath {
2929 path: ScsiPath { path, target, lun },
2930 device,
2931 };
2932
2933 scsi.call_failable(ScsiControllerRequest::AddDevice, cfg)
2934 .await?;
2935
2936 anyhow::Result::<_>::Ok(())
2937 };
2938
2939 if let Err(error) = action.await {
2940 tracing::error!(error = error.as_error(), "error adding disk")
2941 }
2942 }
2943 InteractiveCommand::RmDisk { target, path, lun } => {
2944 let action = async {
2945 let scsi = resources.scsi_rpc.as_ref().context("no scsi controller")?;
2946 scsi.call_failable(
2947 ScsiControllerRequest::RemoveDevice,
2948 ScsiPath { target, path, lun },
2949 )
2950 .await?;
2951 anyhow::Ok(())
2952 };
2953
2954 if let Err(error) = action.await {
2955 tracing::error!(error = error.as_error(), "error removing disk")
2956 }
2957 }
2958 InteractiveCommand::Vtl2Settings(cmd) => {
2959 if resources.vtl2_settings.is_none() {
2960 eprintln!("error: no VTL2 settings (not running with VTL2?)");
2961 continue;
2962 }
2963 let action = async {
2964 match cmd {
2965 Vtl2SettingsCommand::Show => {
2966 let settings = resources.vtl2_settings.as_ref().unwrap();
2967 println!("{:#?}", settings);
2968 }
2969 Vtl2SettingsCommand::AddScsiDisk {
2970 controller,
2971 lun,
2972 backing_nvme_nsid,
2973 backing_scsi_lun,
2974 } => {
2975 let (device_type, device_path, sub_device_path) = match (
2977 backing_nvme_nsid,
2978 backing_scsi_lun,
2979 ) {
2980 (Some(nsid), None) => (
2981 vtl2_settings_proto::physical_device::DeviceType::Nvme,
2982 storage_builder::NVME_VTL2_INSTANCE_ID,
2983 nsid,
2984 ),
2985 (None, Some(scsi_lun)) => (
2986 vtl2_settings_proto::physical_device::DeviceType::Vscsi,
2987 storage_builder::SCSI_VTL2_INSTANCE_ID,
2988 scsi_lun,
2989 ),
2990 (Some(_), Some(_)) => {
2991 anyhow::bail!(
2992 "can't specify both --backing-nvme-nsid and --backing-scsi-lun"
2993 );
2994 }
2995 (None, None) => {
2996 anyhow::bail!(
2997 "must specify either --backing-nvme-nsid or --backing-scsi-lun"
2998 );
2999 }
3000 };
3001
3002 let controller_guid = controller
3004 .map(|s| s.parse())
3005 .transpose()
3006 .context("invalid controller GUID")?
3007 .unwrap_or(storage_builder::UNDERHILL_VTL0_SCSI_INSTANCE);
3008
3009 resources
3010 .add_vtl0_scsi_disk(
3011 controller_guid,
3012 lun,
3013 device_type,
3014 device_path,
3015 sub_device_path,
3016 )
3017 .await?;
3018
3019 let backing_desc = if backing_nvme_nsid.is_some() {
3020 format!("nvme_nsid={}", sub_device_path)
3021 } else {
3022 format!("scsi_lun={}", sub_device_path)
3023 };
3024 println!(
3025 "Added VTL0 SCSI disk: controller={}, lun={}, backing={}",
3026 controller_guid, lun, backing_desc
3027 );
3028 }
3029 Vtl2SettingsCommand::RmScsiDisk { controller, lun } => {
3030 let controller_guid = controller
3032 .map(|s| s.parse())
3033 .transpose()
3034 .context("invalid controller GUID")?
3035 .unwrap_or(storage_builder::UNDERHILL_VTL0_SCSI_INSTANCE);
3036
3037 resources
3038 .remove_vtl0_scsi_disk(controller_guid, lun)
3039 .await?;
3040
3041 println!(
3042 "Removed VTL0 SCSI disk: controller={}, lun={}",
3043 controller_guid, lun
3044 );
3045 }
3046 }
3047 anyhow::Ok(())
3048 };
3049
3050 if let Err(error) = action.await {
3051 eprintln!("error: {}", error);
3052 }
3053 }
3054 InteractiveCommand::AddNvmeNs {
3055 read_only,
3056 nsid,
3057 ram,
3058 file_path,
3059 vtl0_lun,
3060 } => {
3061 if resources.vtl2_settings.is_none() {
3062 eprintln!("error: add-nvme-ns requires --vtl2 mode");
3063 continue;
3064 }
3065 let action = async {
3066 let nvme = resources
3067 .nvme_vtl2_rpc
3068 .as_ref()
3069 .context("no vtl2 nvme controller")?;
3070 let disk_type = match (ram, file_path) {
3071 (None, Some(path)) => open_disk_type(path.as_ref(), read_only)
3072 .with_context(|| format!("failed to open {}", path.display()))?,
3073 (Some(size), None) => {
3074 Resource::new(disk_backend_resources::LayeredDiskHandle::single_layer(
3075 RamDiskLayerHandle { len: Some(size) },
3076 ))
3077 }
3078 (None, None) => {
3079 anyhow::bail!("must specify either file path or --ram");
3080 }
3081 (Some(_), Some(_)) => {
3082 anyhow::bail!("cannot specify both file path and --ram");
3083 }
3084 };
3085
3086 let ns = NamespaceDefinition {
3087 nsid,
3088 read_only,
3089 disk: disk_type,
3090 };
3091
3092 nvme.call_failable(NvmeControllerRequest::AddNamespace, ns)
3093 .await?;
3094 println!("Added namespace {}", nsid);
3095
3096 if let Some(lun) = vtl0_lun {
3098 resources
3099 .add_vtl0_scsi_disk(
3100 storage_builder::UNDERHILL_VTL0_SCSI_INSTANCE,
3101 lun,
3102 vtl2_settings_proto::physical_device::DeviceType::Nvme,
3103 storage_builder::NVME_VTL2_INSTANCE_ID,
3104 nsid,
3105 )
3106 .await?;
3107 println!("Exposed namespace {} to VTL0 as SCSI lun={}", nsid, lun);
3108 }
3109
3110 Ok(())
3111 };
3112
3113 if let Err(error) = action.await {
3114 eprintln!("error adding nvme namespace: {}", error);
3115 }
3116 }
3117 InteractiveCommand::RmNvmeNs { nsid, vtl0 } => {
3118 if resources.vtl2_settings.is_none() {
3119 eprintln!("error: rm-nvme-ns requires --vtl2 mode");
3120 continue;
3121 }
3122 let action = async {
3123 if vtl0 {
3125 let removed_lun = resources
3126 .remove_vtl0_scsi_disk_by_nvme_nsid(
3127 storage_builder::UNDERHILL_VTL0_SCSI_INSTANCE,
3128 storage_builder::NVME_VTL2_INSTANCE_ID,
3129 nsid,
3130 )
3131 .await?;
3132 if let Some(lun) = removed_lun {
3133 println!("Removed VTL0 SCSI lun={}", lun);
3134 } else {
3135 println!("No VTL0 SCSI disk found backed by NVMe nsid={}", nsid);
3136 }
3137 }
3138
3139 let nvme = resources
3140 .nvme_vtl2_rpc
3141 .as_ref()
3142 .context("no vtl2 nvme controller")?;
3143 nvme.call_failable(NvmeControllerRequest::RemoveNamespace, nsid)
3144 .await?;
3145 println!("Removed NVMe namespace {}", nsid);
3146 anyhow::Ok(())
3147 };
3148
3149 if let Err(error) = action.await {
3150 eprintln!("error removing nvme namespace: {}", error);
3151 }
3152 }
3153 InteractiveCommand::Inspect {
3154 recursive,
3155 limit,
3156 paravisor,
3157 element,
3158 update,
3159 } => {
3160 let obj = inspect_obj(
3161 if paravisor {
3162 InspectTarget::Paravisor
3163 } else {
3164 InspectTarget::Host
3165 },
3166 mesh,
3167 &vm_worker,
3168 vnc_worker.as_ref(),
3169 gdb_worker.as_ref(),
3170 &mut diag_inspector,
3171 );
3172
3173 if let Some(value) = update {
3174 let Some(element) = element else {
3175 anyhow::bail!("must provide element for update")
3176 };
3177
3178 let value = async {
3179 let update = inspect::update(&element, &value, obj);
3180 let value = CancelContext::new()
3181 .with_timeout(Duration::from_secs(1))
3182 .until_cancelled(update)
3183 .await??;
3184 anyhow::Ok(value)
3185 }
3186 .await;
3187 match value {
3188 Ok(node) => match &node.kind {
3189 inspect::ValueKind::String(s) => println!("{s}"),
3190 _ => println!("{:#}", node),
3191 },
3192 Err(err) => println!("error: {:#}", err),
3193 }
3194 } else {
3195 let element = element.unwrap_or_default();
3196 let depth = if recursive { limit } else { Some(0) };
3197 let node = async {
3198 let mut inspection =
3199 InspectionBuilder::new(&element).depth(depth).inspect(obj);
3200 let _ = CancelContext::new()
3201 .with_timeout(Duration::from_secs(1))
3202 .until_cancelled(inspection.resolve())
3203 .await;
3204 inspection.results()
3205 }
3206 .await;
3207
3208 println!("{:#}", node);
3209 }
3210 }
3211 InteractiveCommand::RestartVnc => {
3212 if let Some(vnc) = &mut vnc_worker {
3213 let action = async {
3214 let vnc_host = mesh
3215 .make_host("vnc", None)
3216 .await
3217 .context("spawning vnc process failed")?;
3218
3219 vnc.restart(&vnc_host);
3220 anyhow::Result::<_>::Ok(())
3221 };
3222
3223 if let Err(error) = action.await {
3224 eprintln!("error: {}", error);
3225 }
3226 } else {
3227 eprintln!("ERROR: no VNC server running");
3228 }
3229 }
3230 InteractiveCommand::Hvsock { term, port } => {
3231 let vm_rpc = &vm_rpc;
3232 let action = async || {
3233 let service_id = new_hvsock_service_id(port);
3234 let socket = vm_rpc
3235 .call_failable(
3236 VmRpc::ConnectHvsock,
3237 (
3238 CancelContext::new().with_timeout(Duration::from_secs(2)),
3239 service_id,
3240 DeviceVtl::Vtl0,
3241 ),
3242 )
3243 .await?;
3244 let socket = PolledSocket::new(driver, socket)?;
3245 let mut console = console_relay::Console::new(
3246 driver.clone(),
3247 term.or_else(openvmm_terminal_app).as_deref(),
3248 Some(ConsoleLaunchOptions {
3249 window_title: Some(format!("HVSock{} [OpenVMM]", port)),
3250 }),
3251 )?;
3252 driver
3253 .spawn("console-relay", async move { console.relay(socket).await })
3254 .detach();
3255 anyhow::Result::<_>::Ok(())
3256 };
3257
3258 if let Err(error) = (action)().await {
3259 eprintln!("error: {}", error);
3260 }
3261 }
3262 InteractiveCommand::ServiceVtl2 {
3263 user_mode_only,
3264 igvm,
3265 mana_keepalive,
3266 nvme_keepalive,
3267 } => {
3268 let paravisor_diag = paravisor_diag.clone();
3269 let vm_rpc = vm_rpc.clone();
3270 let igvm = igvm.or_else(|| opt.igvm.clone());
3271 let ged_rpc = resources.ged_rpc.clone();
3272 let r = async move {
3273 let start;
3274 if user_mode_only {
3275 start = Instant::now();
3276 paravisor_diag.restart().await?;
3277 } else {
3278 let path = igvm.context("no igvm file loaded")?;
3279 let file = fs_err::File::open(path)?;
3280 start = Instant::now();
3281 openvmm_helpers::underhill::save_underhill(
3282 &vm_rpc,
3283 ged_rpc.as_ref().context("no GED")?,
3284 GuestServicingFlags {
3285 nvme_keepalive,
3286 mana_keepalive,
3287 },
3288 file.into(),
3289 )
3290 .await?;
3291 openvmm_helpers::underhill::restore_underhill(
3292 &vm_rpc,
3293 ged_rpc.as_ref().context("no GED")?,
3294 )
3295 .await?;
3296 }
3297 let end = Instant::now();
3298 Ok(end - start)
3299 }
3300 .map(|r| Ok(StateChange::ServiceVtl2(r)));
3301 if state_change_task.is_some() {
3302 tracing::error!("state change already in progress");
3303 } else {
3304 state_change_task = Some(driver.spawn("state-change", r));
3305 }
3306 }
3307 InteractiveCommand::Quit => {
3308 tracing::info!("quitting");
3309 resources.scsi_rpc = None;
3312 resources.nvme_vtl2_rpc = None;
3313
3314 vm_worker.stop();
3315 quit = true;
3316 }
3317 InteractiveCommand::ReadMemory { gpa, size, file } => {
3318 let size = size as usize;
3319 let data = vm_rpc.call(VmRpc::ReadMemory, (gpa, size)).await?;
3320
3321 match data {
3322 Ok(bytes) => {
3323 if let Some(file) = file {
3324 if let Err(err) = fs_err::write(file, bytes) {
3325 eprintln!("error: {err:?}");
3326 }
3327 } else {
3328 let width = 16;
3329 let show_ascii = true;
3330
3331 let mut dump = String::new();
3332 for (i, chunk) in bytes.chunks(width).enumerate() {
3333 let hex_part: Vec<String> =
3334 chunk.iter().map(|byte| format!("{:02x}", byte)).collect();
3335 let hex_line = hex_part.join(" ");
3336
3337 if show_ascii {
3338 let ascii_part: String = chunk
3339 .iter()
3340 .map(|&byte| {
3341 if byte.is_ascii_graphic() || byte == b' ' {
3342 byte as char
3343 } else {
3344 '.'
3345 }
3346 })
3347 .collect();
3348 dump.push_str(&format!(
3349 "{:04x}: {:<width$} {}\n",
3350 i * width,
3351 hex_line,
3352 ascii_part,
3353 width = width * 3 - 1
3354 ));
3355 } else {
3356 dump.push_str(&format!("{:04x}: {}\n", i * width, hex_line));
3357 }
3358 }
3359
3360 println!("{dump}");
3361 }
3362 }
3363 Err(err) => {
3364 eprintln!("error: {err:?}");
3365 }
3366 }
3367 }
3368 InteractiveCommand::WriteMemory { gpa, hex, file } => {
3369 if hex.is_some() == file.is_some() {
3370 eprintln!("error: either path to the file or the hex string must be specified");
3371 continue;
3372 }
3373
3374 let data = if let Some(file) = file {
3375 let data = fs_err::read(file);
3376 match data {
3377 Ok(data) => data,
3378 Err(err) => {
3379 eprintln!("error: {err:?}");
3380 continue;
3381 }
3382 }
3383 } else if let Some(hex) = hex {
3384 if hex.len() & 1 != 0 {
3385 eprintln!(
3386 "error: expected even number of hex digits (2 hex digits per byte)"
3387 );
3388 continue;
3389 }
3390 let data: Result<Vec<u8>, String> = (0..hex.len())
3391 .step_by(2)
3392 .map(|i| {
3393 u8::from_str_radix(&hex[i..i + 2], 16).map_err(|e| {
3394 format!("invalid hex character at position {}: {}", i, e)
3395 })
3396 })
3397 .collect();
3398
3399 match data {
3400 Ok(data) => data,
3401 Err(err) => {
3402 eprintln!("error: {err}");
3403 continue;
3404 }
3405 }
3406 } else {
3407 unreachable!();
3408 };
3409
3410 if data.is_empty() {
3411 eprintln!("error: no data to write");
3412 continue;
3413 }
3414
3415 if let Err(err) = vm_rpc.call(VmRpc::WriteMemory, (gpa, data)).await? {
3416 eprintln!("error: {err:?}");
3417 }
3418 }
3419 InteractiveCommand::Kvp(command) => {
3420 let Some(kvp) = &resources.kvp_ic else {
3421 eprintln!("error: no kvp ic configured");
3422 continue;
3423 };
3424 if let Err(err) = kvp::handle_kvp(kvp, command).await {
3425 eprintln!("error: {err:#}");
3426 }
3427 }
3428 InteractiveCommand::Input { .. } | InteractiveCommand::InputMode => unreachable!(),
3429 }
3430 }
3431
3432 vm_worker.stop();
3433 vm_worker.join().await?;
3434 Ok(())
3435}
3436
3437struct DiagDialer {
3438 driver: DefaultDriver,
3439 vm_rpc: mesh::Sender<VmRpc>,
3440 openhcl_vtl: DeviceVtl,
3441}
3442
3443impl mesh_rpc::client::Dial for DiagDialer {
3444 type Stream = PolledSocket<unix_socket::UnixStream>;
3445
3446 async fn dial(&mut self) -> io::Result<Self::Stream> {
3447 let service_id = new_hvsock_service_id(1);
3448 let socket = self
3449 .vm_rpc
3450 .call_failable(
3451 VmRpc::ConnectHvsock,
3452 (
3453 CancelContext::new().with_timeout(Duration::from_secs(2)),
3454 service_id,
3455 self.openhcl_vtl,
3456 ),
3457 )
3458 .await
3459 .map_err(io::Error::other)?;
3460
3461 PolledSocket::new(&self.driver, socket)
3462 }
3463}
3464
3465pub struct DiagInspector(DiagInspectorInner);
3472
3473enum DiagInspectorInner {
3474 NotStarted(DefaultDriver, Arc<diag_client::DiagClient>),
3475 Started {
3476 send: mesh::Sender<inspect::Deferred>,
3477 _task: Task<()>,
3478 },
3479 Invalid,
3480}
3481
3482impl DiagInspector {
3483 pub fn new(driver: DefaultDriver, diag_client: Arc<diag_client::DiagClient>) -> Self {
3484 Self(DiagInspectorInner::NotStarted(driver, diag_client))
3485 }
3486
3487 fn start(&mut self) -> &mesh::Sender<inspect::Deferred> {
3488 loop {
3489 match self.0 {
3490 DiagInspectorInner::NotStarted { .. } => {
3491 let DiagInspectorInner::NotStarted(driver, client) =
3492 std::mem::replace(&mut self.0, DiagInspectorInner::Invalid)
3493 else {
3494 unreachable!()
3495 };
3496 let (send, recv) = mesh::channel();
3497 let task = driver.clone().spawn("diag-inspect", async move {
3498 Self::run(&client, recv).await
3499 });
3500
3501 self.0 = DiagInspectorInner::Started { send, _task: task };
3502 }
3503 DiagInspectorInner::Started { ref send, .. } => break send,
3504 DiagInspectorInner::Invalid => unreachable!(),
3505 }
3506 }
3507 }
3508
3509 async fn run(
3510 diag_client: &diag_client::DiagClient,
3511 mut recv: mesh::Receiver<inspect::Deferred>,
3512 ) {
3513 while let Some(deferred) = recv.next().await {
3514 let info = deferred.external_request();
3515 let result = match info.request_type {
3516 inspect::ExternalRequestType::Inspect { depth } => {
3517 if depth == 0 {
3518 Ok(inspect::Node::Unevaluated)
3519 } else {
3520 diag_client
3522 .inspect(info.path, Some(depth - 1), Some(Duration::from_secs(1)))
3523 .await
3524 }
3525 }
3526 inspect::ExternalRequestType::Update { value } => {
3527 (diag_client.update(info.path, value).await).map(inspect::Node::Value)
3528 }
3529 };
3530 deferred.complete_external(
3531 result.unwrap_or_else(|err| {
3532 inspect::Node::Failed(inspect::Error::Mesh(format!("{err:#}")))
3533 }),
3534 inspect::SensitivityLevel::Unspecified,
3535 )
3536 }
3537 }
3538}
3539
3540impl InspectMut for DiagInspector {
3541 fn inspect_mut(&mut self, req: inspect::Request<'_>) {
3542 self.start().send(req.defer());
3543 }
3544}
3545
3546enum InspectTarget {
3547 Host,
3548 Paravisor,
3549}
3550
3551mod interactive_console {
3552 use super::InteractiveCommand;
3553 use rustyline::Helper;
3554 use rustyline::Highlighter;
3555 use rustyline::Hinter;
3556 use rustyline::Validator;
3557
3558 #[derive(Helper, Highlighter, Hinter, Validator)]
3559 pub(crate) struct OpenvmmRustylineEditor {
3560 pub openvmm_inspect_req: std::sync::Arc<
3561 mesh::Sender<(
3562 super::InspectTarget,
3563 String,
3564 mesh::OneshotSender<inspect::Node>,
3565 )>,
3566 >,
3567 }
3568
3569 impl rustyline::completion::Completer for OpenvmmRustylineEditor {
3570 type Candidate = String;
3571
3572 fn complete(
3573 &self,
3574 line: &str,
3575 pos: usize,
3576 _ctx: &rustyline::Context<'_>,
3577 ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
3578 let Ok(cmd) = shell_words::split(line) else {
3579 return Ok((0, Vec::with_capacity(0)));
3580 };
3581
3582 let completions = futures::executor::block_on(
3583 clap_dyn_complete::Complete {
3584 cmd,
3585 raw: Some(line.into()),
3586 position: Some(pos),
3587 }
3588 .generate_completions::<InteractiveCommand>(None, self),
3589 );
3590
3591 let pos_from_end = {
3592 let line = line.chars().take(pos).collect::<String>();
3593
3594 let trailing_ws = line.len() - line.trim_end().len();
3595
3596 if trailing_ws > 0 {
3597 line.len() - trailing_ws + 1 } else {
3599 let last_word = shell_words::split(&line)
3600 .unwrap_or_default()
3601 .last()
3602 .cloned()
3603 .unwrap_or_default();
3604
3605 line.len() - last_word.len()
3606 }
3607 };
3608
3609 Ok((pos_from_end, completions))
3610 }
3611 }
3612
3613 impl clap_dyn_complete::CustomCompleterFactory for &OpenvmmRustylineEditor {
3614 type CustomCompleter = OpenvmmComplete;
3615 async fn build(&self, _ctx: &clap_dyn_complete::RootCtx<'_>) -> Self::CustomCompleter {
3616 OpenvmmComplete {
3617 openvmm_inspect_req: self.openvmm_inspect_req.clone(),
3618 }
3619 }
3620 }
3621
3622 pub struct OpenvmmComplete {
3623 openvmm_inspect_req: std::sync::Arc<
3624 mesh::Sender<(
3625 super::InspectTarget,
3626 String,
3627 mesh::OneshotSender<inspect::Node>,
3628 )>,
3629 >,
3630 }
3631
3632 impl clap_dyn_complete::CustomCompleter for OpenvmmComplete {
3633 async fn complete(
3634 &self,
3635 ctx: &clap_dyn_complete::RootCtx<'_>,
3636 subcommand_path: &[&str],
3637 arg_id: &str,
3638 ) -> Vec<String> {
3639 match (subcommand_path, arg_id) {
3640 (["openvmm", "inspect"], "element") => {
3641 let on_error = vec!["failed/to/connect".into()];
3642
3643 let (parent_path, to_complete) = (ctx.to_complete)
3644 .rsplit_once('/')
3645 .unwrap_or(("", ctx.to_complete));
3646
3647 let node = {
3648 let paravisor = {
3649 let raw_arg = ctx
3650 .matches
3651 .subcommand()
3652 .unwrap()
3653 .1
3654 .get_one::<String>("paravisor")
3655 .map(|x| x.as_str())
3656 .unwrap_or_default();
3657 raw_arg == "true"
3658 };
3659
3660 let (tx, rx) = mesh::oneshot();
3661 self.openvmm_inspect_req.send((
3662 if paravisor {
3663 super::InspectTarget::Paravisor
3664 } else {
3665 super::InspectTarget::Host
3666 },
3667 parent_path.to_owned(),
3668 tx,
3669 ));
3670 let Ok(node) = rx.await else {
3671 return on_error;
3672 };
3673
3674 node
3675 };
3676
3677 let mut completions = Vec::new();
3678
3679 if let inspect::Node::Dir(dir) = node {
3680 for entry in dir {
3681 if entry.name.starts_with(to_complete) {
3682 if parent_path.is_empty() {
3683 completions.push(format!("{}/", entry.name))
3684 } else {
3685 completions.push(format!(
3686 "{}/{}{}",
3687 parent_path,
3688 entry.name,
3689 if matches!(entry.node, inspect::Node::Dir(..)) {
3690 "/"
3691 } else {
3692 ""
3693 }
3694 ))
3695 }
3696 }
3697 }
3698 } else {
3699 return on_error;
3700 }
3701
3702 completions
3703 }
3704 _ => Vec::new(),
3705 }
3706 }
3707 }
3708}