summary refs log tree commit diff
diff options
context:
space:
mode:
authorAlyssa Ross <hi@alyssa.is>2022-04-30 17:41:17 +0000
committerAlyssa Ross <hi@alyssa.is>2022-10-09 11:18:55 +0000
commitbb138456bd1cd06f7df806063ddd23634f94b505 (patch)
tree68e7898267cf46978e4536c5c843143cb69b4faa
parent4a9248b240a77b255ab6afbcf456dd03300402ad (diff)
downloadspectrum-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.rs111
-rw-r--r--host/start-vm/meson.build7
-rw-r--r--host/start-vm/start-vm.rs117
-rw-r--r--host/start-vm/tests/helper.rs66
-rw-r--r--host/start-vm/tests/meson.build6
-rw-r--r--host/start-vm/tests/vm_command-basic.rs53
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(())
+}