blob: 1c0c602dc9c94b0e5ec0827b17af7643c88b38f7 [file] [log] [blame]
// Copyright 2018 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
pub mod disk;
pub mod methods;
pub mod proto;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use std::io::{stdin, stdout, BufRead, Write};
use getopts::Options;
use crate::disk::{DiskOpType, VmState};
use crate::methods::{ContainerSource, Methods, UserDisks, VmFeatures};
use system_api::cicerone_service::start_lxd_container_request::PrivilegeLevel;
use system_api::cicerone_service::VmDeviceAction;
/// A string to string mapping of environment variables to values.
pub type EnvMap<'a> = BTreeMap<&'a str, &'a str>;
enum VmcError {
Command(&'static str, Box<dyn Error>),
BadProblemReportArguments(getopts::Fail),
DiskOperation(String, Box<dyn Error>),
ExpectedCrosUserIdHash,
ExpectedUIntSize,
ExpectedName,
ExpectedNoArgs,
ExpectedPath,
ExpectedSize,
ExpectedU8Bus,
ExpectedU8Device,
ExpectedU8Port,
ExpectedUUID,
ExpectedVmAndContainer,
ExpectedVmAndFileName,
ExpectedVmAndMaybeFileName,
ExpectedVmAndPath,
ExpectedVmAndSize,
ExpectedVmBusDevice,
ExpectedVmDeviceUpdates,
ExpectedVmHidrawDevice,
ExpectedVmPort,
UnexpectedSizeWithPluginVm,
InvalidVmDevice(String),
InvalidVmDeviceAction(String),
ExpectedPrivilegedFlagValue,
UnknownSubcommand(String),
UserCancelled,
}
use self::VmcError::*;
// Optional flag used with "vmc container" command. Use this with the getopts crate API.
static PRIVILEGED_FLAG: &str = "privileged";
// Option names for pvm.send-porblem-report command. Use this with the getopts crate API.
static EMAIL_OPTION: &str = "email";
static VM_NAME_OPTION: &str = "vm-name";
// Remove useless expression items that the `try_command!()` macro captures and stringifies when
// generating a `VmcError::Command`.
fn trim_routine(s: &str) -> String {
// We are guaranteed to have at least one element after splitn()
s.trim_start_matches("self.methods.")
.split('(')
.next()
.unwrap()
.to_string()
}
fn parse_disk_size(s: &str) -> Result<u64, VmcError> {
match s.chars().last() {
Some('M') => s[..s.len() - 1].parse::<u64>().map(|x| x * 1024 * 1024),
Some('G') => s[..s.len() - 1]
.parse::<u64>()
.map(|x| x * 1024 * 1024 * 1024),
_ => s.parse(),
}
.map_err(|_| ExpectedUIntSize)
}
impl fmt::Display for VmcError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
BadProblemReportArguments(e) => write!(f, "failed to parse arguments: {:?}", e),
Command(routine, e) => {
write!(f, "operation `{}` failed: {}", trim_routine(routine), e)
}
DiskOperation(op, e) => write!(f, "{} failed: {}", op, e),
ExpectedCrosUserIdHash => write!(f, "expected CROS_USER_ID_HASH environment variable"),
ExpectedUIntSize => write!(
f,
"expected unsigned integer for the disk size. (e.g. 1000000000, 256M, 1G)"
),
ExpectedName => write!(f, "expected <name>"),
ExpectedPath => write!(f, "expected <path>"),
ExpectedSize => write!(f, "expected <size>"),
ExpectedVmAndContainer => write!(
f,
"expected <vm name> <container name> [ <image server> <image alias> ]"
),
ExpectedVmAndFileName => {
write!(f, "expected <vm name> <file name> [removable storage name]")
}
ExpectedVmAndMaybeFileName => write!(
f,
"expected <vm name> [<file name> [removable storage name]]"
),
ExpectedVmAndPath => write!(f, "expected <vm name> <path>"),
ExpectedVmAndSize => write!(f, "expected <vm name> <size>"),
ExpectedVmBusDevice => {
write!(f, "expected <vm name> <bus>:<device> [<container name>]")
}
ExpectedNoArgs => write!(f, "expected no arguments"),
ExpectedU8Bus => write!(f, "expected <bus> to fit into an 8-bit integer"),
ExpectedU8Device => write!(f, "expected <device> to fit into an 8-bit integer"),
ExpectedU8Port => write!(f, "expected <port> to fit into an 8-bit integer"),
ExpectedUUID => write!(f, "expected <command UUID>"),
ExpectedVmDeviceUpdates => write!(f, "expected args `<device>:<enable|disable>`"),
ExpectedVmHidrawDevice => {
write!(f, "expected <vm name> <hidraw file>")
}
ExpectedVmPort => write!(f, "expected <vm name> <port>"),
UnexpectedSizeWithPluginVm => {
write!(f, "unexpected --size parameter; -p doesn't support --size")
}
InvalidVmDevice(v) => write!(f, "invalid vm device {}", v),
InvalidVmDeviceAction(a) => write!(f, "invalid vm device action {}", a),
ExpectedPrivilegedFlagValue => {
write!(f, "Expected <true/false> after the privileged flag")
}
UnknownSubcommand(s) => write!(f, "no such subcommand: `{}`", s),
UserCancelled => write!(f, "cancelled by user"),
}
}
}
impl fmt::Debug for VmcError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
<Self as fmt::Display>::fmt(self, f)
}
}
impl Error for VmcError {}
type VmcResult = Result<(), Box<dyn Error>>;
macro_rules! try_command {
($x:expr) => {
if cfg!(test) {
// Ignore the command's result for testing.
$x.map_err(|e| Command(stringify!($x), e))
.unwrap_or_default()
} else {
$x.map_err(|e| Command(stringify!($x), e))?
}
};
}
struct Command<'a, 'b, 'c> {
methods: &'a mut Methods,
args: &'b [&'b str],
user_id_hash: &'c str,
interactive: bool,
}
impl<'a, 'b, 'c> Command<'a, 'b, 'c> {
// Metrics are on a best-effort basis. We print errors related to sending metrics, but stop
// propagation of the error, which is why this function never returns an error.
fn metrics_send_sample(&mut self, name: &str) {
if let Err(e) = self.methods.metrics_send_sample(name) {
eprintln!(
"warning: failed attempt to send metrics sample `{}`: {}",
name, e
);
}
}
fn user_id_hash_to_username(&mut self, user_id_hash: &str) -> Result<String, Box<dyn Error>> {
if cfg!(test) {
// Fake IPC result for the test environment.
assert_eq!(user_id_hash, "fake_hash");
Ok("fake_user".to_owned())
} else {
self.methods.user_id_hash_to_username(user_id_hash)
}
}
fn start(&mut self) -> VmcResult {
let mut opts = Options::new();
opts.optflag("", "enable-gpu", "when starting the vm, enable gpu support");
opts.optflag(
"",
"enable-dgpu-passthrough",
"when starting the VM, enable discrete GPU passthrough support",
);
opts.optflag(
"",
"enable-big-gl",
"when starting the vm, request Big GL renderer (implies --enable-gpu)",
);
opts.optflag(
"",
"enable-virtgpu-native-context",
"when starting the vm, enable virtgpu native context support (implies --enable-gpu)",
);
opts.optflag("", "vtpm-proxy", "connect the virtio-tpm to vtpm daemon");
opts.optflag(
"",
"enable-audio-capture",
"when starting the vm, enable audio capture support",
);
opts.optopt("", "extra-disk", "path to an extra disk image.", "PATH");
opts.optopt(
"",
"dlc-id",
"Identifier for the DLC used to boot this VM.",
"ID",
);
opts.optopt(
"",
"tools-dlc",
"Identifier for the DLC from which guest tools should be pulled.",
"ID",
);
opts.optopt("", "kernel", "path to a custom kernel image.", "PATH");
opts.optopt("", "initrd", "path to a custom initrd.", "PATH");
opts.optopt("", "rootfs", "path to a custom rootfs image.", "PATH");
opts.optopt(
"",
"vm-type",
"type of VM (TERMINA / ARC_VM / PLUGIN_VM / BOREALIS / BRUSCHETTA / BAGUETTE)",
"TYPE",
);
opts.optflag(
"",
"no-start-lxd",
"Don't start LXD (the container manager)",
);
opts.optflag("", "writable-rootfs", "Mount the rootfs as writable.");
opts.optmulti(
"",
"kernel-param",
"Additional kernel cmdline parameter for the host.",
"PARAM",
);
opts.optmulti(
"",
"oem-string",
"Type 11 SMBIOS DMI OEM string to pass to the host.",
"STRING",
);
opts.optopt("", "bios", "path to a custom bios image.", "PATH");
opts.optopt("", "pflash", "path to a r/w bios flash image.", "PATH");
opts.optopt(
"",
"bios-dlc",
"Identifier for the DLC from which the bios should be pulled.",
"ID",
);
opts.optopt("", "timeout", "seconds to wait until timeout.", "PARAM");
opts.optflag("", "no-shell", "Don't start a shell in the started VM.");
opts.optflag("", "help-start", "print this help menu");
let matches = opts.parse(self.args)?;
if matches.opt_present("help-start") {
println!("{}", opts.usage("Usage: vmc start [options] <name>"));
return Ok(());
}
if matches.free.len() != 1 {
return Err(ExpectedName.into());
}
let vm_name = &matches.free[0];
let username = self.user_id_hash_to_username(self.user_id_hash)?;
let big_gl = matches.opt_present("enable-big-gl");
let virtgpu_native_context = matches.opt_present("enable-virtgpu-native-context");
let gpu = virtgpu_native_context || big_gl || matches.opt_present("enable-gpu");
let dgpu_passthrough = matches.opt_present("enable-dgpu-passthrough");
let timeout = matches
.opt_str("timeout")
.map(|x| x.parse())
.transpose()?
.unwrap_or(0);
let features = VmFeatures {
gpu,
dgpu_passthrough,
big_gl,
virtgpu_native_context,
vtpm_proxy: matches.opt_present("vtpm-proxy"),
audio_capture: matches.opt_present("enable-audio-capture"),
dlc: matches.opt_str("dlc-id"),
kernel_params: matches.opt_strs("kernel-param"),
tools_dlc_id: matches.opt_str("tools-dlc"),
timeout,
oem_strings: matches.opt_strs("oem-string"),
bios_dlc_id: matches.opt_str("bios-dlc"),
vm_type: matches.opt_str("vm-type"),
};
let user_disks = UserDisks {
kernel: matches.opt_str("kernel"),
rootfs: matches.opt_str("rootfs"),
writable_rootfs: matches.opt_present("writable-rootfs"),
extra_disk: matches.opt_str("extra-disk"),
initrd: matches.opt_str("initrd"),
bios: matches.opt_str("bios"),
pflash: matches.opt_str("pflash"),
};
self.metrics_send_sample("Vm.VmcStart");
try_command!(self.methods.vm_start(
vm_name,
self.user_id_hash,
&username,
features,
user_disks,
!matches.opt_present("no-start-lxd"),
));
self.metrics_send_sample("Vm.VmcStartSuccess");
if !matches.opt_present("no-shell") {
try_command!(self.methods.vsh_exec(vm_name, self.user_id_hash));
}
Ok(())
}
fn stop(&mut self) -> VmcResult {
if self.args.len() != 1 {
return Err(ExpectedName.into());
}
let vm_name = self.args[0];
try_command!(self.methods.vm_stop(vm_name, self.user_id_hash));
Ok(())
}
fn launch(&mut self) -> VmcResult {
if self.args.is_empty() {
return Err(ExpectedName.into());
}
try_command!(self.methods.vm_launch(self.user_id_hash, self.args));
Ok(())
}
fn create(&mut self) -> VmcResult {
let mut opts = Options::new();
// By using StopAtFirstFree we allow this command to continue using `--`
// as a separator for params which avoids breaking the existing
// interface.
opts.parsing_style(getopts::ParsingStyle::StopAtFirstFree);
opts.optflag("p", "pluginvm", "create a pluginvm vm");
opts.optopt("", "size", "size of the created vm's disk", "SIZE");
let matches = opts.parse(self.args)?;
let plugin_vm = matches.opt_present("p");
let size = match matches.opt_str("size") {
Some(s) => Some(parse_disk_size(&s)?),
None => None,
};
if plugin_vm && size.is_some() {
return Err(UnexpectedSizeWithPluginVm.into());
}
let mut s = matches.free.splitn(2, |arg| *arg == "--");
let args = s.next().expect("failed to split argument list");
let params = s.next().unwrap_or(&[]);
let (vm_name, file_name, removable_media) = match args.len() {
1 => (args[0].as_str(), None, None),
2 => (args[0].as_str(), Some(args[1].as_str()), None),
3 => (
args[0].as_str(),
Some(args[1].as_str()),
Some(args[2].as_str()),
),
_ => return Err(ExpectedVmAndMaybeFileName.into()),
};
if let Some(uuid) = try_command!(self.methods.vm_create(
vm_name,
self.user_id_hash,
plugin_vm,
size,
file_name,
removable_media,
params,
)) {
println!("VM creation in progress: {}", uuid);
self.wait_disk_op_completion(&uuid, DiskOpType::Create, "VM creation")?;
}
Ok(())
}
fn adjust(&mut self) -> VmcResult {
if self.args.len() < 2 {
return Err(ExpectedName.into());
}
let vm_name = self.args[0];
let operation = &self.args[1];
try_command!(self.methods.vm_adjust(
vm_name,
self.user_id_hash,
operation,
&self.args[2..]
));
Ok(())
}
fn destroy(&mut self) -> VmcResult {
let mut opts = Options::new();
opts.optflag("y", "yes", "destroy without prompting");
let matches = opts.parse(self.args)?;
if matches.free.len() != 1 {
return Err(ExpectedName.into());
}
let vm_name = &matches.free[0];
let skip_prompt = matches.opt_present("yes") || !self.interactive;
if !skip_prompt {
println!(
"WARNING: this will delete all data stored in VM '{}'",
vm_name
);
print!("Continue? (y/N) ");
stdout().flush()?;
let mut line = String::new();
if stdin().lock().read_line(&mut line).is_err() {
return Err(UserCancelled.into());
}
line = line.trim_end().to_string();
if !(line == "y" || line == "yes") {
return Err(UserCancelled.into());
}
}
match self.methods.disk_destroy(vm_name, self.user_id_hash) {
Ok(()) => Ok(()),
Err(e) => {
if cfg!(test) {
Ok(())
} else {
self.metrics_send_sample("Vm.DiskEraseFailed");
Err(Command("disk_destroy", e).into())
}
}
}
}
fn wait_disk_op_completion(
&mut self,
uuid: &str,
op_type: DiskOpType,
op_name: &str,
) -> VmcResult {
let mut progress_reported = false;
loop {
match self.methods.wait_disk_op(uuid, self.user_id_hash, op_type) {
Ok((done, progress)) => {
if done {
println!("\rOperation completed successfully");
return Ok(());
}
print!("\rOperation in progress: {}% done", progress);
stdout().flush()?;
progress_reported = true;
}
Err(e) => {
// Ignore the result for testing.
if cfg!(test) {
return Ok(());
}
if progress_reported {
println!();
}
return Err(DiskOperation(op_name.to_string(), e).into());
}
}
}
}
fn resize(&mut self) -> VmcResult {
if self.args.len() != 2 {
return Err(ExpectedVmAndSize.into());
}
let vm_name = self.args[0];
let size: u64 = self.args[1].parse().or(Err(ExpectedSize))?;
match try_command!(self.methods.disk_resize(vm_name, self.user_id_hash, size)) {
Some(uuid) => {
println!("Resize in progress: {}", uuid);
self.wait_disk_op_completion(&uuid, DiskOpType::Resize, "resize")?;
}
None => {
println!("Operation completed successfully");
}
}
Ok(())
}
fn export(&mut self) -> VmcResult {
let mut opts = Options::new();
opts.optflag(
"d",
"digest",
"generate checksum/digest for the exported image",
);
opts.optflag(
"f",
"force",
"force export even if VM is running or not shut down",
);
let matches = opts.parse(self.args)?;
let generate_digest = matches.opt_present("digest");
let force = matches.opt_present("force");
let (vm_name, file_name, removable_media) = match matches.free.len() {
2 => (&matches.free[0], &matches.free[1], None),
3 => (
&matches.free[0],
&matches.free[1],
Some(matches.free[2].as_str()),
),
_ => return Err(ExpectedVmAndFileName.into()),
};
let digest_name = file_name.to_owned() + ".sha256.txt";
let digest_option = if generate_digest {
Some(digest_name.as_str())
} else {
None
};
if let Some(uuid) = try_command!(self.methods.vm_export(
vm_name,
self.user_id_hash,
file_name,
digest_option,
removable_media,
force,
)) {
println!("Export in progress: {}", uuid);
self.wait_disk_op_completion(&uuid, DiskOpType::Create, "export")?;
}
Ok(())
}
fn import(&mut self) -> VmcResult {
let plugin_vm = !self.args.is_empty() && self.args[0] == "-p";
if plugin_vm {
// Discard the first argument (-p).
self.args = &self.args[1..];
}
let (vm_name, file_name, removable_media) = match self.args.len() {
2 => (self.args[0], self.args[1], None),
3 => (self.args[0], self.args[1], Some(self.args[2])),
_ => return Err(ExpectedVmAndFileName.into()),
};
if let Some(uuid) = try_command!(self.methods.vm_import(
vm_name,
self.user_id_hash,
plugin_vm,
file_name,
removable_media
)) {
println!("Import in progress: {}", uuid);
self.wait_disk_op_completion(&uuid, DiskOpType::Create, "import")?;
}
Ok(())
}
fn disk_op_status(&mut self) -> VmcResult {
if self.args.len() != 1 {
return Err(ExpectedUUID.into());
}
let uuid = self.args[0];
let (done, progress) =
try_command!(self
.methods
.disk_op_status(uuid, self.user_id_hash, DiskOpType::Create));
if done {
println!("Operation completed successfully");
} else {
println!("Operation in progress: {}% done", progress);
}
Ok(())
}
fn create_extra_disk(&mut self) -> VmcResult {
let mut opts = Options::new();
opts.reqopt("", "size", "size of extra disk", "SIZE");
let matches = opts.parse(self.args)?;
let s = matches.opt_str("size").ok_or_else(|| ExpectedSize)?;
let size = parse_disk_size(&s)?;
let (file_name, removable_media) = match matches.free.len() {
1 => (&matches.free[0], None),
2 => (&matches.free[0], Some(matches.free[1].as_str())),
_ => return Err(ExpectedPath.into()),
};
let path = try_command!(self.methods.extra_disk_create(
self.user_id_hash,
file_name,
removable_media,
size
));
println!("A raw disk is created at {}.", path.display());
Ok(())
}
fn list(&mut self) -> VmcResult {
if !self.args.is_empty() {
return Err(ExpectedNoArgs.into());
}
let (disk_image_list, total_size) = try_command!(self.methods.disk_list(self.user_id_hash));
for disk in disk_image_list {
let mut extra_info = String::new();
if let Some(min_size) = disk.min_size {
extra_info.push_str(&format!(", min shrinkable size {} bytes", min_size));
}
extra_info.push_str(&format!(", {}", disk.image_type));
if !disk.user_chosen_size {
extra_info.push_str(", sparse");
}
let state_str = match disk.state {
VmState::Starting | VmState::Running => format!(" {}", disk.state),
_ => String::new(),
};
println!(
"{} ({} bytes{}){}",
disk.name, disk.size, extra_info, state_str
);
}
println!("Total Size (bytes): {}", total_size);
Ok(())
}
fn logs(&mut self) -> VmcResult {
if self.args.len() != 1 {
return Err(ExpectedName.into());
}
let vm_name = self.args[0];
let logs = try_command!(self.methods.get_vm_logs(vm_name, self.user_id_hash));
print!("{}", logs);
Ok(())
}
fn share(&mut self) -> VmcResult {
if self.args.len() != 2 {
return Err(ExpectedVmAndPath.into());
}
let vm_name = self.args[0];
let path = self.args[1];
let vm_path = try_command!(self.methods.vm_share_path(vm_name, self.user_id_hash, path));
println!("{} is available at path {}", path, vm_path);
Ok(())
}
fn unshare(&mut self) -> VmcResult {
if self.args.len() != 2 {
return Err(ExpectedVmAndPath.into());
}
let vm_name = self.args[0];
let path = self.args[1];
try_command!(self
.methods
.vm_unshare_path(vm_name, self.user_id_hash, path));
Ok(())
}
fn container(&mut self) -> VmcResult {
let mut opts = Options::new();
opts.optopt(
"p",
PRIVILEGED_FLAG,
"is the container privileged.",
"true / false",
);
opts.optopt("", "timeout", "seconds to wait until timeout.", "PARAM");
let matches = opts
.parse(self.args)
.map_err(|_| ExpectedPrivilegedFlagValue)?;
// The privileged flag is optional but when its given it must be followed by a valid value.
let privilege_level = match matches.opt_str(PRIVILEGED_FLAG) {
Some(s) => match s.as_str() {
"True" | "true" => PrivilegeLevel::PRIVILEGED,
"False" | "false" => PrivilegeLevel::UNPRIVILEGED,
_ => return Err(ExpectedPrivilegedFlagValue.into()),
},
None => PrivilegeLevel::UNCHANGED,
};
let timeout = matches.opt_str("timeout").map(|x| x.parse()).transpose()?;
let required_args = &matches.free;
let (vm_name, container_name, source) = match required_args.len() {
2 => (
required_args[0].as_str(),
required_args[1].as_str(),
ContainerSource::ImageServer {
image_server: "https://storage.googleapis.com/cros-containers/%d".to_string(),
image_alias: "debian/bookworm".to_string(),
},
),
4 => (
required_args[0].as_str(),
required_args[1].as_str(),
// If this argument looks like an absolute path, treat it and the following
// parameter as local paths to tarballs. Otherwise, assume they are an
// image server URL and image alias.
if required_args[2].starts_with('/') {
ContainerSource::Tarballs {
rootfs_path: required_args[2].clone(),
metadata_path: required_args[3].clone(),
}
} else {
ContainerSource::ImageServer {
image_server: required_args[2].clone(),
image_alias: required_args[3].clone(),
}
},
),
_ => return Err(ExpectedVmAndContainer.into()),
};
let username = self.user_id_hash_to_username(self.user_id_hash)?;
try_command!(self.methods.container_create(
vm_name,
self.user_id_hash,
container_name,
source,
timeout
));
try_command!(self.methods.container_setup_user(
vm_name,
self.user_id_hash,
container_name,
&username
));
// If the container was already running then this will update the privilege level of the
// container if |privilege_level| is not |UNCHANGED|. This will take into effect on next
// container boot.
try_command!(self.methods.container_start(
vm_name,
self.user_id_hash,
container_name,
privilege_level,
timeout
));
try_command!(self
.methods
.vsh_exec_container(vm_name, self.user_id_hash, container_name));
Ok(())
}
fn update_container_devices(&mut self) -> VmcResult {
let (vm_name, container_name) = match self.args.len() {
0 | 1 => return Err(ExpectedVmAndPath.into()),
2 => return Err(ExpectedVmDeviceUpdates.into()),
_ => (self.args[0], self.args[1]),
};
let mut updates = HashMap::<String, VmDeviceAction>::new();
for u in &self.args[2..] {
let update_parts: Vec<&str> = u.splitn(2, ':').collect();
match update_parts.len() {
2 => {
println!("{:?}", update_parts);
match (update_parts[0], update_parts[1]) {
("", &_) => return Err(InvalidVmDevice("<empty>".to_string()).into()),
(d, "enable") => updates.insert(d.to_string(), VmDeviceAction::ENABLE),
(d, "disable") => updates.insert(d.to_string(), VmDeviceAction::DISABLE),
(_, a) => return Err(InvalidVmDeviceAction(a.to_string()).into()),
}
}
_ => return Err(InvalidVmDeviceAction(u.to_string()).into()),
};
}
let res = try_command!(self.methods.container_update_devices(
vm_name,
self.user_id_hash,
container_name,
&updates
));
println!("{}", res);
Ok(())
}
fn usb_attach(&mut self) -> VmcResult {
let (vm_name, bus_device, container_name) = match self.args.len() {
3 => (self.args[0], self.args[1], Some(self.args[2])),
2 => (self.args[0], self.args[1], None),
_ => return Err(ExpectedVmBusDevice.into()),
};
let mut bus_device_parts = bus_device.splitn(2, ':');
let (bus, device) = match (bus_device_parts.next(), bus_device_parts.next()) {
(Some(bus_str), Some(device_str)) => (
bus_str.parse().or(Err(ExpectedU8Bus))?,
device_str.parse().or(Err(ExpectedU8Device))?,
),
_ => return Err(ExpectedVmBusDevice.into()),
};
let guest_port = try_command!(self.methods.usb_attach(
vm_name,
self.user_id_hash,
bus,
device,
container_name
));
if let Some(container) = container_name {
println!(
"usb device at bus={} device={} attached to container {}:{} at port={}",
bus, device, vm_name, container, guest_port
);
} else {
println!(
"usb device at bus={} device={} attached to vm {} at port={}",
bus, device, vm_name, guest_port
);
}
Ok(())
}
fn allow_all_io_devices(&mut self) -> VmcResult {
if !self.args.is_empty() {
return Err(ExpectedNoArgs.into());
}
try_command!(self.methods.unset_primary_mouse());
try_command!(self.methods.unset_primary_keyboard());
Ok(())
}
fn list_primary_io_devices(&mut self) -> VmcResult {
if !self.args.is_empty() {
return Err(ExpectedNoArgs.into());
}
let primary_devices = try_command!(self.methods.list_primary_io_devices());
for device in primary_devices {
println!("{}", device)
}
Ok(())
}
fn unset_primary_mouse(&mut self) -> VmcResult {
if !self.args.is_empty() {
return Err(ExpectedNoArgs.into());
}
try_command!(self.methods.unset_primary_mouse());
Ok(())
}
fn unset_primary_keyboard(&mut self) -> VmcResult {
if !self.args.is_empty() {
return Err(ExpectedNoArgs.into());
}
try_command!(self.methods.unset_primary_keyboard());
Ok(())
}
fn key_attach(&mut self) -> VmcResult {
// TODO(morg): need to implement command for sharing security key with container too
let (vm_name, hidraw_device) = match self.args.len() {
2 => (self.args[0], self.args[1]),
_ => return Err(ExpectedVmHidrawDevice.into()),
};
let guest_port =
try_command!(self
.methods
.key_attach(vm_name, self.user_id_hash, hidraw_device,));
println!(
"Security key at {} shared with vm {} at port={}",
hidraw_device, vm_name, guest_port
);
Ok(())
}
fn usb_detach(&mut self) -> VmcResult {
let (vm_name, port) = match self.args.len() {
2 => (self.args[0], self.args[1].parse().or(Err(ExpectedU8Port))?),
_ => return Err(ExpectedVmPort.into()),
};
try_command!(self.methods.usb_detach(vm_name, self.user_id_hash, port));
println!("usb device detached from port {}", port);
Ok(())
}
fn usb_list(&mut self) -> VmcResult {
if self.args.len() != 1 {
return Err(ExpectedName.into());
}
let vm_name = self.args[0];
let devices = try_command!(self.methods.usb_list(vm_name, self.user_id_hash));
if devices.is_empty() {
println!("No attached usb devices");
}
for (port, vendor_id, product_id, name) in devices {
println!(
"Port {:03} ID {:04x}:{:04x} {}",
port, vendor_id, product_id, name
);
}
Ok(())
}
fn pvm_send_problem_report(&mut self) -> VmcResult {
let mut opts = Options::new();
opts.optopt(
"e",
EMAIL_OPTION,
"email to associate with the problem report",
"EMAIL",
);
opts.optopt(
"n",
VM_NAME_OPTION,
"name of the VM for which problem report is generated",
"NAME",
);
let matches = opts.parse(self.args).map_err(BadProblemReportArguments)?;
let vm_name = matches.opt_str(VM_NAME_OPTION);
let email = matches.opt_str(EMAIL_OPTION);
let text = if matches.free.is_empty() {
None
} else {
Some(matches.free.join(" "))
};
let report_id = try_command!(self.methods.pvm_send_problem_report(
vm_name,
self.user_id_hash,
email,
text
));
println!("Problem report has been sent. Report ID: {}", report_id);
Ok(())
}
}
const USAGE: &str = "
[ start [--enable-gpu] [--enable-dgpu-passthrough] [--enable-big-gl] \
[--enable-virtgpu-native-context] [--enable-audio-capture] [--extra-disk PATH] \
[--kernel PATH] [--initrd PATH] [--writable-rootfs] [--kernel-param PARAM] \
[--bios PATH] [--timeout PARAM] [--oem-string STRING] <name> |
stop <name> |
launch <name> |
create [-p] [--size SIZE] <name> [<source media> [<removable storage name>]] \
[-- additional parameters] |
create-extra-disk --size SIZE <file name> [<removable storage name>] |
adjust <name> <operation> [additional parameters] |
destroy [-y] <name> |
disk-op-status <command UUID> |
export [-d] [-f] <vm name> <file name> [<removable storage name>] |
import [-p] <vm name> <file name> [<removable storage name>] |
resize <vm name> <size> |
list |
logs <vm name> |
share <vm name> <path> |
unshare <vm name> <path> |
container <vm name> <container name> [ (<image server> <image alias>) | \
(<rootfs path> <metadata path>) ] \
[--privileged <true/false>] [--timeout PARAM] |
update-container-devices <vm_name> <container_name> (<vm device>:<enable/disable>)... |
usb-attach <vm name> <bus>:<device> [<container name>] |
usb-detach <vm name> <port> |
usb-list <vm name> |
key-attach <vm name> <hidraw path> |
pvm.send-problem-report [-n <vm name>] [-e <reporter's email>] <description of the problem> |
allow-all-io-devices [on chromeboxes, allow all keyboards/mice to connect] |
list-primary-io-devices |
unset-primary-keyboard |
unset-primary-mouse |
--help | -h ]
";
/// `vmc` (Virtual Machine Controller) command line interface.
/// This is the interface accessible from crosh (Ctrl-Alt-T in the browser to access).
pub struct Vmc<'a> {
user_id_hash: &'a str,
interactive: bool,
}
impl Vmc<'_> {
fn print_usage(&self, program_name: &str) {
eprintln!("USAGE: {}{}", program_name, USAGE);
}
fn run(&self, methods: &mut Methods, args: &[&str]) -> VmcResult {
if args.len() < 2 {
self.print_usage("vmc");
return Ok(());
}
for &arg in args {
match arg {
"--" => break,
"--help" | "-h" => {
self.print_usage("vmc");
return Ok(());
}
_ => {}
}
}
let mut command = Command {
methods,
args: &args[2..],
user_id_hash: self.user_id_hash,
interactive: self.interactive,
};
let command_name = args[1];
match command_name {
"start" => command.start(),
"stop" => command.stop(),
"launch" => command.launch(),
"create" => command.create(),
"create-extra-disk" => command.create_extra_disk(),
"adjust" => command.adjust(),
"destroy" => command.destroy(),
"export" => command.export(),
"import" => command.import(),
"disk-op-status" => command.disk_op_status(),
"resize" => command.resize(),
"list" => command.list(),
"logs" => command.logs(),
"share" => command.share(),
"unshare" => command.unshare(),
"container" => command.container(),
"update-container-devices" => command.update_container_devices(),
"usb-attach" => command.usb_attach(),
"usb-detach" => command.usb_detach(),
"usb-list" => command.usb_list(),
"key-attach" => command.key_attach(),
"pvm.send-problem-report" => command.pvm_send_problem_report(),
"allow-all-io-devices" => command.allow_all_io_devices(),
"list-primary-io-devices" => command.list_primary_io_devices(),
"unset-primary-mouse" => command.unset_primary_mouse(),
"unset-primary-keyboard" => command.unset_primary_keyboard(),
_ => Err(UnknownSubcommand(command_name.to_owned()).into()),
}
}
}
fn main() -> Result<(), Box<dyn Error>> {
libchromeos::panic_handler::install_memfd_handler();
let args_string: Vec<String> = std::env::args().collect();
let args: Vec<&str> = args_string.iter().map(|s| s.as_str()).collect();
let interactive = std::env::var("VMC_NONINTERACTIVE").is_err();
// When running inside crosh, `CROS_USER_ID_HASH` will be set in the environment.
// Otherwise ask SessionManager for the primary session ID.
let user_id_hash = if let Ok(hash) = std::env::var("CROS_USER_ID_HASH") {
hash
} else {
libchromeos::chromeos::get_user_id_hash().map_err(|_| ExpectedCrosUserIdHash)?
};
let vmc = Vmc {
user_id_hash: &user_id_hash,
interactive,
};
vmc.run(&mut Methods::new()?, &args)
}
#[cfg(test)]
mod tests {
use super::*;
use dbus::Message;
use protobuf::Message as ProtoMessage;
use std::collections::HashMap;
use system_api::concierge_service::*;
use system_api::dlcservice::*;
fn mocked_connection_filter(mut msg: Message) -> Result<Message, Result<Message, dbus::Error>> {
eprintln!("{:?}", msg);
msg.set_serial(1);
if let Some(member) = msg.member() {
match member.into_cstring().to_bytes() {
b"GetDlcState" => {
let mut dlc_state = DlcState::new();
dlc_state.state = dlc_state::State::INSTALLED.into();
let msg_return = msg
.method_return()
.append1(dlc_state.write_to_bytes().unwrap());
return Err(Ok(msg_return));
}
b"StartVmPluginDispatcher" => {
let msg_return = msg.method_return().append1(true);
return Err(Ok(msg_return));
}
b"DestroyDiskImage" => {
let mut resp = DestroyDiskImageResponse::new();
resp.status = DiskImageStatus::DISK_STATUS_DOES_NOT_EXIST.into();
let msg_return = msg.method_return().append1(resp.write_to_bytes().unwrap());
return Err(Ok(msg_return));
}
b"RetrieveActiveSessions" => {
let sessions: HashMap<String, String> =
[("testuser@example.com".to_owned(), "fake_hash".to_owned())]
.iter()
.cloned()
.collect();
let msg_return = msg.method_return().append1(sessions);
return Err(Ok(msg_return));
}
b"ListVmDisks" => {
let mut resp = ListVmDisksResponse::new();
resp.images.push(VmDiskInfo {
name: "PvmDefault".to_owned(),
..Default::default()
});
resp.success = true;
let msg_return = msg.method_return().append1(resp.write_to_bytes().unwrap());
return Err(Ok(msg_return));
}
_ => {}
}
}
Ok(msg)
}
fn mocked_methods() -> Methods {
let mut methods = Methods::dummy();
methods
.connection_proxy_mut()
.set_filter(mocked_connection_filter);
methods
}
#[test]
fn arg_parsing() {
const DUMMY_SUCCESS_ARGS: &[&[&str]] = &[
&["vmc", "start", "termina"],
&["vmc", "start", "--enable-gpu", "termina"],
&["vmc", "start", "termina", "--enable-gpu"],
&["vmc", "start", "termina", "--enable-big-gl"],
&["vmc", "start", "termina", "--enable-gpu", "--enable-big-gl"],
&["vmc", "start", "termina", "--enable-virtgpu-native-context"],
&[
"vmc",
"start",
"termina",
"--enable-gpu",
"--enable-virtgpu-native-context",
],
&["vmc", "start", "termina", "--vtpm-proxy"],
&["vmc", "start", "termina", "--enable-audio-capture"],
&[
"vmc",
"start",
"termina",
"--enable-audio-capture",
"--enable-gpu",
],
&[
"vmc",
"start",
"termina",
"--enable-gpu",
"--enable-audio-capture",
],
&["vmc", "start", "termina", "--extra-disk=foo.img"],
&["vmc", "start", "termina", "--extra-disk", "foo.img"],
&[
"vmc",
"start",
"termina",
"--extra-disk",
"foo.img",
"--enable-audio-capture",
"--enable-gpu",
],
&["vmc", "start", "termina", "--no-start-lxd"],
&["vmc", "start", "termina", "--dlc-id=foo"],
&["vmc", "start", "termina", "--initrd=myinitrd"],
&["vmc", "start", "termina", "--initrd", "myinitrd"],
&["vmc", "start", "termina", "--rootfs", "myrootfs"],
&["vmc", "start", "termina", "--rootfs=myrootfs"],
&["vmc", "start", "termina", "--timeout", "3"],
&[
"vmc",
"start",
"termina",
"--rootfs",
"myrootfs",
"--writable-rootfs",
],
&["vmc", "start", "termina", "--kernel-param=quiet"],
&["vmc", "start", "termina", "--kernel-param", "quiet"],
&[
"vmc",
"start",
"termina",
"--kernel-param=quiet",
"--kernel-param=slub_debug",
],
&[
"vmc",
"start",
"termina",
"--kernel-param",
"quiet",
"--kernel-param",
"slub_debug",
],
&[
"vmc",
"start",
"termina",
"--kernel-param",
"quiet slub_debug",
],
&["vmc", "start", "--kernel-param", "quiet", "termina"],
&["vmc", "start", "--oem-string=my-oem-string-1", "termina"],
&["vmc", "start", "--oem-string", "my-oem-string-1", "termina"],
&[
"vmc",
"start",
"--oem-string=my-oem-string-1",
"--oem-string=my-oem-string-2",
"termina",
],
&[
"vmc",
"start",
"--oem-string",
"my-oem-string-1",
"--oem-string",
"my-oem-string-2",
"termina",
],
&["vmc", "start", "--bios", "mybios", "termina"],
&["vmc", "start", "--bios=mybios", "termina"],
&["vmc", "start", "--tools-dlc", "my-dlc", "termina"],
&["vmc", "start", "--tools-dlc=my-dlc", "termina"],
&["vmc", "stop", "termina"],
&["vmc", "launch", "foo"],
&["vmc", "launch", "a", "b", "c", "d", "e", "f"],
&["vmc", "create", "termina"],
&["vmc", "create", "-p", "termina"],
&["vmc", "create", "--pluginvm", "termina"],
&[
"vmc",
"create",
"-p",
"termina",
"file name",
"removable media",
],
&["vmc", "create", "-p", "termina", "--"],
&["vmc", "create", "-p", "termina", "--", "param"],
&["vmc", "create", "-p", "termina", "--", "param1", "param2"],
&["vmc", "create", "--size", "1000000", "termina"],
&["vmc", "create", "--size", "256M", "termina"],
&["vmc", "create", "--size", "1G", "termina"],
&["vmc", "create-extra-disk", "--size=1000000", "foo.img"],
&["vmc", "create-extra-disk", "--size=256M", "foo.img"],
&["vmc", "create-extra-disk", "--size=1G", "foo.img"],
&["vmc", "create-extra-disk", "--size", "1G", "foo.img"],
&[
"vmc",
"create-extra-disk",
"--size=1G",
"foo.img",
"USB Drive",
],
&[
"vmc",
"update-container-devices",
"termina",
"penguin",
"microphone:disable",
],
&[
"vmc",
"update-container-devices",
"termina",
"penguin",
"microphone:enable",
"camera:disable",
],
&["vmc", "adjust", "termina", "op"],
&["vmc", "adjust", "termina", "op", "param"],
&["vmc", "destroy", "termina"],
&["vmc", "destroy", "-y", "termina"],
&["vmc", "disk-op-status", "12345"],
&["vmc", "export", "termina", "file name"],
&["vmc", "export", "-d", "termina", "file name"],
&["vmc", "export", "-d", "-f", "termina", "file name"],
&["vmc", "export", "termina", "file name", "removable media"],
&[
"vmc",
"export",
"-d",
"termina",
"file name",
"removable media",
],
&["vmc", "import", "termina", "file name"],
&["vmc", "import", "termina", "file name", "removable media"],
&["vmc", "import", "-p", "termina", "file name"],
&[
"vmc",
"import",
"-p",
"termina",
"file name",
"removable media",
],
&["vmc", "list"],
&["vmc", "logs", "cowcat"],
&["vmc", "share", "termina", "my-folder"],
&["vmc", "unshare", "termina", "my-folder"],
&["vmc", "usb-attach", "termina", "1:2"],
&["vmc", "usb-attach", "termina", "1:2", "penguin"],
&["vmc", "usb-detach", "termina", "5"],
&["vmc", "usb-detach", "termina", "5"],
&["vmc", "usb-list", "termina"],
&["vmc", "pvm.send-problem-report"],
&["vmc", "pvm.send-problem-report", "text"],
&["vmc", "pvm.send-problem-report", "text", "text2"],
&[
"vmc",
"pvm.send-problem-report",
"-n",
"termina",
"text",
"text2",
],
&[
"vmc",
"pvm.send-problem-report",
"-e",
"someone@somewhere.com",
"text",
"text2",
],
&[
"vmc",
"pvm.send-problem-report",
"-n",
"termina",
"-e",
"someone@somewhere.com",
"text",
"text2",
],
&["vmc", "--help"],
&["vmc", "-h"],
];
const DUMMY_FAILURE_ARGS: &[&[&str]] = &[
&["vmc", "start"],
&["vmc", "start", "--i-made-this-up", "termina"],
&["vmc", "start", "termina", "extra args"],
&["vmc", "start", "termina", "--extra-disk"],
&["vmc", "start", "termina", "--dlc-id"],
&["vmc", "start", "termina", "--initrd"],
&["vmc", "start", "termina", "--rootfs"],
&["vmc", "start", "termina", "--writable-rootfs", "myrootfs"],
&["vmc", "start", "termina", "--kernel-param"],
&["vmc", "start", "termina", "--oem-string"],
&["vmc", "start", "termina", "--bios"],
&["vmc", "start", "termina", "--tools-dlc"],
&["vmc", "start", "termina", "--timeout"],
&["vmc", "start", "termina", "--timeout", "xyz"],
&["vmc", "start", "termina", "--bios-dlc"],
&["vmc", "stop"],
&["vmc", "stop", "termina", "extra args"],
&["vmc", "launch"],
&["vmc", "create"],
&["vmc", "create", "-p"],
&[
"vmc",
"create",
"-p",
"termina",
"file name",
"removable media",
"extra args",
],
&["vmc", "create", "--size", "termina"],
&["vmc", "create", "--size", "52J", "termina"],
&["vmc", "create", "--size", "foo", "termina"],
&["vmc", "create", "-p", "--size", "10G", "termina"],
&["vmc", "create-extra-disk"],
&["vmc", "create-extra-disk", "foo.img"],
&["vmc", "create-extra-disk", "--size", "1G"],
&["vmc", "create-extra-disk", "--size", "foo.img"],
&["vmc", "create-extra-disk", "--size=1G"],
&[
"vmc",
"update-container-devices",
"termina",
"penguin",
"microphone:eat",
"camera:enable",
],
&[
"vmc",
"update-container-devices",
"termina",
"penguin",
":enable",
],
&[
"vmc",
"update-container-devices",
"termina",
"penguin",
"enable:",
],
&["vmc", "adjust"],
&["vmc", "adjust", "termina"],
&["vmc", "destroy"],
&["vmc", "destroy", "termina", "extra args"],
&["vmc", "disk-op-status"],
&["vmc", "destroy", "12345", "extra args"],
&["vmc", "export", "termina"],
&["vmc", "export", "-d", "termina"],
&["vmc", "export", "termina", "too", "many", "args"],
&["vmc", "export", "-d", "termina", "too", "many", "args"],
&[
"vmc", "export", "-d", "-f", "termina", "too", "many", "args",
],
&["vmc", "import", "termina"],
&["vmc", "import", "termina", "too", "many", "args"],
&["vmc", "import", "-p", "termina"],
&["vmc", "import", "-p", "termina", "too", "many", "args"],
&["vmc", "list", "extra args"],
&["vmc", "logs"],
&["vmc", "logs", "too", "many args"],
&["vmc", "share"],
&["vmc", "share", "too", "many", "args"],
&["vmc", "unshare"],
&["vmc", "unshare", "too", "many", "args"],
&["vmc", "usb-attach"],
&["vmc", "usb-attach", "termina"],
&["vmc", "usb-attach", "termina", "whatever"],
&["vmc", "usb-attach", "termina", "1:2:1dee:93d2"],
&["vmc", "usb-attach", "termina", "whatever", "whatever"],
&["vmc", "usb-attach", "termina", "1:2", "penguin", "whatever"],
&["vmc", "usb-detach"],
&["vmc", "usb-detach", "not-a-number"],
&["vmc", "usb-list"],
&["vmc", "usb-list", "termina", "args"],
&["vmc", "pvm.send-problem-report", "-e"],
&["vmc", "pvm.send-problem-report", "-n"],
];
let mut methods = mocked_methods();
let vmc = Vmc {
user_id_hash: "fake_hash",
interactive: false,
};
for args in DUMMY_SUCCESS_ARGS {
if let Err(e) = vmc.run(&mut methods, args) {
panic!("test args failed: {:?}: {}", args, e)
}
}
for args in DUMMY_FAILURE_ARGS {
if let Ok(()) = vmc.run(&mut methods, args) {
panic!("test args should have failed: {:?}", args)
}
}
}
#[test]
fn container() {
const CONTAINER_ARGS: &[&[&str]] = &[
&["vmc", "container", "termina", "penguin"],
&[
"vmc",
"container",
"termina",
"penguin",
"https://my-image-server.com/",
"custom/os",
],
];
// How |PRIVILEGED_FLAG| appears on the command line.
const PRIVILEGED_FLAG_CMDLINE: &str = "--privileged";
let mut methods = mocked_methods();
let vmc = Vmc {
user_id_hash: "fake_hash",
interactive: false,
};
for args in CONTAINER_ARGS {
if let Err(e) = vmc.run(&mut methods, args) {
panic!("test args failed: {:?}: {}", args, e)
}
}
// Test "--privileged" flag.
const DUMMY_PRIVILEGED_SUCCESS_ARGS: &[&[&str]] = &[
&[
"vmc",
"container",
"termina",
"penguin",
PRIVILEGED_FLAG_CMDLINE,
"true",
],
&[
"vmc",
"container",
"termina",
PRIVILEGED_FLAG_CMDLINE,
"false",
"penguin",
],
&[
"vmc",
"container",
PRIVILEGED_FLAG_CMDLINE,
"true",
"termina",
"penguin",
],
];
for args in DUMMY_PRIVILEGED_SUCCESS_ARGS {
if let Err(e) = vmc.run(&mut methods, args) {
panic!("test args failed: {:?}: {}", args, e)
}
}
const DUMMY_PRIVILEGED_FAILURE_ARGS: &[&[&str]] = &[
&["vmc", "container", PRIVILEGED_FLAG_CMDLINE],
&["vmc", "container", PRIVILEGED_FLAG_CMDLINE, "termina"],
&["vmc", "container", PRIVILEGED_FLAG_CMDLINE, "termina"],
&[
"vmc",
"container",
PRIVILEGED_FLAG_CMDLINE,
"termina",
"penguin",
],
];
for args in DUMMY_PRIVILEGED_FAILURE_ARGS {
if let Ok(()) = vmc.run(&mut methods, args) {
panic!("test args should have failed: {:?}", args)
}
}
let args = &["vmc", "container", "termina", "a", "--timeout", "600"];
if let Err(e) = vmc.run(&mut methods, args) {
panic!("test args failed: {:?}: {}", args, e)
}
}
}