📰 newsreader

hackernews score 0.92 好み 0.00 en

Virtual Oxide Labs:Oxide ラックのフルエミュレーション

原題: Virtual Oxide Labs: Emulating a Full Oxide Rack

rack-scale hardwarehardware emulationvirtualizationoxidecontrol planep4 switchesnetworking topologyvoxel
原文 ↗
原文(英語)を表示

Virtual Oxide Labs: Emulating a Full Oxide Rack

The trouble with testing and developing for rack-scale hardware is that rack-scale is not all that easy to come by. I mean, nevermind the whole DDR5-as-a-currency thing; even if all the parts were available in abundance, any completed racks go swiftly to the front lines (AKA our customers' data centers).

As a member of the Product Assurance Team at Oxide, I find that totally unfair yet completely reasonable. And so we have to get a bit creative. We have to be able to:

Replicate diverse customer networking topologies

Run our actual control plane

Test up to date versions of our software

Represent the hardware components (we make racks, after all) including our switches and microcontrollers

And the cherry on top: we need to do that without cutting special "test version" releases. Because we want to keep our CI sleek, but also because that wouldn’t be playing fair.

Now, don’t get me wrong — we’re not without our tricks. When I joined Oxide last year, one of the first things I was introduced to was our racklettes; that is, smaller than average racks of four sleds and two switches each. These racklettes can be initialized and run just like a real rack, precisely because they are a real rack. Just not in any form factor it would be economical to sell. But for testing they work like a charm. We have four of them today, soon to be eight, where we can test any combination of hardware/software requirement.

We also have our various simulated methods of running Omicron. You can read more about those at How to Run Simulated and the full(er) How to Run Guide. I won’t go into a ton of detail here, but suffice it to say that most parts of our control plane (and more) can be run independently, in tandem, and in various degrees of simulation.

Do you remember the game Q*bert? It was one of the first wildly popular isometric games, and looked a little like this:

Why am I showing you a ridiculously small game screenshot? Well, each cube in that image represents a 'voxel,' or a point in 3D space. It’s actually the 3D version of a pixel. Volume Element + Pixel = Voxel. Neat, right?

Anyway, for Oxide, Virtual OXidE Labs (or Voxel) means an incredible amount of fidelity packed into a single piece of hardware. It is the spiritual successor to our prior virtual labs (known as a4x2) which allowed us to hack out decent racks running in VMs.

Pay attention to that word "fidelity" by the way. For it is, as Bryan Cantrill puts it, the Prime Directive: labs should use stock Oxide software without hack or modification.

This is what Voxel gives us. A complete software emulation stack, built on illumos in Rust, where we can build and test a variety of realistic configurations:

Sled virtualization provided by Propolis

BGP (numbered and unnumbered) or static networking with Debian and FRR

P4-powered switches via Oxide’s own SoftNPU, a software P4 switch

Microcontroller simulation and emulation for our Service Processor (SP) and Root of Trust (RoT)

And what’s more, it gives us all these features in a fairly small footprint. At home, I run Voxel labs on a Minisforum MS-A2 mini-PC with a 32-thread AMD Ryzen 9 9955HX and 96GB RAM. On that computer, I can spin up 6 sleds with ease, either in the form of one rack or in a multirack configuration (two separate racks, one management plane). All it requires is Oxide Helios and a decently fast NVMe drive. Launch, and in about 10 minutes you have a full Oxide rack with each "sled" in its own Propolis VM — alongside switches (also in Propolis) and other infrastructure.

Remember how I said we have to use stock Oxide software? That actually makes things easier. The first thing we do (following a common practice in hypervisor tooling) is to create a reusable snapshot.

Oxide sleds are only ever one of two things: a normal sled (a "gimlet" for the first generation, "cosmo" for the current) or a "scrimlet," which is a sled wired to the sidecar switches that runs a switch zone. It’s all amazingly elegant in its complexity, you should read more about it in our networking docs.

To create a snapshot for Voxel, all we must do is create a "sled" (virtual machine), install our control plane (Omicron) on it, snapshot it before the point of configuration, et voilà: a ready-to-launch sled. The control plane can be installed one of two ways:

- From source

Voxel clones the Omicron repo on GitHub (or uses a locally defined source for rapid development) and uses

cargo

to compile- From TUF repo

We use Tufaceous to package Omicron releases using The Update Framework (TUF) and our CI tool Buildomat

Using the second option as an example, here’s the beginning of the process where voxel image create

pulls out the key pieces that make up the sled config:

$ pfexec ./target/debug/voxel image create --from-tuf ../5d176e0be7e5cc0d3288c1bef10005ec31f2cd7c.zip 5d176e0be7e5cc0d3288c1bef10005ec31f2cd7c

[voxel] TUF source ../5d176e0be7e5cc0d3288c1bef10005ec31f2cd7c.zip: system version 23.0.0-0.ci+git5d176e0be7e, omicron 5d176e0be7e

[voxel] extracted 12 zones from ../5d176e0be7e5cc0d3288c1bef10005ec31f2cd7c.zip

[voxel] extracted 3 measurement corpus artifacts

[voxel] extracting host artifacts -> /export/home/helios/voxel-rawfix/target/debug/../../voxel-image/.tuf-host

[voxel] host boot image 1258295296 bytes, phase 1 rom 33554432 bytes

[voxel] extracted SP/RoT firmware -> /export/home/helios/voxel-rawfix/target/debug/../../voxel-image/.tuf-fw/ca9184058bd7

After a decent amount of unpacking and staging, it will launch a VM that everything gets written/installed to. And finally:

[voxel] ready: voxel-cp version=unknown unpacked_artifacts=18 built=2026-09-06T21:37:58

[voxel] clearing device-instance map, halting

[voxel] flushing, up to 25s

[voxel] stopping hypervisor

[voxel] writing voxel image voxel-cp-5d176e0be7e-tuf

That image is a zfs snapshot which can be used and reused to create our rack.

Launching the rack uses a similar process, except the control plane is already installed. All we need at this point is:

An "frr" image (the Debian VMs for networking)

A control plane image (like the one we just created)

The

voxel-init

agent that gives each virtual sled its assignment

The log output is pretty long, but you can scroll through it if you’d like. I’ll recap after.

$ pfexec ./target/debug/voxel launch --emu

[voxel] sled-agent config schema (image properties): data_links=Tagged disks=Hardcoded

Writing key pair to "./cargo-bay/sprockets/test-root-a.key.pem"

Writing key pair to "./cargo-bay/sprockets/test-signer-a1.key.pem"

...

Writing certificate to "./cargo-bay/sprockets/test-root-a.cert.pem"

Writing certificate to "./cargo-bay/sprockets/test-signer-a1.cert.pem"

...

Writing pki path to "./cargo-bay/sprockets/test-sprockets-auth-0.certlist.pem"

Writing pki path to "./cargo-bay/sprockets/test-alias-0.certlist.pem"

...

add net fdb0:a840:2500:1::/64: gateway fdb0:a840:25ff:0::2

add net fdb0:a840:2500:5::/64: gateway fdb0:a840:25ff:0::4

[sp-emu] instance state dir: /export/home/helios/voxel-unzip/sp-fleet/r0/sp-emu/state/33300

[identity] seed = 0x2aac7ae3f8cfbb1e (3074967765137734430)

[identity] SP UID 13120858064df80da1642f2b, RoT UUID 00a94a8e206f3f138a790fba1b12db36

[flash] Hubris archive /export/home/helios/voxel-unzip/sp-fleet/r0/sp-emu/sidecar.archive -> /export/home/helios/voxel-unzip/sp-fleet/r0/sp-emu/state/33300/archives/sp.zip

flashed 786688 bytes into slot A (base 0x08000000, reset PC 0x080002e8) of /export/home/helios/voxel-unzip/sp-fleet/r0/sp-emu/state/33300/sp-flash.bin

[sp-emu] instance state dir: /export/home/helios/voxel-unzip/sp-fleet/r0/sp-emu/state/33310

[identity] seed = 0xa65abed0e20d08d1 (11987103162554976465)

[identity] SP UID 72a3c3327682e0df38de478a, RoT UUID fcb600f7322f3393899f72036aa2b4fd

[flash] Hubris archive /export/home/helios/voxel-unzip/sp-fleet/r0/sp-emu/gimlet.archive -> /export/home/helios/voxel-unzip/sp-fleet/r0/sp-emu/state/33310/archives/sp.zip

flashed 728320 bytes into slot A (base 0x08000000, reset PC 0x080002e8) of /export/home/helios/voxel-unzip/sp-fleet/r0/sp-emu/state/33310/sp-flash.bin

[sp-emu] instance state dir: /export/home/helios/voxel-unzip/sp-fleet/r0/sp-emu/state/33320

[identity] seed = 0xbab29f9c81fda354 (13452990531453690708)

[identity] SP UID 0883f59f877ab40da88e8072, RoT UUID 15468fb5bffd30d3b381e8759621fb4f

[flash] Hubris archive /export/home/helios/voxel-unzip/sp-fleet/r0/sp-emu/gimlet.archive -> /export/home/helios/voxel-unzip/sp-fleet/r0/sp-emu/state/33320/archives/sp.zip

flashed 728320 bytes into slot A (base 0x08000000, reset PC 0x080002e8) of /export/home/helios/voxel-unzip/sp-fleet/r0/sp-emu/state/33320/sp-flash.bin

[sp-emu] instance state dir: /export/home/helios/voxel-unzip/sp-fleet/r0/sp-emu/state/33330

[identity] seed = 0xea954465eef85936 (16903491980996663606)

[identity] SP UID 49c0e331f4877c7d0afc71eb, RoT UUID 03ec945840463823a95d38dda9e261d1

[flash] Hubris archive /export/home/helios/voxel-unzip/sp-fleet/r0/sp-emu/gimlet.archive -> /export/home/helios/voxel-unzip/sp-fleet/r0/sp-emu/state/33330/archives/sp.zip

flashed 728320 bytes into slot A (base 0x08000000, reset PC 0x080002e8) of /export/home/helios/voxel-unzip/sp-fleet/r0/sp-emu/state/33330/sp-flash.bin

[voxel] rack 0 SP fleet up on fdb0:a840:25ff:0::1 (4 SP(s): [33300, 33310, 33320, 33330])

Sep 06 22:15:47.126 INFO launching runner: deployment(voxel) persistent(true) custom_propolis_binary(Some("/export/home/helios/propolis-server")) dataset(testbed/falcon) falcon_dir(.falcon)

Sep 06 22:15:47.126 INFO starting preflight for deployment voxel

Sep 06 22:15:48.162 INFO creating links

Sep 06 22:15:48.162 INFO creating simnet link 'voxel_ce_vn_sim0'

Sep 06 22:15:48.164 INFO creating vnic link 'voxel_ce_vn_vnic0'

...

Sep 06 22:15:48.249 INFO creating external links

Sep 06 22:15:48.249 INFO creating external link voxel_ce_vn_vnic2

Sep 06 22:15:48.251 INFO creating external link voxel_g0_vn_vnic3

...

Sep 06 22:15:48.255 INFO creating nodes

Sep 06 22:15:48.255 INFO g0: launching node

Sep 06 22:15:48.257 INFO g1: launching node

...

Sep 06 22:15:48.290 INFO launched instance g0 with pid 13041 on port 43736

Sep 06 22:15:48.291 INFO g0: instance ensure

Sep 06 22:15:48.295 INFO launched instance g1 with pid 13042 on port 33187

Sep 06 22:15:48.295 INFO g1: instance ensure

Sep 06 22:15:48.297 INFO launched instance g2 with pid 13043 on port 51110

...

Sep 06 22:15:52.423 INFO g1: instance ensure completed after 0 retries

Sep 06 22:15:52.423 INFO g1: instance run

Sep 06 22:15:52.479 INFO g2: instance ensure completed after 0 retries

Sep 06 22:15:52.479 INFO g2: instance run

Sep 06 22:15:52.534 INFO g0: instance ensure completed after 0 retries

Sep 06 22:15:52.534 INFO g0: instance run

Sep 06 22:16:03.961 INFO cr1: mounting /opt/cargo-bay

Sep 06 22:16:03.969 INFO cr2: mounting /opt/cargo-bay

Sep 06 22:16:03.994 INFO cr1: finished mounting /opt/cargo-bay

Sep 06 22:16:03.995 INFO ce: mounting /opt/cargo-bay

Sep 06 22:16:04.002 INFO cr2: finished mounting /opt/cargo-bay

...

Sep 06 22:16:09.236 INFO ce: launch start

Sep 06 22:16:09.237 INFO cr1: launch start

Sep 06 22:16:09.237 INFO cr2: launch start

Sep 06 22:16:12.893 INFO ce: [voxel-init] pinned enp0s10 DHCP client-id to MAC; re-DHCPing host-LAN uplink

Sep 06 22:16:12.893 INFO ce: [voxel-init] NAT rack egress via enp0s10

Sep 06 22:16:12.894 INFO ce: [voxel-init] static edge IP 192.168.68.170/22 added on enp0s10

Sep 06 22:16:12.894 INFO ce: [voxel-init] router bring-up complete

Sep 06 22:16:12.894 INFO ce: launch ok

Sep 06 22:16:12.956 INFO cr1: [voxel-init] pinned enp0s11 DHCP client-id to MAC; re-DHCPing host-LAN uplink

Sep 06 22:16:12.956 INFO cr1: [voxel-init] NAT rack egress via enp0s11

Sep 06 22:16:12.956 INFO cr1: [voxel-init] router bring-up complete

Sep 06 22:16:12.956 INFO cr1: launch ok

Sep 06 22:16:12.985 INFO cr2: [voxel-init] pinned enp0s11 DHCP client-id to MAC; re-DHCPing host-LAN uplink

Sep 06 22:16:12.985 INFO cr2: [voxel-init] NAT rack egress via enp0s11

Sep 06 22:16:12.985 INFO cr2: [voxel-init] router bring-up complete

Sep 06 22:16:12.985 INFO cr2: launch ok

Sep 06 22:16:12.985 INFO g0: launch start

Sep 06 22:16:12.986 INFO g1: launch start

Sep 06 22:16:12.987 INFO g2: launch start

Sep 06 22:16:28.581 INFO g1: [voxel-init] underlay(jumbo)=["vioif0", "vioif1"] ext-candidates=["vioif2"]

Sep 06 22:16:28.581 INFO g1: [voxel-init] c2t1d0 (2FAKE001-M20): M.2 layout written

Sep 06 22:16:28.581 INFO g1: [voxel-init] c2t1d0: host phase 2 seeded

Sep 06 22:16:28.581 INFO g1: [voxel-init] c3t1d0 (2FAKE001-M21): M.2 layout written

Sep 06 22:16:28.582 INFO g1: [voxel-init] c3t1d0: host phase 2 seeded

Sep 06 22:16:28.582 INFO g1: [voxel-init] c4t1d0 (2FAKE001-U20): labeled

Sep 06 22:16:28.582 INFO g1: [voxel-init] c5t1d0 (2FAKE001-U21): labeled

Sep 06 22:16:28.582 INFO g1: [voxel-init] c6t1d0 (2FAKE001-U22): labeled

Sep 06 22:16:28.582 INFO g1: [voxel-init] c7t1d0 (2FAKE001-U23): labeled

Sep 06 22:16:28.582 INFO g1: [voxel-init] c8t1d0 (2FAKE001-U24): labeled

Sep 06 22:16:28.582 INFO g1: [voxel-init] 7 sled disks (2 M.2, 5 U.2)

Sep 06 22:16:28.582 INFO g1: [voxel-init] preseed: staged install dataset on /dev/dsk/c2t1d0s5 (oxi_626cc492-5a93-4b5c-833b-c45a504d8300)

Sep 06 22:16:28.582 INFO g1: [voxel-init] preseed: staged install dataset on /dev/dsk/c3t1d0s5 (oxi_711d472d-edbc-4426-9e33-4b000914f4da)

Sep 06 22:16:28.582 INFO g1: [voxel-init] sled-agent activated

Sep 06 22:16:28.582 INFO g1: [voxel-init] gimlet bring-up complete

Sep 06 22:16:28.582 INFO g1: launch ok

Sep 06 22:16:33.159 INFO g0: [voxel-init] underlay(jumbo)=["vioif1", "vioif2"] ext-candidates=["vioif0", "vioif3"]

Sep 06 22:16:33.159 INFO g0: [voxel-init] SP network fdb0:a840:25ff:0::2/64 on vioif3 (deprecated as a source)

Sep 06 22:16:33.159 INFO g0: [voxel-init] c2t1d0 (2FAKE000-M20): M.2 layout written

Sep 06 22:16:33.159 INFO g0: [voxel-init] c2t1d0: host phase 2 seeded

Sep 06 22:16:33.159 INFO g0: [voxel-init] c3t1d0 (2FAKE000-M21): M.2 layout written

Sep 06 22:16:33.159 INFO g0: [voxel-init] c3t1d0: host phase 2 seeded

Sep 06 22:16:33.159 INFO g0: [voxel-init] c4t1d0 (2FAKE000-U20): labeled

Sep 06 22:16:33.159 INFO g0: [voxel-init] c5t1d0 (2FAKE000-U21): labeled

Sep 06 22:16:33.159 INFO g0: [voxel-init] c6t1d0 (2FAKE000-U22): labeled

Sep 06 22:16:33.159 INFO g0: [voxel-init] c7t1d0 (2FAKE000-U23): labeled

Sep 06 22:16:33.160 INFO g0: [voxel-init] c8t1d0 (2FAKE000-U24): labeled

Sep 06 22:16:33.160 INFO g0: [voxel-init] 7 sled disks (2 M.2, 5 U.2)

Sep 06 22:16:33.160 INFO g0: [voxel-init] preseed: staged install dataset on /dev/dsk/c2t1d0s5 (oxi_ab4a1425-a7f2-47c7-9ef8-c70180ef02c9)

Sep 06 22:16:33.160 INFO g0: [voxel-init] preseed: staged install dataset on /dev/dsk/c3t1d0s5 (oxi_410f70f9-1eea-49fe-8aae-8331cebc4ea1)

Sep 06 22:16:33.160 INFO g0: [voxel-init] switch0 enforcer started (pid 769), log /tmp/switch-enforcer.log

Sep 06 22:16:33.160 INFO g0: [voxel-init] sled-agent activated

Sep 06 22:16:33.160 INFO g0: [voxel-init] gimlet bring-up complete

Sep 06 22:16:33.160 INFO g0: launch ok

Sep 06 22:16:38.179 INFO g2: [voxel-init] underlay(jumbo)=["vioif1", "vioif2"] ext-candidates=["vioif0", "vioif3"]

Sep 06 22:16:38.179 INFO g2: [voxel-init] SP network fdb0:a840:25ff:0::4/64 on vioif3 (deprecated as a source)

Sep 06 22:16:38.179 INFO g2: [voxel-init] c2t1d0 (2FAKE002-M20): M.2 layout written

Sep 06 22:16:38.179 INFO g2: [voxel-init] c2t1d0: host phase 2 seeded

Sep 06 22:16:38.179 INFO g2: [voxel-init] c3t1d0 (2FAKE002-M21): M.2 layout written

Sep 06 22:16:38.179 INFO g2: [voxel-init] c3t1d0: host phase 2 seeded

Sep 06 22:16:38.179 INFO g2: [voxel-init] c4t1d0 (2FAKE002-U20): labeled

Sep 06 22:16:38.179 INFO g2: [voxel-init] c5t1d0 (2FAKE002-U21): labeled

Sep 06 22:16:38.179 INFO g2: [voxel-init] c6t1d0 (2FAKE002-U22): labeled

Sep 06 22:16:38.179 INFO g2: [voxel-init] c7t1d0 (2FAKE002-U23): labeled

Sep 06 22:16:38.179 INFO g2: [voxel-init] c8t1d0 (2FAKE002-U24): labeled

Sep 06 22:16:38.179 INFO g2: [voxel-init] 7 sled disks (2 M.2, 5 U.2)

Sep 06 22:16:38.179 INFO g2: [voxel-init] preseed: staged install dataset on /dev/dsk/c2t1d0s5 (oxi_270f44df-c4df-43fd-b02f-e5440857b547)

Sep 06 22:16:38.179 INFO g2: [voxel-init] preseed: staged install dataset on /dev/dsk/c3t1d0s5 (oxi_1c986103-d332-4270-b7e1-9341310a621d)

Sep 06 22:16:38.179 INFO g2: [voxel-init] switch1 enforcer started (pid 779), log /tmp/switch-enforcer.log

Sep 06 22:16:38.179 INFO g2: [voxel-init] sled-agent activated

Sep 06 22:16:38.179 INFO g2: [voxel-init] gimlet bring-up complete

Sep 06 22:16:38.179 INFO g2: launch ok

Sep 06 22:16:38.652 INFO rack-init: waiting for the commission API: switch-zone bootstrap address: no bootstrap address on oxz_switch

Sep 06 22:17:54.860 INFO rack-init: waiting for the commission API: tunnel opened

Sep 06 22:17:59.876 INFO rack-init: commission API reachable on g0

Sep 06 22:18:23.892 INFO rack-init: 3 bootstrap sleds ready

..+.......+.....+.+..+.......+..+...............+....+.....+.+.....+......+....+...+..+....+.....+++++++++++++++++++++++++++++++++++++++*.....+.+.....+......+.+++++++++++

............+..+.+..+...+.........+......+...+..........+++++++++++++++++++++++++++++++++++++++*...+...+..+++++++++++++++++++++++++++++++++++++++*..................+.....

-----

Sep 06 22:18:24.023 INFO rack-init: commission-driven RSS started

Sep 06 22:18:24.027 INFO rack-init: watching RSS progress on the RSS node ...

Sep 06 22:18:24.228 INFO rack-init: polling RSS status via ssh root@192.168.68.167

Sep 06 22:18:32.410 INFO rack-init [9/16]: initializing internal DNS

Sep 06 22:18:49.186 INFO rack-init [11/16]: initializing NTP

Sep 06 22:19:13.745 INFO rack-init [12/16]: waiting for time sync

Sep 06 22:20:51.850 INFO rack-init [12/16]: waiting for time sync ... still working (1m in this step)

Sep 06 22:22:22.134 INFO rack-init [13/16]: waiting for database

Sep 06 22:22:47.334 INFO rack-init [14/16]: initializing cluster

Sep 06 22:22:55.805 INFO rack-init [15/16]: initializing zones

Sep 06 22:24:33.717 INFO rack-init [16/16]: handing off to Nexus

Sep 06 22:26:11.824 INFO rack-init [16/16]: handing off to Nexus ... still working (1m in this step)

Sep 06 22:27:49.894 INFO rack-init [16/16]: handing off to Nexus ... still working (3m in this step)

Sep 06 22:28:06.231 INFO rack-init: complete, rack initialized (RSS run b20a2f8d-e511-4a67-a666-7b74b2ce68d7)

Sep 06 22:28:06.231 INFO launch complete

Sep 06 22:28:06.250 INFO external route set: 198.51.100.0/24 -> 192.168.68.170 (ce)

Sep 06 22:28:06.292 INFO rack: external network reachable (dns 198.51.100.20)

Like I said, a lot of steps. Here’s what happened:

Trust material is generated in the form of sprockets PKI

Emulated STM32H753 and LPC55 microcontrollers are flashed with Hubris firmware

Our automated network topology suite (known as falcon) wires the topology with simnet and VNIC pairs for every link, and stages the sleds (g0, g1, and g2 in this case)

Propolis VMs launch; 6 total in this example, with 3 driving the network and 3 as Oxide sleds

voxel-init

runs: a lightweight Rust agent on each node thatSets up NAT and uplink addressing

Configures underlay setup on each sled

Writes the M.2 partition layout on emulated boot disks and labels five U.2 data disks

Seeds the host phase 2 image and install dataset to the M.2s

Assigns the scrimlets and runs a switch enforcer so they "remember" their identity across restarts

Activates

sled-agent

, the per-sled configuration layer for the control plane

Connects to the commission API for

wicketd

, which brings the rack through its initialization phases

It’s a complex dance of steps, and I really buried the lede on the whole "emulated

microcontrollers" front. But suffice it to say these are the complete and right steps for

any Oxide rack, except the hardware is represented as distinct pieces of code. For example,

need a new interconnect between two virtual racks? Add a couple lines of code in rack.rs

.

There are a few things that don’t follow the Prime Directive (yet!). For example, those certs that make up the sprockets PKI up top aren’t set at manufacturing and coming from the RoT like our production racks. But that’s one of those things that we can live with for now, as it doesn’t affect actual rack runtime.

This is the bit I mentioned a moment ago. A big part of what makes our racks unique is

the embedded RoT and SP

in every sled. We’ve long had an SpSim

construct that served as a stub for the service

processor and would return the right info at the right times. But that’s simulation, not

emulation. It’s always going to be happy path, and that’s just not enough fidelity for

real testing cases.

As such, we built actual emulated Cortex-M7 and Cortex-M33 devices for illumos and Linux (x86 and ARM). The repo for this tool (sp-emu) is open source. Each of the two devices runs in a single thread, and (mostly) faithfully emulates the three main requirements: instructions, time, and interrupts.

From there, you can flash our Hubris firmware directly to these emulated devices, and then communicate by way of an ordinary socket. The Voxel lab can speak with these virtual devices via the Management Gateway Service (MGS) and Inter-Processor Communication Channel (IPCC). You can learn everything you want and more about these concepts in the Management Gateway Service repo and Hubris docs.

There really is no "run real microcontrollers as virtual devices with this one neat trick!"

I can think of. But the real challenge of sp-emu’s job is time. Remember all of the code

we’re running there is meant for silicon. Hubris schedules off a 1ms SysTick and the emulator doesn’t

use wall time; as such, we had to invent a clock. That said, emulated hardware is also

slower than the real thing. So there’s… well, less time per time. So instead of trying

to get all temporal about it, time became instructions. For example, I give you maybe_tick

:

if self.systick == 0 {

self.systick = self.syst_rvr;

}

self.systick -= 1;

if self.systick == 0 {

self.systick = self.syst_rvr;

self.tick_events = self.tick_events.wrapping_add(1);

if self.syst_csr & 2 != 0

&& self.mode == Mode::Thread

&& !self.primask

{

self.exception_entry(15, bus); // SysTick exception

}

}

In this case, we count instructions against the kernel’s own reload value and fire exceptions anywhere the single-priority design makes it safe to take it — in thread mode, interrupts unmasked. By the same token, peripheral access costs one tick of our clock (an instruction), whereas on silicon it might stall for dozens of cycles on the bus.

You may have noticed self.exception_entry

which is defined

elsewhere in the code:

fn exception_entry(&mut self, vecnum: u32, bus: &mut Bus)

In short, this function models everything the hardware does on a Cortex-M core taking

an exception, all the way between "exception becomes active" to the first handler

instruction. vecnum

here is an ARMv7-M exception number (also the vector table index

for that exception’s handler). bus

is the memory system which is used to actually

push the frame and read the table.

Hubris has a non-preemptible kernel, though it schedules tasks preemptively. SVCall, PendSV, SysTick and all IRQs run at the same priority level, while faults keep a higher priority. As such, the emulator doesn’t have to nest handlers. It processes pending IRQs based on the PRIMASK, FAULTMASK, and BASEPRI settings. If one of them pended PendSV, that will perform the deferred context switch for the kernel. For more details on how it works internally, you can check this lovely bit of documentation.

if let Some(irq) = bus.next_irq() {

if self.basepri == 0 || (bus.irq_prio(irq) as u32) < self.basepri {

bus.clear_pending(irq);

self.exception_entry(16 + irq as u32, bus);

return;

}

}

if bus.take_pendsv() {

self.exception_entry(14, bus);

}

There’s a lot more we can talk about here, but to sum it all up you can look at this pseudo-ish loop:

loop {

cpu.step(&mut bus, host.as_mut())?; // one Thumb-2 instruction

cpu.maybe_tick(&mut bus); // SysTick: time is instructions

cpu.maybe_interrupt(&mut bus); // NVIC and PendSV delivery

if cpu.cycles % 4096 == 0 {

bus.pump_eth(host.as_mut()); // MGS packets in and out

bus.pump_uart(host.as_mut());

}

}

Note: yes I’m aware I said there’s no cycles then have a field named cycles. That’s the "fake it till you make it" part of emulation.

That’s the overall heartbeat of sp-emu, and it runs ad infinitum. Everything else exists to make those calls seem real.

Past the actual working of the emulated sleds and microcontrollers, it’s all good ol'

fashion user experience. When you’re running that many components on a single system, it

can get really confusing trying to figure out what pieces are where, how to connect

to one sled on one rack versus another, etc. So the voxel

command that we use to

launch and destroy virtual racks also serves as a connectivity tool. For example:

$ pfexec ./target/debug/voxel host login g0

[voxel] ssh root@192.168.68.167 (g0 global zone)

The illumos Project helios-3.0.23976 May 2026

root@g0:~# zoneadm list

global

oxz_switch

oxz_internal_dns_5b07f3e7-444c-462c-9633-6967e52d6a3b

oxz_ntp_b308987c-3408-4077-b9a0-d803ed1e420a

oxz_cockroachdb_daa29fcd-fff0-4b3d-a722-b969e03398d9

oxz_cockroachdb_117986ee-8b2d-4567-8c6f-e01f3e63447e

oxz_crucible_310a91b5-1b67-46cb-b08d-ad6534702a3d

oxz_crucible_627c130b-97d6-4e8f-b13e-eb03b671c2a3

oxz_crucible_pantry_65e1b527-26fc-46fd-8584-d9792771926c

oxz_crucible_2692a74a-3a22-420c-857a-a3c786e4b600

oxz_external_dns_baa049eb-7bdd-4d8e-ace3-c0ef054845bd

oxz_crucible_2ffc1456-b0e1-4f8e-8b6d-6ab01377bd25

oxz_crucible_c5f7d004-9d9b-4741-9246-c4793b9c7976

oxz_nexus_c1a31871-bdb5-422c-b5b1-2de62aa31e30

That command logs the user straight into the g0 sled in the global zone. Alternatively:

$ pfexec ./target/debug/voxel tp login switch0

[voxel] ssh root@192.168.68.167 -> zlogin oxz_switch (g0 switch0)

[Connected to zone 'oxz_switch' pts/3]

Last login: Sun Sep 6 22:17:52 from fdb0:a840:2500:

The illumos Project helios-3.0.23976 May 2026

root@oxz_switch:~#

tp

in this case acts like our technician ports,

and allows the user to connect straight to a switch zone for rack-wide commands and networking.

There are also numerous commands to manage things like images (export/import, delete), config

(network topology, sled sizing, multi-rack, etc.), and more features coming as we build.

← 一覧に戻る