blob: 7c917b0165758cadf0f9a5d77a63f3c548483327 [file] [log] [blame]
// Copyright 2021, The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Struct for VM configuration with JSON (de)serialization and AIDL parcelables
use android_system_virtualizationservice::{
aidl::android::system::virtualizationservice::CpuTopology::CpuTopology,
aidl::android::system::virtualizationservice::DiskImage::DiskImage as AidlDiskImage,
aidl::android::system::virtualizationservice::Partition::Partition as AidlPartition,
aidl::android::system::virtualizationservice::VirtualMachineAppConfig::DebugLevel::DebugLevel,
aidl::android::system::virtualizationservice::VirtualMachineConfig::VirtualMachineConfig,
aidl::android::system::virtualizationservice::VirtualMachineRawConfig::VirtualMachineRawConfig,
binder::ParcelFileDescriptor,
};
use anyhow::{anyhow, bail, Context, Error, Result};
use semver::VersionReq;
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
use std::fs::{File, OpenOptions};
use std::io::BufReader;
use std::num::NonZeroU32;
use std::path::{Path, PathBuf};
/// Configuration for a particular VM to be started.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct VmConfig {
/// The name of VM.
pub name: Option<String>,
/// The filename of the kernel image, if any.
pub kernel: Option<PathBuf>,
/// The filename of the initial ramdisk for the kernel, if any.
pub initrd: Option<PathBuf>,
/// Parameters to pass to the kernel. As far as the VMM and boot protocol are concerned this is
/// just a string, but typically it will contain multiple parameters separated by spaces.
pub params: Option<String>,
/// The bootloader to use. If this is supplied then the kernel and initrd must not be supplied;
/// the bootloader is instead responsibly for loading the kernel from one of the disks.
pub bootloader: Option<PathBuf>,
/// Disk images to be made available to the VM.
#[serde(default)]
pub disks: Vec<DiskImage>,
/// Whether the VM should be a protected VM.
#[serde(default)]
pub protected: bool,
/// The amount of RAM to give the VM, in MiB.
#[serde(default)]
pub memory_mib: Option<NonZeroU32>,
/// The CPU topology: either "one_cpu"(default) or "match_host"
pub cpu_topology: Option<String>,
/// Version or range of versions of the virtual platform that this config is compatible with.
/// The format follows SemVer (https://semver.org).
pub platform_version: VersionReq,
/// SysFS paths of devices assigned to the VM.
#[serde(default)]
pub devices: Vec<PathBuf>,
}
impl VmConfig {
/// Ensure that the configuration has a valid combination of fields set, or return an error if
/// not.
pub fn validate(&self) -> Result<(), Error> {
if self.bootloader.is_none() && self.kernel.is_none() {
bail!("VM must have either a bootloader or a kernel image.");
}
if self.bootloader.is_some() && (self.kernel.is_some() || self.initrd.is_some()) {
bail!("Can't have both bootloader and kernel/initrd image.");
}
for disk in &self.disks {
if disk.image.is_none() == disk.partitions.is_empty() {
bail!("Exactly one of image and partitions must be specified. (Was {:?}.)", disk);
}
}
Ok(())
}
/// Load the configuration for a VM from the given JSON file, and check that it is valid.
pub fn load(file: &File) -> Result<VmConfig, Error> {
let buffered = BufReader::new(file);
let config: VmConfig = serde_json::from_reader(buffered)?;
config.validate()?;
Ok(config)
}
/// Convert the `VmConfig` to a [`VirtualMachineConfig`] which can be passed to the Virt
/// Manager.
pub fn to_parcelable(&self) -> Result<VirtualMachineRawConfig, Error> {
let memory_mib = if let Some(memory_mib) = self.memory_mib {
memory_mib.get().try_into().context("Invalid memory_mib")?
} else {
0
};
let cpu_topology = match self.cpu_topology.as_deref() {
None => CpuTopology::ONE_CPU,
Some("one_cpu") => CpuTopology::ONE_CPU,
Some("match_host") => CpuTopology::MATCH_HOST,
Some(cpu_topology) => bail!("Invalid cpu topology {}", cpu_topology),
};
Ok(VirtualMachineRawConfig {
kernel: maybe_open_parcel_file(&self.kernel, false)?,
initrd: maybe_open_parcel_file(&self.initrd, false)?,
params: self.params.clone(),
bootloader: maybe_open_parcel_file(&self.bootloader, false)?,
disks: self.disks.iter().map(DiskImage::to_parcelable).collect::<Result<_, Error>>()?,
protectedVm: self.protected,
memoryMib: memory_mib,
cpuTopology: cpu_topology,
platformVersion: self.platform_version.to_string(),
devices: self
.devices
.iter()
.map(|x| {
x.to_str().map(String::from).ok_or(anyhow!("Failed to convert {x:?} to String"))
})
.collect::<Result<_>>()?,
..Default::default()
})
}
}
/// Returns the debug level of the VM from its configuration.
pub fn get_debug_level(config: &VirtualMachineConfig) -> Option<DebugLevel> {
match config {
VirtualMachineConfig::AppConfig(config) => Some(config.debugLevel),
VirtualMachineConfig::RawConfig(_) => None,
}
}
/// A disk image to be made available to the VM.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct DiskImage {
/// The filename of the disk image, if it already exists. Exactly one of this and `partitions`
/// must be specified.
#[serde(default)]
pub image: Option<PathBuf>,
/// A set of partitions to be assembled into a composite image.
#[serde(default)]
pub partitions: Vec<Partition>,
/// Whether this disk should be writable by the VM.
pub writable: bool,
}
impl DiskImage {
fn to_parcelable(&self) -> Result<AidlDiskImage, Error> {
let partitions =
self.partitions.iter().map(Partition::to_parcelable).collect::<Result<_>>()?;
Ok(AidlDiskImage {
image: maybe_open_parcel_file(&self.image, self.writable)?,
writable: self.writable,
partitions,
})
}
}
/// A partition to be assembled into a composite image.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Partition {
/// A label for the partition.
pub label: String,
/// The filename of the partition image.
pub path: PathBuf,
/// Whether the partition should be writable.
#[serde(default)]
pub writable: bool,
}
impl Partition {
fn to_parcelable(&self) -> Result<AidlPartition> {
Ok(AidlPartition {
image: Some(open_parcel_file(&self.path, self.writable)?),
writable: self.writable,
label: self.label.to_owned(),
})
}
}
/// Try to open the given file and wrap it in a [`ParcelFileDescriptor`].
pub fn open_parcel_file(filename: &Path, writable: bool) -> Result<ParcelFileDescriptor> {
Ok(ParcelFileDescriptor::new(
OpenOptions::new()
.read(true)
.write(writable)
.open(filename)
.with_context(|| format!("Failed to open {:?}", filename))?,
))
}
/// If the given filename is `Some`, try to open it and wrap it in a [`ParcelFileDescriptor`].
fn maybe_open_parcel_file(
filename: &Option<PathBuf>,
writable: bool,
) -> Result<Option<ParcelFileDescriptor>> {
filename.as_deref().map(|filename| open_parcel_file(filename, writable)).transpose()
}