diff options
author | Alyssa Ross <hi@alyssa.is> | 2022-04-30 17:41:17 +0000 |
---|---|---|
committer | Alyssa Ross <hi@alyssa.is> | 2022-10-09 11:18:55 +0000 |
commit | bb138456bd1cd06f7df806063ddd23634f94b505 (patch) | |
tree | 68e7898267cf46978e4536c5c843143cb69b4faa | |
parent | 4a9248b240a77b255ab6afbcf456dd03300402ad (diff) | |
download | spectrum-bb138456bd1cd06f7df806063ddd23634f94b505.tar spectrum-bb138456bd1cd06f7df806063ddd23634f94b505.tar.gz spectrum-bb138456bd1cd06f7df806063ddd23634f94b505.tar.bz2 spectrum-bb138456bd1cd06f7df806063ddd23634f94b505.tar.lz spectrum-bb138456bd1cd06f7df806063ddd23634f94b505.tar.xz spectrum-bb138456bd1cd06f7df806063ddd23634f94b505.tar.zst spectrum-bb138456bd1cd06f7df806063ddd23634f94b505.zip |
host/start-vm: test cloud-hypervisor command
Pull out all the logic from start-vm into its own file, that can be built as a library and tested.
-rw-r--r-- | host/start-vm/lib.rs | 111 | ||||
-rw-r--r-- | host/start-vm/meson.build | 7 | ||||
-rw-r--r-- | host/start-vm/start-vm.rs | 117 | ||||
-rw-r--r-- | host/start-vm/tests/helper.rs | 66 | ||||
-rw-r--r-- | host/start-vm/tests/meson.build | 6 | ||||
-rw-r--r-- | host/start-vm/tests/vm_command-basic.rs | 53 |
6 files changed, 246 insertions, 114 deletions
diff --git a/host/start-vm/lib.rs b/host/start-vm/lib.rs new file mode 100644 index 0000000..1230a6e --- /dev/null +++ b/host/start-vm/lib.rs @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is> + +mod ch; +mod net; + +use std::borrow::Cow; +use std::env::args_os; +use std::ffi::{CString, OsStr, OsString}; +use std::io::{self, ErrorKind}; +use std::os::unix::prelude::*; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use net::{format_mac, net_setup, NetConfig}; + +pub fn prog_name() -> String { + args_os() + .next() + .as_ref() + .map(Path::new) + .and_then(Path::file_name) + .map(OsStr::to_string_lossy) + .unwrap_or(Cow::Borrowed("start-vm")) + .into_owned() +} + +pub fn vm_command(dir: PathBuf, config_root: &Path) -> Result<Command, String> { + let dir = dir.into_os_string().into_vec(); + let dir = PathBuf::from(OsString::from_vec(dir)); + + let vm_name = dir + .file_name() + .ok_or_else(|| "directory has no name".to_string())?; + + if vm_name.as_bytes().contains(&b',') { + return Err(format!("VM name may not contain a comma: {:?}", vm_name)); + } + + let config_dir = config_root.join(vm_name); + + let mut command = Command::new("s6-notifyoncheck"); + command.args(&["-dc", "test -S env/cloud-hypervisor.sock"]); + command.arg("cloud-hypervisor"); + command.args(&["--api-socket", "env/cloud-hypervisor.sock"]); + command.args(&["--cmdline", "console=ttyS0 root=/dev/vda"]); + command.args(&["--memory", "size=128M"]); + command.args(&["--console", "pty"]); + command.arg("--kernel"); + command.arg(config_dir.join("vmlinux")); + + let net_providers_dir = config_dir.join("providers/net"); + match net_providers_dir.read_dir() { + Ok(entries) => { + for r in entries { + let entry = r + .map_err(|e| format!("examining directory entry: {}", e))? + .file_name(); + + // Safe because provider_name is the name of a directory entry, so + // can't contain a null byte. + let provider_name = unsafe { CString::from_vec_unchecked(entry.into_vec()) }; + + // Safe because we pass a valid pointer and check the result. + let NetConfig { fd, mac } = unsafe { net_setup(provider_name.as_ptr()) }; + if fd == -1 { + let e = io::Error::last_os_error(); + return Err(format!("setting up networking failed: {}", e)); + } + + command + .arg("--net") + .arg(format!("fd={},mac={}", fd, format_mac(&mac))); + + // TODO: to support multiple net providers, we'll need + // a better naming scheme for tap and bridge devices. + break; + } + } + Err(e) if e.kind() == ErrorKind::NotFound => {} + Err(e) => return Err(format!("reading directory {:?}: {}", net_providers_dir, e)), + } + + command.arg("--disk").arg({ + let mut disk = OsString::from("path=/ext/svc/data/"); + disk.push(&vm_name); + disk.push("/rootfs.ext4,readonly=on"); + disk + }); + + command.arg("--serial").arg({ + let mut serial = OsString::from("file=/run/"); + serial.push(&vm_name); + serial.push(".log"); + serial + }); + + Ok(command) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_vm_name_comma() { + assert!(vm_command("/v,m".into(), Path::new("/")) + .unwrap_err() + .contains("comma")); + } +} diff --git a/host/start-vm/meson.build b/host/start-vm/meson.build index e0081c8..9ee6b2e 100644 --- a/host/start-vm/meson.build +++ b/host/start-vm/meson.build @@ -7,11 +7,12 @@ project('start-vm', 'rust', 'c', add_project_arguments('-D_GNU_SOURCE', language : 'c') add_project_arguments('-C', 'panic=abort', language : 'rust') -c_lib = static_library('start-vm-c', 'net.c', 'net-util.c') +c_lib = static_library('start-vm', 'net.c', 'net-util.c') +rust_lib = static_library('start_vm', 'lib.rs', link_with : c_lib) -executable('start-vm', 'start-vm.rs', link_with : c_lib, install : true) +executable('start-vm', 'start-vm.rs', link_with : rust_lib, install : true) -test('Rust unit tests', executable('start-vm-test', 'start-vm.rs', +test('Rust unit tests', executable('start-vm-test', 'lib.rs', rust_args : ['--test', '-C', 'panic=unwind'], link_with : c_lib)) diff --git a/host/start-vm/start-vm.rs b/host/start-vm/start-vm.rs index 2ea3dbf..4790841 100644 --- a/host/start-vm/start-vm.rs +++ b/host/start-vm/start-vm.rs @@ -1,109 +1,14 @@ // SPDX-License-Identifier: EUPL-1.2+ // SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is> -mod ch; -mod net; - -use std::borrow::Cow; -use std::env::{args_os, current_dir}; -use std::ffi::{CString, OsStr, OsString}; -use std::io::{self, ErrorKind}; +use std::env::current_dir; use std::os::unix::prelude::*; -use std::path::{Path, PathBuf}; -use std::process::{exit, Command}; - -use net::{format_mac, net_setup, NetConfig}; - -fn prog_name() -> String { - args_os() - .next() - .as_ref() - .map(Path::new) - .and_then(Path::file_name) - .map(OsStr::to_string_lossy) - .unwrap_or(Cow::Borrowed("start-vm")) - .into_owned() -} - -fn vm_command(dir: PathBuf) -> Result<Command, String> { - let dir = dir.into_os_string().into_vec(); - let dir = PathBuf::from(OsString::from_vec(dir)); - - let vm_name = dir - .file_name() - .ok_or_else(|| "directory has no name".to_string())?; - - if vm_name.as_bytes().contains(&b',') { - return Err(format!("VM name may not contain a comma: {:?}", vm_name)); - } - - let mut command = Command::new("s6-notifyoncheck"); - command.args(&["-dc", "test -S env/cloud-hypervisor.sock"]); - command.arg("cloud-hypervisor"); - command.args(&["--api-socket", "env/cloud-hypervisor.sock"]); - command.args(&["--cmdline", "console=ttyS0 root=/dev/vda"]); - command.args(&["--memory", "size=128M"]); - command.args(&["--console", "pty"]); - - let mut net_providers_dir = PathBuf::new(); - net_providers_dir.push("/ext/svc/data"); - net_providers_dir.push(vm_name); - net_providers_dir.push("providers/net"); - - match net_providers_dir.read_dir() { - Ok(entries) => { - for r in entries { - let entry = r - .map_err(|e| format!("examining directory entry: {}", e))? - .file_name(); - - // Safe because provider_name is the name of a directory entry, so - // can't contain a null byte. - let provider_name = unsafe { CString::from_vec_unchecked(entry.into_vec()) }; +use std::path::Path; +use std::process::exit; - // Safe because we pass a valid pointer and check the result. - let NetConfig { fd, mac } = unsafe { net_setup(provider_name.as_ptr()) }; - if fd == -1 { - let e = io::Error::last_os_error(); - return Err(format!("setting up networking failed: {}", e)); - } +use start_vm::{prog_name, vm_command}; - command - .arg("--net") - .arg(format!("fd={},mac={}", fd, format_mac(&mac))); - - // TODO: to support multiple net providers, we'll need - // a better naming scheme for tap and bridge devices. - break; - } - } - Err(e) if e.kind() == ErrorKind::NotFound => {} - Err(e) => return Err(format!("reading directory {:?}: {}", net_providers_dir, e)), - } - - command.arg("--kernel").arg({ - let mut kernel = OsString::from("/ext/svc/data/"); - kernel.push(&vm_name); - kernel.push("/vmlinux"); - kernel - }); - - command.arg("--disk").arg({ - let mut disk = OsString::from("path=/ext/svc/data/"); - disk.push(&vm_name); - disk.push("/rootfs.ext4,readonly=on"); - disk - }); - - command.arg("--serial").arg({ - let mut serial = OsString::from("file=/run/"); - serial.push(&vm_name); - serial.push(".log"); - serial - }); - - Ok(command) -} +const CONFIG_ROOT: &str = "/ext/svc/data"; fn run() -> String { let dir = match current_dir().map_err(|e| format!("getting current directory: {}", e)) { @@ -111,7 +16,7 @@ fn run() -> String { Err(e) => return e, }; - match vm_command(dir) { + match vm_command(dir, Path::new(CONFIG_ROOT)) { Ok(mut command) => format!("failed to exec: {}", command.exec()), Err(e) => e, } @@ -121,13 +26,3 @@ fn main() { eprintln!("{}: {}", prog_name(), run()); exit(1); } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_vm_name_comma() { - assert!(vm_command("/v,m".into()).unwrap_err().contains("comma")); - } -} diff --git a/host/start-vm/tests/helper.rs b/host/start-vm/tests/helper.rs new file mode 100644 index 0000000..8cb8572 --- /dev/null +++ b/host/start-vm/tests/helper.rs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is> + +use std::ffi::OsString; +use std::io; +use std::mem::{forget, swap}; +use std::os::raw::c_char; +use std::os::unix::prelude::*; +use std::path::{Path, PathBuf}; + +use start_vm::prog_name; + +extern "C" { + fn mkdtemp(template: *mut c_char) -> *mut c_char; +} + +// FIXME: once OnceCell is in the standard library, we won't need a +// function for this any more. +// https://github.com/rust-lang/rust/issues/74465 +fn tmpdir() -> std::path::PathBuf { + std::env::var_os("TMPDIR") + .unwrap_or_else(|| OsString::from("/tmp")) + .into() +} + +pub struct TempDir(PathBuf); + +impl TempDir { + pub fn new() -> std::io::Result<Self> { + let mut dirname = OsString::from("spectrum-start-vm-test-"); + dirname.push(prog_name()); + dirname.push(".XXXXXX"); + let mut template = tmpdir(); + template.push(dirname); + + let c_path = Box::into_raw(template.into_os_string().into_vec().into_boxed_slice()); + + // Safe because we own c_path. + if unsafe { mkdtemp(c_path as *mut c_char) }.is_null() { + return Err(io::Error::last_os_error()); + } + + // Safe because we own c_path and it came from Box::into_raw. + let path = PathBuf::from(OsString::from_vec(unsafe { Box::from_raw(c_path) }.into())); + Ok(Self(path)) + } + + pub fn path(&self) -> &Path { + self.0.as_path() + } + + /// The `TempDir` Drop handler will not be run, so the caller takes responsibility + /// for removing the directory when no longer required. + pub fn into_path_buf(mut self) -> PathBuf { + let mut path = PathBuf::new(); + swap(&mut path, &mut self.0); + forget(self); + path + } +} + +impl Drop for TempDir { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} diff --git a/host/start-vm/tests/meson.build b/host/start-vm/tests/meson.build index 229c58d..00bd33d 100644 --- a/host/start-vm/tests/meson.build +++ b/host/start-vm/tests/meson.build @@ -1,6 +1,9 @@ # SPDX-License-Identifier: EUPL-1.2+ # SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is> +rust_helper = static_library('test_helper', 'helper.rs', + link_with : [c_lib, rust_lib]) + test('if_up', executable('if_up', 'if_up.c', '../net-util.c')) test('if_rename', executable('if_rename', 'if_rename.c', '../net-util.c')) test('if_rename (%d)', executable('if_rename-%d', @@ -25,3 +28,6 @@ test('bridge_remove', executable('bridge_remove', test('tap_open', executable('tap_open', 'tap_open.c', '../net-util.c')) test('tap_open (name too long)', executable('tap_open-name-too-long', 'tap_open-name-too-long.c', '../net-util.c')) + +test('vm_command-basic', executable('vm_command-basic', + 'vm_command-basic.rs', link_with : [rust_lib, rust_helper])) diff --git a/host/start-vm/tests/vm_command-basic.rs b/host/start-vm/tests/vm_command-basic.rs new file mode 100644 index 0000000..b2edb7c --- /dev/null +++ b/host/start-vm/tests/vm_command-basic.rs @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is> + +use std::ffi::{OsStr, OsString}; +use std::fs::{create_dir, create_dir_all, File}; + +use start_vm::vm_command; +use test_helper::TempDir; + +fn main() -> std::io::Result<()> { + let tmp_dir = TempDir::new()?; + + let service_dir = tmp_dir.path().join("testvm"); + create_dir(&service_dir)?; + + let kernel_path = tmp_dir.path().join("svc/data/testvm/vmlinux"); + let image_path = tmp_dir.path().join("svc/data/testvm/rootfs.ext4"); + + create_dir_all(kernel_path.parent().unwrap())?; + create_dir_all(image_path.parent().unwrap())?; + File::create(&kernel_path)?; + File::create(&image_path)?; + + let command = vm_command(service_dir, &tmp_dir.path().join("svc/data")).unwrap(); + assert_eq!(command.get_program(), "s6-notifyoncheck"); + + let mut expected_disk_arg = OsString::from("path="); + expected_disk_arg.push(image_path); + expected_disk_arg.push(",readonly=on"); + + let expected_args = vec![ + OsStr::new("-dc"), + OsStr::new("test -S env/cloud-hypervisor.sock"), + OsStr::new("cloud-hypervisor"), + OsStr::new("--api-socket"), + OsStr::new("env/cloud-hypervisor.sock"), + OsStr::new("--cmdline"), + OsStr::new("console=ttyS0 root=/dev/vda"), + OsStr::new("--memory"), + OsStr::new("size=128M"), + OsStr::new("--console"), + OsStr::new("pty"), + OsStr::new("--kernel"), + kernel_path.as_os_str(), + OsStr::new("--disk"), + OsStr::new("path=/ext/svc/data/testvm/rootfs.ext4,readonly=on"), + OsStr::new("--serial"), + OsStr::new("file=/run/testvm.log"), + ]; + + assert!(command.get_args().eq(expected_args.into_iter())); + Ok(()) +} |