vmm/
resources.rs

1// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::convert::From;
5use std::path::PathBuf;
6use std::sync::{Arc, Mutex, MutexGuard};
7
8use serde::{Deserialize, Serialize};
9use vm_memory::GuestAddress;
10
11use crate::cpu_config::templates::CustomCpuTemplate;
12use crate::logger::info;
13use crate::mmds;
14use crate::mmds::data_store::{Mmds, MmdsVersion};
15use crate::mmds::ns::MmdsNetworkStack;
16use crate::utils::mib_to_bytes;
17use crate::utils::net::ipv4addr::is_link_local_valid;
18use crate::vmm_config::balloon::*;
19use crate::vmm_config::boot_source::{
20    BootConfig, BootSource, BootSourceConfig, BootSourceConfigError,
21};
22use crate::vmm_config::drive::*;
23use crate::vmm_config::entropy::*;
24use crate::vmm_config::instance_info::InstanceInfo;
25use crate::vmm_config::machine_config::{MachineConfig, MachineConfigError, MachineConfigUpdate};
26use crate::vmm_config::memory_hotplug::{MemoryHotplugConfig, MemoryHotplugConfigError};
27use crate::vmm_config::metrics::{MetricsConfig, MetricsConfigError, init_metrics};
28use crate::vmm_config::mmds::{MmdsConfig, MmdsConfigError};
29use crate::vmm_config::net::*;
30use crate::vmm_config::pmem::{PmemBuilder, PmemConfig, PmemConfigError};
31use crate::vmm_config::serial::SerialConfig;
32use crate::vmm_config::vsock::*;
33use crate::vstate::memory;
34use crate::vstate::memory::{GuestRegionMmap, MemoryError};
35
36/// Errors encountered when configuring microVM resources.
37#[derive(Debug, thiserror::Error, displaydoc::Display)]
38pub enum ResourcesError {
39    /// Balloon device error: {0}
40    BalloonDevice(#[from] BalloonConfigError),
41    /// Block device error: {0}
42    BlockDevice(#[from] DriveError),
43    /// Boot source error: {0}
44    BootSource(#[from] BootSourceConfigError),
45    /// File operation error: {0}
46    File(#[from] std::io::Error),
47    /// Invalid JSON: {0}
48    InvalidJson(#[from] serde_json::Error),
49    /// Logger error: {0}
50    Logger(#[from] crate::logger::LoggerUpdateError),
51    /// Metrics error: {0}
52    Metrics(#[from] MetricsConfigError),
53    /// MMDS error: {0}
54    Mmds(#[from] mmds::data_store::MmdsDatastoreError),
55    /// MMDS config error: {0}
56    MmdsConfig(#[from] MmdsConfigError),
57    /// Network device error: {0}
58    NetDevice(#[from] NetworkInterfaceError),
59    /// VM config error: {0}
60    MachineConfig(#[from] MachineConfigError),
61    /// Vsock device error: {0}
62    VsockDevice(#[from] VsockConfigError),
63    /// Entropy device error: {0}
64    EntropyDevice(#[from] EntropyDeviceError),
65    /// Pmem device error: {0}
66    PmemDevice(#[from] PmemConfigError),
67    /// Memory hotplug config error: {0}
68    MemoryHotplugConfig(#[from] MemoryHotplugConfigError),
69}
70
71#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
72#[serde(untagged)]
73enum CustomCpuTemplateOrPath {
74    Path(PathBuf),
75    Template(CustomCpuTemplate),
76}
77
78/// Used for configuring a vmm from one single json passed to the Firecracker process.
79#[derive(Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
80#[serde(rename_all = "kebab-case")]
81pub struct VmmConfig {
82    balloon: Option<BalloonDeviceConfig>,
83    drives: Vec<BlockDeviceConfig>,
84    boot_source: BootSourceConfig,
85    cpu_config: Option<CustomCpuTemplateOrPath>,
86    logger: Option<crate::logger::LoggerConfig>,
87    machine_config: Option<MachineConfig>,
88    metrics: Option<MetricsConfig>,
89    mmds_config: Option<MmdsConfig>,
90    #[serde(default)]
91    network_interfaces: Vec<NetworkInterfaceConfig>,
92    vsock: Option<VsockDeviceConfig>,
93    entropy: Option<EntropyDeviceConfig>,
94    #[serde(default, rename = "pmem")]
95    pmem_devices: Vec<PmemConfig>,
96    #[serde(skip)]
97    serial_config: Option<SerialConfig>,
98    memory_hotplug: Option<MemoryHotplugConfig>,
99}
100
101/// A data structure that encapsulates the device configurations
102/// held in the Vmm.
103#[derive(Debug, Default)]
104pub struct VmResources {
105    /// The vCpu and memory configuration for this microVM.
106    pub machine_config: MachineConfig,
107    /// The boot source spec (contains both config and builder) for this microVM.
108    pub boot_source: BootSource,
109    /// The block devices.
110    pub block: BlockBuilder,
111    /// The vsock device.
112    pub vsock: VsockBuilder,
113    /// The balloon device.
114    pub balloon: BalloonBuilder,
115    /// The network devices builder.
116    pub net_builder: NetBuilder,
117    /// The entropy device builder.
118    pub entropy: EntropyDeviceBuilder,
119    /// The pmem devices.
120    pub pmem: PmemBuilder,
121    /// The memory hotplug configuration.
122    pub memory_hotplug: Option<MemoryHotplugConfig>,
123    /// The optional Mmds data store.
124    // This is initialised on demand (if ever used), so that we don't allocate it unless it's
125    // actually used.
126    pub mmds: Option<Arc<Mutex<Mmds>>>,
127    /// Data store limit for the mmds.
128    pub mmds_size_limit: usize,
129    /// Whether or not to load boot timer device.
130    pub boot_timer: bool,
131    /// Whether or not to use PCIe transport for VirtIO devices.
132    pub pci_enabled: bool,
133    /// Where serial console output should be written to
134    pub serial_out_path: Option<PathBuf>,
135}
136
137impl VmResources {
138    /// Configures Vmm resources as described by the `config_json` param.
139    pub fn from_json(
140        config_json: &str,
141        instance_info: &InstanceInfo,
142        mmds_size_limit: usize,
143        metadata_json: Option<&str>,
144    ) -> Result<Self, ResourcesError> {
145        let vmm_config = serde_json::from_str::<VmmConfig>(config_json)?;
146
147        if let Some(logger_config) = vmm_config.logger {
148            crate::logger::LOGGER.update(logger_config)?;
149        }
150
151        if let Some(metrics) = vmm_config.metrics {
152            init_metrics(metrics)?;
153        }
154
155        let mut resources: Self = Self {
156            mmds_size_limit,
157            ..Default::default()
158        };
159        if let Some(machine_config) = vmm_config.machine_config {
160            let machine_config = MachineConfigUpdate::from(machine_config);
161            resources.update_machine_config(&machine_config)?;
162        }
163
164        if let Some(either) = vmm_config.cpu_config {
165            match either {
166                CustomCpuTemplateOrPath::Path(path) => {
167                    let cpu_config_json =
168                        std::fs::read_to_string(path).map_err(ResourcesError::File)?;
169                    let cpu_template = CustomCpuTemplate::try_from(cpu_config_json.as_str())?;
170                    resources.set_custom_cpu_template(cpu_template);
171                }
172                CustomCpuTemplateOrPath::Template(template) => {
173                    resources.set_custom_cpu_template(template)
174                }
175            }
176        }
177
178        resources.build_boot_source(vmm_config.boot_source)?;
179
180        for drive_config in vmm_config.drives.into_iter() {
181            resources.set_block_device(drive_config)?;
182        }
183
184        for net_config in vmm_config.network_interfaces.into_iter() {
185            resources.build_net_device(net_config)?;
186        }
187
188        if let Some(vsock_config) = vmm_config.vsock {
189            resources.set_vsock_device(vsock_config)?;
190        }
191
192        if let Some(balloon_config) = vmm_config.balloon {
193            resources.set_balloon_device(balloon_config)?;
194        }
195
196        // Init the data store from file, if present.
197        if let Some(data) = metadata_json {
198            resources.locked_mmds_or_default()?.put_data(
199                serde_json::from_str(data).expect("MMDS error: metadata provided not valid json"),
200            )?;
201            info!("Successfully added metadata to mmds from file");
202        }
203
204        if let Some(mmds_config) = vmm_config.mmds_config {
205            resources.set_mmds_config(mmds_config, &instance_info.id)?;
206        }
207
208        if let Some(entropy_device_config) = vmm_config.entropy {
209            resources.build_entropy_device(entropy_device_config)?;
210        }
211
212        for pmem_config in vmm_config.pmem_devices.into_iter() {
213            resources.build_pmem_device(pmem_config)?;
214        }
215
216        if let Some(serial_cfg) = vmm_config.serial_config {
217            resources.serial_out_path = serial_cfg.serial_out_path;
218        }
219
220        if let Some(memory_hotplug_config) = vmm_config.memory_hotplug {
221            resources.set_memory_hotplug_config(memory_hotplug_config)?;
222        }
223
224        Ok(resources)
225    }
226
227    /// If not initialised, create the mmds data store with the default config.
228    pub fn mmds_or_default(&mut self) -> Result<&Arc<Mutex<Mmds>>, MmdsConfigError> {
229        Ok(self
230            .mmds
231            .get_or_insert(Arc::new(Mutex::new(Mmds::try_new(self.mmds_size_limit)?))))
232    }
233
234    /// If not initialised, create the mmds data store with the default config.
235    pub fn locked_mmds_or_default(&mut self) -> Result<MutexGuard<'_, Mmds>, MmdsConfigError> {
236        let mmds = self.mmds_or_default()?;
237        Ok(mmds.lock().expect("Poisoned lock"))
238    }
239
240    /// Add a custom CPU template to the VM resources
241    /// to configure vCPUs.
242    pub fn set_custom_cpu_template(&mut self, cpu_template: CustomCpuTemplate) {
243        self.machine_config.set_custom_cpu_template(cpu_template);
244    }
245
246    /// Updates the configuration of the microVM.
247    pub fn update_machine_config(
248        &mut self,
249        update: &MachineConfigUpdate,
250    ) -> Result<(), MachineConfigError> {
251        let updated = self.machine_config.update(update)?;
252
253        // The VM cannot have a memory size smaller than the target size
254        // of the balloon device, if present.
255        if self.balloon.get().is_some()
256            && updated.mem_size_mib
257                < self
258                    .balloon
259                    .get_config()
260                    .map_err(|_| MachineConfigError::InvalidVmState)?
261                    .amount_mib as usize
262        {
263            return Err(MachineConfigError::IncompatibleBalloonSize);
264        }
265
266        self.machine_config = updated;
267
268        Ok(())
269    }
270
271    // Repopulate the MmdsConfig based on information from the data store
272    // and the associated net devices.
273    fn mmds_config(&self) -> Option<MmdsConfig> {
274        // If the data store is not initialised, we can be sure that the user did not configure
275        // mmds.
276        let mmds = self.mmds.as_ref()?;
277
278        let mut mmds_config = None;
279        let net_devs_with_mmds: Vec<_> = self
280            .net_builder
281            .iter()
282            .filter(|net| net.lock().expect("Poisoned lock").mmds_ns().is_some())
283            .collect();
284
285        if !net_devs_with_mmds.is_empty() {
286            let mmds_guard = mmds.lock().expect("Poisoned lock");
287            let mut inner_mmds_config = MmdsConfig {
288                version: mmds_guard.version(),
289                network_interfaces: vec![],
290                ipv4_address: None,
291                imds_compat: mmds_guard.imds_compat(),
292            };
293
294            for net_dev in net_devs_with_mmds {
295                let net = net_dev.lock().unwrap();
296                inner_mmds_config.network_interfaces.push(net.id().clone());
297                // Only need to get one ip address, as they will all be equal.
298                if inner_mmds_config.ipv4_address.is_none() {
299                    // Safe to unwrap the mmds_ns as the filter() explicitly checks for
300                    // its existence.
301                    inner_mmds_config.ipv4_address = Some(net.mmds_ns().unwrap().ipv4_addr());
302                }
303            }
304
305            mmds_config = Some(inner_mmds_config);
306        }
307
308        mmds_config
309    }
310
311    /// Sets a balloon device to be attached when the VM starts.
312    pub fn set_balloon_device(
313        &mut self,
314        config: BalloonDeviceConfig,
315    ) -> Result<(), BalloonConfigError> {
316        // The balloon cannot have a target size greater than the size of
317        // the guest memory.
318        if config.amount_mib as usize > self.machine_config.mem_size_mib {
319            return Err(BalloonConfigError::TooManyPagesRequested);
320        }
321
322        self.balloon.set(config)
323    }
324
325    /// Obtains the boot source hooks (kernel fd, command line creation and validation).
326    pub fn build_boot_source(
327        &mut self,
328        boot_source_cfg: BootSourceConfig,
329    ) -> Result<(), BootSourceConfigError> {
330        self.boot_source = BootSource {
331            builder: Some(BootConfig::new(&boot_source_cfg)?),
332            config: boot_source_cfg,
333        };
334
335        Ok(())
336    }
337
338    /// Inserts a block to be attached when the VM starts.
339    // Only call this function as part of user configuration.
340    // If the drive_id does not exist, a new Block Device Config is added to the list.
341    pub fn set_block_device(
342        &mut self,
343        block_device_config: BlockDeviceConfig,
344    ) -> Result<(), DriveError> {
345        let has_pmem_root = self.pmem.has_root_device();
346        self.block.insert(block_device_config, has_pmem_root)
347    }
348
349    /// Builds a network device to be attached when the VM starts.
350    pub fn build_net_device(
351        &mut self,
352        body: NetworkInterfaceConfig,
353    ) -> Result<(), NetworkInterfaceError> {
354        let _ = self.net_builder.build(body)?;
355        Ok(())
356    }
357
358    /// Sets a vsock device to be attached when the VM starts.
359    pub fn set_vsock_device(&mut self, config: VsockDeviceConfig) -> Result<(), VsockConfigError> {
360        self.vsock.insert(config)
361    }
362
363    /// Builds an entropy device to be attached when the VM starts.
364    pub fn build_entropy_device(
365        &mut self,
366        body: EntropyDeviceConfig,
367    ) -> Result<(), EntropyDeviceError> {
368        self.entropy.insert(body)
369    }
370
371    /// Builds a pmem device to be attached when the VM starts.
372    pub fn build_pmem_device(&mut self, body: PmemConfig) -> Result<(), PmemConfigError> {
373        let has_block_root = self.block.has_root_device();
374        self.pmem.build(body, has_block_root)
375    }
376
377    /// Sets the memory hotplug configuration.
378    pub fn set_memory_hotplug_config(
379        &mut self,
380        config: MemoryHotplugConfig,
381    ) -> Result<(), MemoryHotplugConfigError> {
382        config.validate()?;
383        self.memory_hotplug = Some(config);
384        Ok(())
385    }
386
387    /// Setter for mmds config.
388    pub fn set_mmds_config(
389        &mut self,
390        config: MmdsConfig,
391        instance_id: &str,
392    ) -> Result<(), MmdsConfigError> {
393        self.set_mmds_network_stack_config(&config)?;
394        self.set_mmds_basic_config(config.version, config.imds_compat, instance_id)?;
395
396        Ok(())
397    }
398
399    /// Updates MMDS-related config other than MMDS network stack.
400    pub fn set_mmds_basic_config(
401        &mut self,
402        version: MmdsVersion,
403        imds_compat: bool,
404        instance_id: &str,
405    ) -> Result<(), MmdsConfigError> {
406        let mut mmds_guard = self.locked_mmds_or_default()?;
407        mmds_guard.set_version(version);
408        mmds_guard.set_imds_compat(imds_compat);
409        mmds_guard.set_aad(instance_id);
410
411        Ok(())
412    }
413
414    // Updates MMDS Network Stack for network interfaces to allow forwarding
415    // requests to MMDS (or not).
416    fn set_mmds_network_stack_config(
417        &mut self,
418        config: &MmdsConfig,
419    ) -> Result<(), MmdsConfigError> {
420        // Check IPv4 address validity.
421        let ipv4_addr = match config.ipv4_addr() {
422            Some(ipv4_addr) if is_link_local_valid(ipv4_addr) => Ok(ipv4_addr),
423            None => Ok(MmdsNetworkStack::default_ipv4_addr()),
424            _ => Err(MmdsConfigError::InvalidIpv4Addr),
425        }?;
426
427        let network_interfaces = config.network_interfaces();
428        // Ensure that at least one network ID is specified.
429        if network_interfaces.is_empty() {
430            return Err(MmdsConfigError::EmptyNetworkIfaceList);
431        }
432
433        // Ensure all interface IDs specified correspond to existing net devices.
434        if !network_interfaces.iter().all(|id| {
435            self.net_builder
436                .iter()
437                .map(|device| device.lock().expect("Poisoned lock").id().clone())
438                .any(|x| &x == id)
439        }) {
440            return Err(MmdsConfigError::InvalidNetworkInterfaceId);
441        }
442
443        // Safe to unwrap because we've just made sure that it's initialised.
444        let mmds = self.mmds_or_default()?.clone();
445
446        // Create `MmdsNetworkStack` and configure the IPv4 address for
447        // existing built network devices whose names are defined in the
448        // network interface ID list.
449        for net_device in self.net_builder.iter() {
450            let mut net_device_lock = net_device.lock().expect("Poisoned lock");
451            if network_interfaces.contains(net_device_lock.id()) {
452                net_device_lock.configure_mmds_network_stack(ipv4_addr, mmds.clone());
453            } else {
454                net_device_lock.disable_mmds_network_stack();
455            }
456        }
457
458        Ok(())
459    }
460
461    /// Allocates the given guest memory regions.
462    ///
463    /// If vhost-user-blk devices are in use, allocates memfd-backed shared memory, otherwise
464    /// prefers anonymous memory for performance reasons.
465    fn allocate_memory_regions(
466        &self,
467        regions: &[(GuestAddress, usize)],
468    ) -> Result<Vec<GuestRegionMmap>, MemoryError> {
469        let vhost_user_device_used = self
470            .block
471            .devices
472            .iter()
473            .any(|b| b.lock().expect("Poisoned lock").is_vhost_user());
474
475        // Page faults are more expensive for shared memory mapping, including  memfd.
476        // For this reason, we only back guest memory with a memfd
477        // if a vhost-user-blk device is configured in the VM, otherwise we fall back to
478        // an anonymous private memory.
479        //
480        // The vhost-user-blk branch is not currently covered by integration tests in Rust,
481        // because that would require running a backend process. If in the future we converge to
482        // a single way of backing guest memory for vhost-user and non-vhost-user cases,
483        // that would not be worth the effort.
484        if vhost_user_device_used {
485            memory::memfd_backed(
486                regions,
487                self.machine_config.track_dirty_pages,
488                self.machine_config.huge_pages,
489            )
490        } else {
491            memory::anonymous(
492                regions.iter().copied(),
493                self.machine_config.track_dirty_pages,
494                self.machine_config.huge_pages,
495            )
496        }
497    }
498
499    /// Allocates guest memory in a configuration most appropriate for these [`VmResources`].
500    pub fn allocate_guest_memory(&self) -> Result<Vec<GuestRegionMmap>, MemoryError> {
501        let regions =
502            crate::arch::arch_memory_regions(mib_to_bytes(self.machine_config.mem_size_mib));
503        self.allocate_memory_regions(&regions)
504    }
505
506    /// Allocates a single guest memory region.
507    pub fn allocate_memory_region(
508        &self,
509        start: GuestAddress,
510        size: usize,
511    ) -> Result<GuestRegionMmap, MemoryError> {
512        Ok(self
513            .allocate_memory_regions(&[(start, size)])?
514            .pop()
515            .unwrap())
516    }
517}
518
519impl From<&VmResources> for VmmConfig {
520    fn from(resources: &VmResources) -> Self {
521        VmmConfig {
522            balloon: resources.balloon.get_config().ok(),
523            drives: resources.block.configs(),
524            boot_source: resources.boot_source.config.clone(),
525            cpu_config: None,
526            logger: None,
527            machine_config: Some(resources.machine_config.clone()),
528            metrics: None,
529            mmds_config: resources.mmds_config(),
530            network_interfaces: resources.net_builder.configs(),
531            vsock: resources.vsock.config(),
532            entropy: resources.entropy.config(),
533            pmem_devices: resources.pmem.configs(),
534            // serial_config is marked serde(skip) so that it doesnt end up in snapshots.
535            serial_config: None,
536            memory_hotplug: resources.memory_hotplug.clone(),
537        }
538    }
539}
540
541#[cfg(test)]
542mod tests {
543    use std::fs::File;
544    use std::io::Write;
545    use std::os::linux::fs::MetadataExt;
546    use std::str::FromStr;
547
548    use serde_json::{Map, Value};
549    use vmm_sys_util::tempfile::TempFile;
550
551    use super::*;
552    use crate::HTTP_MAX_PAYLOAD_SIZE;
553    use crate::cpu_config::templates::test_utils::TEST_TEMPLATE_JSON;
554    use crate::cpu_config::templates::{CpuTemplateType, StaticCpuTemplate};
555    use crate::devices::virtio::block::virtio::VirtioBlockError;
556    use crate::devices::virtio::block::{BlockError, CacheType};
557    use crate::devices::virtio::vsock::VSOCK_DEV_ID;
558    use crate::resources::VmResources;
559    use crate::utils::net::mac::MacAddr;
560    use crate::vmm_config::RateLimiterConfig;
561    use crate::vmm_config::boot_source::{
562        BootConfig, BootSource, BootSourceConfig, DEFAULT_KERNEL_CMDLINE,
563    };
564    use crate::vmm_config::drive::{BlockBuilder, BlockDeviceConfig};
565    use crate::vmm_config::machine_config::{HugePageConfig, MachineConfig, MachineConfigError};
566    use crate::vmm_config::net::{NetBuilder, NetworkInterfaceConfig};
567    use crate::vmm_config::vsock::tests::default_config;
568
569    fn default_net_cfg() -> NetworkInterfaceConfig {
570        NetworkInterfaceConfig {
571            iface_id: "net_if1".to_string(),
572            // TempFile::new_with_prefix("") generates a random file name used as random net_if
573            // name.
574            host_dev_name: TempFile::new_with_prefix("")
575                .unwrap()
576                .as_path()
577                .to_str()
578                .unwrap()
579                .to_string(),
580            guest_mac: Some(MacAddr::from_str("01:23:45:67:89:0a").unwrap()),
581            rx_rate_limiter: Some(RateLimiterConfig::default()),
582            tx_rate_limiter: Some(RateLimiterConfig::default()),
583        }
584    }
585
586    fn default_net_builder() -> NetBuilder {
587        let mut net_builder = NetBuilder::new();
588        net_builder.build(default_net_cfg()).unwrap();
589
590        net_builder
591    }
592
593    fn default_block_cfg() -> (BlockDeviceConfig, TempFile) {
594        let tmp_file = TempFile::new().unwrap();
595        (
596            BlockDeviceConfig {
597                drive_id: "block1".to_string(),
598                partuuid: Some("0eaa91a0-01".to_string()),
599                is_root_device: false,
600                cache_type: CacheType::Unsafe,
601
602                is_read_only: Some(false),
603                path_on_host: Some(tmp_file.as_path().to_str().unwrap().to_string()),
604                rate_limiter: Some(RateLimiterConfig::default()),
605                file_engine_type: None,
606
607                socket: None,
608            },
609            tmp_file,
610        )
611    }
612
613    fn default_blocks() -> BlockBuilder {
614        let mut blocks = BlockBuilder::new();
615        let (cfg, _file) = default_block_cfg();
616        blocks.insert(cfg, false).unwrap();
617        blocks
618    }
619
620    fn default_boot_cfg() -> BootSource {
621        let kernel_cmdline =
622            linux_loader::cmdline::Cmdline::try_from(DEFAULT_KERNEL_CMDLINE, 4096).unwrap();
623        let tmp_file = TempFile::new().unwrap();
624        BootSource {
625            config: BootSourceConfig::default(),
626            builder: Some(BootConfig {
627                cmdline: kernel_cmdline,
628                kernel_file: File::open(tmp_file.as_path()).unwrap(),
629                initrd_file: Some(File::open(tmp_file.as_path()).unwrap()),
630            }),
631        }
632    }
633
634    fn default_vm_resources() -> VmResources {
635        VmResources {
636            machine_config: MachineConfig::default(),
637            boot_source: default_boot_cfg(),
638            block: default_blocks(),
639            vsock: Default::default(),
640            balloon: Default::default(),
641            net_builder: default_net_builder(),
642            mmds: None,
643            boot_timer: false,
644            mmds_size_limit: HTTP_MAX_PAYLOAD_SIZE,
645            entropy: Default::default(),
646            pmem: Default::default(),
647            pci_enabled: false,
648            serial_out_path: None,
649            memory_hotplug: Default::default(),
650        }
651    }
652
653    #[test]
654    fn test_from_json() {
655        let kernel_file = TempFile::new().unwrap();
656        let rootfs_file = TempFile::new().unwrap();
657        let scratch_file = TempFile::new().unwrap();
658        scratch_file.as_file().set_len(0x1000).unwrap();
659        let default_instance_info = InstanceInfo::default();
660
661        // We will test different scenarios with invalid resources configuration and
662        // check the expected errors. We include configuration for the kernel and rootfs
663        // in every json because they are mandatory fields. If we don't configure
664        // these resources, it is considered an invalid json and the test will crash.
665
666        // Invalid JSON string must yield a `serde_json` error.
667        let error =
668            VmResources::from_json(r#"}"#, &default_instance_info, HTTP_MAX_PAYLOAD_SIZE, None)
669                .unwrap_err();
670        assert!(
671            matches!(error, ResourcesError::InvalidJson(_)),
672            "{:?}",
673            error
674        );
675
676        // Valid JSON string without the configuration for kernel or rootfs
677        // result in an invalid JSON error.
678        let error =
679            VmResources::from_json(r#"{}"#, &default_instance_info, HTTP_MAX_PAYLOAD_SIZE, None)
680                .unwrap_err();
681        assert!(
682            matches!(error, ResourcesError::InvalidJson(_)),
683            "{:?}",
684            error
685        );
686
687        // Invalid kernel path.
688        let mut json = format!(
689            r#"{{
690                    "boot-source": {{
691                        "kernel_image_path": "/invalid/path",
692                        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
693                    }},
694                    "drives": [
695                        {{
696                            "drive_id": "rootfs",
697                            "path_on_host": "{}",
698                            "is_root_device": true,
699                            "is_read_only": false
700                        }}
701                    ]
702            }}"#,
703            rootfs_file.as_path().to_str().unwrap()
704        );
705
706        let error = VmResources::from_json(
707            json.as_str(),
708            &default_instance_info,
709            HTTP_MAX_PAYLOAD_SIZE,
710            None,
711        )
712        .unwrap_err();
713        assert!(
714            matches!(
715                error,
716                ResourcesError::BootSource(BootSourceConfigError::InvalidKernelPath(_))
717            ),
718            "{:?}",
719            error
720        );
721
722        // Invalid rootfs path.
723        json = format!(
724            r#"{{
725                    "boot-source": {{
726                        "kernel_image_path": "{}",
727                        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
728                    }},
729                    "drives": [
730                        {{
731                            "drive_id": "rootfs",
732                            "path_on_host": "/invalid/path",
733                            "is_root_device": true,
734                            "is_read_only": false
735                        }}
736                    ]
737            }}"#,
738            kernel_file.as_path().to_str().unwrap()
739        );
740
741        let error = VmResources::from_json(
742            json.as_str(),
743            &default_instance_info,
744            HTTP_MAX_PAYLOAD_SIZE,
745            None,
746        )
747        .unwrap_err();
748        assert!(
749            matches!(
750                error,
751                ResourcesError::BlockDevice(DriveError::CreateBlockDevice(
752                    BlockError::VirtioBackend(VirtioBlockError::BackingFile(_, _)),
753                ))
754            ),
755            "{:?}",
756            error
757        );
758        // Valid config for x86 but invalid on aarch64 since it uses cpu_template.
759        json = format!(
760            r#"{{
761                    "boot-source": {{
762                        "kernel_image_path": "{}",
763                        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
764                    }},
765                    "drives": [
766                        {{
767                            "drive_id": "rootfs",
768                            "path_on_host": "{}",
769                            "is_root_device": true,
770                            "is_read_only": false
771                        }}
772                    ],
773                    "machine-config": {{
774                        "vcpu_count": 2,
775                        "mem_size_mib": 1024,
776                        "cpu_template": "C3"
777                    }}
778            }}"#,
779            kernel_file.as_path().to_str().unwrap(),
780            rootfs_file.as_path().to_str().unwrap()
781        );
782        #[cfg(target_arch = "x86_64")]
783        VmResources::from_json(
784            json.as_str(),
785            &default_instance_info,
786            HTTP_MAX_PAYLOAD_SIZE,
787            None,
788        )
789        .unwrap();
790        #[cfg(target_arch = "aarch64")]
791        VmResources::from_json(
792            json.as_str(),
793            &default_instance_info,
794            HTTP_MAX_PAYLOAD_SIZE,
795            None,
796        )
797        .unwrap_err();
798
799        // Invalid memory size.
800        json = format!(
801            r#"{{
802                    "boot-source": {{
803                        "kernel_image_path": "{}",
804                        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
805                    }},
806                    "drives": [
807                        {{
808                            "drive_id": "rootfs",
809                            "path_on_host": "{}",
810                            "is_root_device": true,
811                            "is_read_only": false
812                        }}
813                    ],
814                    "machine-config": {{
815                        "vcpu_count": 2,
816                        "mem_size_mib": 0
817                    }}
818            }}"#,
819            kernel_file.as_path().to_str().unwrap(),
820            rootfs_file.as_path().to_str().unwrap()
821        );
822
823        let error = VmResources::from_json(
824            json.as_str(),
825            &default_instance_info,
826            HTTP_MAX_PAYLOAD_SIZE,
827            None,
828        )
829        .unwrap_err();
830        assert!(
831            matches!(
832                error,
833                ResourcesError::MachineConfig(MachineConfigError::InvalidMemorySize)
834            ),
835            "{:?}",
836            error
837        );
838
839        // Invalid path for logger pipe.
840        json = format!(
841            r#"{{
842                    "boot-source": {{
843                        "kernel_image_path": "{}",
844                        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
845                    }},
846                    "drives": [
847                        {{
848                            "drive_id": "rootfs",
849                            "path_on_host": "{}",
850                            "is_root_device": true,
851                            "is_read_only": false
852                        }}
853                    ],
854                    "logger": {{
855	                    "log_path": "/invalid/path"
856                    }}
857            }}"#,
858            kernel_file.as_path().to_str().unwrap(),
859            rootfs_file.as_path().to_str().unwrap()
860        );
861
862        let error = VmResources::from_json(
863            json.as_str(),
864            &default_instance_info,
865            HTTP_MAX_PAYLOAD_SIZE,
866            None,
867        )
868        .unwrap_err();
869        assert!(
870            matches!(
871                error,
872                ResourcesError::Logger(crate::logger::LoggerUpdateError(_))
873            ),
874            "{:?}",
875            error
876        );
877
878        // Invalid path for metrics pipe.
879        json = format!(
880            r#"{{
881                    "boot-source": {{
882                        "kernel_image_path": "{}",
883                        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
884                    }},
885                    "drives": [
886                        {{
887                            "drive_id": "rootfs",
888                            "path_on_host": "{}",
889                            "is_root_device": true,
890                            "is_read_only": false
891                        }}
892                    ],
893                    "metrics": {{
894	                    "metrics_path": "/invalid/path"
895                    }}
896            }}"#,
897            kernel_file.as_path().to_str().unwrap(),
898            rootfs_file.as_path().to_str().unwrap()
899        );
900
901        let error = VmResources::from_json(
902            json.as_str(),
903            &default_instance_info,
904            HTTP_MAX_PAYLOAD_SIZE,
905            None,
906        )
907        .unwrap_err();
908        assert!(
909            matches!(
910                error,
911                ResourcesError::Metrics(MetricsConfigError::InitializationFailure { .. })
912            ),
913            "{:?}",
914            error
915        );
916
917        // Reuse of a host name.
918        json = format!(
919            r#"{{
920                    "boot-source": {{
921                        "kernel_image_path": "{}",
922                        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
923                    }},
924                    "drives": [
925                        {{
926                            "drive_id": "rootfs",
927                            "path_on_host": "{}",
928                            "is_root_device": true,
929                            "is_read_only": false
930                        }}
931                    ],
932                    "network-interfaces": [
933                        {{
934                            "iface_id": "netif1",
935                            "host_dev_name": "hostname7"
936                        }},
937                        {{
938                            "iface_id": "netif2",
939                            "host_dev_name": "hostname7"
940                        }}
941                    ]
942            }}"#,
943            kernel_file.as_path().to_str().unwrap(),
944            rootfs_file.as_path().to_str().unwrap()
945        );
946
947        let error = VmResources::from_json(
948            json.as_str(),
949            &default_instance_info,
950            HTTP_MAX_PAYLOAD_SIZE,
951            None,
952        )
953        .unwrap_err();
954
955        assert!(
956            matches!(
957                error,
958                ResourcesError::NetDevice(NetworkInterfaceError::CreateNetworkDevice(
959                    crate::devices::virtio::net::NetError::TapOpen { .. },
960                ))
961            ),
962            "{:?}",
963            error
964        );
965
966        // Let's try now passing a valid configuration. We won't include any logger
967        // or metrics configuration because these were already initialized in other
968        // tests of this module and the reinitialization of them will cause crashing.
969        json = format!(
970            r#"{{
971                    "boot-source": {{
972                        "kernel_image_path": "{}",
973                        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
974                    }},
975                    "drives": [
976                        {{
977                            "drive_id": "rootfs",
978                            "path_on_host": "{}",
979                            "is_root_device": true,
980                            "is_read_only": false
981                        }}
982                    ],
983                    "network-interfaces": [
984                        {{
985                            "iface_id": "netif",
986                            "host_dev_name": "hostname8"
987                        }}
988                    ],
989                    "machine-config": {{
990                        "vcpu_count": 2,
991                        "mem_size_mib": 1024,
992                        "smt": false
993                    }},
994                    "mmds-config": {{
995                        "version": "V2",
996                        "ipv4_address": "169.254.170.2",
997                        "network_interfaces": ["netif"]
998                    }}
999            }}"#,
1000            kernel_file.as_path().to_str().unwrap(),
1001            rootfs_file.as_path().to_str().unwrap(),
1002        );
1003        VmResources::from_json(
1004            json.as_str(),
1005            &default_instance_info,
1006            HTTP_MAX_PAYLOAD_SIZE,
1007            None,
1008        )
1009        .unwrap();
1010
1011        // Test all configuration, this time trying to set default configuration
1012        // for version and IPv4 address.
1013        let kernel_file = TempFile::new().unwrap();
1014        json = format!(
1015            r#"{{
1016                    "balloon": {{
1017                        "amount_mib": 0,
1018                        "deflate_on_oom": false,
1019                        "stats_polling_interval_s": 0
1020                    }},
1021                    "boot-source": {{
1022                        "kernel_image_path": "{}",
1023                        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
1024                    }},
1025                    "drives": [
1026                        {{
1027                            "drive_id": "rootfs",
1028                            "path_on_host": "{}",
1029                            "is_root_device": true,
1030                            "is_read_only": false
1031                        }}
1032                    ],
1033                    "pmem": [
1034                        {{
1035                            "id": "pmem",
1036                            "path_on_host": "{}",
1037                            "root_device": false,
1038                            "read_only": false
1039                        }}
1040                    ],
1041                    "network-interfaces": [
1042                        {{
1043                            "iface_id": "netif",
1044                            "host_dev_name": "hostname9"
1045                        }}
1046                    ],
1047                    "machine-config": {{
1048                        "vcpu_count": 2,
1049                        "mem_size_mib": 1024,
1050                        "smt": false
1051                    }},
1052                    "mmds-config": {{
1053                        "network_interfaces": ["netif"],
1054                        "ipv4_address": "169.254.1.1"
1055                    }}
1056            }}"#,
1057            kernel_file.as_path().to_str().unwrap(),
1058            rootfs_file.as_path().to_str().unwrap(),
1059            scratch_file.as_path().to_str().unwrap(),
1060        );
1061        let resources = VmResources::from_json(
1062            json.as_str(),
1063            &default_instance_info,
1064            1200,
1065            Some(r#"{"key": "value"}"#),
1066        )
1067        .unwrap();
1068        let mut map = Map::new();
1069        map.insert("key".to_string(), Value::String("value".to_string()));
1070        assert_eq!(
1071            resources.mmds.unwrap().lock().unwrap().data_store_value(),
1072            Value::Object(map)
1073        );
1074    }
1075
1076    #[test]
1077    fn test_cpu_config_from_invalid_json() {
1078        // Invalid cpu config file path.
1079        // `VmResources::from_json()` should fail with `Error::File`.
1080        let kernel_file = TempFile::new().unwrap();
1081        let rootfs_file = TempFile::new().unwrap();
1082        let default_instance_info = InstanceInfo::default();
1083
1084        let json = format!(
1085            r#"{{
1086                    "boot-source": {{
1087                        "kernel_image_path": "{}",
1088                        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
1089                    }},
1090                    "cpu-config": "/invalid/path",
1091                    "drives": [
1092                        {{
1093                            "drive_id": "rootfs",
1094                            "path_on_host": "{}",
1095                            "is_root_device": true,
1096                            "is_read_only": false
1097                        }}
1098                    ]
1099            }}"#,
1100            kernel_file.as_path().to_str().unwrap(),
1101            rootfs_file.as_path().to_str().unwrap(),
1102        );
1103
1104        let error = VmResources::from_json(
1105            json.as_str(),
1106            &default_instance_info,
1107            HTTP_MAX_PAYLOAD_SIZE,
1108            None,
1109        )
1110        .unwrap_err();
1111        assert!(matches!(error, ResourcesError::File(_)), "{:?}", error);
1112    }
1113
1114    #[test]
1115    fn test_cpu_config_inline() {
1116        // Include custom cpu template directly inline in config json
1117        let kernel_file = TempFile::new().unwrap();
1118        let rootfs_file = TempFile::new().unwrap();
1119        let default_instance_info = InstanceInfo::default();
1120
1121        let json = format!(
1122            r#"{{
1123                    "boot-source": {{
1124                        "kernel_image_path": "{}",
1125                        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
1126                    }},
1127                    "cpu-config": {},
1128                    "drives": [
1129                        {{
1130                            "drive_id": "rootfs",
1131                            "path_on_host": "{}",
1132                            "is_root_device": true,
1133                            "is_read_only": false
1134                        }}
1135                    ]
1136            }}"#,
1137            kernel_file.as_path().to_str().unwrap(),
1138            TEST_TEMPLATE_JSON,
1139            rootfs_file.as_path().to_str().unwrap(),
1140        );
1141
1142        VmResources::from_json(
1143            json.as_str(),
1144            &default_instance_info,
1145            HTTP_MAX_PAYLOAD_SIZE,
1146            None,
1147        )
1148        .unwrap();
1149    }
1150
1151    #[test]
1152    fn test_cpu_config_from_valid_json() {
1153        // Valid cpu config file path.
1154        // `VmResources::from_json()` should succeed and it should have a custom CPU template.
1155        let kernel_file = TempFile::new().unwrap();
1156        let rootfs_file = TempFile::new().unwrap();
1157        let default_instance_info = InstanceInfo::default();
1158        let cpu_config_file = TempFile::new().unwrap();
1159        cpu_config_file
1160            .as_file()
1161            .write_all("{}".as_bytes())
1162            .unwrap();
1163
1164        let json = format!(
1165            r#"{{
1166                    "boot-source": {{
1167                        "kernel_image_path": "{}",
1168                        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
1169                    }},
1170                    "cpu-config": "{}",
1171                    "drives": [
1172                        {{
1173                            "drive_id": "rootfs",
1174                            "path_on_host": "{}",
1175                            "is_root_device": true,
1176                            "is_read_only": false
1177                        }}
1178                    ]
1179            }}"#,
1180            kernel_file.as_path().to_str().unwrap(),
1181            cpu_config_file.as_path().to_str().unwrap(),
1182            rootfs_file.as_path().to_str().unwrap(),
1183        );
1184
1185        let vm_resources = VmResources::from_json(
1186            json.as_str(),
1187            &default_instance_info,
1188            HTTP_MAX_PAYLOAD_SIZE,
1189            None,
1190        )
1191        .unwrap();
1192        assert_eq!(
1193            vm_resources.machine_config.cpu_template,
1194            Some(CpuTemplateType::Custom(CustomCpuTemplate::default()))
1195        );
1196    }
1197
1198    #[test]
1199    fn test_cast_to_vmm_config() {
1200        // No mmds config.
1201        {
1202            let kernel_file = TempFile::new().unwrap();
1203            let rootfs_file = TempFile::new().unwrap();
1204            let json = format!(
1205                r#"{{
1206                    "balloon": {{
1207                        "amount_mib": 0,
1208                        "deflate_on_oom": false,
1209                        "stats_polling_interval_s": 0
1210                    }},
1211                    "boot-source": {{
1212                        "kernel_image_path": "{}",
1213                        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
1214                    }},
1215                    "drives": [
1216                        {{
1217                            "drive_id": "rootfs",
1218                            "path_on_host": "{}",
1219                            "is_root_device": true,
1220                            "is_read_only": false,
1221                            "io_engine": "Sync"
1222                        }}
1223                    ],
1224                    "network-interfaces": [
1225                        {{
1226                            "iface_id": "netif1",
1227                            "host_dev_name": "hostname9"
1228                        }},
1229                        {{
1230                            "iface_id": "netif2",
1231                            "host_dev_name": "hostname10"
1232                        }}
1233                    ],
1234                    "machine-config": {{
1235                        "vcpu_count": 2,
1236                        "mem_size_mib": 1024,
1237                        "smt": false
1238                    }},
1239                    "entropy": {{}}
1240            }}"#,
1241                kernel_file.as_path().to_str().unwrap(),
1242                rootfs_file.as_path().to_str().unwrap(),
1243            );
1244
1245            {
1246                let resources = VmResources::from_json(
1247                    json.as_str(),
1248                    &InstanceInfo::default(),
1249                    HTTP_MAX_PAYLOAD_SIZE,
1250                    None,
1251                )
1252                .unwrap();
1253
1254                let initial_vmm_config = serde_json::from_str::<VmmConfig>(&json).unwrap();
1255                let vmm_config: VmmConfig = (&resources).into();
1256                assert_eq!(initial_vmm_config, vmm_config);
1257            }
1258
1259            {
1260                // In this case the mmds data store will be initialised but the config still None.
1261                let resources = VmResources::from_json(
1262                    json.as_str(),
1263                    &InstanceInfo::default(),
1264                    HTTP_MAX_PAYLOAD_SIZE,
1265                    Some(r#"{"key": "value"}"#),
1266                )
1267                .unwrap();
1268
1269                let initial_vmm_config = serde_json::from_str::<VmmConfig>(&json).unwrap();
1270                let vmm_config: VmmConfig = (&resources).into();
1271                assert_eq!(initial_vmm_config, vmm_config);
1272            }
1273        }
1274
1275        // Single interface for MMDS.
1276        {
1277            let kernel_file = TempFile::new().unwrap();
1278            let rootfs_file = TempFile::new().unwrap();
1279            let json = format!(
1280                r#"{{
1281                    "balloon": {{
1282                        "amount_mib": 0,
1283                        "deflate_on_oom": false,
1284                        "stats_polling_interval_s": 0
1285                    }},
1286                    "boot-source": {{
1287                        "kernel_image_path": "{}",
1288                        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
1289                    }},
1290                    "drives": [
1291                        {{
1292                            "drive_id": "rootfs",
1293                            "path_on_host": "{}",
1294                            "is_root_device": true,
1295                            "is_read_only": false,
1296                            "io_engine": "Sync"
1297                        }}
1298                    ],
1299                    "network-interfaces": [
1300                        {{
1301                            "iface_id": "netif1",
1302                            "host_dev_name": "hostname9"
1303                        }},
1304                        {{
1305                            "iface_id": "netif2",
1306                            "host_dev_name": "hostname10"
1307                        }}
1308                    ],
1309                    "machine-config": {{
1310                        "vcpu_count": 2,
1311                        "mem_size_mib": 1024,
1312                        "smt": false
1313                    }},
1314                    "mmds-config": {{
1315                        "network_interfaces": ["netif1"],
1316                        "ipv4_address": "169.254.1.1"
1317                    }}
1318            }}"#,
1319                kernel_file.as_path().to_str().unwrap(),
1320                rootfs_file.as_path().to_str().unwrap(),
1321            );
1322            let resources = VmResources::from_json(
1323                json.as_str(),
1324                &InstanceInfo::default(),
1325                HTTP_MAX_PAYLOAD_SIZE,
1326                None,
1327            )
1328            .unwrap();
1329
1330            let initial_vmm_config = serde_json::from_str::<VmmConfig>(&json).unwrap();
1331            let vmm_config: VmmConfig = (&resources).into();
1332            assert_eq!(initial_vmm_config, vmm_config);
1333        }
1334
1335        // Multiple interfaces configured for MMDS.
1336        {
1337            let kernel_file = TempFile::new().unwrap();
1338            let rootfs_file = TempFile::new().unwrap();
1339            let json = format!(
1340                r#"{{
1341                    "balloon": {{
1342                        "amount_mib": 0,
1343                        "deflate_on_oom": false,
1344                        "stats_polling_interval_s": 0
1345                    }},
1346                    "boot-source": {{
1347                        "kernel_image_path": "{}",
1348                        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
1349                    }},
1350                    "drives": [
1351                        {{
1352                            "drive_id": "rootfs",
1353                            "path_on_host": "{}",
1354                            "is_root_device": true,
1355                            "is_read_only": false,
1356                            "io_engine": "Sync"
1357                        }}
1358                    ],
1359                    "network-interfaces": [
1360                        {{
1361                            "iface_id": "netif1",
1362                            "host_dev_name": "hostname9"
1363                        }},
1364                        {{
1365                            "iface_id": "netif2",
1366                            "host_dev_name": "hostname10"
1367                        }}
1368                    ],
1369                    "machine-config": {{
1370                        "vcpu_count": 2,
1371                        "mem_size_mib": 1024,
1372                        "smt": false
1373                    }},
1374                    "mmds-config": {{
1375                        "network_interfaces": ["netif1", "netif2"],
1376                        "ipv4_address": "169.254.1.1"
1377                    }}
1378            }}"#,
1379                kernel_file.as_path().to_str().unwrap(),
1380                rootfs_file.as_path().to_str().unwrap(),
1381            );
1382            let resources = VmResources::from_json(
1383                json.as_str(),
1384                &InstanceInfo::default(),
1385                HTTP_MAX_PAYLOAD_SIZE,
1386                None,
1387            )
1388            .unwrap();
1389
1390            let initial_vmm_config = serde_json::from_str::<VmmConfig>(&json).unwrap();
1391            let vmm_config: VmmConfig = (&resources).into();
1392            assert_eq!(initial_vmm_config, vmm_config);
1393        }
1394    }
1395
1396    #[test]
1397    fn test_update_machine_config() {
1398        let mut vm_resources = default_vm_resources();
1399        let mut aux_vm_config = MachineConfigUpdate {
1400            vcpu_count: Some(32),
1401            mem_size_mib: Some(512),
1402            smt: Some(false),
1403            #[cfg(target_arch = "x86_64")]
1404            cpu_template: Some(StaticCpuTemplate::T2),
1405            #[cfg(target_arch = "aarch64")]
1406            cpu_template: Some(StaticCpuTemplate::V1N1),
1407            track_dirty_pages: Some(false),
1408            huge_pages: Some(HugePageConfig::None),
1409            #[cfg(feature = "gdb")]
1410            gdb_socket_path: None,
1411        };
1412
1413        assert_ne!(
1414            MachineConfigUpdate::from(vm_resources.machine_config.clone()),
1415            aux_vm_config
1416        );
1417        vm_resources.update_machine_config(&aux_vm_config).unwrap();
1418        assert_eq!(
1419            MachineConfigUpdate::from(vm_resources.machine_config.clone()),
1420            aux_vm_config
1421        );
1422
1423        // Invalid vcpu count.
1424        aux_vm_config.vcpu_count = Some(0);
1425        assert_eq!(
1426            vm_resources.update_machine_config(&aux_vm_config),
1427            Err(MachineConfigError::InvalidVcpuCount)
1428        );
1429        aux_vm_config.vcpu_count = Some(33);
1430        assert_eq!(
1431            vm_resources.update_machine_config(&aux_vm_config),
1432            Err(MachineConfigError::InvalidVcpuCount)
1433        );
1434
1435        // Check that SMT is not supported on aarch64, and that on x86_64 enabling it requires vcpu
1436        // count to be even.
1437        aux_vm_config.smt = Some(true);
1438        #[cfg(target_arch = "aarch64")]
1439        assert_eq!(
1440            vm_resources.update_machine_config(&aux_vm_config),
1441            Err(MachineConfigError::SmtNotSupported)
1442        );
1443        aux_vm_config.vcpu_count = Some(3);
1444        #[cfg(target_arch = "x86_64")]
1445        assert_eq!(
1446            vm_resources.update_machine_config(&aux_vm_config),
1447            Err(MachineConfigError::InvalidVcpuCount)
1448        );
1449        aux_vm_config.vcpu_count = Some(32);
1450        #[cfg(target_arch = "x86_64")]
1451        vm_resources.update_machine_config(&aux_vm_config).unwrap();
1452        aux_vm_config.smt = Some(false);
1453
1454        // Invalid mem_size_mib.
1455        aux_vm_config.mem_size_mib = Some(0);
1456        assert_eq!(
1457            vm_resources.update_machine_config(&aux_vm_config),
1458            Err(MachineConfigError::InvalidMemorySize)
1459        );
1460
1461        // Incompatible mem_size_mib with balloon size.
1462        vm_resources.machine_config.mem_size_mib = 128;
1463        vm_resources
1464            .set_balloon_device(BalloonDeviceConfig {
1465                amount_mib: 100,
1466                deflate_on_oom: false,
1467                stats_polling_interval_s: 0,
1468                free_page_hinting: false,
1469                free_page_reporting: false,
1470            })
1471            .unwrap();
1472        aux_vm_config.mem_size_mib = Some(90);
1473        assert_eq!(
1474            vm_resources.update_machine_config(&aux_vm_config),
1475            Err(MachineConfigError::IncompatibleBalloonSize)
1476        );
1477
1478        // mem_size_mib compatible with balloon size.
1479        aux_vm_config.mem_size_mib = Some(256);
1480        vm_resources.update_machine_config(&aux_vm_config).unwrap();
1481
1482        // mem_size_mib incompatible with huge pages configuration
1483        aux_vm_config.mem_size_mib = Some(129);
1484        aux_vm_config.huge_pages = Some(HugePageConfig::Hugetlbfs2M);
1485        assert_eq!(
1486            vm_resources
1487                .update_machine_config(&aux_vm_config)
1488                .unwrap_err(),
1489            MachineConfigError::InvalidMemorySize
1490        );
1491
1492        // mem_size_mib compatible with huge page configuration
1493        aux_vm_config.mem_size_mib = Some(2048);
1494        // Remove the balloon device config that's added by `default_vm_resources` as it would
1495        // trigger the "ballooning incompatible with huge pages" check.
1496        vm_resources.balloon = BalloonBuilder::new();
1497        vm_resources.update_machine_config(&aux_vm_config).unwrap();
1498    }
1499
1500    #[test]
1501    fn test_set_balloon_device() {
1502        let mut vm_resources = default_vm_resources();
1503        vm_resources.balloon = BalloonBuilder::new();
1504        let mut new_balloon_cfg = BalloonDeviceConfig {
1505            amount_mib: 100,
1506            deflate_on_oom: false,
1507            stats_polling_interval_s: 0,
1508            free_page_hinting: false,
1509            free_page_reporting: false,
1510        };
1511        assert!(vm_resources.balloon.get().is_none());
1512        vm_resources
1513            .set_balloon_device(new_balloon_cfg.clone())
1514            .unwrap();
1515
1516        let actual_balloon_cfg = vm_resources.balloon.get_config().unwrap();
1517        assert_eq!(actual_balloon_cfg.amount_mib, new_balloon_cfg.amount_mib);
1518        assert_eq!(
1519            actual_balloon_cfg.deflate_on_oom,
1520            new_balloon_cfg.deflate_on_oom
1521        );
1522        assert_eq!(
1523            actual_balloon_cfg.stats_polling_interval_s,
1524            new_balloon_cfg.stats_polling_interval_s
1525        );
1526
1527        let mut vm_resources = default_vm_resources();
1528        vm_resources.balloon = BalloonBuilder::new();
1529        new_balloon_cfg.amount_mib = 256;
1530        vm_resources
1531            .set_balloon_device(new_balloon_cfg)
1532            .unwrap_err();
1533    }
1534
1535    #[test]
1536    fn test_set_entropy_device() {
1537        let mut vm_resources = default_vm_resources();
1538        vm_resources.entropy = EntropyDeviceBuilder::new();
1539        let entropy_device_cfg = EntropyDeviceConfig::default();
1540
1541        assert!(vm_resources.entropy.get().is_none());
1542        vm_resources
1543            .build_entropy_device(entropy_device_cfg.clone())
1544            .unwrap();
1545
1546        let actual_entropy_cfg = vm_resources.entropy.config().unwrap();
1547        assert_eq!(actual_entropy_cfg, entropy_device_cfg);
1548    }
1549
1550    #[test]
1551    fn test_set_boot_source() {
1552        let tmp_file = TempFile::new().unwrap();
1553        let cmdline = "reboot=k panic=1 pci=off nomodule 8250.nr_uarts=0";
1554        let expected_boot_cfg = BootSourceConfig {
1555            kernel_image_path: String::from(tmp_file.as_path().to_str().unwrap()),
1556            initrd_path: Some(String::from(tmp_file.as_path().to_str().unwrap())),
1557            boot_args: Some(cmdline.to_string()),
1558        };
1559
1560        let mut vm_resources = default_vm_resources();
1561        let boot_builder = vm_resources.boot_source.builder.as_ref().unwrap();
1562        let tmp_ino = tmp_file.as_file().metadata().unwrap().st_ino();
1563
1564        assert_ne!(
1565            boot_builder
1566                .cmdline
1567                .as_cstring()
1568                .unwrap()
1569                .as_bytes_with_nul(),
1570            [cmdline.as_bytes(), b"\0"].concat()
1571        );
1572        assert_ne!(
1573            boot_builder.kernel_file.metadata().unwrap().st_ino(),
1574            tmp_ino
1575        );
1576        assert_ne!(
1577            boot_builder
1578                .initrd_file
1579                .as_ref()
1580                .unwrap()
1581                .metadata()
1582                .unwrap()
1583                .st_ino(),
1584            tmp_ino
1585        );
1586
1587        vm_resources.build_boot_source(expected_boot_cfg).unwrap();
1588        let boot_source_builder = vm_resources.boot_source.builder.unwrap();
1589        assert_eq!(
1590            boot_source_builder
1591                .cmdline
1592                .as_cstring()
1593                .unwrap()
1594                .as_bytes_with_nul(),
1595            [cmdline.as_bytes(), b"\0"].concat()
1596        );
1597        assert_eq!(
1598            boot_source_builder.kernel_file.metadata().unwrap().st_ino(),
1599            tmp_ino
1600        );
1601        assert_eq!(
1602            boot_source_builder
1603                .initrd_file
1604                .as_ref()
1605                .unwrap()
1606                .metadata()
1607                .unwrap()
1608                .st_ino(),
1609            tmp_ino
1610        );
1611    }
1612
1613    #[test]
1614    fn test_set_block_device() {
1615        let mut vm_resources = default_vm_resources();
1616        let (mut new_block_device_cfg, _file) = default_block_cfg();
1617        let tmp_file = TempFile::new().unwrap();
1618        new_block_device_cfg.drive_id = "block2".to_string();
1619        new_block_device_cfg.path_on_host = Some(tmp_file.as_path().to_str().unwrap().to_string());
1620        assert_eq!(vm_resources.block.devices.len(), 1);
1621        vm_resources.set_block_device(new_block_device_cfg).unwrap();
1622        assert_eq!(vm_resources.block.devices.len(), 2);
1623    }
1624
1625    #[test]
1626    fn test_set_vsock_device() {
1627        let mut vm_resources = default_vm_resources();
1628        let mut tmp_sock_file = TempFile::new().unwrap();
1629        tmp_sock_file.remove().unwrap();
1630        let new_vsock_cfg = default_config(&tmp_sock_file);
1631        assert!(vm_resources.vsock.get().is_none());
1632        vm_resources.set_vsock_device(new_vsock_cfg).unwrap();
1633        let actual_vsock_cfg = vm_resources.vsock.get().unwrap();
1634        assert_eq!(actual_vsock_cfg.lock().unwrap().id(), VSOCK_DEV_ID);
1635    }
1636
1637    #[test]
1638    fn test_set_net_device() {
1639        let mut vm_resources = default_vm_resources();
1640
1641        // Clone the existing net config in order to obtain a new one.
1642        let mut new_net_device_cfg = default_net_cfg();
1643        new_net_device_cfg.iface_id = "new_net_if".to_string();
1644        new_net_device_cfg.guest_mac = Some(MacAddr::from_str("01:23:45:67:89:0c").unwrap());
1645        new_net_device_cfg.host_dev_name = "dummy_path2".to_string();
1646        assert_eq!(vm_resources.net_builder.len(), 1);
1647
1648        vm_resources.build_net_device(new_net_device_cfg).unwrap();
1649        assert_eq!(vm_resources.net_builder.len(), 2);
1650    }
1651
1652    #[test]
1653    fn test_set_pmem_device() {
1654        let mut vm_resources = default_vm_resources();
1655
1656        let tmp_file = TempFile::new().unwrap();
1657        tmp_file.as_file().set_len(0x1000).unwrap();
1658        let cfg = PmemConfig {
1659            id: "pmem".to_string(),
1660            path_on_host: tmp_file.as_path().to_str().unwrap().to_string(),
1661            ..Default::default()
1662        };
1663        assert_eq!(vm_resources.pmem.devices.len(), 0);
1664        vm_resources.build_pmem_device(cfg).unwrap();
1665        assert_eq!(vm_resources.pmem.devices.len(), 1);
1666    }
1667}