mirror of
https://github.com/hanzoai/vm.git
synced 2026-10-01 00:47:56 +00:00
vm-ch: cloud-hypervisor backend for x86_64 Linux
Exports the same device vocabulary as vm-darwin/vm-linux. The VirtualMachine spawns cloud-hypervisor and drives it over the API socket (vm.create/vm.boot on start, vm.shutdown + vmm.shutdown on stop), spawns one virtiofsd per shared directory, and serves the proxy socketpair from an in-process vhost-user-net backend (queue 0 RX, queue 1 TX, 12-byte virtio-net header stripped/prepended at the socketpair boundary). Guest memory is shared=on for the vhost-user devices. connect_to_vsock_port speaks the hybrid vsock handshake (CONNECT <port> / OK <n>) and rewraps the UnixStream fd as TcpStream for the platform-neutral sandbox API. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
Generated
+172
-7
@@ -79,6 +79,15 @@ version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
@@ -160,7 +169,7 @@ dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"constant_time_eq",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -246,6 +255,17 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.1",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
@@ -356,6 +376,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
@@ -628,10 +657,22 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"r-efi 5.3.0",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
@@ -946,7 +987,7 @@ version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2efe3f1a4437bffe000e6297a593b98184213cd27486776c335f95ab53d48e3a"
|
||||
dependencies = [
|
||||
"vmm-sys-util",
|
||||
"vmm-sys-util 0.12.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -958,7 +999,7 @@ dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"kvm-bindings",
|
||||
"libc",
|
||||
"vmm-sys-util",
|
||||
"vmm-sys-util 0.12.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1000,7 +1041,7 @@ version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d379d0089d0fbf4161c35a4fdfd76125923f1a93632c49195f5372b4c0b1472"
|
||||
dependencies = [
|
||||
"vm-memory",
|
||||
"vm-memory 0.15.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1309,6 +1350,29 @@ version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom 0.4.3",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "rcgen"
|
||||
version = "0.13.2"
|
||||
@@ -1430,6 +1494,12 @@ dependencies = [
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@@ -1899,6 +1969,16 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
@@ -1911,6 +1991,53 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vhost"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c63c14485260662b4257bd929d5fd4aaf96bbecbe61d2224bad8807849f5f8f"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"libc",
|
||||
"uuid",
|
||||
"vm-memory 0.18.0",
|
||||
"vmm-sys-util 0.15.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vhost-user-backend"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "555753b65bc33837bd011f981e2c6d52d8ddc330d92c2deb9aa3f0b378a58bcc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"vhost",
|
||||
"virtio-bindings",
|
||||
"virtio-queue",
|
||||
"vm-memory 0.18.0",
|
||||
"vmm-sys-util 0.15.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "virtio-bindings"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "091f1f09cfbf2a78563b562e7a949465cce1aef63b6065645188d995162f8868"
|
||||
|
||||
[[package]]
|
||||
name = "virtio-queue"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f631bfd09362a9b17f0cfd5ca3b0a1b179d153fa276f9ce86919d560891e61d6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"virtio-bindings",
|
||||
"vm-memory 0.18.0",
|
||||
"vmm-sys-util 0.15.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vm"
|
||||
version = "0.1.0"
|
||||
@@ -1921,11 +2048,27 @@ dependencies = [
|
||||
"serde_json",
|
||||
"socket2 0.5.10",
|
||||
"tracing",
|
||||
"vm-ch",
|
||||
"vm-darwin",
|
||||
"vm-linux",
|
||||
"vm-proto",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vm-ch"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"libc",
|
||||
"serde_json",
|
||||
"vhost",
|
||||
"vhost-user-backend",
|
||||
"virtio-bindings",
|
||||
"virtio-queue",
|
||||
"vm-memory 0.18.0",
|
||||
"vmm-sys-util 0.15.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vm-darwin"
|
||||
version = "0.1.0"
|
||||
@@ -1969,8 +2112,8 @@ dependencies = [
|
||||
"libc",
|
||||
"linux-loader",
|
||||
"vm-fdt",
|
||||
"vm-memory",
|
||||
"vmm-sys-util",
|
||||
"vm-memory 0.15.0",
|
||||
"vmm-sys-util 0.12.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1984,6 +2127,18 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vm-memory"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b55e753c7725603745cb32b2287ef7ef3da05c03c7702cda3fa8abe25ae0465"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"libc",
|
||||
"thiserror 2.0.18",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vm-proto"
|
||||
version = "0.1.0"
|
||||
@@ -2052,6 +2207,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vmm-sys-util"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "506c62fdf617a5176827c2f9afbcf1be155b03a9b4bf9617a60dbc07e3a1642f"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
|
||||
@@ -4,6 +4,7 @@ members = [
|
||||
"crates/vm-cli",
|
||||
"crates/vm-darwin",
|
||||
"crates/vm-linux",
|
||||
"crates/vm-ch",
|
||||
"crates/vm-proto",
|
||||
"crates/vm-proxy",
|
||||
"crates/vm-sdk",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "vm-ch"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "cloud-hypervisor backend for Hanzo VM"
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/hanzoai/vm"
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
crossbeam-channel = "0.5"
|
||||
serde_json = "1"
|
||||
vhost = { version = "0.17", features = ["vhost-user-backend"] }
|
||||
vhost-user-backend = "0.23"
|
||||
virtio-bindings = "0.2"
|
||||
virtio-queue = "0.18"
|
||||
vm-memory = { version = "0.18", features = ["backend-mmap", "backend-atomic"] }
|
||||
vmm-sys-util = "0.15"
|
||||
@@ -0,0 +1,87 @@
|
||||
//! Minimal HTTP/1.1 client for the cloud-hypervisor API socket.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::error::{Result, VzError};
|
||||
|
||||
/// PUT `/api/v1/<endpoint>` with an optional JSON body.
|
||||
pub(crate) fn put(socket: &str, endpoint: &str, body: Option<&str>) -> Result<()> {
|
||||
let mut stream = UnixStream::connect(socket)
|
||||
.map_err(|e| VzError::new(format!("connect to {}: {}", socket, e)))?;
|
||||
let _ = stream.set_read_timeout(Some(Duration::from_secs(10)));
|
||||
|
||||
let body = body.unwrap_or("");
|
||||
let request = format!(
|
||||
"PUT /api/v1/{} HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
|
||||
endpoint,
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
stream
|
||||
.write_all(request.as_bytes())
|
||||
.map_err(|e| VzError::new(format!("{}: send: {}", endpoint, e)))?;
|
||||
|
||||
// Read the status line and headers, then a Content-Length body if any.
|
||||
let mut buf = Vec::new();
|
||||
let mut chunk = [0u8; 4096];
|
||||
let header_end = loop {
|
||||
let n = stream
|
||||
.read(&mut chunk)
|
||||
.map_err(|e| VzError::new(format!("{}: read: {}", endpoint, e)))?;
|
||||
if n == 0 {
|
||||
break buf.len();
|
||||
}
|
||||
buf.extend_from_slice(&chunk[..n]);
|
||||
if let Some(pos) = find_header_end(&buf) {
|
||||
break pos;
|
||||
}
|
||||
if buf.len() > 64 * 1024 {
|
||||
return Err(VzError::new(format!("{}: oversized response", endpoint)));
|
||||
}
|
||||
};
|
||||
|
||||
let head = String::from_utf8_lossy(&buf[..header_end]).to_string();
|
||||
let status: u16 = head
|
||||
.split_whitespace()
|
||||
.nth(1)
|
||||
.and_then(|s| s.parse().ok())
|
||||
.ok_or_else(|| VzError::new(format!("{}: malformed response", endpoint)))?;
|
||||
|
||||
let content_length: usize = head
|
||||
.lines()
|
||||
.find_map(|l| {
|
||||
let (k, v) = l.split_once(':')?;
|
||||
k.eq_ignore_ascii_case("content-length")
|
||||
.then(|| v.trim().parse().ok())?
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
let mut rest = buf[header_end..].to_vec();
|
||||
while rest.len() < content_length {
|
||||
let n = stream
|
||||
.read(&mut chunk)
|
||||
.map_err(|e| VzError::new(format!("{}: read body: {}", endpoint, e)))?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
rest.extend_from_slice(&chunk[..n]);
|
||||
}
|
||||
|
||||
if (200..300).contains(&status) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(VzError::new(format!(
|
||||
"{}: HTTP {}: {}",
|
||||
endpoint,
|
||||
status,
|
||||
String::from_utf8_lossy(&rest).trim()
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Offset just past the `\r\n\r\n` separator, if present.
|
||||
fn find_header_end(buf: &[u8]) -> Option<usize> {
|
||||
buf.windows(4).position(|w| w == b"\r\n\r\n").map(|p| p + 4)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
/// Stores the kernel, initrd, and command-line paths for booting a Linux guest.
|
||||
/// On macOS this wraps VZLinuxBootLoader; here it is a plain data holder
|
||||
/// consumed by `VirtualMachine::new()`.
|
||||
///
|
||||
/// Uses `RefCell` for interior mutability to match Darwin`s `&self` setter API.
|
||||
pub struct LinuxBootLoader {
|
||||
pub(crate) kernel_path: String,
|
||||
pub(crate) initrd_path: RefCell<Option<String>>,
|
||||
pub(crate) command_line: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
impl LinuxBootLoader {
|
||||
pub fn new(kernel_path: &str, initrd_path: &str, command_line: &str) -> Self {
|
||||
let bl = Self::new_with_kernel(kernel_path);
|
||||
bl.set_initrd(initrd_path);
|
||||
bl.set_command_line(command_line);
|
||||
bl
|
||||
}
|
||||
|
||||
pub fn new_with_kernel(kernel_path: &str) -> Self {
|
||||
LinuxBootLoader {
|
||||
kernel_path: kernel_path.to_string(),
|
||||
initrd_path: RefCell::new(None),
|
||||
command_line: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_initrd(&self, initrd_path: &str) {
|
||||
*self.initrd_path.borrow_mut() = Some(initrd_path.to_string());
|
||||
}
|
||||
|
||||
/// cloud-hypervisor attaches a virtio-console, so `console=hvc0` from the
|
||||
/// caller is kept as-is.
|
||||
pub fn set_command_line(&self, command_line: &str) {
|
||||
*self.command_line.borrow_mut() = Some(command_line.to_string());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
use std::cell::RefCell;
|
||||
use std::os::fd::RawFd;
|
||||
|
||||
use crate::bootloader::LinuxBootLoader;
|
||||
use crate::directory_sharing::VirtioFileSystemDevice;
|
||||
use crate::entropy::VirtioEntropyDevice;
|
||||
use crate::error::{Result, VzError};
|
||||
use crate::memory_balloon::VirtioMemoryBalloonDevice;
|
||||
use crate::network::VirtioNetworkDevice;
|
||||
use crate::serial::VirtioConsoleSerialPort;
|
||||
use crate::socket::VirtioSocketDevice;
|
||||
use crate::storage::StorageDevice;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ConfigData {
|
||||
pub cpu_count: usize,
|
||||
pub memory_size: u64,
|
||||
pub kernel_path: String,
|
||||
pub initrd_path: Option<String>,
|
||||
pub command_line: String,
|
||||
pub serial_read_fd: Option<RawFd>,
|
||||
pub serial_write_fd: Option<RawFd>,
|
||||
pub disk_path: Option<String>,
|
||||
pub disk_read_only: bool,
|
||||
pub network_fd: Option<RawFd>,
|
||||
pub network_mac: Option<[u8; 6]>,
|
||||
pub has_socket: bool,
|
||||
pub mounts: Vec<(String, String, bool)>, // (tag, host_path, read_only)
|
||||
}
|
||||
|
||||
pub struct VirtualMachineConfiguration {
|
||||
pub(crate) inner: RefCell<ConfigData>,
|
||||
}
|
||||
|
||||
impl VirtualMachineConfiguration {
|
||||
pub fn new(boot_loader: &LinuxBootLoader, cpus: usize, memory: u64) -> Self {
|
||||
VirtualMachineConfiguration {
|
||||
inner: RefCell::new(ConfigData {
|
||||
cpu_count: cpus,
|
||||
memory_size: memory,
|
||||
kernel_path: boot_loader.kernel_path.clone(),
|
||||
initrd_path: boot_loader.initrd_path.borrow().clone(),
|
||||
command_line: boot_loader
|
||||
.command_line
|
||||
.borrow()
|
||||
.clone()
|
||||
.unwrap_or_default(),
|
||||
serial_read_fd: None,
|
||||
serial_write_fd: None,
|
||||
disk_path: None,
|
||||
disk_read_only: false,
|
||||
network_fd: None,
|
||||
network_mac: None,
|
||||
has_socket: false,
|
||||
mounts: Vec::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_cpu_count(&self, cpus: usize) {
|
||||
self.inner.borrow_mut().cpu_count = cpus;
|
||||
}
|
||||
|
||||
pub fn set_memory_size(&self, memory: u64) {
|
||||
self.inner.borrow_mut().memory_size = memory;
|
||||
}
|
||||
|
||||
pub fn set_boot_loader(&self, boot_loader: &LinuxBootLoader) {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
inner.kernel_path = boot_loader.kernel_path.clone();
|
||||
inner.initrd_path = boot_loader.initrd_path.borrow().clone();
|
||||
if let Some(ref cmdline) = *boot_loader.command_line.borrow() {
|
||||
inner.command_line = cmdline.clone();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_serial_ports(&self, ports: &[VirtioConsoleSerialPort]) {
|
||||
if let Some(port) = ports.first() {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
inner.serial_read_fd = port.read_fd;
|
||||
inner.serial_write_fd = port.write_fd;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_storage_devices(&self, devices: &[&dyn StorageDevice]) {
|
||||
if let Some(device) = devices.first() {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
inner.disk_path = device.get_disk_path().map(|s| s.to_string());
|
||||
inner.disk_read_only = device.get_read_only();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_network_devices(&self, devices: &[VirtioNetworkDevice]) {
|
||||
if let Some(device) = devices.first() {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
inner.network_fd = device.fd;
|
||||
let mac = device.mac_bytes();
|
||||
if mac != [0; 6] {
|
||||
inner.network_mac = Some(mac);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_socket_devices(&self, devices: &[VirtioSocketDevice]) {
|
||||
if !devices.is_empty() {
|
||||
self.inner.borrow_mut().has_socket = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_directory_sharing_devices(&self, devices: &[VirtioFileSystemDevice]) {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
for dev in devices {
|
||||
inner
|
||||
.mounts
|
||||
.push((dev.tag.clone(), dev.host_path.clone(), dev.read_only));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_entropy_devices(&self, _devices: &[VirtioEntropyDevice]) {
|
||||
// Entropy device is a no-op on KVM — /dev/urandom is available.
|
||||
}
|
||||
|
||||
pub fn set_memory_balloon_devices(&self, _devices: &[VirtioMemoryBalloonDevice]) {
|
||||
// Memory balloon is not yet implemented for KVM.
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
let inner = self.inner.borrow();
|
||||
if inner.kernel_path.is_empty() {
|
||||
return Err(VzError::new("kernel path is required"));
|
||||
}
|
||||
if inner.memory_size == 0 {
|
||||
return Err(VzError::new("memory size must be > 0"));
|
||||
}
|
||||
if inner.cpu_count == 0 {
|
||||
return Err(VzError::new("CPU count must be > 0"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/// Stores host directory path and read-only flag for virtiofs sharing.
|
||||
/// On Linux/KVM, virtiofs requires an external `virtiofsd` process;
|
||||
/// this type only holds config and is consumed by VirtualMachine.
|
||||
pub struct SharedDirectory {
|
||||
pub(crate) path: String,
|
||||
pub(crate) read_only: bool,
|
||||
}
|
||||
|
||||
impl SharedDirectory {
|
||||
pub fn new(path: &str, read_only: bool) -> Self {
|
||||
SharedDirectory {
|
||||
path: path.to_string(),
|
||||
read_only,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VirtioFileSystemDevice {
|
||||
pub(crate) tag: String,
|
||||
pub(crate) host_path: String,
|
||||
pub(crate) read_only: bool,
|
||||
}
|
||||
|
||||
impl VirtioFileSystemDevice {
|
||||
pub fn new(tag: &str, directory: &SharedDirectory) -> Self {
|
||||
VirtioFileSystemDevice {
|
||||
tag: tag.to_string(),
|
||||
host_path: directory.path.clone(),
|
||||
read_only: directory.read_only,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/// Placeholder — the guest gets entropy from the kernel's virtio-rng
|
||||
/// if present in the FDT. Kept for API compatibility with vm-darwin.
|
||||
pub struct VirtioEntropyDevice;
|
||||
|
||||
impl VirtioEntropyDevice {
|
||||
pub fn new() -> Self {
|
||||
VirtioEntropyDevice
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VirtioEntropyDevice {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VzError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl VzError {
|
||||
pub fn new(message: impl Into<String>) -> Self {
|
||||
VzError {
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for VzError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for VzError {}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, VzError>;
|
||||
@@ -0,0 +1,30 @@
|
||||
mod api;
|
||||
mod net_backend;
|
||||
|
||||
mod bootloader;
|
||||
mod configuration;
|
||||
mod directory_sharing;
|
||||
mod entropy;
|
||||
mod error;
|
||||
mod memory_balloon;
|
||||
pub mod network;
|
||||
mod serial;
|
||||
mod socket;
|
||||
mod storage;
|
||||
pub mod terminal;
|
||||
mod vm;
|
||||
|
||||
pub use bootloader::LinuxBootLoader;
|
||||
pub use configuration::VirtualMachineConfiguration;
|
||||
pub use directory_sharing::{SharedDirectory, VirtioFileSystemDevice};
|
||||
pub use entropy::VirtioEntropyDevice;
|
||||
pub use error::{Result, VzError};
|
||||
pub use memory_balloon::VirtioMemoryBalloonDevice;
|
||||
pub use network::{FileHandleNetworkAttachment, MACAddress, VirtioNetworkDevice};
|
||||
pub use serial::{FileHandleSerialAttachment, VirtioConsoleSerialPort};
|
||||
pub use socket::VirtioSocketDevice;
|
||||
pub use storage::{
|
||||
DiskImageAttachment, DiskImageCachingMode, DiskImageSynchronizationMode, NbdAttachment,
|
||||
StorageAttachment, StorageDevice, VirtioBlockDevice,
|
||||
};
|
||||
pub use vm::{VirtualMachine, VmState};
|
||||
@@ -0,0 +1,15 @@
|
||||
/// Placeholder — memory balloon is not yet implemented for KVM.
|
||||
/// Kept for API compatibility with vm-darwin.
|
||||
pub struct VirtioMemoryBalloonDevice;
|
||||
|
||||
impl VirtioMemoryBalloonDevice {
|
||||
pub fn new() -> Self {
|
||||
VirtioMemoryBalloonDevice
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VirtioMemoryBalloonDevice {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
//! vhost-user-net backend bridging cloud-hypervisor's virtio-net device to
|
||||
//! the proxy's `SOCK_DGRAM` socketpair (one raw Ethernet frame per datagram,
|
||||
//! same wire format as VZFileHandleNetworkDeviceAttachment on macOS).
|
||||
//!
|
||||
//! Queue 0 is RX (host -> guest), queue 1 is TX (guest -> host). Frames carry
|
||||
//! a 12-byte virtio-net header on the queues and none on the socketpair. A
|
||||
//! frame arriving while the guest has no RX buffer posted is dropped; the
|
||||
//! proxy's TCP stack retransmits.
|
||||
|
||||
use std::io::Result as IoResult;
|
||||
use std::os::fd::RawFd;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use vhost::vhost_user::message::{VhostUserProtocolFeatures, VhostUserVirtioFeatures};
|
||||
use vhost::vhost_user::Listener;
|
||||
use vhost_user_backend::{VhostUserBackendMut, VhostUserDaemon, VringRwLock, VringT};
|
||||
use virtio_bindings::virtio_config::VIRTIO_F_VERSION_1;
|
||||
use virtio_bindings::virtio_net::VIRTIO_NET_F_MAC;
|
||||
use virtio_queue::QueueT;
|
||||
use vm_memory::{Bytes, GuestAddressSpace, GuestMemoryAtomic, GuestMemoryMmap};
|
||||
use vmm_sys_util::epoll::EventSet;
|
||||
|
||||
const VIRTIO_NET_HDR_SIZE: usize = 12;
|
||||
const MAX_FRAME: usize = 65536;
|
||||
|
||||
const RX_QUEUE: u16 = 0;
|
||||
const TX_QUEUE: u16 = 1;
|
||||
/// epoll data for the socketpair fd (must be > num_queues).
|
||||
const SOCK_EVENT: u16 = 3;
|
||||
|
||||
struct Net {
|
||||
fd: RawFd,
|
||||
mem: Option<GuestMemoryAtomic<GuestMemoryMmap>>,
|
||||
event_idx: bool,
|
||||
}
|
||||
|
||||
impl Net {
|
||||
/// Guest -> host: drain the TX queue, stripping the virtio-net header.
|
||||
fn process_tx(&self, vring: &VringRwLock) -> IoResult<()> {
|
||||
let Some(atomic_mem) = self.mem.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
let mem = atomic_mem.memory();
|
||||
let mut vring = vring.get_mut();
|
||||
let mut used = false;
|
||||
let mut frame = vec![0u8; MAX_FRAME];
|
||||
|
||||
while let Some(chain) = vring.get_queue_mut().pop_descriptor_chain(mem.clone()) {
|
||||
let head = chain.head_index();
|
||||
let mut len = 0usize;
|
||||
for desc in chain {
|
||||
if desc.is_write_only() {
|
||||
continue;
|
||||
}
|
||||
let n = (desc.len() as usize).min(MAX_FRAME - len);
|
||||
if mem.read_slice(&mut frame[len..len + n], desc.addr()).is_err() {
|
||||
break;
|
||||
}
|
||||
len += n;
|
||||
}
|
||||
if len > VIRTIO_NET_HDR_SIZE {
|
||||
unsafe {
|
||||
libc::send(
|
||||
self.fd,
|
||||
frame[VIRTIO_NET_HDR_SIZE..].as_ptr() as *const libc::c_void,
|
||||
len - VIRTIO_NET_HDR_SIZE,
|
||||
libc::MSG_DONTWAIT,
|
||||
);
|
||||
}
|
||||
}
|
||||
vring.add_used(head, 0).ok();
|
||||
used = true;
|
||||
}
|
||||
if used {
|
||||
vring.signal_used_queue().ok();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Host -> guest: drain the socketpair into RX queue buffers, prepending
|
||||
/// a zeroed virtio-net header.
|
||||
fn process_rx(&self, vring: &VringRwLock) -> IoResult<()> {
|
||||
let Some(atomic_mem) = self.mem.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
let mem = atomic_mem.memory();
|
||||
let mut vring = vring.get_mut();
|
||||
let mut used = false;
|
||||
let mut buf = vec![0u8; VIRTIO_NET_HDR_SIZE + MAX_FRAME];
|
||||
|
||||
loop {
|
||||
let n = unsafe {
|
||||
libc::recv(
|
||||
self.fd,
|
||||
buf[VIRTIO_NET_HDR_SIZE..].as_mut_ptr() as *mut libc::c_void,
|
||||
MAX_FRAME,
|
||||
libc::MSG_DONTWAIT,
|
||||
)
|
||||
};
|
||||
if n <= 0 {
|
||||
break; // EAGAIN or closed
|
||||
}
|
||||
let total = VIRTIO_NET_HDR_SIZE + n as usize;
|
||||
|
||||
let Some(chain) = vring.get_queue_mut().pop_descriptor_chain(mem.clone()) else {
|
||||
continue; // no guest buffer posted: drop the frame
|
||||
};
|
||||
let head = chain.head_index();
|
||||
let mut written = 0usize;
|
||||
for desc in chain {
|
||||
if !desc.is_write_only() || written == total {
|
||||
continue;
|
||||
}
|
||||
let n = (desc.len() as usize).min(total - written);
|
||||
if mem.write_slice(&buf[written..written + n], desc.addr()).is_err() {
|
||||
break;
|
||||
}
|
||||
written += n;
|
||||
}
|
||||
vring.add_used(head, written as u32).ok();
|
||||
used = true;
|
||||
}
|
||||
if used {
|
||||
vring.signal_used_queue().ok();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl VhostUserBackendMut for Net {
|
||||
type Bitmap = ();
|
||||
type Vring = VringRwLock;
|
||||
|
||||
fn num_queues(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn max_queue_size(&self) -> usize {
|
||||
256
|
||||
}
|
||||
|
||||
fn features(&self) -> u64 {
|
||||
(1 << VIRTIO_F_VERSION_1)
|
||||
| (1 << VIRTIO_NET_F_MAC)
|
||||
| VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits()
|
||||
}
|
||||
|
||||
fn protocol_features(&self) -> VhostUserProtocolFeatures {
|
||||
VhostUserProtocolFeatures::MQ | VhostUserProtocolFeatures::REPLY_ACK
|
||||
}
|
||||
|
||||
fn set_event_idx(&mut self, enabled: bool) {
|
||||
self.event_idx = enabled;
|
||||
}
|
||||
|
||||
fn update_memory(&mut self, mem: GuestMemoryAtomic<GuestMemoryMmap>) -> IoResult<()> {
|
||||
self.mem = Some(mem);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
device_event: u16,
|
||||
_evset: EventSet,
|
||||
vrings: &[VringRwLock],
|
||||
_thread_id: usize,
|
||||
) -> IoResult<()> {
|
||||
match device_event {
|
||||
RX_QUEUE | SOCK_EVENT => self.process_rx(&vrings[RX_QUEUE as usize]),
|
||||
TX_QUEUE => self.process_tx(&vrings[TX_QUEUE as usize]),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Serve a vhost-user-net backend on `socket_path` for one connection.
|
||||
/// Returns after the listener is ready; the daemon runs on its own thread
|
||||
/// and exits when cloud-hypervisor disconnects.
|
||||
pub(crate) fn spawn(socket_path: &str, fd: RawFd) -> crate::error::Result<()> {
|
||||
unsafe {
|
||||
let flags = libc::fcntl(fd, libc::F_GETFL);
|
||||
libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
|
||||
}
|
||||
|
||||
let backend = Arc::new(RwLock::new(Net {
|
||||
fd,
|
||||
mem: None,
|
||||
event_idx: false,
|
||||
}));
|
||||
let mut daemon = VhostUserDaemon::new(
|
||||
"hanzo-vm-net".to_string(),
|
||||
backend,
|
||||
GuestMemoryAtomic::new(GuestMemoryMmap::new()),
|
||||
)
|
||||
.map_err(|e| crate::error::VzError::new(format!("vhost-user-net daemon: {}", e)))?;
|
||||
|
||||
let mut listener = Listener::new(socket_path, true)
|
||||
.map_err(|e| crate::error::VzError::new(format!("vhost-user-net listener: {}", e)))?;
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("hanzo-vm-net".into())
|
||||
.spawn(move || {
|
||||
// start() blocks until cloud-hypervisor connects.
|
||||
if daemon.start(&mut listener).is_ok() {
|
||||
if let Some(handler) = daemon.get_epoll_handlers().first() {
|
||||
let _ = handler.register_listener(fd, EventSet::IN, SOCK_EVENT as u64);
|
||||
}
|
||||
let _ = daemon.wait();
|
||||
}
|
||||
})
|
||||
.map_err(|e| crate::error::VzError::new(format!("vhost-user-net thread: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
use std::cell::Cell;
|
||||
use std::os::unix::io::RawFd;
|
||||
|
||||
pub struct FileHandleNetworkAttachment {
|
||||
pub(crate) fd: RawFd,
|
||||
}
|
||||
|
||||
impl FileHandleNetworkAttachment {
|
||||
/// Creates a network attachment from a connected datagram socket fd.
|
||||
/// The fd should be one end of `socketpair(AF_UNIX, SOCK_DGRAM)`.
|
||||
pub fn new(fd: RawFd) -> Self {
|
||||
FileHandleNetworkAttachment { fd }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MACAddress {
|
||||
pub(crate) bytes: [u8; 6],
|
||||
}
|
||||
|
||||
impl MACAddress {
|
||||
pub fn new() -> Self {
|
||||
MACAddress { bytes: [0; 6] }
|
||||
}
|
||||
|
||||
/// Generate a random locally-administered MAC address.
|
||||
pub fn random_local() -> Self {
|
||||
let mut bytes = [0u8; 6];
|
||||
let fd = unsafe { libc::open(b"/dev/urandom\0".as_ptr() as *const _, libc::O_RDONLY) };
|
||||
if fd >= 0 {
|
||||
unsafe {
|
||||
libc::read(fd, bytes.as_mut_ptr() as *mut libc::c_void, 6);
|
||||
libc::close(fd);
|
||||
}
|
||||
}
|
||||
// Set locally administered + unicast bits
|
||||
bytes[0] = (bytes[0] & 0xFC) | 0x02;
|
||||
MACAddress { bytes }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MACAddress {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VirtioNetworkDevice {
|
||||
pub(crate) fd: Option<RawFd>,
|
||||
pub(crate) mac: Cell<[u8; 6]>,
|
||||
}
|
||||
|
||||
impl VirtioNetworkDevice {
|
||||
pub fn new() -> Self {
|
||||
VirtioNetworkDevice {
|
||||
fd: None,
|
||||
mac: Cell::new([0; 6]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_attachment(attachment: &FileHandleNetworkAttachment) -> Self {
|
||||
VirtioNetworkDevice {
|
||||
fd: Some(attachment.fd),
|
||||
mac: Cell::new([0; 6]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_attachment(&mut self, attachment: &FileHandleNetworkAttachment) {
|
||||
self.fd = Some(attachment.fd);
|
||||
}
|
||||
|
||||
pub fn set_mac_address(&self, address: &MACAddress) {
|
||||
self.mac.set(address.bytes);
|
||||
}
|
||||
|
||||
pub(crate) fn mac_bytes(&self) -> [u8; 6] {
|
||||
self.mac.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VirtioNetworkDevice {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use std::os::fd::RawFd;
|
||||
|
||||
pub struct FileHandleSerialAttachment {
|
||||
pub(crate) read_fd: Option<RawFd>,
|
||||
pub(crate) write_fd: Option<RawFd>,
|
||||
}
|
||||
|
||||
impl FileHandleSerialAttachment {
|
||||
pub fn new(read_fd: RawFd, write_fd: RawFd) -> Self {
|
||||
FileHandleSerialAttachment {
|
||||
read_fd: Some(read_fd),
|
||||
write_fd: Some(write_fd),
|
||||
}
|
||||
}
|
||||
|
||||
/// Serial attachment with no read handle (stdin disconnected).
|
||||
/// Output goes to the given write fd.
|
||||
pub fn new_write_only(write_fd: RawFd) -> Self {
|
||||
FileHandleSerialAttachment {
|
||||
read_fd: None,
|
||||
write_fd: Some(write_fd),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VirtioConsoleSerialPort {
|
||||
pub(crate) read_fd: Option<RawFd>,
|
||||
pub(crate) write_fd: Option<RawFd>,
|
||||
}
|
||||
|
||||
impl VirtioConsoleSerialPort {
|
||||
pub fn new() -> Self {
|
||||
VirtioConsoleSerialPort {
|
||||
read_fd: None,
|
||||
write_fd: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_attachment(attachment: &FileHandleSerialAttachment) -> Self {
|
||||
VirtioConsoleSerialPort {
|
||||
read_fd: attachment.read_fd,
|
||||
write_fd: attachment.write_fd,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_attachment(&mut self, attachment: &FileHandleSerialAttachment) {
|
||||
self.read_fd = attachment.read_fd;
|
||||
self.write_fd = attachment.write_fd;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VirtioConsoleSerialPort {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
pub struct VirtioSocketDevice;
|
||||
|
||||
impl VirtioSocketDevice {
|
||||
pub fn new() -> Self {
|
||||
VirtioSocketDevice
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VirtioSocketDevice {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
use crate::error::{Result, VzError};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DiskImageCachingMode {
|
||||
Automatic,
|
||||
Cached,
|
||||
Uncached,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DiskImageSynchronizationMode {
|
||||
Full,
|
||||
Fsync,
|
||||
None,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Attachment types – store config consumed later by the VM builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub trait StorageAttachment {
|
||||
fn disk_path(&self) -> Option<&str>;
|
||||
fn nbd_uri(&self) -> Option<&str>;
|
||||
fn is_read_only(&self) -> bool;
|
||||
}
|
||||
|
||||
pub struct DiskImageAttachment {
|
||||
pub(crate) path: String,
|
||||
pub(crate) read_only: bool,
|
||||
}
|
||||
|
||||
impl DiskImageAttachment {
|
||||
pub fn new(path: &str, read_only: bool) -> Result<Self> {
|
||||
Ok(DiskImageAttachment {
|
||||
path: path.to_string(),
|
||||
read_only,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_options(
|
||||
path: &str,
|
||||
read_only: bool,
|
||||
_caching_mode: DiskImageCachingMode,
|
||||
_sync_mode: DiskImageSynchronizationMode,
|
||||
) -> Result<Self> {
|
||||
Self::new(path, read_only)
|
||||
}
|
||||
}
|
||||
|
||||
impl StorageAttachment for DiskImageAttachment {
|
||||
fn disk_path(&self) -> Option<&str> {
|
||||
Some(&self.path)
|
||||
}
|
||||
fn nbd_uri(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
fn is_read_only(&self) -> bool {
|
||||
self.read_only
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NbdAttachment {
|
||||
pub(crate) uri: String,
|
||||
pub(crate) read_only: bool,
|
||||
}
|
||||
|
||||
impl NbdAttachment {
|
||||
pub fn new(uri: &str, _timeout_secs: f64, _read_only: bool) -> Result<Self> {
|
||||
// TODO: implement userspace NBD client for Linux.
|
||||
// For now fall back to HANZO_VM_STORAGE=direct.
|
||||
Err(VzError::new(format!(
|
||||
"NBD storage ({}) not yet supported on Linux. Set HANZO_VM_STORAGE=direct",
|
||||
uri
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl StorageAttachment for NbdAttachment {
|
||||
fn disk_path(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
fn nbd_uri(&self) -> Option<&str> {
|
||||
Some(&self.uri)
|
||||
}
|
||||
fn is_read_only(&self) -> bool {
|
||||
self.read_only
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Block device config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub trait StorageDevice {
|
||||
fn get_disk_path(&self) -> Option<&str>;
|
||||
fn get_read_only(&self) -> bool;
|
||||
}
|
||||
|
||||
pub struct VirtioBlockDevice {
|
||||
pub(crate) path: Option<String>,
|
||||
pub(crate) read_only: bool,
|
||||
}
|
||||
|
||||
impl VirtioBlockDevice {
|
||||
pub fn new(attachment: &dyn StorageAttachment) -> Self {
|
||||
VirtioBlockDevice {
|
||||
path: attachment.disk_path().map(|s| s.to_string()),
|
||||
read_only: attachment.is_read_only(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StorageDevice for VirtioBlockDevice {
|
||||
fn get_disk_path(&self) -> Option<&str> {
|
||||
self.path.as_deref()
|
||||
}
|
||||
fn get_read_only(&self) -> bool {
|
||||
self.read_only
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
use std::os::fd::RawFd;
|
||||
|
||||
/// Saved terminal state for later restoration.
|
||||
pub struct TerminalState {
|
||||
fd: RawFd,
|
||||
termios: libc::termios,
|
||||
}
|
||||
|
||||
impl TerminalState {
|
||||
/// Save the current terminal attributes and switch to raw mode.
|
||||
/// Returns `None` if the fd is not a terminal.
|
||||
pub fn enter_raw_mode(fd: RawFd) -> Option<Self> {
|
||||
unsafe {
|
||||
let mut saved: libc::termios = std::mem::zeroed();
|
||||
if libc::tcgetattr(fd, &mut saved) != 0 {
|
||||
return None;
|
||||
}
|
||||
let mut raw = saved;
|
||||
libc::cfmakeraw(&mut raw);
|
||||
libc::tcsetattr(fd, libc::TCSANOW, &raw);
|
||||
Some(TerminalState { fd, termios: saved })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restore(&self) {
|
||||
unsafe {
|
||||
libc::tcsetattr(self.fd, libc::TCSANOW, &self.termios);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TerminalState {
|
||||
fn drop(&mut self) {
|
||||
self.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the terminal size (rows, cols) for the given fd.
|
||||
/// Returns (24, 80) as fallback if the ioctl fails.
|
||||
pub fn terminal_size(fd: RawFd) -> (u16, u16) {
|
||||
unsafe {
|
||||
let mut ws: libc::winsize = std::mem::zeroed();
|
||||
if libc::ioctl(fd, libc::TIOCGWINSZ, &mut ws) == 0 {
|
||||
(ws.ws_row, ws.ws_col)
|
||||
} else {
|
||||
(24, 80)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read bytes from a raw file descriptor.
|
||||
/// Returns the number of bytes read, or 0 on EOF/error.
|
||||
pub fn read_raw(fd: RawFd, buf: &mut [u8]) -> usize {
|
||||
unsafe {
|
||||
let n = libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len());
|
||||
if n > 0 {
|
||||
n as usize
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- epoll + signalfd-based stdin relay ---
|
||||
|
||||
/// Events returned by `StdinRelay::wait()`.
|
||||
pub enum StdinEvent {
|
||||
/// stdin has data available to read.
|
||||
Ready,
|
||||
/// Terminal was resized (SIGWINCH).
|
||||
Resize,
|
||||
/// Shutdown was signaled by the other thread.
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
/// Write-end of the shutdown pipe.
|
||||
pub struct ShutdownSignal {
|
||||
pipe_write: RawFd,
|
||||
}
|
||||
|
||||
unsafe impl Send for ShutdownSignal {}
|
||||
|
||||
impl ShutdownSignal {
|
||||
pub fn signal(&self) {
|
||||
unsafe {
|
||||
libc::write(self.pipe_write, [1u8].as_ptr() as *const libc::c_void, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ShutdownSignal {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
libc::close(self.pipe_write);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Event source identifiers stored in epoll_event.u64
|
||||
const EV_STDIN: u64 = 0;
|
||||
const EV_PIPE: u64 = 1;
|
||||
const EV_SIGNAL: u64 = 2;
|
||||
|
||||
/// epoll-based event multiplexer for stdin, SIGWINCH, and shutdown.
|
||||
/// Linux equivalent of macOS kqueue-based StdinRelay.
|
||||
pub struct StdinRelay {
|
||||
epoll_fd: RawFd,
|
||||
signal_fd: RawFd,
|
||||
pipe_read: RawFd,
|
||||
}
|
||||
|
||||
impl StdinRelay {
|
||||
/// Create a new relay watching the given stdin fd.
|
||||
pub fn new(stdin_fd: RawFd) -> Option<(StdinRelay, ShutdownSignal)> {
|
||||
unsafe {
|
||||
// Create shutdown pipe
|
||||
let mut fds = [0i32; 2];
|
||||
if libc::pipe(fds.as_mut_ptr()) != 0 {
|
||||
return None;
|
||||
}
|
||||
let pipe_read = fds[0];
|
||||
let pipe_write = fds[1];
|
||||
|
||||
// Create epoll instance
|
||||
let epoll_fd = libc::epoll_create1(0);
|
||||
if epoll_fd < 0 {
|
||||
libc::close(pipe_read);
|
||||
libc::close(pipe_write);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Register stdin
|
||||
let mut ev = libc::epoll_event {
|
||||
events: libc::EPOLLIN as u32,
|
||||
u64: EV_STDIN,
|
||||
};
|
||||
if libc::epoll_ctl(epoll_fd, libc::EPOLL_CTL_ADD, stdin_fd, &mut ev) < 0 {
|
||||
libc::close(epoll_fd);
|
||||
libc::close(pipe_read);
|
||||
libc::close(pipe_write);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Register shutdown pipe
|
||||
let mut ev = libc::epoll_event {
|
||||
events: libc::EPOLLIN as u32,
|
||||
u64: EV_PIPE,
|
||||
};
|
||||
libc::epoll_ctl(epoll_fd, libc::EPOLL_CTL_ADD, pipe_read, &mut ev);
|
||||
|
||||
// Create signalfd for SIGWINCH
|
||||
let mut sigset: libc::sigset_t = std::mem::zeroed();
|
||||
libc::sigemptyset(&mut sigset);
|
||||
libc::sigaddset(&mut sigset, libc::SIGWINCH);
|
||||
// Block SIGWINCH so signalfd receives it
|
||||
libc::sigprocmask(libc::SIG_BLOCK, &sigset, std::ptr::null_mut());
|
||||
|
||||
let signal_fd = libc::signalfd(-1, &sigset, libc::SFD_NONBLOCK);
|
||||
if signal_fd < 0 {
|
||||
// signalfd not available — proceed without resize support
|
||||
libc::close(epoll_fd);
|
||||
libc::close(pipe_read);
|
||||
libc::close(pipe_write);
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut ev = libc::epoll_event {
|
||||
events: libc::EPOLLIN as u32,
|
||||
u64: EV_SIGNAL,
|
||||
};
|
||||
libc::epoll_ctl(epoll_fd, libc::EPOLL_CTL_ADD, signal_fd, &mut ev);
|
||||
|
||||
Some((
|
||||
StdinRelay {
|
||||
epoll_fd,
|
||||
signal_fd,
|
||||
pipe_read,
|
||||
},
|
||||
ShutdownSignal { pipe_write },
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Block until stdin is readable, a resize signal arrives, or shutdown
|
||||
/// is signaled.
|
||||
pub fn wait(&self) -> StdinEvent {
|
||||
unsafe {
|
||||
let mut events = [libc::epoll_event { events: 0, u64: 0 }; 4];
|
||||
|
||||
let n = libc::epoll_wait(self.epoll_fd, events.as_mut_ptr(), 4, -1);
|
||||
if n < 1 {
|
||||
return StdinEvent::Shutdown;
|
||||
}
|
||||
|
||||
for i in 0..n as usize {
|
||||
match events[i].u64 {
|
||||
EV_STDIN => return StdinEvent::Ready,
|
||||
EV_PIPE => return StdinEvent::Shutdown,
|
||||
EV_SIGNAL => {
|
||||
// Drain the signalfd
|
||||
let mut info: libc::signalfd_siginfo = std::mem::zeroed();
|
||||
libc::read(
|
||||
self.signal_fd,
|
||||
&mut info as *mut _ as *mut libc::c_void,
|
||||
std::mem::size_of::<libc::signalfd_siginfo>(),
|
||||
);
|
||||
return StdinEvent::Resize;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
StdinEvent::Shutdown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for StdinRelay {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
libc::close(self.epoll_fd);
|
||||
libc::close(self.signal_fd);
|
||||
libc::close(self.pipe_read);
|
||||
// Unblock SIGWINCH
|
||||
let mut sigset: libc::sigset_t = std::mem::zeroed();
|
||||
libc::sigemptyset(&mut sigset);
|
||||
libc::sigaddset(&mut sigset, libc::SIGWINCH);
|
||||
libc::sigprocmask(libc::SIG_UNBLOCK, &sigset, std::ptr::null_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
//! VirtualMachine driving a `cloud-hypervisor` child process over its API
|
||||
//! socket: `vm.create` + `vm.boot` on start, `vm.shutdown` + `vmm.shutdown`
|
||||
//! on stop. One `virtiofsd` child serves each shared directory, and the
|
||||
//! in-process vhost-user-net backend serves the proxy socketpair, so guest
|
||||
//! memory is always mapped `shared=on`.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::os::fd::{FromRawFd, IntoRawFd, OwnedFd};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crossbeam_channel::{bounded, Receiver, Sender};
|
||||
|
||||
use crate::api;
|
||||
use crate::configuration::{ConfigData, VirtualMachineConfiguration};
|
||||
use crate::error::{Result, VzError};
|
||||
use crate::net_backend;
|
||||
|
||||
const GUEST_CID: u32 = 3;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum VmState {
|
||||
Stopped = 0,
|
||||
Running = 1,
|
||||
Error = 3,
|
||||
// Paused/Starting/etc. kept as discriminants for Darwin compatibility.
|
||||
Unknown = -1,
|
||||
}
|
||||
|
||||
pub struct VirtualMachine {
|
||||
config: ConfigData,
|
||||
run_dir: PathBuf,
|
||||
api_socket: String,
|
||||
vsock_socket: String,
|
||||
ch_pid: Mutex<Option<i32>>,
|
||||
virtiofsd: Mutex<Vec<Child>>,
|
||||
state_tx: Sender<VmState>,
|
||||
state_rx: Receiver<VmState>,
|
||||
running: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
/// Locate a helper binary on PATH or in `~/.local/bin`.
|
||||
fn find_binary(name: &str) -> Option<PathBuf> {
|
||||
if let Ok(path) = std::env::var("PATH") {
|
||||
for dir in path.split(':') {
|
||||
let candidate = Path::new(dir).join(name);
|
||||
if candidate.is_file() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
let candidate = Path::new(&home).join(".local/bin").join(name);
|
||||
if candidate.is_file() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Duplicate a raw fd into an owned handle for child stdio.
|
||||
fn dup_stdio(fd: i32) -> Result<Stdio> {
|
||||
let duped = unsafe { libc::dup(fd) };
|
||||
if duped < 0 {
|
||||
return Err(VzError::new(format!(
|
||||
"dup({}) failed: {}",
|
||||
fd,
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
Ok(Stdio::from(unsafe { OwnedFd::from_raw_fd(duped) }))
|
||||
}
|
||||
|
||||
fn wait_for_socket(path: &str, timeout: Duration) -> bool {
|
||||
let deadline = Instant::now() + timeout;
|
||||
while Instant::now() < deadline {
|
||||
if Path::new(path).exists() {
|
||||
return true;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
impl VirtualMachine {
|
||||
pub fn new(config: &VirtualMachineConfiguration) -> Self {
|
||||
let inner = config.inner.borrow().clone();
|
||||
let (state_tx, state_rx) = bounded(1);
|
||||
|
||||
// Sockets live next to the disk (the per-instance directory), or in
|
||||
// a private temp directory when there is no disk.
|
||||
let run_dir = inner
|
||||
.disk_path
|
||||
.as_deref()
|
||||
.and_then(|p| Path::new(p).parent())
|
||||
.filter(|p| !p.as_os_str().is_empty())
|
||||
.map(Path::to_path_buf)
|
||||
.unwrap_or_else(|| {
|
||||
std::env::temp_dir().join(format!("hanzo-vm-{}", std::process::id()))
|
||||
});
|
||||
let _ = std::fs::create_dir_all(&run_dir);
|
||||
|
||||
let api_socket = run_dir.join("ch-api.sock").to_string_lossy().into_owned();
|
||||
let vsock_socket = run_dir.join("vsock.sock").to_string_lossy().into_owned();
|
||||
|
||||
VirtualMachine {
|
||||
config: inner,
|
||||
run_dir,
|
||||
api_socket,
|
||||
vsock_socket,
|
||||
ch_pid: Mutex::new(None),
|
||||
virtiofsd: Mutex::new(Vec::new()),
|
||||
state_tx,
|
||||
state_rx,
|
||||
running: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supported() -> bool {
|
||||
Path::new("/dev/kvm").exists() && find_binary("cloud-hypervisor").is_some()
|
||||
}
|
||||
|
||||
pub fn start(&self) -> Result<()> {
|
||||
let ch_bin = find_binary("cloud-hypervisor").ok_or_else(|| {
|
||||
VzError::new("cloud-hypervisor not found on PATH or in ~/.local/bin")
|
||||
})?;
|
||||
|
||||
// One virtiofsd child per shared directory; the socket must be
|
||||
// listening before cloud-hypervisor creates the device.
|
||||
let mut fs_sockets = Vec::new();
|
||||
if !self.config.mounts.is_empty() {
|
||||
let fsd_bin = find_binary("virtiofsd").ok_or_else(|| {
|
||||
VzError::new("virtiofsd not found on PATH or in ~/.local/bin")
|
||||
})?;
|
||||
for (tag, host_path, _read_only) in &self.config.mounts {
|
||||
let socket = self.run_dir.join(format!("fs-{}.sock", tag));
|
||||
let log = std::fs::File::create(self.run_dir.join(format!("fs-{}.log", tag)))
|
||||
.map_err(|e| VzError::new(format!("virtiofsd log: {}", e)))?;
|
||||
let child = Command::new(&fsd_bin)
|
||||
.arg(format!("--socket-path={}", socket.display()))
|
||||
.arg("--shared-dir")
|
||||
.arg(host_path)
|
||||
.arg("--cache")
|
||||
.arg("auto")
|
||||
.arg("--sandbox")
|
||||
.arg("none")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::from(log))
|
||||
.spawn()
|
||||
.map_err(|e| VzError::new(format!("spawn virtiofsd: {}", e)))?;
|
||||
self.virtiofsd.lock().unwrap().push(child);
|
||||
fs_sockets.push((tag.clone(), socket));
|
||||
}
|
||||
for (tag, socket) in &fs_sockets {
|
||||
if !wait_for_socket(&socket.to_string_lossy(), Duration::from_secs(5)) {
|
||||
return Err(VzError::new(format!(
|
||||
"virtiofsd socket for {} did not appear",
|
||||
tag
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In-process vhost-user-net backend over the proxy socketpair.
|
||||
let net_socket = self.run_dir.join("net.sock");
|
||||
if let Some(fd) = self.config.network_fd {
|
||||
net_backend::spawn(&net_socket.to_string_lossy(), fd)?;
|
||||
}
|
||||
|
||||
// Serial console: the virtio-console is wired to the child's stdio.
|
||||
let stdin = match self.config.serial_read_fd {
|
||||
Some(fd) => dup_stdio(fd)?,
|
||||
None => Stdio::null(),
|
||||
};
|
||||
let stdout = match self.config.serial_write_fd {
|
||||
Some(fd) => dup_stdio(fd)?,
|
||||
None => Stdio::null(),
|
||||
};
|
||||
|
||||
let _ = std::fs::remove_file(&self.api_socket);
|
||||
let _ = std::fs::remove_file(&self.vsock_socket);
|
||||
|
||||
let mut child = Command::new(&ch_bin)
|
||||
.arg("--api-socket")
|
||||
.arg(format!("path={}", self.api_socket))
|
||||
.stdin(stdin)
|
||||
.stdout(stdout)
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.map_err(|e| VzError::new(format!("spawn cloud-hypervisor: {}", e)))?;
|
||||
|
||||
if !wait_for_socket(&self.api_socket, Duration::from_secs(5)) {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err(VzError::new("cloud-hypervisor API socket did not appear"));
|
||||
}
|
||||
|
||||
let vm_config = self.vm_config_json(&fs_sockets, &net_socket);
|
||||
if let Err(e) = api::put(&self.api_socket, "vm.create", Some(&vm_config.to_string()))
|
||||
.and_then(|_| api::put(&self.api_socket, "vm.boot", None))
|
||||
{
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
*self.ch_pid.lock().unwrap() = Some(child.id() as i32);
|
||||
self.running.store(true, Ordering::Release);
|
||||
let _ = self.state_tx.try_send(VmState::Running);
|
||||
|
||||
// Monitor thread: the cloud-hypervisor process exits when the guest
|
||||
// shuts down (or on vmm.shutdown / kill from stop()).
|
||||
let running = self.running.clone();
|
||||
let state_tx = self.state_tx.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("hanzo-vm-monitor".into())
|
||||
.spawn(move || {
|
||||
let _ = child.wait();
|
||||
running.store(false, Ordering::Release);
|
||||
let _ = state_tx.try_send(VmState::Stopped);
|
||||
})
|
||||
.map_err(|e| VzError::new(format!("spawn monitor thread: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn vm_config_json(
|
||||
&self,
|
||||
fs_sockets: &[(String, PathBuf)],
|
||||
net_socket: &Path,
|
||||
) -> serde_json::Value {
|
||||
let c = &self.config;
|
||||
let mut cfg = serde_json::json!({
|
||||
"cpus": { "boot_vcpus": c.cpu_count, "max_vcpus": c.cpu_count },
|
||||
// vhost-user devices (fs, net) require shared guest memory.
|
||||
"memory": { "size": c.memory_size, "shared": true },
|
||||
"payload": { "kernel": c.kernel_path, "cmdline": c.command_line },
|
||||
"serial": { "mode": "Off" },
|
||||
"console": { "mode": "Tty" },
|
||||
"rng": { "src": "/dev/urandom" },
|
||||
});
|
||||
if let Some(ref initrd) = c.initrd_path {
|
||||
cfg["payload"]["initramfs"] = serde_json::json!(initrd);
|
||||
}
|
||||
if let Some(ref disk) = c.disk_path {
|
||||
cfg["disks"] = serde_json::json!([{ "path": disk, "readonly": c.disk_read_only }]);
|
||||
}
|
||||
if c.has_socket {
|
||||
cfg["vsock"] = serde_json::json!({ "cid": GUEST_CID, "socket": self.vsock_socket });
|
||||
}
|
||||
if !fs_sockets.is_empty() {
|
||||
let fs: Vec<_> = fs_sockets
|
||||
.iter()
|
||||
.map(|(tag, socket)| {
|
||||
serde_json::json!({
|
||||
"tag": tag,
|
||||
"socket": socket.to_string_lossy(),
|
||||
"num_queues": 1,
|
||||
"queue_size": 1024,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
cfg["fs"] = serde_json::json!(fs);
|
||||
}
|
||||
if c.network_fd.is_some() {
|
||||
let mut net = serde_json::json!({
|
||||
"vhost_user": true,
|
||||
"vhost_socket": net_socket.to_string_lossy(),
|
||||
"num_queues": 2,
|
||||
"queue_size": 256,
|
||||
});
|
||||
if let Some(mac) = c.network_mac {
|
||||
net["mac"] = serde_json::json!(format!(
|
||||
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
|
||||
));
|
||||
}
|
||||
cfg["net"] = serde_json::json!([net]);
|
||||
}
|
||||
cfg
|
||||
}
|
||||
|
||||
pub fn stop(&self) -> Result<()> {
|
||||
// Ask the VMM to shut down; fall back to SIGKILL.
|
||||
let _ = api::put(&self.api_socket, "vm.shutdown", None);
|
||||
let _ = api::put(&self.api_socket, "vmm.shutdown", None);
|
||||
|
||||
if let Some(pid) = *self.ch_pid.lock().unwrap() {
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
while self.running.load(Ordering::Acquire) && Instant::now() < deadline {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
if self.running.load(Ordering::Acquire) {
|
||||
unsafe { libc::kill(pid, libc::SIGKILL) };
|
||||
}
|
||||
}
|
||||
|
||||
for mut child in self.virtiofsd.lock().unwrap().drain(..) {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
|
||||
self.running.store(false, Ordering::Release);
|
||||
let _ = self.state_tx.try_send(VmState::Stopped);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn state_channel(&self) -> Receiver<VmState> {
|
||||
self.state_rx.clone()
|
||||
}
|
||||
|
||||
pub fn can_start(&self) -> bool {
|
||||
!self.running.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
pub fn can_stop(&self) -> bool {
|
||||
self.running.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
pub fn can_pause(&self) -> bool {
|
||||
false
|
||||
}
|
||||
pub fn can_resume(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn can_request_stop(&self) -> bool {
|
||||
self.can_stop()
|
||||
}
|
||||
|
||||
/// Connect to a vsock port on the guest through cloud-hypervisor's
|
||||
/// hybrid vsock socket: send `CONNECT <port>\n`, expect `OK <n>\n`,
|
||||
/// then the stream is a raw pipe to the guest listener.
|
||||
///
|
||||
/// The connected `UnixStream` fd is rewrapped as a `TcpStream` because
|
||||
/// the platform-neutral sandbox API is written against `TcpStream`;
|
||||
/// both are plain stream sockets, so read/write/shutdown/try_clone all
|
||||
/// behave (`set_nodelay` fails and is ignored by callers).
|
||||
pub fn connect_to_vsock_port(&self, port: u32) -> Result<TcpStream> {
|
||||
let mut stream = UnixStream::connect(&self.vsock_socket)
|
||||
.map_err(|e| VzError::new(format!("vsock connect failed: {}", e)))?;
|
||||
let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
|
||||
|
||||
stream
|
||||
.write_all(format!("CONNECT {}\n", port).as_bytes())
|
||||
.map_err(|e| VzError::new(format!("vsock handshake send: {}", e)))?;
|
||||
|
||||
// Read the response a byte at a time so no guest data is consumed.
|
||||
let mut line = Vec::with_capacity(16);
|
||||
let mut byte = [0u8; 1];
|
||||
loop {
|
||||
match stream.read(&mut byte) {
|
||||
Ok(1) => {
|
||||
if byte[0] == b'\n' {
|
||||
break;
|
||||
}
|
||||
line.push(byte[0]);
|
||||
if line.len() > 32 {
|
||||
return Err(VzError::new("vsock handshake: oversized response"));
|
||||
}
|
||||
}
|
||||
Ok(_) => return Err(VzError::new("vsock connect refused (EOF)")),
|
||||
Err(e) => return Err(VzError::new(format!("vsock handshake read: {}", e))),
|
||||
}
|
||||
}
|
||||
if !line.starts_with(b"OK ") {
|
||||
return Err(VzError::new(format!(
|
||||
"vsock connect refused: {}",
|
||||
String::from_utf8_lossy(&line)
|
||||
)));
|
||||
}
|
||||
|
||||
let _ = stream.set_read_timeout(None);
|
||||
Ok(unsafe { TcpStream::from_raw_fd(stream.into_raw_fd()) })
|
||||
}
|
||||
|
||||
pub fn state(&self) -> VmState {
|
||||
if self.running.load(Ordering::Acquire) {
|
||||
VmState::Running
|
||||
} else {
|
||||
VmState::Stopped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for VirtualMachine {
|
||||
fn drop(&mut self) {
|
||||
// Child processes outlive a dropped VirtualMachine unless killed.
|
||||
if let Some(pid) = *self.ch_pid.lock().unwrap() {
|
||||
if self.running.load(Ordering::Acquire) {
|
||||
unsafe { libc::kill(pid, libc::SIGKILL) };
|
||||
}
|
||||
}
|
||||
for mut child in self.virtiofsd.lock().unwrap().drain(..) {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user