The S5 executor's PowerMeter had only the powercap RaplMeter, which reads /sys/class/powercap/intel-rapl:0/energy_uj. But common KVM hosts expose the RAPL MSR while shipping no powercap node at all — AWS Nitro bare metal (kernel *-aws) is exactly this: /dev/cpu/N/msr yields a live MSR_PKG_ENERGY_STATUS (0x611) counter with the unit in MSR_RAPL_POWER_UNIT (0x606), but /sys/class/powercap is empty. MsrRaplMeter reads that counter directly (root + msr module), scaling ticks by 1/2^ESU to microjoules — the same physical energy RaplMeter reports, on hosts where the sysfs path doesn't exist. Where the MSR is unreadable, package0() returns None and the executor falls back to the honest ModelBased vCPU-seconds estimate, unchanged. The boot_microvm example drives a real Firecracker through the reference FirecrackerVmm (spawn firecracker --api-sock; PUT boot-source/drives/ machine-config + InstanceStart over our unix-socket HTTP/1.1 client), meters the boot window on the MSR, and seals a Sandbox S5 JCR-1 receipt (HwShunt where metered, ModelBased otherwise) — the executor's full path, end to end, on a Linux KVM host. Co-Authored-By: Claude Fable 5 <[email protected]>
136 lines
5.3 KiB
Rust
136 lines
5.3 KiB
Rust
//! Boot a real Firecracker microVM through the reference [`FirecrackerVmm`]
|
||
//! driver, meter the boot's CPU energy on real hardware (RAPL via MSR), and
|
||
//! seal the reading as a Sandbox JCR-1 receipt — the S5 executor's full path,
|
||
//! end to end, on a Linux KVM host.
|
||
//!
|
||
//! ```bash
|
||
//! # on a Linux KVM host (e.g. AWS *.metal), as root for MSR access:
|
||
//! sudo FC_BIN=/usr/local/bin/firecracker \
|
||
//! KERNEL=/tmp/vmlinux.bin ROOTFS=/tmp/rootfs.ext4 \
|
||
//! ./boot_microvm
|
||
//! ```
|
||
//!
|
||
//! It spawns `firecracker --api-sock <sock>`, drives `PUT /boot-source`,
|
||
//! `/drives/rootfs`, `/machine-config`, and the `InstanceStart` action through
|
||
//! our unix-socket HTTP/1.1 client, lets the guest run briefly, and reads the
|
||
//! package energy counter before and after. A genuine `HwShunt` figure where
|
||
//! the MSR is readable; an honest `ModelBased` vCPU-seconds estimate otherwise.
|
||
|
||
use sandbox_exec_microvm::firecracker::{FirecrackerConfig, FirecrackerVmm, VmState, Vmm};
|
||
use sandbox_exec_microvm::power::PowerMeter;
|
||
use sandbox_meter::{DeviceEnergy, Provenance, Receipt};
|
||
use sandbox_schema::{ContentId, DeviceClass, Tier};
|
||
use std::path::Path;
|
||
use std::process::Command;
|
||
use std::time::{Duration, Instant};
|
||
|
||
fn env(key: &str, default: &str) -> String {
|
||
std::env::var(key).unwrap_or_else(|_| default.into())
|
||
}
|
||
|
||
fn main() {
|
||
let fc_bin = env("FC_BIN", "/usr/local/bin/firecracker");
|
||
let kernel = env("KERNEL", "/tmp/vmlinux.bin");
|
||
let rootfs = env("ROOTFS", "/tmp/rootfs.ext4");
|
||
let run_secs: f64 = env("RUN_SECS", "4").parse().unwrap_or(4.0);
|
||
let sock = "/tmp/fc-demo.sock";
|
||
let _ = std::fs::remove_file(sock);
|
||
|
||
// Spawn the Firecracker process listening on the API socket.
|
||
let mut child = Command::new(&fc_bin)
|
||
.args(["--api-sock", sock])
|
||
.spawn()
|
||
.unwrap_or_else(|e| panic!("spawn {fc_bin}: {e}"));
|
||
for _ in 0..100 {
|
||
if Path::new(sock).exists() {
|
||
break;
|
||
}
|
||
std::thread::sleep(Duration::from_millis(50));
|
||
}
|
||
|
||
// The meter: real RAPL via MSR where available (root on Nitro metal),
|
||
// else None → an honest vCPU-seconds ModelBased estimate.
|
||
#[cfg(target_os = "linux")]
|
||
let meter: Option<sandbox_exec_microvm::power::MsrRaplMeter> =
|
||
sandbox_exec_microvm::power::MsrRaplMeter::package0();
|
||
#[cfg(not(target_os = "linux"))]
|
||
let meter: Option<sandbox_exec_microvm::power::NoMeter> = None;
|
||
|
||
let before = meter.as_ref().and_then(|m| m.sample_uj());
|
||
println!(
|
||
"meter: {}",
|
||
if before.is_some() { "RAPL via MSR (HwShunt)" } else { "none — ModelBased fallback" }
|
||
);
|
||
|
||
let cfg = FirecrackerConfig {
|
||
api_socket: sock.into(),
|
||
kernel_image_path: kernel,
|
||
rootfs_path: rootfs,
|
||
boot_args: "console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw".into(),
|
||
vcpus: 2,
|
||
mem_mib: 256,
|
||
};
|
||
|
||
let t0 = Instant::now();
|
||
let outcome = FirecrackerVmm.run(&cfg);
|
||
match &outcome {
|
||
Ok(o) => println!("boot: {:?} ({} vcpu-s attributed)", o.state, o.vcpu_seconds),
|
||
Err(e) => println!("boot error: {e}"),
|
||
}
|
||
// Let the guest run so the metered window covers real execution.
|
||
std::thread::sleep(Duration::from_secs_f64(run_secs));
|
||
let after = meter.as_ref().and_then(|m| m.sample_uj());
|
||
let wall = t0.elapsed();
|
||
let _ = child.kill();
|
||
let _ = child.wait();
|
||
let _ = std::fs::remove_file(sock);
|
||
|
||
let booted = matches!(outcome.map(|o| o.state), Ok(VmState::Booted | VmState::Completed));
|
||
println!("guest ran {:.2}s; booted={booted}", wall.as_secs_f64());
|
||
|
||
// Build the energy record: HwShunt from the MSR delta, else ModelBased.
|
||
let device = match (before, after) {
|
||
(Some(a), Some(b)) => {
|
||
let uj = b.saturating_sub(a);
|
||
println!("CPU energy (RAPL pkg-0, MSR): {:.3} J over the window", uj as f64 / 1e6);
|
||
DeviceEnergy {
|
||
class: DeviceClass::Cpu,
|
||
measured_uj: Some(uj),
|
||
estimated_uj: uj,
|
||
provenance: Provenance::HwShunt,
|
||
}
|
||
}
|
||
_ => {
|
||
// No hardware: vCPU-seconds × a coarse per-vCPU-second figure.
|
||
let est = (cfg.vcpus as u64).saturating_mul(wall.as_secs().max(1)).saturating_mul(15_000_000);
|
||
println!("CPU energy (estimate): {:.3} J", est as f64 / 1e6);
|
||
DeviceEnergy {
|
||
class: DeviceClass::Cpu,
|
||
measured_uj: None,
|
||
estimated_uj: est,
|
||
provenance: Provenance::ModelBased,
|
||
}
|
||
}
|
||
};
|
||
|
||
// Seal it as a Sandbox S5 receipt.
|
||
let receipt = Receipt::build(
|
||
ContentId::of_bytes(b"firecracker microvm boot: vmlinux + bionic rootfs"),
|
||
Tier::S5,
|
||
vec![device],
|
||
1,
|
||
[9u8; 8],
|
||
)
|
||
.expect("receipt builds");
|
||
let sk = ed25519_dalek::SigningKey::from_bytes(&[5u8; 32]);
|
||
let envelope = receipt.seal("did:sandbox:metal-demo", &sk).expect("seal");
|
||
receipt
|
||
.verify_sealed(&envelope, &sk.verifying_key(), Some("did:sandbox:metal-demo"))
|
||
.expect("verify");
|
||
std::fs::write("/tmp/microvm-receipt.cose", &envelope).ok();
|
||
println!(
|
||
"sealed S5 receipt {} ({} bytes) — verified ✓; written to /tmp/microvm-receipt.cose",
|
||
receipt.content_id().0,
|
||
envelope.len()
|
||
);
|
||
}
|