blob: 8d1b67b69c99a5253763696faeefb7946c98362d [file] [log] [blame]
// Copyright 2018 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::error::Error;
use std::fmt;
use getopts::Options;
use backends::Backend;
use frontends::Frontend;
use EnvMap;
enum VmcError {
Command(&'static str, u32, Box<Error>),
ExpectedCrosUserIdHash,
ExpectedName,
ExpectedNoArgs,
ExpectedU8Bus,
ExpectedU8Device,
ExpectedU8Port,
ExpectedVmAndContainer,
ExpectedVmAndFileName,
ExpectedVmAndPath,
ExpectedVmBusDevice,
ExpectedVmPort,
InvalidEmail,
MissingActiveSession,
UnknownSubcommand(String),
}
use self::VmcError::*;
// Remove useless expression items that the `try_command!()` macro captures and stringifies when
// generating a `VmcError::Command`.
fn trim_routine(s: &str) -> String {
s.trim_start_matches("self.backend.")
.replace(char::is_whitespace, "")
}
impl fmt::Display for VmcError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Command(routine, line_num, e) => write!(
f,
"routine at {}:{} `{}` failed: {}",
file!(),
line_num,
trim_routine(&routine),
e
),
ExpectedCrosUserIdHash => write!(f, "expected CROS_USER_ID_HASH environment variable"),
ExpectedName => write!(f, "expected <name>"),
ExpectedVmAndContainer => write!(
f,
"expected <vm name> <container name> [ <image server> <image alias> ]"
),
ExpectedVmAndFileName => {
write!(f, "expected <vm name> <file name> [removable storage name]")
}
ExpectedVmAndPath => write!(f, "expected <vm name> <path>"),
ExpectedVmBusDevice => write!(f, "expected <vm name> <bus>:<device>"),
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"),
ExpectedVmPort => write!(f, "expected <vm name> <port>"),
InvalidEmail => write!(f, "the active session has an invalid email address"),
MissingActiveSession => write!(
f,
"missing active session corresponding to $CROS_USER_ID_HASH"
),
UnknownSubcommand(s) => write!(f, "no such subcommand: `{}`", s),
}
}
}
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<Error>>;
macro_rules! try_command {
($x:expr) => {
$x.map_err(|e| Command(stringify!($x), line!(), e))?
};
}
struct Command<'a, 'b, 'c> {
backend: &'a mut Backend,
args: &'b [&'b str],
environ: &'c EnvMap<'c>,
}
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.backend.metrics_send_sample(name) {
eprintln!(
"warning: failed attempt to send metrics sample `{}`: {}",
name, e
);
}
}
fn start(&mut self) -> VmcResult {
let mut opts = Options::new();
opts.optflag("", "enable-gpu", "when starting the vm, enable gpu support");
let matches = opts.parse(self.args)?;
if matches.free.len() != 1 {
return Err(ExpectedName.into());
}
let vm_name = &matches.free[0];
let user_id_hash = self
.environ
.get("CROS_USER_ID_HASH")
.ok_or(ExpectedCrosUserIdHash)?;
self.metrics_send_sample("Vm.VmcStart");
try_command!(self.backend.vm_start(
vm_name,
user_id_hash,
matches.opt_present("enable-gpu")
));
self.metrics_send_sample("Vm.VmcStartSuccess");
try_command!(self.backend.vsh_exec(vm_name, user_id_hash));
Ok(())
}
fn stop(&mut self) -> VmcResult {
if self.args.len() != 1 {
return Err(ExpectedName.into());
}
let vm_name = self.args[0];
let user_id_hash = self
.environ
.get("CROS_USER_ID_HASH")
.ok_or(ExpectedCrosUserIdHash)?;
try_command!(self.backend.vm_stop(vm_name, user_id_hash));
Ok(())
}
fn destroy(&mut self) -> VmcResult {
if self.args.len() != 1 {
return Err(ExpectedName.into());
}
let vm_name = self.args[0];
let user_id_hash = self
.environ
.get("CROS_USER_ID_HASH")
.ok_or(ExpectedCrosUserIdHash)?;
match self.backend.disk_destroy(vm_name, user_id_hash) {
Ok(()) => Ok(()),
Err(e) => {
self.metrics_send_sample("Vm.DiskEraseFailed");
Err(Command("disk_destroy", line!(), e).into())
}
}
}
fn export(&mut self) -> VmcResult {
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()),
};
let user_id_hash = self
.environ
.get("CROS_USER_ID_HASH")
.ok_or(ExpectedCrosUserIdHash)?;
try_command!(self
.backend
.vm_export(vm_name, user_id_hash, file_name, removable_media));
Ok(())
}
fn list(&mut self) -> VmcResult {
if !self.args.is_empty() {
return Err(ExpectedNoArgs.into());
}
let user_id_hash = self
.environ
.get("CROS_USER_ID_HASH")
.ok_or(ExpectedCrosUserIdHash)?;
let (disk_image_list, total_size) = try_command!(self.backend.disk_list(user_id_hash));
for disk_image in disk_image_list {
println!("{}", disk_image);
}
println!("Total Size (bytes): {}", total_size);
Ok(())
}
fn share(&mut self) -> VmcResult {
if self.args.len() != 2 {
return Err(ExpectedVmAndPath.into());
}
let user_id_hash = self
.environ
.get("CROS_USER_ID_HASH")
.ok_or(ExpectedCrosUserIdHash)?;
let vm_name = self.args[0];
let path = self.args[1];
let vm_path = try_command!(self.backend.vm_share_path(vm_name, user_id_hash, path));
println!("{} is available at path {}", path, vm_path);
Ok(())
}
fn container(&mut self) -> VmcResult {
let (vm_name, container_name, image_server, image_alias) = match self.args.len() {
2 => (
self.args[0],
self.args[1],
"https://storage.googleapis.com/cros-containers",
"debian/stretch",
),
4 => (self.args[0], self.args[1], self.args[2], self.args[3]),
_ => return Err(ExpectedVmAndContainer.into()),
};
let user_id_hash = self
.environ
.get("CROS_USER_ID_HASH")
.ok_or(ExpectedCrosUserIdHash)?;
let sessions = try_command!(self.backend.sessions_list());
let email = sessions
.iter()
.find(|(_, hash)| hash == user_id_hash)
.map(|(email, _)| email)
.ok_or(MissingActiveSession)?;
let username = match email.find('@') {
Some(0) | None => return Err(InvalidEmail.into()),
Some(end) => &email[..end],
};
try_command!(self.backend.container_create(
vm_name,
user_id_hash,
container_name,
image_server,
image_alias
));
try_command!(self
.backend
.container_start(vm_name, user_id_hash, container_name));
try_command!(self.backend.container_setup_user(
vm_name,
user_id_hash,
container_name,
username
));
try_command!(self
.backend
.vsh_exec_container(vm_name, user_id_hash, container_name));
Ok(())
}
fn usb_attach(&mut self) -> VmcResult {
let (vm_name, bus_device) = match self.args.len() {
2 => (self.args[0], self.args[1]),
_ => 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 user_id_hash = self
.environ
.get("CROS_USER_ID_HASH")
.ok_or(ExpectedCrosUserIdHash)?;
let guest_port = try_command!(self.backend.usb_attach(vm_name, user_id_hash, bus, device));
println!(
"usb device at bus={} device={} attached to vm {} at port={}",
bus, 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()),
};
let user_id_hash = self
.environ
.get("CROS_USER_ID_HASH")
.ok_or(ExpectedCrosUserIdHash)?;
try_command!(self.backend.usb_detach(vm_name, 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 user_id_hash = self
.environ
.get("CROS_USER_ID_HASH")
.ok_or(ExpectedCrosUserIdHash)?;
let devices = try_command!(self.backend.usb_list(vm_name, 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(())
}
}
const USAGE: &str = r#"
[ start [--enable-gpu] <name> |
stop <name> |
destroy <name> |
export <vm name> <file name> [removable storage name] |
list |
share <vm name> <path> |
container <vm name> <container name> [ <image server> <image alias> ] |
usb-attach <vm name> <bus>:<device> |
usb-detach <vm name> <port> |
usb-list <vm name> |
help ]
"#;
/// A frontend that implements a `vmc` (Virtual Machine Controller) style command line interface.
/// This is the interface accessible from crosh (Ctrl-Alt-T in the browser to access).
pub struct Vmc;
impl Frontend for Vmc {
fn name(&self) -> &str {
"vmc"
}
fn print_usage(&self, program_name: &str) {
eprintln!("USAGE: {}{}", program_name, USAGE);
}
fn run(&self, backend: &mut Backend, args: &[&str], environ: &EnvMap) -> 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 {
backend,
args: &args[2..],
environ,
};
let command_name = args[1];
match command_name {
"start" => command.start(),
"stop" => command.stop(),
"destroy" => command.destroy(),
"export" => command.export(),
"list" => command.list(),
"share" => command.share(),
"container" => command.container(),
"usb-attach" => command.usb_attach(),
"usb-detach" => command.usb_detach(),
"usb-list" => command.usb_list(),
_ => Err(UnknownSubcommand(command_name.to_owned()).into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use backends::DummyDefaultBackend;
#[test]
fn dummy_default_backend() {
const DUMMY_SUCCESS_ARGS: &[&[&str]] = &[
&["vmc", "start", "termina"],
&["vmc", "start", "--enable-gpu", "termina"],
&["vmc", "start", "termina", "--enable-gpu"],
&["vmc", "stop", "termina"],
&["vmc", "destroy", "termina"],
&["vmc", "export", "termina", "file name"],
&["vmc", "export", "termina", "file name", "removable media"],
&["vmc", "list"],
&["vmc", "share", "termina", "my-folder"],
&["vmc", "usb-attach", "termina", "1:2"],
&["vmc", "usb-detach", "termina", "5"],
&["vmc", "usb-detach", "termina", "5"],
&["vmc", "usb-list", "termina"],
&["vmc", "--help"],
&["vmc", "-h"],
];
const DUMMY_FAILURE_ARGS: &[&[&str]] = &[
&["vmc", "start"],
&["vmc", "start", "--i-made-this-up", "termina"],
&["vmc", "start", "termina", "extra args"],
&["vmc", "stop"],
&["vmc", "stop", "termina", "extra args"],
&["vmc", "destroy"],
&["vmc", "destroy", "termina", "extra args"],
&["vmc", "export", "termina"],
&["vmc", "export", "termina", "too", "many", "args"],
&["vmc", "list", "extra args"],
&["vmc", "share"],
&["vmc", "share", "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-detach"],
&["vmc", "usb-detach", "not-a-number"],
&["vmc", "usb-list"],
&["vmc", "usb-list", "termina", "args"],
];
let environ = vec![("CROS_USER_ID_HASH", "fake_hash")]
.into_iter()
.collect();
for args in DUMMY_SUCCESS_ARGS {
if let Err(e) = Vmc.run(&mut DummyDefaultBackend, args, &environ) {
panic!("test args failed: {:?}: {}", args, e)
}
}
for args in DUMMY_FAILURE_ARGS {
if let Ok(()) = Vmc.run(&mut DummyDefaultBackend, args, &environ) {
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",
],
];
struct SessionsListBackend;
impl Backend for SessionsListBackend {
fn name(&self) -> &'static str {
"Sessions List"
}
fn sessions_list(&mut self) -> Result<Vec<(String, String)>, Box<Error>> {
Ok(vec![(
"testuser@example.com".to_owned(),
"fake_hash".to_owned(),
)])
}
fn vsh_exec_container(
&mut self,
_vm_name: &str,
_user_id_hash: &str,
_container_name: &str,
) -> Result<(), Box<Error>> {
Ok(())
}
fn container_create(
&mut self,
_vm_name: &str,
_user_id_hash: &str,
_container_name: &str,
_image_server: &str,
_image_alias: &str,
) -> Result<(), Box<Error>> {
Ok(())
}
fn container_start(
&mut self,
_vm_name: &str,
_user_id_hash: &str,
_container_name: &str,
) -> Result<(), Box<Error>> {
Ok(())
}
fn container_setup_user(
&mut self,
_vm_name: &str,
_user_id_hash: &str,
_container_name: &str,
_username: &str,
) -> Result<(), Box<Error>> {
Ok(())
}
}
let environ = vec![("CROS_USER_ID_HASH", "fake_hash")]
.into_iter()
.collect();
for args in CONTAINER_ARGS {
if let Err(e) = Vmc.run(&mut SessionsListBackend, args, &environ) {
panic!("test args failed: {:?}: {}", args, e)
}
}
}
}