| // Copyright 2022 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use std::collections::BTreeMap; |
| use std::net; |
| use std::path::{Path, PathBuf}; |
| use std::str::FromStr; |
| use std::sync::atomic::{AtomicUsize, Ordering}; |
| |
| use arch::{ |
| set_default_serial_parameters, MsrAction, MsrConfig, MsrFilter, MsrRWType, MsrValueFrom, |
| Pstore, VcpuAffinity, |
| }; |
| use base::{debug, pagesize}; |
| use devices::serial_device::{SerialHardware, SerialParameters}; |
| use devices::virtio::block::block::DiskOption; |
| #[cfg(feature = "audio_cras")] |
| use devices::virtio::cras_backend::Parameters as CrasSndParameters; |
| #[cfg(feature = "gpu")] |
| use devices::virtio::gpu::GpuParameters; |
| #[cfg(any(feature = "video-decoder", feature = "video-encoder"))] |
| use devices::virtio::VideoBackendType; |
| #[cfg(feature = "direct")] |
| use devices::BusRange; |
| use devices::{PciAddress, PciClassCode, StubPciParameters}; |
| use hypervisor::ProtectionType; |
| use resources::AddressRange; |
| use serde::{Deserialize, Serialize}; |
| use uuid::Uuid; |
| use vm_control::BatteryType; |
| #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] |
| use x86_64::{set_enable_pnp_data_msr_config, set_itmt_msr_config}; |
| |
| #[cfg(feature = "audio")] |
| use devices::{Ac97Backend, Ac97Parameters}; |
| |
| use super::{ |
| argument::{self, parse_hex_or_decimal}, |
| check_opt_path, |
| }; |
| |
| cfg_if::cfg_if! { |
| if #[cfg(unix)] { |
| use std::time::Duration; |
| use base::RawDescriptor; |
| use devices::virtio::fs::passthrough; |
| #[cfg(feature = "gpu")] |
| use crate::crosvm::sys::GpuRenderServerParameters; |
| use libc::{getegid, geteuid}; |
| |
| static KVM_PATH: &str = "/dev/kvm"; |
| static VHOST_NET_PATH: &str = "/dev/vhost-net"; |
| static SECCOMP_POLICY_DIR: &str = "/usr/share/policy/crosvm"; |
| } |
| } |
| |
| const ONE_MB: u64 = 1 << 20; |
| const MB_ALIGNED: u64 = ONE_MB - 1; |
| // the max bus number is 256 and each bus occupy 1MB, so the max pcie cfg mmio size = 256M |
| const MAX_PCIE_ECAM_SIZE: u64 = ONE_MB * 256; |
| |
| /// Indicates the location and kind of executable kernel for a VM. |
| #[allow(dead_code)] |
| #[derive(Debug, Serialize, Deserialize)] |
| pub enum Executable { |
| /// An executable intended to be run as a BIOS directly. |
| Bios(PathBuf), |
| /// A elf linux kernel, loaded and executed by crosvm. |
| Kernel(PathBuf), |
| /// Path to a plugin executable that is forked by crosvm. |
| Plugin(PathBuf), |
| } |
| |
| #[derive(Serialize, Deserialize)] |
| pub struct VhostUserOption { |
| pub socket: PathBuf, |
| } |
| |
| impl FromStr for VhostUserOption { |
| type Err = <PathBuf as FromStr>::Err; |
| |
| fn from_str(s: &str) -> Result<Self, Self::Err> { |
| Ok(Self { socket: s.parse()? }) |
| } |
| } |
| |
| #[derive(Serialize, Deserialize)] |
| pub struct VhostUserFsOption { |
| pub socket: PathBuf, |
| pub tag: String, |
| } |
| |
| impl FromStr for VhostUserFsOption { |
| type Err = &'static str; |
| |
| fn from_str(param: &str) -> Result<Self, Self::Err> { |
| // (socket:tag) |
| let mut components = param.split(':'); |
| let socket = PathBuf::from( |
| components |
| .next() |
| .ok_or("missing socket path for `vhost-user-fs`")?, |
| ); |
| let tag = components |
| .next() |
| .ok_or("missing tag for `vhost-user-fs`")? |
| .to_owned(); |
| |
| Ok(Self { socket, tag }) |
| } |
| } |
| |
| #[derive(Serialize, Deserialize)] |
| pub struct VhostUserWlOption { |
| pub socket: PathBuf, |
| pub vm_tube: PathBuf, |
| } |
| |
| impl FromStr for VhostUserWlOption { |
| type Err = &'static str; |
| |
| fn from_str(s: &str) -> Result<Self, Self::Err> { |
| let mut components = s.splitn(2, ":"); |
| let socket = components |
| .next() |
| .map(PathBuf::from) |
| .ok_or("missing socket path")?; |
| let vm_tube = components |
| .next() |
| .map(PathBuf::from) |
| .ok_or("missing vm tube path")?; |
| Ok(Self { vm_tube, socket }) |
| } |
| } |
| |
| /// Options for virtio-vhost-user proxy device. |
| #[derive(Serialize, Deserialize)] |
| pub struct VvuOption { |
| pub socket: PathBuf, |
| pub addr: Option<PciAddress>, |
| pub uuid: Option<Uuid>, |
| } |
| |
| impl FromStr for VvuOption { |
| type Err = String; |
| |
| fn from_str(s: &str) -> Result<Self, Self::Err> { |
| let opts: Vec<_> = s.splitn(2, ',').collect(); |
| let socket = PathBuf::from(opts[0]); |
| let mut vvu_opt = VvuOption { |
| socket, |
| addr: None, |
| uuid: Default::default(), |
| }; |
| |
| if let Some(kvs) = opts.get(1) { |
| for kv in argument::parse_key_value_options("vvu-proxy", kvs, ',') { |
| match kv.key() { |
| "addr" => { |
| let pci_address = kv.value().map_err(|e| e.to_string())?; |
| if vvu_opt.addr.is_some() { |
| return Err("`addr` already given".to_owned()); |
| } |
| |
| vvu_opt.addr = Some(PciAddress::from_str(pci_address).map_err(|e| { |
| invalid_value_err(pci_address, format!("vvu-proxy PCI address: {}", e)) |
| })?); |
| } |
| "uuid" => { |
| let value = kv.value().map_err(|e| e.to_string())?; |
| if vvu_opt.uuid.is_some() { |
| return Err("`uuid` already given".to_owned()); |
| } |
| let uuid = Uuid::parse_str(value).map_err(|e| { |
| invalid_value_err( |
| value, |
| format!("invalid UUID is given for vvu-proxy: {}", e), |
| ) |
| })?; |
| vvu_opt.uuid = Some(uuid); |
| } |
| _ => { |
| kv.invalid_key_err(); |
| } |
| } |
| } |
| } |
| Ok(vvu_opt) |
| } |
| } |
| |
| /// A bind mount for directories in the plugin process. |
| #[derive(Debug, Serialize, Deserialize)] |
| pub struct BindMount { |
| pub src: PathBuf, |
| pub dst: PathBuf, |
| pub writable: bool, |
| } |
| |
| impl FromStr for BindMount { |
| type Err = String; |
| |
| fn from_str(value: &str) -> Result<Self, Self::Err> { |
| let components: Vec<&str> = value.split(':').collect(); |
| if components.is_empty() || components.len() > 3 || components[0].is_empty() { |
| return Err(invalid_value_err( |
| value, |
| "`plugin-mount` should be in a form of: <src>[:[<dst>][:<writable>]]", |
| )); |
| } |
| |
| let src = PathBuf::from(components[0]); |
| if src.is_relative() { |
| return Err(invalid_value_err( |
| components[0], |
| "the source path for `plugin-mount` must be absolute", |
| )); |
| } |
| if !src.exists() { |
| return Err(invalid_value_err( |
| components[0], |
| "the source path for `plugin-mount` does not exist", |
| )); |
| } |
| |
| let dst = PathBuf::from(match components.get(1) { |
| None | Some(&"") => components[0], |
| Some(path) => path, |
| }); |
| if dst.is_relative() { |
| return Err(invalid_value_err( |
| components[1], |
| "the destination path for `plugin-mount` must be absolute", |
| )); |
| } |
| |
| let writable: bool = match components.get(2) { |
| None => false, |
| Some(s) => s.parse().map_err(|_| { |
| invalid_value_err( |
| components[2], |
| "the <writable> component for `plugin-mount` is not valid bool", |
| ) |
| })?, |
| }; |
| |
| Ok(BindMount { src, dst, writable }) |
| } |
| } |
| |
| /// A mapping of linux group IDs for the plugin process. |
| #[cfg(feature = "plugin")] |
| #[derive(Debug, Deserialize, Serialize)] |
| pub struct GidMap { |
| pub inner: base::platform::Gid, |
| pub outer: base::platform::Gid, |
| pub count: u32, |
| } |
| |
| #[cfg(feature = "plugin")] |
| impl FromStr for GidMap { |
| type Err = String; |
| |
| fn from_str(value: &str) -> Result<Self, Self::Err> { |
| let components: Vec<&str> = value.split(':').collect(); |
| if components.is_empty() || components.len() > 3 || components[0].is_empty() { |
| return Err(invalid_value_err( |
| value, |
| "`plugin-gid-map` must have exactly 3 components: <inner>[:[<outer>][:<count>]]", |
| )); |
| } |
| |
| let inner: base::platform::Gid = components[0].parse().map_err(|_| { |
| invalid_value_err( |
| components[0], |
| "the <inner> component for `plugin-gid-map` is not valid gid", |
| ) |
| })?; |
| |
| let outer: base::platform::Gid = match components.get(1) { |
| None | Some(&"") => inner, |
| Some(s) => s.parse().map_err(|_| { |
| invalid_value_err( |
| components[1], |
| "the <outer> component for `plugin-gid-map` is not valid gid", |
| ) |
| })?, |
| }; |
| |
| let count: u32 = match components.get(2) { |
| None => 1, |
| Some(s) => s.parse().map_err(|_| { |
| invalid_value_err( |
| components[2], |
| "the <count> component for `plugin-gid-map` is not valid number", |
| ) |
| })?, |
| }; |
| |
| Ok(GidMap { |
| inner, |
| outer, |
| count, |
| }) |
| } |
| } |
| |
| /// Direct IO forwarding options |
| #[cfg(feature = "direct")] |
| #[derive(Debug, Deserialize, Serialize)] |
| pub struct DirectIoOption { |
| pub path: PathBuf, |
| pub ranges: Vec<BusRange>, |
| } |
| |
| pub const DEFAULT_TOUCH_DEVICE_HEIGHT: u32 = 1024; |
| pub const DEFAULT_TOUCH_DEVICE_WIDTH: u32 = 1280; |
| |
| #[derive(Serialize, Deserialize)] |
| pub struct TouchDeviceOption { |
| path: PathBuf, |
| width: Option<u32>, |
| height: Option<u32>, |
| default_width: u32, |
| default_height: u32, |
| } |
| |
| impl TouchDeviceOption { |
| pub fn new(path: PathBuf) -> TouchDeviceOption { |
| TouchDeviceOption { |
| path, |
| width: None, |
| height: None, |
| default_width: DEFAULT_TOUCH_DEVICE_WIDTH, |
| default_height: DEFAULT_TOUCH_DEVICE_HEIGHT, |
| } |
| } |
| |
| /// Getter for the path to the input event streams. |
| #[cfg_attr(windows, allow(unused))] |
| pub fn get_path(&self) -> &Path { |
| self.path.as_path() |
| } |
| |
| /// When a user specifies the parameters for a touch device, width and height are optional. |
| /// If the width and height are missing, default values are used. Default values can be set |
| /// dynamically, for example from the display sizes specified by the gpu argument. |
| #[cfg(feature = "gpu")] |
| pub fn set_default_size(&mut self, width: u32, height: u32) { |
| self.default_width = width; |
| self.default_height = height; |
| } |
| |
| /// Setter for the width specified by the user. |
| pub fn set_width(&mut self, width: u32) { |
| self.width.replace(width); |
| } |
| |
| /// Setter for the height specified by the user. |
| pub fn set_height(&mut self, height: u32) { |
| self.height.replace(height); |
| } |
| |
| /// If the user specifies the size, use it. Otherwise, use the default values. |
| pub fn get_size(&self) -> (u32, u32) { |
| ( |
| self.width.unwrap_or(self.default_width), |
| self.height.unwrap_or(self.default_height), |
| ) |
| } |
| } |
| |
| impl FromStr for TouchDeviceOption { |
| type Err = &'static str; |
| |
| fn from_str(s: &str) -> Result<Self, Self::Err> { |
| let mut it = s.split(':'); |
| let mut touch_spec = TouchDeviceOption::new(PathBuf::from(it.next().unwrap().to_owned())); |
| if let Some(width) = it.next() { |
| touch_spec.set_width(width.trim().parse().unwrap()); |
| } |
| if let Some(height) = it.next() { |
| touch_spec.set_height(height.trim().parse().unwrap()); |
| } |
| Ok(touch_spec) |
| } |
| } |
| |
| #[derive(Eq, PartialEq, Serialize, Deserialize)] |
| pub enum SharedDirKind { |
| FS, |
| P9, |
| } |
| |
| impl FromStr for SharedDirKind { |
| type Err = &'static str; |
| |
| fn from_str(s: &str) -> Result<Self, Self::Err> { |
| use SharedDirKind::*; |
| match s { |
| "fs" | "FS" => Ok(FS), |
| "9p" | "9P" | "p9" | "P9" => Ok(P9), |
| _ => Err("invalid file system type"), |
| } |
| } |
| } |
| |
| impl Default for SharedDirKind { |
| fn default() -> SharedDirKind { |
| SharedDirKind::P9 |
| } |
| } |
| |
| #[cfg(unix)] |
| pub struct SharedDir { |
| pub src: PathBuf, |
| pub tag: String, |
| pub kind: SharedDirKind, |
| pub uid_map: String, |
| pub gid_map: String, |
| pub fs_cfg: passthrough::Config, |
| pub p9_cfg: p9::Config, |
| } |
| |
| #[cfg(unix)] |
| impl Default for SharedDir { |
| fn default() -> SharedDir { |
| SharedDir { |
| src: Default::default(), |
| tag: Default::default(), |
| kind: Default::default(), |
| uid_map: format!("0 {} 1", unsafe { geteuid() }), |
| gid_map: format!("0 {} 1", unsafe { getegid() }), |
| fs_cfg: Default::default(), |
| p9_cfg: Default::default(), |
| } |
| } |
| } |
| |
| #[cfg(unix)] |
| impl FromStr for SharedDir { |
| type Err = &'static str; |
| |
| fn from_str(param: &str) -> Result<Self, Self::Err> { |
| // This is formatted as multiple fields, each separated by ":". The first 2 fields are |
| // fixed (src:tag). The rest may appear in any order: |
| // |
| // * type=TYPE - must be one of "p9" or "fs" (default: p9) |
| // * uidmap=UIDMAP - a uid map in the format "inner outer count[,inner outer count]" |
| // (default: "0 <current euid> 1") |
| // * gidmap=GIDMAP - a gid map in the same format as uidmap |
| // (default: "0 <current egid> 1") |
| // * privileged_quota_uids=UIDS - Space-separated list of privileged uid values. When |
| // performing quota-related operations, these UIDs are treated as if they have |
| // CAP_FOWNER. |
| // * timeout=TIMEOUT - a timeout value in seconds, which indicates how long attributes |
| // and directory contents should be considered valid (default: 5) |
| // * cache=CACHE - one of "never", "always", or "auto" (default: auto) |
| // * writeback=BOOL - indicates whether writeback caching should be enabled (default: false) |
| let mut components = param.split(':'); |
| let src = PathBuf::from( |
| components |
| .next() |
| .ok_or("missing source path for `shared-dir`")?, |
| ); |
| let tag = components |
| .next() |
| .ok_or("missing tag for `shared-dir`")? |
| .to_owned(); |
| |
| if !src.is_dir() { |
| return Err("source path for `shared-dir` must be a directory"); |
| } |
| |
| let mut shared_dir = SharedDir { |
| src, |
| tag, |
| ..Default::default() |
| }; |
| for opt in components { |
| let mut o = opt.splitn(2, '='); |
| let kind = o.next().ok_or("`shared-dir` options must not be empty")?; |
| let value = o |
| .next() |
| .ok_or("`shared-dir` options must be of the form `kind=value`")?; |
| |
| match kind { |
| "type" => { |
| shared_dir.kind = value |
| .parse() |
| .map_err(|_| "`type` must be one of `fs` or `9p`")? |
| } |
| "uidmap" => shared_dir.uid_map = value.into(), |
| "gidmap" => shared_dir.gid_map = value.into(), |
| #[cfg(feature = "chromeos")] |
| "privileged_quota_uids" => { |
| shared_dir.fs_cfg.privileged_quota_uids = |
| value.split(' ').map(|s| s.parse().unwrap()).collect(); |
| } |
| "timeout" => { |
| let seconds = value.parse().map_err(|_| "`timeout` must be an integer")?; |
| |
| let dur = Duration::from_secs(seconds); |
| shared_dir.fs_cfg.entry_timeout = dur; |
| shared_dir.fs_cfg.attr_timeout = dur; |
| } |
| "cache" => { |
| let policy = value |
| .parse() |
| .map_err(|_| "`cache` must be one of `never`, `always`, or `auto`")?; |
| shared_dir.fs_cfg.cache_policy = policy; |
| } |
| "writeback" => { |
| let writeback = value.parse().map_err(|_| "`writeback` must be a boolean")?; |
| shared_dir.fs_cfg.writeback = writeback; |
| } |
| "rewrite-security-xattrs" => { |
| let rewrite_security_xattrs = value |
| .parse() |
| .map_err(|_| "`rewrite-security-xattrs` must be a boolean")?; |
| shared_dir.fs_cfg.rewrite_security_xattrs = rewrite_security_xattrs; |
| } |
| "ascii_casefold" => { |
| let ascii_casefold = value |
| .parse() |
| .map_err(|_| "`ascii_casefold` must be a boolean")?; |
| shared_dir.fs_cfg.ascii_casefold = ascii_casefold; |
| shared_dir.p9_cfg.ascii_casefold = ascii_casefold; |
| } |
| "dax" => { |
| let use_dax = value.parse().map_err(|_| "`dax` must be a boolean")?; |
| shared_dir.fs_cfg.use_dax = use_dax; |
| } |
| "posix_acl" => { |
| let posix_acl = value.parse().map_err(|_| "`posix_acl` must be a boolean")?; |
| shared_dir.fs_cfg.posix_acl = posix_acl; |
| } |
| _ => return Err("unrecognized option for `shared-dir`"), |
| } |
| } |
| |
| Ok(shared_dir) |
| } |
| } |
| |
| #[derive(Debug, Serialize, Deserialize)] |
| pub struct FileBackedMappingParameters { |
| pub address: u64, |
| pub size: u64, |
| pub path: PathBuf, |
| pub offset: u64, |
| pub writable: bool, |
| pub sync: bool, |
| } |
| |
| #[derive(Clone, Deserialize, Serialize)] |
| pub struct HostPcieRootPortParameters { |
| pub host_path: PathBuf, |
| pub hp_gpe: Option<u32>, |
| } |
| |
| #[derive(Debug, Serialize, Deserialize, PartialEq, serde_keyvalue::FromKeyValues)] |
| #[serde(deny_unknown_fields)] |
| pub struct JailConfig { |
| pub pivot_root: PathBuf, |
| #[cfg(unix)] |
| pub seccomp_policy_dir: Option<PathBuf>, |
| pub seccomp_log_failures: bool, |
| } |
| |
| impl Default for JailConfig { |
| fn default() -> Self { |
| JailConfig { |
| pivot_root: PathBuf::from(option_env!("DEFAULT_PIVOT_ROOT").unwrap_or("/var/empty")), |
| #[cfg(unix)] |
| seccomp_policy_dir: Some(PathBuf::from(SECCOMP_POLICY_DIR)), |
| seccomp_log_failures: false, |
| } |
| } |
| } |
| |
| pub fn parse_mmio_address_range(s: &str) -> Result<Vec<AddressRange>, String> { |
| s.split(",") |
| .map(|s| { |
| let r: Vec<&str> = s.split("-").collect(); |
| if r.len() != 2 { |
| return Err(invalid_value_err(s, "invalid range")); |
| } |
| let parse = |s: &str| -> Result<u64, String> { |
| match parse_hex_or_decimal(s) { |
| Ok(v) => Ok(v), |
| Err(_) => { |
| return Err(invalid_value_err(s, "expected u64 value")); |
| } |
| } |
| }; |
| Ok(AddressRange { |
| start: parse(r[0])?, |
| end: parse(r[1])?, |
| }) |
| }) |
| .collect() |
| } |
| |
| #[cfg(any(feature = "video-decoder", feature = "video-encoder"))] |
| pub fn parse_video_options(s: &str) -> Result<VideoBackendType, String> { |
| const VALID_VIDEO_BACKENDS: &[&str] = &[ |
| #[cfg(feature = "libvda")] |
| "libvda", |
| #[cfg(feature = "ffmpeg")] |
| "ffmpeg", |
| #[cfg(feature = "vaapi")] |
| "vaapi", |
| ]; |
| |
| match s { |
| "" => { |
| cfg_if::cfg_if! { |
| if #[cfg(feature = "libvda")] { |
| Ok(VideoBackendType::Libvda) |
| } else if #[cfg(feature = "vaapi")] { |
| Ok(VideoBackendType::Vaapi) |
| } else if #[cfg(feature = "ffmpeg")] { |
| Ok(VideoBackendType::Ffmpeg) |
| } else { |
| // Cannot be reached because at least one video backend needs to be enabled for |
| // the decoder to be compiled. |
| unreachable!() |
| } |
| } |
| } |
| #[cfg(feature = "libvda")] |
| "libvda" => Ok(VideoBackendType::Libvda), |
| #[cfg(feature = "libvda")] |
| "libvda-vd" => Ok(VideoBackendType::LibvdaVd), |
| #[cfg(feature = "ffmpeg")] |
| "ffmpeg" => Ok(VideoBackendType::Ffmpeg), |
| #[cfg(feature = "vaapi")] |
| "vaapi" => Ok(VideoBackendType::Vaapi), |
| _ => Err(invalid_value_err( |
| s, |
| format!("should be one of ({})", VALID_VIDEO_BACKENDS.join("|")), |
| )), |
| } |
| } |
| |
| pub fn parse_pstore(value: &str) -> Result<Pstore, String> { |
| let components: Vec<&str> = value.split(',').collect(); |
| if components.len() != 2 { |
| return Err(invalid_value_err( |
| value, |
| "pstore must have exactly 2 components: path=<path>,size=<size>", |
| )); |
| } |
| Ok(Pstore { |
| path: { |
| if components[0].len() <= 5 || !components[0].starts_with("path=") { |
| return Err(invalid_value_err( |
| components[0], |
| "pstore path must follow with `path=`", |
| )); |
| }; |
| PathBuf::from(&components[0][5..]) |
| }, |
| size: { |
| if components[1].len() <= 5 || !components[1].starts_with("size=") { |
| return Err(invalid_value_err( |
| components[1], |
| "pstore size must follow with `size=`", |
| )); |
| }; |
| components[1][5..] |
| .parse() |
| .map_err(|_| invalid_value_err(value, "pstore size must be an integer"))? |
| }, |
| }) |
| } |
| |
| pub fn parse_userspace_msr_options(value: &str) -> Result<(u32, MsrConfig), String> { |
| let mut rw_type: Option<MsrRWType> = None; |
| let mut action: Option<MsrAction> = None; |
| let mut from = MsrValueFrom::RWFromRunningCPU; |
| let mut filter = MsrFilter::Default; |
| |
| let mut options = super::argument::parse_key_value_options("userspace-msr", value, ','); |
| let index: u32 = options |
| .next() |
| .ok_or(String::from("userspace-msr: expected index"))? |
| .key_numeric() |
| .map_err(|e| e.to_string())?; |
| |
| for opt in options { |
| match opt.key() { |
| "type" => match opt.value().map_err(|e| e.to_string())? { |
| "r" => rw_type = Some(MsrRWType::ReadOnly), |
| "w" => rw_type = Some(MsrRWType::WriteOnly), |
| "rw" | "wr" => rw_type = Some(MsrRWType::ReadWrite), |
| _ => { |
| return Err(String::from("bad type")); |
| } |
| }, |
| "action" => match opt.value().map_err(|e| e.to_string())? { |
| "pass" => action = Some(MsrAction::MsrPassthrough), |
| "emu" => action = Some(MsrAction::MsrEmulate), |
| _ => return Err(String::from("bad action")), |
| }, |
| "from" => match opt.value().map_err(|e| e.to_string())? { |
| "cpu0" => from = MsrValueFrom::RWFromCPU0, |
| _ => return Err(String::from("bad from")), |
| }, |
| "filter" => match opt.value().map_err(|e| e.to_string())? { |
| "yes" => filter = MsrFilter::Override, |
| "no" => filter = MsrFilter::Default, |
| _ => return Err(String::from("bad filter")), |
| }, |
| |
| _ => return Err(opt.invalid_key_err().to_string()), |
| } |
| } |
| |
| let rw_type = rw_type.ok_or(String::from("userspace-msr: type is required"))?; |
| |
| let action = action.ok_or(String::from("userspace-msr: action is required"))?; |
| |
| Ok(( |
| index, |
| MsrConfig { |
| rw_type, |
| action, |
| from, |
| filter, |
| }, |
| )) |
| } |
| |
| pub fn parse_serial_options(s: &str) -> Result<SerialParameters, String> { |
| let serial_setting: SerialParameters = from_key_values(s)?; |
| |
| if serial_setting.stdin && serial_setting.input.is_some() { |
| return Err("Cannot specify both stdin and input options".to_string()); |
| } |
| if serial_setting.num < 1 { |
| return Err(invalid_value_err( |
| serial_setting.num.to_string(), |
| "Serial port num must be at least 1", |
| )); |
| } |
| |
| if serial_setting.hardware == SerialHardware::Serial && serial_setting.num > 4 { |
| return Err(invalid_value_err( |
| format!("{}", serial_setting.num), |
| "Serial port num must be 4 or less", |
| )); |
| } |
| |
| Ok(serial_setting) |
| } |
| |
| #[cfg(feature = "plugin")] |
| pub fn parse_plugin_mount_option(value: &str) -> Result<BindMount, String> { |
| let components: Vec<&str> = value.split(':').collect(); |
| if components.is_empty() || components.len() > 3 || components[0].is_empty() { |
| return Err(invalid_value_err( |
| value, |
| "`plugin-mount` should be in a form of: <src>[:[<dst>][:<writable>]]", |
| )); |
| } |
| |
| let src = PathBuf::from(components[0]); |
| if src.is_relative() { |
| return Err(invalid_value_err( |
| components[0], |
| "the source path for `plugin-mount` must be absolute", |
| )); |
| } |
| if !src.exists() { |
| return Err(invalid_value_err( |
| components[0], |
| "the source path for `plugin-mount` does not exist", |
| )); |
| } |
| |
| let dst = PathBuf::from(match components.get(1) { |
| None | Some(&"") => components[0], |
| Some(path) => path, |
| }); |
| if dst.is_relative() { |
| return Err(invalid_value_err( |
| components[1], |
| "the destination path for `plugin-mount` must be absolute", |
| )); |
| } |
| |
| let writable: bool = match components.get(2) { |
| None => false, |
| Some(s) => s.parse().map_err(|_| { |
| invalid_value_err( |
| components[2], |
| "the <writable> component for `plugin-mount` is not valid bool", |
| ) |
| })?, |
| }; |
| |
| Ok(BindMount { src, dst, writable }) |
| } |
| |
| #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] |
| pub fn parse_memory_region(value: &str) -> Result<AddressRange, String> { |
| let paras: Vec<&str> = value.split(',').collect(); |
| if paras.len() != 2 { |
| return Err(invalid_value_err( |
| value, |
| "pcie-ecam must have exactly 2 parameters: ecam_base,ecam_size", |
| )); |
| } |
| let base = parse_hex_or_decimal(paras[0]).map_err(|_| { |
| invalid_value_err( |
| value, |
| "pcie-ecam, the first parameter base should be integer", |
| ) |
| })?; |
| let mut len = parse_hex_or_decimal(paras[1]).map_err(|_| { |
| invalid_value_err( |
| value, |
| "pcie-ecam, the second parameter size should be integer", |
| ) |
| })?; |
| |
| if (base & MB_ALIGNED != 0) || (len & MB_ALIGNED != 0) { |
| return Err(invalid_value_err( |
| value, |
| "pcie-ecam, the base and len should be aligned to 1MB", |
| )); |
| } |
| |
| if len > MAX_PCIE_ECAM_SIZE { |
| len = MAX_PCIE_ECAM_SIZE; |
| } |
| |
| if base + len >= 0x1_0000_0000 { |
| return Err(invalid_value_err( |
| value, |
| "pcie-ecam, the end address couldn't beyond 4G", |
| )); |
| } |
| |
| if base % len != 0 { |
| return Err(invalid_value_err( |
| value, |
| "pcie-ecam, base should be multiple of len", |
| )); |
| } |
| |
| if let Some(range) = AddressRange::from_start_and_size(base, len) { |
| Ok(range) |
| } else { |
| Err(invalid_value_err( |
| value, |
| "pcie-ecam must be representable as AddressRange", |
| )) |
| } |
| } |
| |
| #[cfg(feature = "direct")] |
| pub fn parse_pcie_root_port_params(value: &str) -> Result<HostPcieRootPortParameters, String> { |
| let opts: Vec<_> = value.split(',').collect(); |
| if opts.len() > 2 { |
| return Err(invalid_value_err( |
| value, |
| "pcie-root-port has maxmimum two arguments", |
| )); |
| } |
| let pcie_path = PathBuf::from(opts[0]); |
| if !pcie_path.exists() { |
| return Err(invalid_value_err( |
| value, |
| "the pcie root port path does not exist", |
| )); |
| } |
| if !pcie_path.is_dir() { |
| return Err(invalid_value_err( |
| value, |
| "the pcie root port path should be directory", |
| )); |
| } |
| |
| let hp_gpe = if opts.len() == 2 { |
| let gpes: Vec<&str> = opts[1].split('=').collect(); |
| if gpes.len() != 2 || gpes[0] != "hp_gpe" { |
| return Err(invalid_value_err(value, "it should be hp_gpe=Num")); |
| } |
| match gpes[1].parse::<u32>() { |
| Ok(gpe) => Some(gpe), |
| Err(_) => { |
| return Err(invalid_value_err( |
| value, |
| "host hp gpe must be a non-negative integer", |
| )); |
| } |
| } |
| } else { |
| None |
| }; |
| |
| Ok(HostPcieRootPortParameters { |
| host_path: pcie_path, |
| hp_gpe, |
| }) |
| } |
| |
| pub fn parse_bus_id_addr(v: &str) -> Result<(u8, u8, u16, u16), String> { |
| debug!("parse_bus_id_addr: {}", v); |
| let mut ids = v.split(':'); |
| let errorre = move |item| move |e| format!("{}: {}", item, e); |
| match (ids.next(), ids.next(), ids.next(), ids.next()) { |
| (Some(bus_id), Some(addr), Some(vid), Some(pid)) => { |
| let bus_id = bus_id.parse::<u8>().map_err(errorre("bus_id"))?; |
| let addr = addr.parse::<u8>().map_err(errorre("addr"))?; |
| let vid = u16::from_str_radix(vid, 16).map_err(errorre("vid"))?; |
| let pid = u16::from_str_radix(pid, 16).map_err(errorre("pid"))?; |
| Ok((bus_id, addr, vid, pid)) |
| } |
| _ => Err(String::from("BUS_ID:ADDR:BUS_NUM:DEV_NUM")), |
| } |
| } |
| |
| #[cfg(feature = "audio")] |
| pub fn parse_ac97_options(s: &str) -> Result<Ac97Parameters, String> { |
| let mut ac97_params: Ac97Parameters = Default::default(); |
| |
| let opts = s |
| .split(',') |
| .map(|frag| frag.split('=')) |
| .map(|mut kv| (kv.next().unwrap_or(""), kv.next().unwrap_or(""))); |
| |
| for (k, v) in opts { |
| match k { |
| "backend" => { |
| ac97_params.backend = v |
| .parse::<Ac97Backend>() |
| .map_err(|e| invalid_value_err(v, e))?; |
| } |
| "capture" => { |
| ac97_params.capture = v |
| .parse::<bool>() |
| .map_err(|e| format!("invalid capture option: {}", e))?; |
| } |
| _ => { |
| super::sys::config::parse_ac97_options(&mut ac97_params, k, v)?; |
| } |
| } |
| } |
| |
| super::sys::config::check_ac97_backend(&ac97_params)?; |
| |
| Ok(ac97_params) |
| } |
| |
| pub fn invalid_value_err<T: AsRef<str>, S: ToString>(value: T, expected: S) -> String { |
| format!("invalid value {}: {}", value.as_ref(), expected.to_string()) |
| } |
| |
| pub fn parse_battery_options(s: &str) -> Result<BatteryType, String> { |
| let mut battery_type: BatteryType = Default::default(); |
| |
| let opts = s |
| .split(',') |
| .map(|frag| frag.split('=')) |
| .map(|mut kv| (kv.next().unwrap_or(""), kv.next().unwrap_or(""))); |
| |
| for (k, v) in opts { |
| match k { |
| "type" => match v.parse::<BatteryType>() { |
| Ok(type_) => battery_type = type_, |
| Err(e) => { |
| return Err(invalid_value_err(v, e)); |
| } |
| }, |
| "" => {} |
| _ => { |
| return Err(format!("battery parameter {}", k)); |
| } |
| } |
| } |
| |
| Ok(battery_type) |
| } |
| |
| pub fn parse_cpu_capacity(s: &str) -> Result<BTreeMap<usize, u32>, String> { |
| let mut cpu_capacity: BTreeMap<usize, u32> = BTreeMap::default(); |
| for cpu_pair in s.split(',') { |
| let assignment: Vec<&str> = cpu_pair.split('=').collect(); |
| if assignment.len() != 2 { |
| return Err(invalid_value_err(cpu_pair, "invalid CPU capacity syntax")); |
| } |
| let cpu = assignment[0].parse().map_err(|_| { |
| invalid_value_err(assignment[0], "CPU index must be a non-negative integer") |
| })?; |
| let capacity = assignment[1].parse().map_err(|_| { |
| invalid_value_err(assignment[1], "CPU capacity must be a non-negative integer") |
| })?; |
| if cpu_capacity.insert(cpu, capacity).is_some() { |
| return Err(invalid_value_err(cpu_pair, "CPU index must be unique")); |
| } |
| } |
| Ok(cpu_capacity) |
| } |
| |
| /// Parse a comma-separated list of CPU numbers and ranges and convert it to a Vec of CPU numbers. |
| pub fn parse_cpu_set(s: &str) -> Result<Vec<usize>, String> { |
| let mut cpuset = Vec::new(); |
| for part in s.split(',') { |
| let range: Vec<&str> = part.split('-').collect(); |
| if range.is_empty() || range.len() > 2 { |
| return Err(invalid_value_err(part, "invalid list syntax")); |
| } |
| let first_cpu: usize = range[0] |
| .parse() |
| .map_err(|_| invalid_value_err(part, "CPU index must be a non-negative integer"))?; |
| let last_cpu: usize = if range.len() == 2 { |
| range[1] |
| .parse() |
| .map_err(|_| invalid_value_err(part, "CPU index must be a non-negative integer"))? |
| } else { |
| first_cpu |
| }; |
| |
| if last_cpu < first_cpu { |
| return Err(invalid_value_err( |
| part, |
| "CPU ranges must be from low to high", |
| )); |
| } |
| |
| for cpu in first_cpu..=last_cpu { |
| cpuset.push(cpu); |
| } |
| } |
| Ok(cpuset) |
| } |
| |
| pub fn from_key_values<'a, T: Deserialize<'a>>(value: &'a str) -> Result<T, String> { |
| serde_keyvalue::from_key_values(value).map_err(|e| e.to_string()) |
| } |
| |
| pub fn numbered_disk_option(value: &str) -> Result<(usize, DiskOption), String> { |
| static DISK_COUNTER: AtomicUsize = AtomicUsize::new(0); |
| let disk = from_key_values(value)?; |
| Ok((DISK_COUNTER.fetch_add(1, Ordering::Relaxed), disk)) |
| } |
| |
| /// Parse a list of guest to host CPU mappings. |
| /// |
| /// Each mapping consists of a single guest CPU index mapped to one or more host CPUs in the form |
| /// accepted by `parse_cpu_set`: |
| /// |
| /// `<GUEST-CPU>=<HOST-CPU-SET>[:<GUEST-CPU>=<HOST-CPU-SET>[:...]]` |
| pub fn parse_cpu_affinity(s: &str) -> Result<VcpuAffinity, String> { |
| if s.contains('=') { |
| let mut affinity_map = BTreeMap::new(); |
| for cpu_pair in s.split(':') { |
| let assignment: Vec<&str> = cpu_pair.split('=').collect(); |
| if assignment.len() != 2 { |
| return Err(invalid_value_err( |
| cpu_pair, |
| "invalid VCPU assignment syntax", |
| )); |
| } |
| let guest_cpu = assignment[0].parse().map_err(|_| { |
| invalid_value_err(assignment[0], "CPU index must be a non-negative integer") |
| })?; |
| let host_cpu_set = parse_cpu_set(assignment[1])?; |
| if affinity_map.insert(guest_cpu, host_cpu_set).is_some() { |
| return Err(invalid_value_err(cpu_pair, "VCPU index must be unique")); |
| } |
| } |
| Ok(VcpuAffinity::PerVcpu(affinity_map)) |
| } else { |
| Ok(VcpuAffinity::Global(parse_cpu_set(s)?)) |
| } |
| } |
| |
| #[cfg(feature = "direct")] |
| pub fn parse_direct_io_options(s: &str) -> Result<DirectIoOption, String> { |
| let parts: Vec<&str> = s.splitn(2, '@').collect(); |
| if parts.len() != 2 { |
| return Err(invalid_value_err( |
| s, |
| "missing port range, use /path@X-Y,Z,.. syntax", |
| )); |
| } |
| let path = PathBuf::from(parts[0]); |
| if !path.exists() { |
| return Err(invalid_value_err(parts[0], "the path does not exist")); |
| }; |
| let ranges: Result<Vec<BusRange>, String> = parts[1] |
| .split(',') |
| .map(|frag| frag.split('-')) |
| .map(|mut range| { |
| let base = range |
| .next() |
| .map(|v| parse_hex_or_decimal(v)) |
| .map_or(Ok(None), |r| r.map(Some)); |
| let last = range |
| .next() |
| .map(|v| parse_hex_or_decimal(v)) |
| .map_or(Ok(None), |r| r.map(Some)); |
| (base, last) |
| }) |
| .map(|range| match range { |
| (Ok(Some(base)), Ok(None)) => Ok(BusRange { base, len: 1 }), |
| (Ok(Some(base)), Ok(Some(last))) => Ok(BusRange { |
| base, |
| len: last.saturating_sub(base).saturating_add(1), |
| }), |
| (Err(_), _) => Err(invalid_value_err(s, "invalid base range value")), |
| (_, Err(_)) => Err(invalid_value_err(s, "invalid last range value")), |
| _ => Err(invalid_value_err(s, "invalid range format")), |
| }) |
| .collect(); |
| Ok(DirectIoOption { |
| path, |
| ranges: ranges?, |
| }) |
| } |
| |
| pub fn parse_file_backed_mapping(s: &str) -> Result<FileBackedMappingParameters, String> { |
| let mut address = None; |
| let mut size = None; |
| let mut path = None; |
| let mut offset = None; |
| let mut writable = false; |
| let mut sync = false; |
| let mut align = false; |
| for opt in super::argument::parse_key_value_options("file-backed-mapping", s, ',') { |
| match opt.key() { |
| "addr" => address = Some(opt.parse_numeric::<u64>().map_err(|e| e.to_string())?), |
| "size" => size = Some(opt.parse_numeric::<u64>().map_err(|e| e.to_string())?), |
| "path" => path = Some(PathBuf::from(opt.value().map_err(|e| e.to_string())?)), |
| "offset" => offset = Some(opt.parse_numeric::<u64>().map_err(|e| e.to_string())?), |
| "ro" => writable = !opt.parse_or::<bool>(true).map_err(|e| e.to_string())?, |
| "rw" => writable = opt.parse_or::<bool>(true).map_err(|e| e.to_string())?, |
| "sync" => sync = opt.parse_or::<bool>(true).map_err(|e| e.to_string())?, |
| "align" => align = opt.parse_or::<bool>(true).map_err(|e| e.to_string())?, |
| _ => return Err(opt.invalid_key_err().to_string()), |
| } |
| } |
| |
| let (address, path, size) = match (address, path, size) { |
| (Some(a), Some(p), Some(s)) => (a, p, s), |
| _ => { |
| return Err(String::from( |
| "file-backed-mapping: address, size, and path parameters are required", |
| )) |
| } |
| }; |
| |
| let pagesize_mask = pagesize() as u64 - 1; |
| let aligned_address = address & !pagesize_mask; |
| let aligned_size = ((address + size + pagesize_mask) & !pagesize_mask) - aligned_address; |
| |
| if !align && (aligned_address != address || aligned_size != size) { |
| return Err(invalid_value_err( |
| s, |
| "addr and size parameters must be page size aligned", |
| )); |
| } |
| |
| Ok(FileBackedMappingParameters { |
| address: aligned_address, |
| size: aligned_size, |
| path, |
| offset: offset.unwrap_or(0), |
| writable, |
| sync, |
| }) |
| } |
| |
| pub fn executable_is_plugin(executable: &Option<Executable>) -> bool { |
| matches!(executable, Some(Executable::Plugin(_))) |
| } |
| |
| pub fn parse_stub_pci_parameters(s: &str) -> Result<StubPciParameters, String> { |
| let mut options = super::argument::parse_key_value_options("stub-pci-device", s, ','); |
| let addr = options |
| .next() |
| .ok_or(String::from("stub-pci-device: expected device address"))? |
| .key(); |
| let mut params = StubPciParameters { |
| address: PciAddress::from_str(addr).map_err(|e| { |
| invalid_value_err( |
| addr, |
| format!("stub-pci-device: expected PCI address: {}", e), |
| ) |
| })?, |
| vendor_id: 0, |
| device_id: 0, |
| class: PciClassCode::Other, |
| subclass: 0, |
| programming_interface: 0, |
| subsystem_device_id: 0, |
| subsystem_vendor_id: 0, |
| revision_id: 0, |
| }; |
| for opt in options { |
| match opt.key() { |
| "vendor" => params.vendor_id = opt.parse_numeric::<u16>().map_err(|e| e.to_string())?, |
| "device" => params.device_id = opt.parse_numeric::<u16>().map_err(|e| e.to_string())?, |
| "class" => { |
| let class = opt.parse_numeric::<u32>().map_err(|e| e.to_string())?; |
| params.class = PciClassCode::try_from((class >> 16) as u8) |
| .map_err(|_| String::from("Unknown class code"))?; |
| params.subclass = (class >> 8) as u8; |
| params.programming_interface = class as u8; |
| } |
| "multifunction" => {} // Ignore but allow the multifunction option for compatibility. |
| "subsystem_vendor" => { |
| params.subsystem_vendor_id = |
| opt.parse_numeric::<u16>().map_err(|e| e.to_string())? |
| } |
| "subsystem_device" => { |
| params.subsystem_device_id = |
| opt.parse_numeric::<u16>().map_err(|e| e.to_string())? |
| } |
| "revision" => { |
| params.revision_id = opt.parse_numeric::<u8>().map_err(|e| e.to_string())? |
| } |
| _ => return Err(opt.invalid_key_err().to_string()), |
| } |
| } |
| |
| Ok(params) |
| } |
| |
| // BTreeMaps serialize fine, as long as their keys are trivial types. A tuple does not |
| // work, hence the need to convert to/from a vector form. |
| mod serde_serial_params { |
| use super::*; |
| use serde::{Deserializer, Serializer}; |
| use std::iter::FromIterator; |
| |
| pub fn serialize<S>( |
| params: &BTreeMap<(SerialHardware, u8), SerialParameters>, |
| ser: S, |
| ) -> Result<S::Ok, S::Error> |
| where |
| S: Serializer, |
| { |
| let v: Vec<(&(SerialHardware, u8), &SerialParameters)> = params.iter().collect(); |
| serde::Serialize::serialize(&v, ser) |
| } |
| |
| pub fn deserialize<'a, D>( |
| de: D, |
| ) -> Result<BTreeMap<(SerialHardware, u8), SerialParameters>, D::Error> |
| where |
| D: Deserializer<'a>, |
| { |
| let params: Vec<((SerialHardware, u8), SerialParameters)> = |
| serde::Deserialize::deserialize(de)?; |
| Ok(BTreeMap::from_iter(params.into_iter())) |
| } |
| } |
| |
| /// Aggregate of all configurable options for a running VM. |
| #[derive(Serialize, Deserialize)] |
| #[remain::sorted] |
| pub struct Config { |
| #[cfg(feature = "audio")] |
| pub ac97_parameters: Vec<Ac97Parameters>, |
| pub acpi_tables: Vec<PathBuf>, |
| pub android_fstab: Option<PathBuf>, |
| pub balloon: bool, |
| pub balloon_bias: i64, |
| pub balloon_control: Option<PathBuf>, |
| pub battery_type: Option<BatteryType>, |
| pub cid: Option<u64>, |
| #[cfg(unix)] |
| pub coiommu_param: Option<devices::CoIommuParameters>, |
| pub cpu_capacity: BTreeMap<usize, u32>, // CPU index -> capacity |
| pub cpu_clusters: Vec<Vec<usize>>, |
| #[cfg(feature = "audio_cras")] |
| #[serde(skip)] |
| pub cras_snds: Vec<CrasSndParameters>, |
| pub delay_rt: bool, |
| #[cfg(feature = "direct")] |
| pub direct_edge_irq: Vec<u32>, |
| #[cfg(feature = "direct")] |
| pub direct_gpe: Vec<u32>, |
| #[cfg(feature = "direct")] |
| pub direct_level_irq: Vec<u32>, |
| #[cfg(feature = "direct")] |
| pub direct_mmio: Option<DirectIoOption>, |
| #[cfg(feature = "direct")] |
| pub direct_pmio: Option<DirectIoOption>, |
| pub disable_virtio_intx: bool, |
| pub disks: Vec<DiskOption>, |
| pub display_window_keyboard: bool, |
| pub display_window_mouse: bool, |
| pub dmi_path: Option<PathBuf>, |
| pub enable_pnp_data: bool, |
| pub executable_path: Option<Executable>, |
| pub file_backed_mappings: Vec<FileBackedMappingParameters>, |
| pub force_calibrated_tsc_leaf: bool, |
| pub force_s2idle: bool, |
| #[cfg(all(target_arch = "x86_64", feature = "gdb"))] |
| pub gdb: Option<u32>, |
| #[cfg(feature = "gpu")] |
| pub gpu_parameters: Option<GpuParameters>, |
| #[cfg(all(unix, feature = "gpu"))] |
| pub gpu_render_server_parameters: Option<GpuRenderServerParameters>, |
| pub host_cpu_topology: bool, |
| pub host_ip: Option<net::Ipv4Addr>, |
| pub hugepages: bool, |
| pub init_memory: Option<u64>, |
| pub initrd_path: Option<PathBuf>, |
| pub itmt: bool, |
| pub jail_config: Option<JailConfig>, |
| #[cfg(unix)] |
| pub kvm_device_path: PathBuf, |
| #[cfg(unix)] |
| pub lock_guest_memory: bool, |
| pub mac_address: Option<net_util::MacAddress>, |
| pub memory: Option<u64>, |
| pub memory_file: Option<PathBuf>, |
| pub mmio_address_ranges: Vec<AddressRange>, |
| pub net_vq_pairs: Option<u16>, |
| pub netmask: Option<net::Ipv4Addr>, |
| pub no_i8042: bool, |
| pub no_rtc: bool, |
| pub no_smt: bool, |
| pub params: Vec<String>, |
| #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] |
| pub pci_low_start: Option<u64>, |
| #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] |
| pub pcie_ecam: Option<AddressRange>, |
| #[cfg(feature = "direct")] |
| pub pcie_rp: Vec<HostPcieRootPortParameters>, |
| pub per_vm_core_scheduling: bool, |
| #[cfg(feature = "plugin")] |
| pub plugin_gid_maps: Vec<GidMap>, |
| pub plugin_mounts: Vec<BindMount>, |
| pub plugin_root: Option<PathBuf>, |
| pub pmem_devices: Vec<DiskOption>, |
| pub privileged_vm: bool, |
| pub protected_vm: ProtectionType, |
| pub pstore: Option<Pstore>, |
| /// Must be `Some` iff `protected_vm == ProtectionType::UnprotectedWithFirmware`. |
| pub pvm_fw: Option<PathBuf>, |
| pub rng: bool, |
| pub rt_cpus: Vec<usize>, |
| #[serde(with = "serde_serial_params")] |
| pub serial_parameters: BTreeMap<(SerialHardware, u8), SerialParameters>, |
| #[cfg(unix)] |
| #[serde(skip)] |
| pub shared_dirs: Vec<SharedDir>, |
| pub socket_path: Option<PathBuf>, |
| #[cfg(feature = "tpm")] |
| pub software_tpm: bool, |
| #[cfg(feature = "audio")] |
| pub sound: Option<PathBuf>, |
| pub split_irqchip: bool, |
| pub strict_balloon: bool, |
| pub stub_pci_devices: Vec<StubPciParameters>, |
| pub swiotlb: Option<u64>, |
| #[cfg(unix)] |
| pub tap_fd: Vec<RawDescriptor>, |
| pub tap_name: Vec<String>, |
| #[cfg(target_os = "android")] |
| pub task_profiles: Vec<String>, |
| pub usb: bool, |
| pub userspace_msr: BTreeMap<u32, MsrConfig>, |
| pub vcpu_affinity: Option<VcpuAffinity>, |
| pub vcpu_cgroup_path: Option<PathBuf>, |
| pub vcpu_count: Option<usize>, |
| #[cfg(unix)] |
| pub vfio: Vec<super::sys::config::VfioCommand>, |
| pub vhost_net: bool, |
| #[cfg(unix)] |
| pub vhost_net_device_path: PathBuf, |
| pub vhost_user_blk: Vec<VhostUserOption>, |
| pub vhost_user_console: Vec<VhostUserOption>, |
| pub vhost_user_fs: Vec<VhostUserFsOption>, |
| pub vhost_user_gpu: Vec<VhostUserOption>, |
| pub vhost_user_mac80211_hwsim: Option<VhostUserOption>, |
| pub vhost_user_net: Vec<VhostUserOption>, |
| #[cfg(feature = "audio")] |
| pub vhost_user_snd: Vec<VhostUserOption>, |
| pub vhost_user_vsock: Vec<VhostUserOption>, |
| pub vhost_user_wl: Vec<VhostUserWlOption>, |
| #[cfg(unix)] |
| pub vhost_vsock_device: Option<PathBuf>, |
| #[cfg(feature = "video-decoder")] |
| pub video_dec: Option<VideoBackendType>, |
| #[cfg(feature = "video-encoder")] |
| pub video_enc: Option<VideoBackendType>, |
| pub virtio_input_evdevs: Vec<PathBuf>, |
| pub virtio_keyboard: Vec<PathBuf>, |
| pub virtio_mice: Vec<PathBuf>, |
| pub virtio_multi_touch: Vec<TouchDeviceOption>, |
| pub virtio_single_touch: Vec<TouchDeviceOption>, |
| pub virtio_switches: Vec<PathBuf>, |
| pub virtio_trackpad: Vec<TouchDeviceOption>, |
| #[cfg(all(feature = "tpm", feature = "chromeos", target_arch = "x86_64"))] |
| pub vtpm_proxy: bool, |
| pub vvu_proxy: Vec<VvuOption>, |
| pub wayland_socket_paths: BTreeMap<String, PathBuf>, |
| pub x_display: Option<String>, |
| } |
| |
| impl Default for Config { |
| fn default() -> Config { |
| Config { |
| #[cfg(feature = "audio")] |
| ac97_parameters: Vec::new(), |
| acpi_tables: Vec::new(), |
| android_fstab: None, |
| balloon: true, |
| balloon_bias: 0, |
| balloon_control: None, |
| battery_type: None, |
| cid: None, |
| #[cfg(unix)] |
| coiommu_param: None, |
| #[cfg(feature = "audio_cras")] |
| cras_snds: Vec::new(), |
| cpu_capacity: BTreeMap::new(), |
| cpu_clusters: Vec::new(), |
| delay_rt: false, |
| #[cfg(feature = "direct")] |
| direct_edge_irq: Vec::new(), |
| #[cfg(feature = "direct")] |
| direct_gpe: Vec::new(), |
| #[cfg(feature = "direct")] |
| direct_level_irq: Vec::new(), |
| #[cfg(feature = "direct")] |
| direct_mmio: None, |
| #[cfg(feature = "direct")] |
| direct_pmio: None, |
| disks: Vec::new(), |
| disable_virtio_intx: false, |
| display_window_keyboard: false, |
| display_window_mouse: false, |
| dmi_path: None, |
| enable_pnp_data: false, |
| executable_path: None, |
| file_backed_mappings: Vec::new(), |
| force_calibrated_tsc_leaf: false, |
| force_s2idle: false, |
| #[cfg(all(target_arch = "x86_64", feature = "gdb"))] |
| gdb: None, |
| #[cfg(feature = "gpu")] |
| gpu_parameters: None, |
| #[cfg(all(unix, feature = "gpu"))] |
| gpu_render_server_parameters: None, |
| host_cpu_topology: false, |
| host_ip: None, |
| hugepages: false, |
| init_memory: None, |
| initrd_path: None, |
| itmt: false, |
| jail_config: if !cfg!(feature = "default-no-sandbox") { |
| Some(Default::default()) |
| } else { |
| None |
| }, |
| #[cfg(unix)] |
| kvm_device_path: PathBuf::from(KVM_PATH), |
| #[cfg(unix)] |
| lock_guest_memory: false, |
| mac_address: None, |
| memory: None, |
| memory_file: None, |
| mmio_address_ranges: Vec::new(), |
| net_vq_pairs: None, |
| netmask: None, |
| no_i8042: false, |
| no_rtc: false, |
| no_smt: false, |
| params: Vec::new(), |
| #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] |
| pci_low_start: None, |
| #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] |
| pcie_ecam: None, |
| #[cfg(feature = "direct")] |
| pcie_rp: Vec::new(), |
| per_vm_core_scheduling: false, |
| #[cfg(feature = "plugin")] |
| plugin_gid_maps: Vec::new(), |
| plugin_mounts: Vec::new(), |
| plugin_root: None, |
| pmem_devices: Vec::new(), |
| privileged_vm: false, |
| protected_vm: ProtectionType::Unprotected, |
| pstore: None, |
| pvm_fw: None, |
| rng: true, |
| rt_cpus: Vec::new(), |
| serial_parameters: BTreeMap::new(), |
| #[cfg(unix)] |
| shared_dirs: Vec::new(), |
| socket_path: None, |
| #[cfg(feature = "tpm")] |
| software_tpm: false, |
| #[cfg(feature = "audio")] |
| sound: None, |
| split_irqchip: false, |
| strict_balloon: false, |
| stub_pci_devices: Vec::new(), |
| swiotlb: None, |
| #[cfg(unix)] |
| tap_fd: Vec::new(), |
| tap_name: Vec::new(), |
| #[cfg(target_os = "android")] |
| task_profiles: Vec::new(), |
| usb: true, |
| userspace_msr: BTreeMap::new(), |
| vcpu_affinity: None, |
| vcpu_cgroup_path: None, |
| vcpu_count: None, |
| #[cfg(unix)] |
| vfio: Vec::new(), |
| vhost_net: false, |
| #[cfg(unix)] |
| vhost_net_device_path: PathBuf::from(VHOST_NET_PATH), |
| vhost_user_blk: Vec::new(), |
| vhost_user_console: Vec::new(), |
| vhost_user_fs: Vec::new(), |
| vhost_user_gpu: Vec::new(), |
| vhost_user_mac80211_hwsim: None, |
| vhost_user_net: Vec::new(), |
| #[cfg(feature = "audio")] |
| vhost_user_snd: Vec::new(), |
| vhost_user_vsock: Vec::new(), |
| vhost_user_wl: Vec::new(), |
| #[cfg(unix)] |
| vhost_vsock_device: None, |
| #[cfg(feature = "video-decoder")] |
| video_dec: None, |
| #[cfg(feature = "video-encoder")] |
| video_enc: None, |
| virtio_input_evdevs: Vec::new(), |
| virtio_keyboard: Vec::new(), |
| virtio_mice: Vec::new(), |
| virtio_multi_touch: Vec::new(), |
| virtio_single_touch: Vec::new(), |
| virtio_switches: Vec::new(), |
| virtio_trackpad: Vec::new(), |
| #[cfg(all(feature = "tpm", feature = "chromeos", target_arch = "x86_64"))] |
| vtpm_proxy: false, |
| vvu_proxy: Vec::new(), |
| wayland_socket_paths: BTreeMap::new(), |
| x_display: None, |
| } |
| } |
| } |
| |
| pub fn validate_config(cfg: &mut Config) -> std::result::Result<(), String> { |
| if !match &cfg.executable_path { |
| Some(Executable::Bios(p)) => p, |
| Some(Executable::Kernel(p)) => p, |
| Some(Executable::Plugin(p)) => p, |
| None => { |
| return Err("Executable is not specified".to_string()); |
| } |
| } |
| .exists() |
| { |
| return Err("Executable does not exist".to_string()); |
| } |
| |
| #[cfg(unix)] |
| if !cfg.kvm_device_path.exists() { |
| return Err(format!("kvm path {:?} does not exist", cfg.kvm_device_path)); |
| }; |
| |
| check_opt_path!(cfg.android_fstab); |
| |
| check_opt_path!(cfg.vcpu_cgroup_path); |
| |
| check_opt_path!(cfg.balloon_control); |
| |
| check_opt_path!(cfg.dmi_path); |
| |
| for disk in cfg.disks.iter() { |
| if !disk.path.exists() { |
| return Err(format!("Disk path {:?} does not exist", disk.path)); |
| } |
| } |
| |
| for disk in cfg.pmem_devices.iter() { |
| if !disk.path.exists() { |
| return Err(format!("PMEM device path {:?} does not exist", disk.path)); |
| } |
| } |
| |
| for dev in cfg.virtio_input_evdevs.iter() { |
| if !dev.exists() { |
| return Err(format!("Virtio evdev device path {:?} does not exist", dev)); |
| } |
| } |
| |
| for p in cfg.acpi_tables.iter() { |
| if !p.exists() { |
| return Err(format!("ACPI table path {:?} does not exist", p)); |
| } |
| if !p.is_file() { |
| return Err(String::from("the acpi-table path should be a file")); |
| } |
| } |
| |
| #[cfg(feature = "audio")] |
| { |
| check_opt_path!(cfg.sound); |
| } |
| |
| if cfg.plugin_root.is_some() && !executable_is_plugin(&cfg.executable_path) { |
| return Err("`plugin-root` requires `plugin`".to_string()); |
| } |
| |
| #[cfg(feature = "gpu")] |
| { |
| crate::crosvm::sys::validate_gpu_config(cfg)?; |
| } |
| #[cfg(all(target_arch = "x86_64", feature = "gdb"))] |
| if cfg.gdb.is_some() && cfg.vcpu_count.unwrap_or(1) != 1 { |
| return Err("`gdb` requires the number of vCPU to be 1".to_string()); |
| } |
| if cfg.host_cpu_topology { |
| if cfg.no_smt { |
| return Err( |
| "`host-cpu-topology` cannot be set at the same time as `no_smt`, since \ |
| the smt of the Guest is the same as that of the Host when \ |
| `host-cpu-topology` is set." |
| .to_string(), |
| ); |
| } |
| |
| let pcpu_count = |
| base::number_of_logical_cores().expect("Could not read number of logical cores"); |
| if let Some(vcpu_count) = cfg.vcpu_count { |
| if pcpu_count != vcpu_count { |
| return Err(format!( |
| "`host-cpu-topology` requires the count of vCPUs({}) to equal the \ |
| count of CPUs({}) on host.", |
| vcpu_count, pcpu_count |
| )); |
| } |
| } else { |
| cfg.vcpu_count = Some(pcpu_count); |
| } |
| |
| match &cfg.vcpu_affinity { |
| None => { |
| let mut affinity_map = BTreeMap::new(); |
| for cpu_id in 0..cfg.vcpu_count.unwrap() { |
| affinity_map.insert(cpu_id, vec![cpu_id]); |
| } |
| cfg.vcpu_affinity = Some(VcpuAffinity::PerVcpu(affinity_map)); |
| } |
| _ => { |
| return Err( |
| "`host-cpu-topology` requires not to set `cpu-affinity` at the same time" |
| .to_string(), |
| ); |
| } |
| } |
| } else { |
| // TODO(b/215297064): Support generic cpuaffinity if there's a need. |
| if !cfg.userspace_msr.is_empty() { |
| for (_, msr_config) in cfg.userspace_msr.iter() { |
| if msr_config.from == MsrValueFrom::RWFromRunningCPU { |
| return Err( |
| "`userspace-msr` must set `cpu0` if `host-cpu-topology` is not set" |
| .to_string(), |
| ); |
| } |
| } |
| } |
| } |
| if cfg.enable_pnp_data { |
| if !cfg.host_cpu_topology { |
| return Err( |
| "setting `enable_pnp_data` must require `host-cpu-topology` is set previously." |
| .to_string(), |
| ); |
| } |
| |
| #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] |
| set_enable_pnp_data_msr_config(cfg.itmt, &mut cfg.userspace_msr) |
| .map_err(|e| format!("MSR can't be passed through {}", e))?; |
| } |
| #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] |
| if cfg.itmt { |
| use std::collections::BTreeSet; |
| // ITMT only works on the case each vCPU is 1:1 mapping to a pCPU. |
| // `host-cpu-topology` has already set this 1:1 mapping. If no |
| // `host-cpu-topology`, we need check the cpu affinity setting. |
| if !cfg.host_cpu_topology { |
| // only VcpuAffinity::PerVcpu supports setting cpu affinity |
| // for each vCPU. |
| if let Some(VcpuAffinity::PerVcpu(v)) = &cfg.vcpu_affinity { |
| // ITMT allows more pCPUs than vCPUs. |
| if v.len() != cfg.vcpu_count.unwrap_or(1) { |
| return Err("`itmt` requires affinity to be set for every vCPU.".to_string()); |
| } |
| |
| let mut pcpu_set = BTreeSet::new(); |
| for cpus in v.values() { |
| if cpus.len() != 1 { |
| return Err( |
| "`itmt` requires affinity to be set 1 pCPU for 1 vCPU.".to_owned() |
| ); |
| } |
| // Ensure that each vCPU corresponds to a different pCPU to avoid pCPU sharing, |
| // otherwise it will seriously affect the ITMT scheduling optimization effect. |
| if !pcpu_set.insert(cpus[0]) { |
| return Err( |
| "`cpu_host` requires affinity to be set different pVPU for each vCPU." |
| .to_owned(), |
| ); |
| } |
| } |
| } else { |
| return Err("`itmt` requires affinity to be set for every vCPU.".to_string()); |
| } |
| } |
| set_itmt_msr_config(&mut cfg.userspace_msr) |
| .map_err(|e| format!("the cpu doesn't support itmt {}", e))?; |
| } |
| |
| if !cfg.balloon && cfg.balloon_control.is_some() { |
| return Err("'balloon-control' requires enabled balloon".to_string()); |
| } |
| |
| #[cfg(unix)] |
| if cfg.lock_guest_memory && cfg.jail_config.is_none() { |
| return Err("'lock-guest-memory' and 'disable-sandbox' are mutually exclusive".to_string()); |
| } |
| |
| set_default_serial_parameters( |
| &mut cfg.serial_parameters, |
| !cfg.vhost_user_console.is_empty(), |
| ); |
| |
| // Validate platform specific things |
| super::sys::config::validate_config(cfg) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use argh::FromArgs; |
| |
| #[test] |
| fn parse_cpu_set_single() { |
| assert_eq!(parse_cpu_set("123").expect("parse failed"), vec![123]); |
| } |
| |
| #[test] |
| fn parse_cpu_set_list() { |
| assert_eq!( |
| parse_cpu_set("0,1,2,3").expect("parse failed"), |
| vec![0, 1, 2, 3] |
| ); |
| } |
| |
| #[test] |
| fn parse_cpu_set_range() { |
| assert_eq!( |
| parse_cpu_set("0-3").expect("parse failed"), |
| vec![0, 1, 2, 3] |
| ); |
| } |
| |
| #[test] |
| fn parse_cpu_set_list_of_ranges() { |
| assert_eq!( |
| parse_cpu_set("3-4,7-9,18").expect("parse failed"), |
| vec![3, 4, 7, 8, 9, 18] |
| ); |
| } |
| |
| #[test] |
| fn parse_cpu_set_repeated() { |
| // For now, allow duplicates - they will be handled gracefully by the vec to cpu_set_t conversion. |
| assert_eq!(parse_cpu_set("1,1,1").expect("parse failed"), vec![1, 1, 1]); |
| } |
| |
| #[test] |
| fn parse_cpu_set_negative() { |
| // Negative CPU numbers are not allowed. |
| parse_cpu_set("-3").expect_err("parse should have failed"); |
| } |
| |
| #[test] |
| fn parse_cpu_set_reverse_range() { |
| // Ranges must be from low to high. |
| parse_cpu_set("5-2").expect_err("parse should have failed"); |
| } |
| |
| #[test] |
| fn parse_cpu_set_open_range() { |
| parse_cpu_set("3-").expect_err("parse should have failed"); |
| } |
| |
| #[test] |
| fn parse_cpu_set_extra_comma() { |
| parse_cpu_set("0,1,2,").expect_err("parse should have failed"); |
| } |
| |
| #[test] |
| fn parse_cpu_affinity_global() { |
| assert_eq!( |
| parse_cpu_affinity("0,5-7,9").expect("parse failed"), |
| VcpuAffinity::Global(vec![0, 5, 6, 7, 9]), |
| ); |
| } |
| |
| #[test] |
| fn parse_cpu_affinity_per_vcpu_one_to_one() { |
| let mut expected_map = BTreeMap::new(); |
| expected_map.insert(0, vec![0]); |
| expected_map.insert(1, vec![1]); |
| expected_map.insert(2, vec![2]); |
| expected_map.insert(3, vec![3]); |
| assert_eq!( |
| parse_cpu_affinity("0=0:1=1:2=2:3=3").expect("parse failed"), |
| VcpuAffinity::PerVcpu(expected_map), |
| ); |
| } |
| |
| #[test] |
| fn parse_cpu_affinity_per_vcpu_sets() { |
| let mut expected_map = BTreeMap::new(); |
| expected_map.insert(0, vec![0, 1, 2]); |
| expected_map.insert(1, vec![3, 4, 5]); |
| expected_map.insert(2, vec![6, 7, 8]); |
| assert_eq!( |
| parse_cpu_affinity("0=0,1,2:1=3-5:2=6,7-8").expect("parse failed"), |
| VcpuAffinity::PerVcpu(expected_map), |
| ); |
| } |
| |
| #[cfg(feature = "audio_cras")] |
| #[test] |
| fn parse_ac97_vaild() { |
| parse_ac97_options("backend=cras").expect("parse should have succeded"); |
| } |
| |
| #[cfg(feature = "audio")] |
| #[test] |
| fn parse_ac97_null_vaild() { |
| parse_ac97_options("backend=null").expect("parse should have succeded"); |
| } |
| |
| #[cfg(feature = "audio_cras")] |
| #[test] |
| fn parse_ac97_capture_vaild() { |
| parse_ac97_options("backend=cras,capture=true").expect("parse should have succeded"); |
| } |
| |
| #[cfg(feature = "audio_cras")] |
| #[test] |
| fn parse_ac97_client_type() { |
| parse_ac97_options("backend=cras,capture=true,client_type=crosvm") |
| .expect("parse should have succeded"); |
| parse_ac97_options("backend=cras,capture=true,client_type=arcvm") |
| .expect("parse should have succeded"); |
| parse_ac97_options("backend=cras,capture=true,client_type=none") |
| .expect_err("parse should have failed"); |
| } |
| |
| #[test] |
| fn parse_serial_vaild() { |
| parse_serial_options("type=syslog,num=1,console=true,stdin=true") |
| .expect("parse should have succeded"); |
| } |
| |
| #[test] |
| fn parse_serial_virtio_console_vaild() { |
| parse_serial_options("type=syslog,num=5,console=true,stdin=true,hardware=virtio-console") |
| .expect("parse should have succeded"); |
| } |
| |
| #[test] |
| fn parse_serial_valid_no_num() { |
| parse_serial_options("type=syslog").expect("parse should have succeded"); |
| } |
| |
| #[test] |
| fn parse_serial_equals_in_value() { |
| let parsed = parse_serial_options("type=syslog,path=foo=bar==.log") |
| .expect("parse should have succeded"); |
| assert_eq!(parsed.path, Some(PathBuf::from("foo=bar==.log"))); |
| } |
| |
| #[test] |
| fn parse_serial_invalid_type() { |
| parse_serial_options("type=wormhole,num=1").expect_err("parse should have failed"); |
| } |
| |
| #[test] |
| fn parse_serial_invalid_num_upper() { |
| parse_serial_options("type=syslog,num=5").expect_err("parse should have failed"); |
| } |
| |
| #[test] |
| fn parse_serial_invalid_num_lower() { |
| parse_serial_options("type=syslog,num=0").expect_err("parse should have failed"); |
| } |
| |
| #[test] |
| fn parse_serial_virtio_console_invalid_num_lower() { |
| parse_serial_options("type=syslog,hardware=virtio-console,num=0") |
| .expect_err("parse should have failed"); |
| } |
| |
| #[test] |
| fn parse_serial_invalid_num_string() { |
| parse_serial_options("type=syslog,num=number3").expect_err("parse should have failed"); |
| } |
| |
| #[test] |
| fn parse_serial_invalid_option() { |
| parse_serial_options("type=syslog,speed=lightspeed").expect_err("parse should have failed"); |
| } |
| |
| #[test] |
| fn parse_serial_invalid_two_stdin() { |
| assert!(TryInto::<Config>::try_into( |
| crate::crosvm::cmdline::RunCommand::from_args( |
| &[], |
| &[ |
| "--serial", |
| "num=1,type=stdout,stdin=true", |
| "--serial", |
| "num=2,type=stdout,stdin=true" |
| ] |
| ) |
| .unwrap() |
| ) |
| .is_err()) |
| } |
| |
| #[test] |
| fn parse_plugin_mount_invalid() { |
| "".parse::<BindMount>().expect_err("parse should fail"); |
| "/dev/null:/dev/null:true:false" |
| .parse::<BindMount>() |
| .expect_err("parse should fail because too many arguments"); |
| |
| "null:/dev/null:true" |
| .parse::<BindMount>() |
| .expect_err("parse should fail because source is not absolute"); |
| "/dev/null:null:true" |
| .parse::<BindMount>() |
| .expect_err("parse should fail because source is not absolute"); |
| "/dev/null:null:blah" |
| .parse::<BindMount>() |
| .expect_err("parse should fail because flag is not boolean"); |
| } |
| |
| #[cfg(feature = "plugin")] |
| #[test] |
| fn parse_plugin_gid_map_valid() { |
| let opt: GidMap = "1:2:3".parse().expect("parse should succeed"); |
| assert_eq!(opt.inner, 1); |
| assert_eq!(opt.outer, 2); |
| assert_eq!(opt.count, 3); |
| } |
| |
| #[cfg(feature = "plugin")] |
| #[test] |
| fn parse_plugin_gid_map_valid_shorthand() { |
| let opt: GidMap = "1".parse().expect("parse should succeed"); |
| assert_eq!(opt.inner, 1); |
| assert_eq!(opt.outer, 1); |
| assert_eq!(opt.count, 1); |
| |
| let opt: GidMap = "1:2".parse().expect("parse should succeed"); |
| assert_eq!(opt.inner, 1); |
| assert_eq!(opt.outer, 2); |
| assert_eq!(opt.count, 1); |
| |
| let opt: GidMap = "1::3".parse().expect("parse should succeed"); |
| assert_eq!(opt.inner, 1); |
| assert_eq!(opt.outer, 1); |
| assert_eq!(opt.count, 3); |
| } |
| |
| #[cfg(feature = "plugin")] |
| #[test] |
| fn parse_plugin_gid_map_invalid() { |
| "".parse::<GidMap>().expect_err("parse should fail"); |
| "1:2:3:4" |
| .parse::<GidMap>() |
| .expect_err("parse should fail because too many arguments"); |
| "blah:2:3" |
| .parse::<GidMap>() |
| .expect_err("parse should fail because inner is not a number"); |
| "1:blah:3" |
| .parse::<GidMap>() |
| .expect_err("parse should fail because outer is not a number"); |
| "1:2:blah" |
| .parse::<GidMap>() |
| .expect_err("parse should fail because count is not a number"); |
| } |
| |
| #[test] |
| fn parse_battery_vaild() { |
| parse_battery_options("type=goldfish").expect("parse should have succeded"); |
| } |
| |
| #[test] |
| fn parse_battery_vaild_no_type() { |
| parse_battery_options("").expect("parse should have succeded"); |
| } |
| |
| #[test] |
| fn parse_battery_invaild_parameter() { |
| parse_battery_options("tyep=goldfish").expect_err("parse should have failed"); |
| } |
| |
| #[test] |
| fn parse_battery_invaild_type_value() { |
| parse_battery_options("type=xxx").expect_err("parse should have failed"); |
| } |
| |
| #[test] |
| fn parse_stub_pci() { |
| let params = parse_stub_pci_parameters("0000:01:02.3,vendor=0xfffe,device=0xfffd,class=0xffc1c2,subsystem_vendor=0xfffc,subsystem_device=0xfffb,revision=0xa").unwrap(); |
| assert_eq!(params.address.bus, 1); |
| assert_eq!(params.address.dev, 2); |
| assert_eq!(params.address.func, 3); |
| assert_eq!(params.vendor_id, 0xfffe); |
| assert_eq!(params.device_id, 0xfffd); |
| assert_eq!(params.class as u8, PciClassCode::Other as u8); |
| assert_eq!(params.subclass, 0xc1); |
| assert_eq!(params.programming_interface, 0xc2); |
| assert_eq!(params.subsystem_vendor_id, 0xfffc); |
| assert_eq!(params.subsystem_device_id, 0xfffb); |
| assert_eq!(params.revision_id, 0xa); |
| } |
| |
| #[cfg(feature = "direct")] |
| #[test] |
| fn parse_direct_io_options_valid() { |
| // Use /dev/zero here which is usually available on any systems, |
| // /dev/mem may not. |
| let params = parse_direct_io_options("/dev/zero@1,100-110").unwrap(); |
| assert_eq!(params.path.to_str(), Some("/dev/zero")); |
| assert_eq!(params.ranges[0], BusRange { base: 1, len: 1 }); |
| assert_eq!(params.ranges[1], BusRange { base: 100, len: 11 }); |
| } |
| |
| #[cfg(feature = "direct")] |
| #[test] |
| fn parse_direct_io_options_hex() { |
| // Use /dev/zero here which is usually available on any systems, |
| // /dev/mem may not. |
| let params = parse_direct_io_options("/dev/zero@1,0x10,100-110,0x10-0x20").unwrap(); |
| assert_eq!(params.path.to_str(), Some("/dev/zero")); |
| assert_eq!(params.ranges[0], BusRange { base: 1, len: 1 }); |
| assert_eq!(params.ranges[1], BusRange { base: 0x10, len: 1 }); |
| assert_eq!(params.ranges[2], BusRange { base: 100, len: 11 }); |
| assert_eq!( |
| params.ranges[3], |
| BusRange { |
| base: 0x10, |
| len: 0x11 |
| } |
| ); |
| } |
| |
| #[cfg(feature = "direct")] |
| #[test] |
| fn parse_direct_io_options_invalid() { |
| // Use /dev/zero here which is usually available on any systems, |
| // /dev/mem may not. |
| assert!(parse_direct_io_options("/dev/zero@0y10") |
| .unwrap_err() |
| .to_string() |
| .contains("invalid base range value")); |
| |
| assert!(parse_direct_io_options("/dev/zero@") |
| .unwrap_err() |
| .to_string() |
| .contains("invalid base range value")); |
| } |
| |
| #[test] |
| fn parse_file_backed_mapping_valid() { |
| let params = parse_file_backed_mapping( |
| "addr=0x1000,size=0x2000,path=/dev/mem,offset=0x3000,ro,rw,sync", |
| ) |
| .unwrap(); |
| assert_eq!(params.address, 0x1000); |
| assert_eq!(params.size, 0x2000); |
| assert_eq!(params.path, PathBuf::from("/dev/mem")); |
| assert_eq!(params.offset, 0x3000); |
| assert!(params.writable); |
| assert!(params.sync); |
| } |
| |
| #[test] |
| fn parse_file_backed_mapping_incomplete() { |
| assert!(parse_file_backed_mapping("addr=0x1000,size=0x2000") |
| .unwrap_err() |
| .contains("required")); |
| assert!(parse_file_backed_mapping("size=0x2000,path=/dev/mem") |
| .unwrap_err() |
| .contains("required")); |
| assert!(parse_file_backed_mapping("addr=0x1000,path=/dev/mem") |
| .unwrap_err() |
| .contains("required")); |
| } |
| |
| #[test] |
| fn parse_file_backed_mapping_unaligned() { |
| assert!( |
| parse_file_backed_mapping("addr=0x1001,size=0x2000,path=/dev/mem") |
| .unwrap_err() |
| .contains("aligned") |
| ); |
| assert!( |
| parse_file_backed_mapping("addr=0x1000,size=0x2001,path=/dev/mem") |
| .unwrap_err() |
| .contains("aligned") |
| ); |
| } |
| |
| #[test] |
| fn parse_file_backed_mapping_align() { |
| let params = |
| parse_file_backed_mapping("addr=0x3042,size=0xff0,path=/dev/mem,align").unwrap(); |
| assert_eq!(params.address, 0x3000); |
| assert_eq!(params.size, 0x2000); |
| } |
| |
| #[test] |
| fn parse_userspace_msr_options_test() { |
| let (pass_cpu0_index, pass_cpu0_cfg) = |
| parse_userspace_msr_options("0x10,type=w,action=pass,filter=yes").unwrap(); |
| assert_eq!(pass_cpu0_index, 0x10); |
| assert_eq!(pass_cpu0_cfg.rw_type, MsrRWType::WriteOnly); |
| assert_eq!(pass_cpu0_cfg.action, MsrAction::MsrPassthrough); |
| assert_eq!(pass_cpu0_cfg.filter, MsrFilter::Override); |
| |
| let (pass_cpu0_index, pass_cpu0_cfg) = |
| parse_userspace_msr_options("0x10,type=r,action=pass,from=cpu0").unwrap(); |
| assert_eq!(pass_cpu0_index, 0x10); |
| assert_eq!(pass_cpu0_cfg.rw_type, MsrRWType::ReadOnly); |
| assert_eq!(pass_cpu0_cfg.action, MsrAction::MsrPassthrough); |
| assert_eq!(pass_cpu0_cfg.from, MsrValueFrom::RWFromCPU0); |
| |
| let (pass_cpus_index, pass_cpus_cfg) = |
| parse_userspace_msr_options("0x10,type=rw,action=emu").unwrap(); |
| assert_eq!(pass_cpus_index, 0x10); |
| assert_eq!(pass_cpus_cfg.rw_type, MsrRWType::ReadWrite); |
| assert_eq!(pass_cpus_cfg.action, MsrAction::MsrEmulate); |
| assert_eq!(pass_cpus_cfg.from, MsrValueFrom::RWFromRunningCPU); |
| |
| assert!(parse_userspace_msr_options("0x10,action=none").is_err()); |
| assert!(parse_userspace_msr_options("0x10,action=pass").is_err()); |
| assert!(parse_userspace_msr_options("0x10,type=none").is_err()); |
| assert!(parse_userspace_msr_options("0x10,type=rw").is_err()); |
| assert!(parse_userspace_msr_options("0x10,type=w,action=pass,from=f").is_err()); |
| assert!(parse_userspace_msr_options("0x10").is_err()); |
| assert!(parse_userspace_msr_options("hoge").is_err()); |
| } |
| } |