mirror of
https://github.com/hanzoai/vm.git
synced 2026-10-01 00:47:56 +00:00
Merge branch 'vm-ch'
# Conflicts: # scripts/prepare-rootfs.sh
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Linux x86_64 backend (`crates/vm-ch`): drives cloud-hypervisor over its API
|
||||
socket, virtiofsd for mounts, an in-process vhost-user-net backend for
|
||||
`--allow-net`, and hybrid vsock. Guest assets build for the host arch
|
||||
(PVH vmlinux kernel, amd64 rootfs); OS tarballs are per-arch
|
||||
(`hanzo-vm-os-<tag>-<arch>.tar.gz`).
|
||||
|
||||
## 0.1.0
|
||||
|
||||
First release of Hanzo VM.
|
||||
|
||||
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.1"
|
||||
@@ -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.1"
|
||||
@@ -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.1"
|
||||
@@ -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",
|
||||
|
||||
@@ -16,9 +16,10 @@ files written inside a run never touch the host or the next run.
|
||||
|
||||
- **macOS** — Apple Virtualization.framework. Apple Silicon only, arm64 guests
|
||||
only, no Rosetta. macOS 14 or later.
|
||||
- **Linux** — an experimental KVM backend (`crates/vm-linux`), arm64 hosts with
|
||||
`/dev/kvm`. cloud-hypervisor (amd64 and arm64, VFIO GPU passthrough) is the
|
||||
planned replacement.
|
||||
- **Linux arm64** — KVM backend (`crates/vm-linux`), hosts with `/dev/kvm`.
|
||||
- **Linux x86_64** — cloud-hypervisor backend (`crates/vm-ch`), hosts with
|
||||
`/dev/kvm` plus the `cloud-hypervisor` and `virtiofsd` binaries (see
|
||||
[Linux hosts](#linux-hosts)).
|
||||
|
||||
## Install
|
||||
|
||||
@@ -36,6 +37,29 @@ Both put `hanzo-vm` in `~/.local/bin`. The first `hanzo-vm run` downloads the
|
||||
guest image (kernel, initramfs, root filesystem) from the matching GitHub
|
||||
release into `~/.hanzo/vm`; set `HANZO_VM_HOME` to put it elsewhere.
|
||||
|
||||
## Linux hosts
|
||||
|
||||
Both Linux backends need read/write access to `/dev/kvm` (membership in the
|
||||
`kvm` group, or an ACL).
|
||||
|
||||
arm64 connects to the guest through the host's vhost-vsock device; grant
|
||||
access once with:
|
||||
|
||||
```sh
|
||||
sudo setfacl -m u:$USER:rw /dev/vhost-vsock
|
||||
```
|
||||
|
||||
x86_64 drives release binaries of cloud-hypervisor (verified with v53.0) and
|
||||
virtiofsd (verified with 1.10.0), looked up on `PATH` and in `~/.local/bin`:
|
||||
|
||||
```sh
|
||||
curl -fsSLo ~/.local/bin/cloud-hypervisor \
|
||||
https://github.com/cloud-hypervisor/cloud-hypervisor/releases/download/v53.0/cloud-hypervisor-static
|
||||
chmod +x ~/.local/bin/cloud-hypervisor
|
||||
sudo apt install virtiofsd # Debian/Ubuntu; installs /usr/libexec/virtiofsd
|
||||
install -m 755 /usr/libexec/virtiofsd ~/.local/bin/virtiofsd
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
@@ -181,8 +205,9 @@ just install # release CLI to ~/.local/bin/hanzo-vm
|
||||
```
|
||||
|
||||
The guest image is built by `scripts/prepare-rootfs.sh` (kernel via
|
||||
`scripts/build-kernel.sh`); it runs natively on an arm64 Linux host and inside
|
||||
Docker on macOS. CI publishes the image with every release.
|
||||
`scripts/build-kernel.sh`); it runs natively on arm64 and x86_64 Linux hosts
|
||||
(building the guest needs `musl-tools` for the musl target) and inside Docker
|
||||
on macOS. CI publishes the image with every release.
|
||||
|
||||
## Changelog
|
||||
|
||||
|
||||
@@ -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,220 @@
|
||||
//! 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(c"/dev/urandom".as_ptr(), 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 event in events.iter().take(n as usize) {
|
||||
match event.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,422 @@
|
||||
//! 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,
|
||||
// Duplicated at construction time: the caller's fds (e.g. an opened
|
||||
// /dev/null) may not outlive the configuration.
|
||||
serial_in: Option<OwnedFd>,
|
||||
serial_out: Option<OwnedFd>,
|
||||
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; None if the fd is invalid.
|
||||
fn dup_owned(fd: i32) -> Option<OwnedFd> {
|
||||
let duped = unsafe { libc::dup(fd) };
|
||||
if duped < 0 {
|
||||
return None;
|
||||
}
|
||||
Some(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 serial_in = inner.serial_read_fd.and_then(dup_owned);
|
||||
let serial_out = inner.serial_write_fd.and_then(dup_owned);
|
||||
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,
|
||||
serial_in,
|
||||
serial_out,
|
||||
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 stdio_of = |fd: &Option<OwnedFd>| -> Result<Stdio> {
|
||||
match fd {
|
||||
Some(fd) => {
|
||||
Ok(Stdio::from(fd.try_clone().map_err(|e| {
|
||||
VzError::new(format!("dup console fd: {}", e))
|
||||
})?))
|
||||
}
|
||||
None => Ok(Stdio::null()),
|
||||
}
|
||||
};
|
||||
let stdin = stdio_of(&self.serial_in)?;
|
||||
let stdout = stdio_of(&self.serial_out)?;
|
||||
|
||||
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" },
|
||||
"rng": { "src": "/dev/urandom" },
|
||||
});
|
||||
// Interactive consoles need Tty for input; otherwise write console
|
||||
// output through the child's own stdout (Tty mode EBADFs on a
|
||||
// non-terminal stdout).
|
||||
cfg["console"] = if c.serial_read_fd.is_some() {
|
||||
serde_json::json!({ "mode": "Tty" })
|
||||
} else {
|
||||
serde_json::json!({ "mode": "File", "file": "/proc/self/fd/1" })
|
||||
};
|
||||
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,
|
||||
"image_type": "Raw",
|
||||
}]);
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ pub fn download_os_image(data_dir: &str) -> Result<()> {
|
||||
|
||||
fn download_os_image_version(data_dir: &str, version: &str) -> Result<()> {
|
||||
let tag = format!("v{}", version);
|
||||
let tarball_name = format!("hanzo-vm-os-{}-aarch64.tar.gz", tag);
|
||||
let tarball_name = format!("hanzo-vm-os-{}-{}.tar.gz", tag, std::env::consts::ARCH);
|
||||
let url = format!(
|
||||
"https://github.com/{}/releases/download/{}/{}",
|
||||
GITHUB_REPO, tag, tarball_name
|
||||
@@ -89,6 +89,7 @@ fn cli_tarball_name(version: &str) -> Result<String> {
|
||||
let platform = match (std::env::consts::OS, std::env::consts::ARCH) {
|
||||
("macos", "aarch64") => "darwin-aarch64",
|
||||
("linux", "aarch64") => "linux-aarch64",
|
||||
("linux", "x86_64") => "linux-x86_64",
|
||||
(os, arch) => bail!("automatic upgrade is not supported on {}-{}", os, arch),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::ffi::CString;
|
||||
use std::io::IsTerminal;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ mod guest {
|
||||
if libc::fcntl(2, libc::F_GETFD) >= 0 {
|
||||
return;
|
||||
}
|
||||
let fd = libc::open(b"/dev/null\0".as_ptr() as *const libc::c_char, libc::O_RDWR);
|
||||
let fd = libc::open(c"/dev/null".as_ptr(), libc::O_RDWR);
|
||||
if fd >= 0 {
|
||||
for t in 0..3 {
|
||||
libc::dup2(fd, t);
|
||||
@@ -240,11 +240,7 @@ mod guest {
|
||||
// Check if eth0 exists (network device present)
|
||||
let has_eth0 = {
|
||||
let mut ifr: libc::ifreq = std::mem::zeroed();
|
||||
std::ptr::copy_nonoverlapping(
|
||||
b"eth0\0".as_ptr(),
|
||||
ifr.ifr_name.as_mut_ptr() as *mut u8,
|
||||
5,
|
||||
);
|
||||
std::ptr::copy_nonoverlapping(c"eth0".as_ptr(), ifr.ifr_name.as_mut_ptr(), 5);
|
||||
libc::ioctl(sock, libc::SIOCGIFFLAGS as _, &mut ifr) == 0
|
||||
};
|
||||
|
||||
@@ -257,11 +253,7 @@ mod guest {
|
||||
// Check if eth0 already has an IP (configured by initramfs)
|
||||
let has_ip = {
|
||||
let mut ifr: libc::ifreq = std::mem::zeroed();
|
||||
std::ptr::copy_nonoverlapping(
|
||||
b"eth0\0".as_ptr(),
|
||||
ifr.ifr_name.as_mut_ptr() as *mut u8,
|
||||
5,
|
||||
);
|
||||
std::ptr::copy_nonoverlapping(c"eth0".as_ptr(), ifr.ifr_name.as_mut_ptr(), 5);
|
||||
libc::ioctl(sock, libc::SIOCGIFADDR as _, &mut ifr) == 0
|
||||
};
|
||||
|
||||
@@ -438,12 +430,7 @@ mod guest {
|
||||
let mut reader = stream.try_clone().expect("failed to clone stream");
|
||||
let mut writer = stream;
|
||||
|
||||
loop {
|
||||
let (msg_type, payload) = match frame::read_frame(&mut reader) {
|
||||
Ok(Some(f)) => f,
|
||||
_ => break, // EOF or error
|
||||
};
|
||||
|
||||
while let Ok(Some((msg_type, payload))) = frame::read_frame(&mut reader) {
|
||||
match msg_type {
|
||||
frame::MOUNT_REQ => {
|
||||
let mount_req: MountRequest = match serde_json::from_slice(&payload) {
|
||||
|
||||
@@ -17,5 +17,7 @@ socket2 = "0.5"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
vm-darwin = { version = "0.1.0", path = "../vm-darwin" }
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
[target.'cfg(all(target_os = "linux", target_arch = "aarch64"))'.dependencies]
|
||||
vm-linux = { version = "0.1.0", path = "../vm-linux" }
|
||||
[target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dependencies]
|
||||
vm-ch = { version = "0.1.0", path = "../vm-ch" }
|
||||
|
||||
+9
-12
@@ -8,20 +8,17 @@ pub use vm_proto::{
|
||||
ReadFileRequest, WriteFileRequest, WriteFileResponse, VSOCK_PORT, VSOCK_PORT_FORWARD,
|
||||
};
|
||||
|
||||
// Re-exports from platform-specific backend for advanced/escape-hatch use
|
||||
// The platform backend: Virtualization.framework on macOS, KVM on arm64
|
||||
// Linux, cloud-hypervisor on x86_64 Linux. All expose the same vocabulary.
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
pub(crate) use vm_ch as backend;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use vm_darwin::VirtualMachine;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use vm_darwin::VmState;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use vm_darwin::VzError;
|
||||
pub(crate) use vm_darwin as backend;
|
||||
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
|
||||
pub(crate) use vm_linux as backend;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use vm_linux::VirtualMachine;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use vm_linux::VmState;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use vm_linux::VzError;
|
||||
// Re-exports from the backend for advanced/escape-hatch use
|
||||
pub use backend::{VirtualMachine, VmState, VzError};
|
||||
|
||||
/// Reject checkpoint names that could escape the checkpoints directory.
|
||||
pub fn validate_checkpoint_name(name: &str) -> Result<(), String> {
|
||||
|
||||
@@ -9,19 +9,9 @@ use std::time::Duration;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use crossbeam_channel::Receiver;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use vm_darwin::network::FileHandleNetworkAttachment;
|
||||
#[cfg(target_os = "macos")]
|
||||
use vm_darwin::terminal;
|
||||
#[cfg(target_os = "macos")]
|
||||
use vm_darwin::*;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use vm_linux::network::FileHandleNetworkAttachment;
|
||||
#[cfg(target_os = "linux")]
|
||||
use vm_linux::terminal;
|
||||
#[cfg(target_os = "linux")]
|
||||
use vm_linux::*;
|
||||
use crate::backend::network::FileHandleNetworkAttachment;
|
||||
use crate::backend::terminal;
|
||||
use crate::backend::*;
|
||||
|
||||
use vm_proto::{
|
||||
frame, ChmodRequest, CopyRequest, DiscardRequest, ExecRequest, ForwardRequest, ForwardResponse,
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
# Hanzo VM microVM kernel config — x86_64 / cloud-hypervisor
|
||||
# Same feature set as vm_defconfig (arm64); arch section swapped for
|
||||
# PVH direct boot, kvmclock, and ACPI CPU/device enumeration.
|
||||
# All drivers built-in (no loadable modules)
|
||||
|
||||
# Architecture
|
||||
CONFIG_64BIT=y
|
||||
CONFIG_SMP=y
|
||||
CONFIG_NR_CPUS=16
|
||||
CONFIG_HYPERVISOR_GUEST=y
|
||||
CONFIG_PARAVIRT=y
|
||||
CONFIG_KVM_GUEST=y
|
||||
CONFIG_PVH=y
|
||||
CONFIG_ACPI=y
|
||||
|
||||
# No loadable modules
|
||||
# CONFIG_MODULES is not set
|
||||
|
||||
# General
|
||||
CONFIG_SYSVIPC=y
|
||||
CONFIG_POSIX_MQUEUE=y
|
||||
CONFIG_NO_HZ_IDLE=y
|
||||
CONFIG_HIGH_RES_TIMERS=y
|
||||
CONFIG_PREEMPT_VOLUNTARY=y
|
||||
CONFIG_HZ_100=y
|
||||
CONFIG_SYSCTL=y
|
||||
CONFIG_MULTIUSER=y
|
||||
CONFIG_FHANDLE=y
|
||||
CONFIG_EPOLL=y
|
||||
CONFIG_SIGNALFD=y
|
||||
CONFIG_TIMERFD=y
|
||||
CONFIG_EVENTFD=y
|
||||
CONFIG_AIO=y
|
||||
CONFIG_IO_URING=y
|
||||
|
||||
# Size optimization
|
||||
CONFIG_CC_OPTIMIZE_FOR_SIZE=y
|
||||
# CONFIG_DEBUG_KERNEL is not set
|
||||
# CONFIG_DEBUG_INFO is not set
|
||||
# CONFIG_FTRACE is not set
|
||||
# CONFIG_KPROBES is not set
|
||||
# CONFIG_PROFILING is not set
|
||||
|
||||
# PCI (cloud-hypervisor uses PCI transport)
|
||||
CONFIG_PCI=y
|
||||
CONFIG_PCI_MSI=y
|
||||
|
||||
# VirtIO core
|
||||
CONFIG_VIRTIO=y
|
||||
CONFIG_VIRTIO_PCI=y
|
||||
CONFIG_VIRTIO_MENU=y
|
||||
|
||||
# VirtIO devices (all built-in)
|
||||
CONFIG_VIRTIO_BLK=y
|
||||
CONFIG_VIRTIO_CONSOLE=y
|
||||
CONFIG_VIRTIO_NET=y
|
||||
CONFIG_VIRTIO_BALLOON=y
|
||||
CONFIG_VIRTIO_MMIO=y
|
||||
CONFIG_HW_RANDOM=y
|
||||
CONFIG_HW_RANDOM_VIRTIO=y
|
||||
CONFIG_VIRTIO_FS=y
|
||||
CONFIG_VIRTIO_INPUT=y
|
||||
|
||||
# vsock (all built-in)
|
||||
CONFIG_VSOCKETS=y
|
||||
CONFIG_VIRTIO_VSOCKETS=y
|
||||
CONFIG_VIRTIO_VSOCKETS_COMMON=y
|
||||
|
||||
# Block layer
|
||||
CONFIG_BLOCK=y
|
||||
CONFIG_BLK_DEV=y
|
||||
CONFIG_BLK_DEV_INITRD=y
|
||||
|
||||
# Filesystems
|
||||
CONFIG_EXT4_FS=y
|
||||
CONFIG_EXT4_FS_POSIX_ACL=y
|
||||
CONFIG_EXT4_USE_FOR_EXT2=y
|
||||
CONFIG_OVERLAY_FS=y
|
||||
CONFIG_FUSE_FS=y
|
||||
CONFIG_TMPFS=y
|
||||
CONFIG_TMPFS_POSIX_ACL=y
|
||||
CONFIG_PROC_FS=y
|
||||
CONFIG_PROC_SYSCTL=y
|
||||
CONFIG_SYSFS=y
|
||||
CONFIG_DEVTMPFS=y
|
||||
CONFIG_DEVTMPFS_MOUNT=y
|
||||
CONFIG_DEVPTS_FS=y
|
||||
CONFIG_CONFIGFS_FS=y
|
||||
|
||||
# Networking
|
||||
CONFIG_NET=y
|
||||
CONFIG_NETDEVICES=y
|
||||
CONFIG_LOOPBACK=y
|
||||
CONFIG_PACKET=y
|
||||
CONFIG_UNIX=y
|
||||
CONFIG_INET=y
|
||||
CONFIG_IP_MULTICAST=y
|
||||
CONFIG_IP_ADVANCED_ROUTER=y
|
||||
CONFIG_IP_MULTIPLE_TABLES=y
|
||||
CONFIG_IP_ROUTE_MULTIPATH=y
|
||||
CONFIG_IPV6=y
|
||||
CONFIG_BRIDGE=y
|
||||
|
||||
# Netfilter (for iptables REDIRECT in proxy mode)
|
||||
CONFIG_NETFILTER=y
|
||||
CONFIG_NETFILTER_ADVANCED=y
|
||||
CONFIG_NETFILTER_XTABLES=y
|
||||
CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y
|
||||
CONFIG_NETFILTER_XT_MATCH_STATE=y
|
||||
CONFIG_NETFILTER_XT_TARGET_REDIRECT=y
|
||||
CONFIG_NF_CONNTRACK=y
|
||||
CONFIG_NF_NAT=y
|
||||
CONFIG_NF_TABLES=y
|
||||
CONFIG_NF_TABLES_INET=y
|
||||
CONFIG_NFT_NAT=y
|
||||
CONFIG_NFT_REDIR=y
|
||||
CONFIG_NFT_CT=y
|
||||
CONFIG_IP_NF_IPTABLES=y
|
||||
CONFIG_IP_NF_FILTER=y
|
||||
CONFIG_IP_NF_NAT=y
|
||||
CONFIG_IP_NF_TARGET_REDIRECT=y
|
||||
CONFIG_IP_NF_TARGET_MASQUERADE=y
|
||||
|
||||
# CRC32 (required by ext4)
|
||||
CONFIG_CRC32=y
|
||||
CONFIG_LIBCRC32C=y
|
||||
CONFIG_CRYPTO_CRC32C=y
|
||||
|
||||
# TTY / Console
|
||||
CONFIG_TTY=y
|
||||
CONFIG_VT=y
|
||||
CONFIG_HVC_DRIVER=y
|
||||
CONFIG_PRINTK=y
|
||||
|
||||
# PTY support (for shell/TTY exec)
|
||||
CONFIG_UNIX98_PTYS=y
|
||||
|
||||
# Cgroups (future container features)
|
||||
CONFIG_CGROUPS=y
|
||||
CONFIG_CGROUP_CPUACCT=y
|
||||
CONFIG_CGROUP_DEVICE=y
|
||||
CONFIG_CGROUP_FREEZER=y
|
||||
CONFIG_CGROUP_PIDS=y
|
||||
CONFIG_CGROUP_SCHED=y
|
||||
CONFIG_MEMCG=y
|
||||
|
||||
# Namespaces
|
||||
CONFIG_NAMESPACES=y
|
||||
CONFIG_UTS_NS=y
|
||||
CONFIG_IPC_NS=y
|
||||
CONFIG_PID_NS=y
|
||||
CONFIG_NET_NS=y
|
||||
CONFIG_USER_NS=y
|
||||
|
||||
# inotify (used by many tools)
|
||||
CONFIG_INOTIFY_USER=y
|
||||
|
||||
# Disable unnecessary subsystems
|
||||
# CONFIG_SOUND is not set
|
||||
# CONFIG_USB_SUPPORT is not set
|
||||
# CONFIG_WLAN is not set
|
||||
# CONFIG_WIRELESS is not set
|
||||
# CONFIG_INPUT_KEYBOARD is not set
|
||||
# CONFIG_INPUT_MOUSE is not set
|
||||
# CONFIG_INPUT_TOUCHSCREEN is not set
|
||||
# CONFIG_SCSI is not set
|
||||
# CONFIG_ATA is not set
|
||||
# CONFIG_DRM is not set
|
||||
# CONFIG_FB is not set
|
||||
# CONFIG_VGA_CONSOLE is not set
|
||||
# CONFIG_SELINUX is not set
|
||||
# CONFIG_SECURITY is not set
|
||||
# CONFIG_AUDIT is not set
|
||||
# CONFIG_SWAP is not set
|
||||
# CONFIG_NFS_FS is not set
|
||||
# CONFIG_CIFS is not set
|
||||
# CONFIG_BTRFS_FS is not set
|
||||
# CONFIG_XFS_FS is not set
|
||||
# CONFIG_BLK_DEV_LOOP is not set
|
||||
# CONFIG_BLK_DEV_RAM is not set
|
||||
# CONFIG_PCMCIA is not set
|
||||
# CONFIG_HWMON is not set
|
||||
# CONFIG_THERMAL is not set
|
||||
# CONFIG_WATCHDOG is not set
|
||||
# CONFIG_MEDIA_SUPPORT is not set
|
||||
# CONFIG_POWER_SUPPLY is not set
|
||||
# CONFIG_MD is not set
|
||||
|
||||
# Cgroup v2 controllers k3s requires (cpuset is fatal when missing)
|
||||
CONFIG_CPUSETS=y
|
||||
CONFIG_PROC_PID_CPUSET=y
|
||||
CONFIG_FAIR_GROUP_SCHED=y
|
||||
CONFIG_CFS_BANDWIDTH=y
|
||||
CONFIG_BLK_CGROUP=y
|
||||
CONFIG_CGROUP_BPF=y
|
||||
CONFIG_BPF=y
|
||||
CONFIG_BPF_SYSCALL=y
|
||||
|
||||
# Container runtime (runc: session keyring, seccomp, cgroup2 device bpf)
|
||||
CONFIG_KEYS=y
|
||||
CONFIG_SECCOMP=y
|
||||
CONFIG_SECCOMP_FILTER=y
|
||||
|
||||
# Pod networking: flannel vxlan, CNI bridge/veth, kube-proxy iptables
|
||||
CONFIG_VETH=y
|
||||
CONFIG_VXLAN=y
|
||||
CONFIG_BRIDGE_NETFILTER=y
|
||||
CONFIG_NETFILTER_XT_MARK=y
|
||||
CONFIG_NETFILTER_XT_NAT=y
|
||||
CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y
|
||||
CONFIG_NETFILTER_XT_MATCH_COMMENT=y
|
||||
CONFIG_NETFILTER_XT_MATCH_MULTIPORT=y
|
||||
CONFIG_NETFILTER_XT_MATCH_STATISTIC=y
|
||||
CONFIG_NETFILTER_XT_MATCH_PHYSDEV=y
|
||||
CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y
|
||||
CONFIG_IP_NF_MANGLE=y
|
||||
CONFIG_IP_NF_RAW=y
|
||||
CONFIG_IP_NF_TARGET_REJECT=y
|
||||
CONFIG_IP6_NF_IPTABLES=y
|
||||
CONFIG_IP6_NF_FILTER=y
|
||||
CONFIG_IP6_NF_MANGLE=y
|
||||
CONFIG_IP6_NF_NAT=y
|
||||
CONFIG_IP6_NF_TARGET_MASQUERADE=y
|
||||
CONFIG_IP6_NF_TARGET_REJECT=y
|
||||
CONFIG_NFT_COMPAT=y
|
||||
CONFIG_NFT_MASQ=y
|
||||
CONFIG_NFT_REJECT=y
|
||||
+15
-1
@@ -28,7 +28,21 @@ if [ ! -d "${BUILD_DIR}/linux-${KERNEL_VERSION}" ]; then
|
||||
rm -f "${BUILD_DIR}/linux-${KERNEL_VERSION}.tar.xz"
|
||||
fi
|
||||
|
||||
if [ "$(uname -m)" = "aarch64" ] && [ "$(uname -s)" = "Linux" ]; then
|
||||
if [ "$(uname -m)" = "x86_64" ] && [ "$(uname -s)" = "Linux" ]; then
|
||||
echo " Native x86_64 Linux detected, building without Docker"
|
||||
|
||||
cd "${BUILD_DIR}/linux-${KERNEL_VERSION}"
|
||||
|
||||
cp "${REPO_DIR}/kernel/vm_defconfig_x86" arch/x86/configs/vm_defconfig
|
||||
make ARCH=x86_64 vm_defconfig
|
||||
|
||||
echo " Compiling kernel (this takes a few minutes)..."
|
||||
make ARCH=x86_64 -j"$(nproc)" vmlinux 2>&1 | tail -5
|
||||
|
||||
# cloud-hypervisor direct-boots the uncompressed ELF via its PVH entry.
|
||||
strip -o "${DATA_DIR}/Image" vmlinux
|
||||
echo " Kernel built: $(du -h "${DATA_DIR}/Image" | cut -f1)"
|
||||
elif [ "$(uname -m)" = "aarch64" ] && [ "$(uname -s)" = "Linux" ]; then
|
||||
echo " Native aarch64 Linux detected, building without Docker"
|
||||
|
||||
cd "${BUILD_DIR}/linux-${KERNEL_VERSION}"
|
||||
|
||||
@@ -13,15 +13,22 @@ DEBIAN_RELEASE="trixie"
|
||||
DATA_DIR="${HANZO_VM_HOME:-$HOME/.hanzo/vm}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
GUEST_BINARY="${REPO_DIR}/target/aarch64-unknown-linux-musl/release/vm-guest"
|
||||
# Matches the CLI's default --disk-size so the per-run clone is never
|
||||
# extended (extending an APFS clone costs ~0.5s at truncate and ~0.7s at
|
||||
# delete). The image is sparse; unwritten space costs nothing on disk.
|
||||
ROOTFS_SIZE_MB=4096
|
||||
|
||||
# The guest image is built for the host architecture (macOS builds arm64
|
||||
# in a container below).
|
||||
case "$(uname -m)" in
|
||||
x86_64) GUEST_TARGET="x86_64-unknown-linux-musl"; DEB_ARCH="amd64" ;;
|
||||
*) GUEST_TARGET="aarch64-unknown-linux-musl"; DEB_ARCH="arm64" ;;
|
||||
esac
|
||||
GUEST_BINARY="${REPO_DIR}/target/${GUEST_TARGET}/release/vm-guest"
|
||||
|
||||
if [ ! -f "$GUEST_BINARY" ]; then
|
||||
echo "ERROR: guest binary not found at ${GUEST_BINARY}"
|
||||
echo " Run: cargo build -p vm-guest --target aarch64-unknown-linux-musl --release"
|
||||
echo " Run: cargo build -p vm-guest --target ${GUEST_TARGET} --release"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -34,11 +41,6 @@ if [ "$(uname)" = "Darwin" ]; then
|
||||
"debian:${DEBIAN_RELEASE}-slim" /src/scripts/prepare-rootfs.sh
|
||||
fi
|
||||
|
||||
if [ "$(uname -m)" != "aarch64" ]; then
|
||||
echo "ERROR: the guest is aarch64; build on an arm64 Linux host"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SUDO=""
|
||||
[ "$(id -u)" = 0 ] || SUDO=sudo
|
||||
|
||||
@@ -99,7 +101,7 @@ if [ -f "${DATA_DIR}/rootfs.ext4" ]; then
|
||||
else
|
||||
echo "==> Building Debian ${DEBIAN_RELEASE} rootfs (${ROOTFS_SIZE_MB}MB)..."
|
||||
ROOT="${WORK}/rootfs"
|
||||
$SUDO debootstrap --arch=arm64 --variant=minbase "$DEBIAN_RELEASE" "$ROOT" http://deb.debian.org/debian
|
||||
$SUDO debootstrap --arch="$DEB_ARCH" --variant=minbase "$DEBIAN_RELEASE" "$ROOT" http://deb.debian.org/debian
|
||||
|
||||
$SUDO mkdir -p "${ROOT}/etc/dpkg/dpkg.cfg.d"
|
||||
cat <<'DPKG' | $SUDO tee "${ROOT}/etc/dpkg/dpkg.cfg.d/01-nodoc" > /dev/null
|
||||
|
||||
Reference in New Issue
Block a user