Skip to content
16 changes: 16 additions & 0 deletions docs/fixtures/manifest/cloud-init.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"api_version": 1,
"id": "01J00000000000000000000004",
"desired": {
"metadata": { "name": "cloud-init", "labels": {}, "annotations": {} },
"compute": { "vcpus": 2, "memory_bytes": 2147483648 },
"storage": [],
"networks": [],
"placement": {},
"boot": { "start": true },
"cloud_init": {
"user_data": "#cloud-config\nusers:\n - name: odorobo\n",
"meta_data": "instance-id: vm-04\nlocal-hostname: cloud-init\n"
}
}
}
12 changes: 12 additions & 0 deletions docs/fixtures/manifest/minimal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"api_version": 1,
"id": "01J00000000000000000000000",
"desired": {
"metadata": { "name": "minimal", "labels": {}, "annotations": {} },
"compute": { "vcpus": 1, "memory_bytes": 536870912 },
"storage": [],
"networks": [],
"placement": {},
"boot": { "start": false }
}
}
14 changes: 14 additions & 0 deletions docs/fixtures/manifest/networked.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"api_version": 1,
"id": "01J00000000000000000000003",
"desired": {
"metadata": { "name": "networked", "labels": {}, "annotations": {} },
"compute": { "vcpus": 2, "memory_bytes": 2147483648 },
"storage": [],
"networks": [
{ "id": "net://private", "mac_address": "02:00:00:00:00:01" }
],
"placement": { "node": "compute-a" },
"boot": { "start": false }
}
}
15 changes: 15 additions & 0 deletions docs/fixtures/manifest/storage-backed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"api_version": 1,
"id": "01J00000000000000000000001",
"desired": {
"metadata": { "name": "storage-backed", "labels": {}, "annotations": {} },
"compute": { "vcpus": 2, "max_vcpus": 4, "memory_bytes": 4294967296 },
"storage": [
{ "id": "root", "uri": "rbd://vms/root", "boot": true },
{ "id": "data", "volume_id": "01J00000000000000000000002" }
],
"networks": [],
"placement": {},
"boot": { "start": true }
}
}
13 changes: 13 additions & 0 deletions docs/fixtures/manifest/vsock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"api_version": 1,
"id": "01J00000000000000000000005",
"desired": {
"metadata": { "name": "vsock", "labels": {}, "annotations": {} },
"compute": { "vcpus": 4, "memory_bytes": 4294967296 },
"storage": [],
"networks": [],
"placement": {},
"boot": { "start": true },
"vsock": { "guest_cid": 42, "socket": "/run/odorobo/vms/vsock/vsock.sock" }
}
}
95 changes: 95 additions & 0 deletions docs/manifest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Odorobo VM manifest contract

The Odorobo VM manifest is the provider-neutral description of VM intent. It is
not a Cloud Hypervisor `VmConfig`; the Cloud Hypervisor driver owns conversion,
node-local paths, and runtime details. The current contract is version `1` and
is represented by `odorobo::manifest::VmManifest`.

## Existing field inventory

The legacy `VirtualMachine` model in `odorobo/src/types.rs` currently combines
intent and runtime data: `VMData` contains identity, name, vCPU limits, memory,
an image, volumes, and network IDs, while `VirtualMachine` adds node, status,
metadata, and affinity. The manifest separates those concerns so the control
plane can provide stable intent without depending on the legacy shape.

The current Cloud Hypervisor conversion in `odorobo/src/ch_driver/actor.rs`
consumes vCPUs, maximum vCPUs, memory, and the image as a disk. Firmware and
serial/platform defaults are currently driver-owned. Network, volume-to-disk,
cloud-init, and vsock conversion remain provider integration work; their
manifest fields are defined here so those later conversions have a stable
contract and explicit ownership boundary.

## State ownership

`desired` is supplied by the control plane and is the source of truth for what
Odorobo should provision. It contains:

- `metadata`: stable name, labels, and annotations.
- `compute`: boot vCPUs, optional scaling ceiling, and memory in bytes.
- `storage`: ordered storage attachments. An attachment references either a storage URI or a
provisioned volume ID; `boot` identifies the boot attachment.
- `networks`: stable network IDs and optional guest MAC addresses.
- `placement`: scheduling hints, including an optional node, required node labels,
and affinity rules. Affinity rules support required or weighted-preferred
VM/agent normal or anti-affinity, with OR-ed label/annotation requirements.
- `boot`: whether to start after provisioning and optional firmware/kernel/
command-line intent.
- `cloud_init`: paired NoCloud user-data and meta-data.
- `vsock`: guest CID and the desired host-side socket location.

`observed` is reported by Odorobo and is never used as desired input. It records
status, the node currently running the VM, the provider's runtime state, and an
error message when applicable. Cloud Hypervisor configuration and generated
paths are observed/driver-owned implementation details, not manifest fields.

## Validation and evolution

A manifest must use a supported `api_version`, have a non-empty metadata name,
non-zero vCPUs and memory, and satisfy these relationships:

- `max_vcpus` must be at least `vcpus`.
- Every storage attachment must have exactly one usable source (URI or volume reference), and
a boot storage attachment cannot be read-only. At most one storage attachment may be marked as boot.
- Affinity requirements within a rule are OR-ed; rules are combined according to
their strictness and direction.
- Every network must have a non-empty, non-whitespace ID.
- Cloud-init must provide non-empty configuration with user-data and meta-data
supplied together.
- A vsock guest CID must be non-zero and its socket must be an absolute path.

Invalid field combinations are rejected during deserialization, as are unknown
fields, rather than silently interpreted. New fields should be added in a future manifest version when they
change semantics; unreleased formats do not require Proxmox compatibility
layers. Providers may reject a valid manifest field they cannot implement, with
a clear unsupported-field error, rather than dropping it. This contract is
therefore intentionally forward-evolving, not a compatibility layer for
Proxmox or unreleased Odorobo formats.

## Examples

Representative JSON fixtures are in [`fixtures/manifest`](fixtures/manifest):

- [`minimal.json`](fixtures/manifest/minimal.json)
- [`storage-backed.json`](fixtures/manifest/storage-backed.json)
- [`networked.json`](fixtures/manifest/networked.json)
- [`cloud-init.json`](fixtures/manifest/cloud-init.json)
- [`vsock.json`](fixtures/manifest/vsock.json)

For example:

```json
{
"api_version": 1,
"id": "01J00000000000000000000005",
"desired": {
"metadata": { "name": "vm", "labels": {}, "annotations": {} },
"compute": { "vcpus": 2, "memory_bytes": 2147483648 },
"storage": [],
"networks": [],
"placement": {},
"boot": { "start": true },
"vsock": { "guest_cid": 42, "socket": "/run/odorobo/vms/vm/vsock.sock" }
}
}
```
1 change: 1 addition & 0 deletions odorobo/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod manifest;
pub mod types;
Loading