Skip to content

Commit 362e6a2

Browse files
define manifest contract (#108)
* define manifest contract * make this a bit more strict * fix disk ID and URI stuff * validation for multiple boot disks * reject empty objects and names * fix issues * split some functions, update wording, reject empty payloads * harden manifest contract schema and validation * add more tests * address manifest review feedback
1 parent 5012f3f commit 362e6a2

8 files changed

Lines changed: 938 additions & 0 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"api_version": 1,
3+
"id": "01J00000000000000000000004",
4+
"desired": {
5+
"metadata": { "name": "cloud-init", "labels": {}, "annotations": {} },
6+
"compute": { "vcpus": 2, "memory_bytes": 2147483648 },
7+
"storage": [],
8+
"networks": [],
9+
"placement": {},
10+
"boot": { "start": true },
11+
"cloud_init": {
12+
"user_data": "#cloud-config\nusers:\n - name: odorobo\n",
13+
"meta_data": "instance-id: vm-04\nlocal-hostname: cloud-init\n"
14+
}
15+
}
16+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"api_version": 1,
3+
"id": "01J00000000000000000000000",
4+
"desired": {
5+
"metadata": { "name": "minimal", "labels": {}, "annotations": {} },
6+
"compute": { "vcpus": 1, "memory_bytes": 536870912 },
7+
"storage": [],
8+
"networks": [],
9+
"placement": {},
10+
"boot": { "start": false }
11+
}
12+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"api_version": 1,
3+
"id": "01J00000000000000000000003",
4+
"desired": {
5+
"metadata": { "name": "networked", "labels": {}, "annotations": {} },
6+
"compute": { "vcpus": 2, "memory_bytes": 2147483648 },
7+
"storage": [],
8+
"networks": [
9+
{ "id": "net://private", "mac_address": "02:00:00:00:00:01" }
10+
],
11+
"placement": { "node": "compute-a" },
12+
"boot": { "start": false }
13+
}
14+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"api_version": 1,
3+
"id": "01J00000000000000000000001",
4+
"desired": {
5+
"metadata": { "name": "storage-backed", "labels": {}, "annotations": {} },
6+
"compute": { "vcpus": 2, "max_vcpus": 4, "memory_bytes": 4294967296 },
7+
"storage": [
8+
{ "id": "root", "uri": "rbd://vms/root", "boot": true },
9+
{ "id": "data", "volume_id": "01J00000000000000000000002" }
10+
],
11+
"networks": [],
12+
"placement": {},
13+
"boot": { "start": true }
14+
}
15+
}

docs/fixtures/manifest/vsock.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"api_version": 1,
3+
"id": "01J00000000000000000000005",
4+
"desired": {
5+
"metadata": { "name": "vsock", "labels": {}, "annotations": {} },
6+
"compute": { "vcpus": 4, "memory_bytes": 4294967296 },
7+
"storage": [],
8+
"networks": [],
9+
"placement": {},
10+
"boot": { "start": true },
11+
"vsock": { "guest_cid": 42, "socket": "/run/odorobo/vms/vsock/vsock.sock" }
12+
}
13+
}

docs/manifest.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Odorobo VM manifest contract
2+
3+
The Odorobo VM manifest is the provider-neutral description of VM intent. It is
4+
not a Cloud Hypervisor `VmConfig`; the Cloud Hypervisor driver owns conversion,
5+
node-local paths, and runtime details. The current contract is version `1` and
6+
is represented by `odorobo::manifest::VmManifest`.
7+
8+
## Existing field inventory
9+
10+
The legacy `VirtualMachine` model in `odorobo/src/types.rs` currently combines
11+
intent and runtime data: `VMData` contains identity, name, vCPU limits, memory,
12+
an image, volumes, and network IDs, while `VirtualMachine` adds node, status,
13+
metadata, and affinity. The manifest separates those concerns so the control
14+
plane can provide stable intent without depending on the legacy shape.
15+
16+
The current Cloud Hypervisor conversion in `odorobo/src/ch_driver/actor.rs`
17+
consumes vCPUs, maximum vCPUs, memory, and the image as a disk. Firmware and
18+
serial/platform defaults are currently driver-owned. Network, volume-to-disk,
19+
cloud-init, and vsock conversion remain provider integration work; their
20+
manifest fields are defined here so those later conversions have a stable
21+
contract and explicit ownership boundary.
22+
23+
## State ownership
24+
25+
`desired` is supplied by the control plane and is the source of truth for what
26+
Odorobo should provision. It contains:
27+
28+
- `metadata`: stable name, labels, and annotations.
29+
- `compute`: boot vCPUs, optional scaling ceiling, and memory in bytes.
30+
- `storage`: ordered storage attachments. An attachment references either a storage URI or a
31+
provisioned volume ID; `boot` identifies the boot attachment. The control plane owns
32+
attachment identity and ordering; the provider driver/storage transform resolves the
33+
URI or volume ID to a node-local device path.
34+
- `networks`: stable network IDs and optional guest MAC addresses. The control plane owns
35+
the attachment identity and requested MAC; the provider networking transform resolves
36+
the network to a host interface or tap device.
37+
- `placement`: scheduling hints, including an optional node, required node labels,
38+
and affinity rules. Affinity rules support required or weighted-preferred
39+
VM/agent matching; `inverse` selects anti-affinity, with OR-ed
40+
label/annotation requirements.
41+
- `boot`: whether to start after provisioning and optional firmware/kernel/
42+
command-line intent.
43+
- `cloud_init`: paired NoCloud user-data and meta-data.
44+
- `vsock`: guest CID and the desired host-side socket location.
45+
46+
`observed` is reported by Odorobo and is never used as desired input. It records
47+
status, the node currently running the VM, the provider's runtime state, and an
48+
error message when applicable. Cloud Hypervisor configuration and generated
49+
paths are observed/driver-owned implementation details, not manifest fields.
50+
51+
Providers may reject a valid manifest field when they cannot implement it, but
52+
must report that explicitly. They must not silently discard storage, network,
53+
boot, cloud-init, or vsock intent.
54+
55+
## Validation and evolution
56+
57+
A manifest must use a supported `api_version`, have a non-empty metadata name,
58+
non-zero vCPUs and memory, and satisfy these relationships:
59+
60+
- `max_vcpus` must be at least `vcpus`.
61+
- Every storage attachment must have a non-empty ID and exactly one usable source (URI or volume reference), and
62+
a boot storage attachment cannot be read-only. At most one storage attachment may be marked as boot.
63+
- Affinity requirements within a rule are OR-ed; rules are combined according to
64+
their strictness, and `inverse` negates a rule's result. `lt` and `gt` comparisons require exactly one
65+
finite numeric value.
66+
- Every network must have a non-empty, non-whitespace ID.
67+
- Cloud-init must provide non-empty configuration with user-data and meta-data
68+
supplied together.
69+
- A vsock guest CID must be non-zero and its socket must be an absolute path.
70+
71+
Invalid field combinations are rejected during deserialization, as are unknown
72+
fields, rather than silently interpreted. New fields should be added in a future manifest version when they
73+
change semantics; unreleased formats do not require Proxmox compatibility
74+
layers. Providers may reject a valid manifest field they cannot implement, with
75+
a clear unsupported-field error, rather than dropping it. This contract is
76+
therefore intentionally forward-evolving, not a compatibility layer for
77+
Proxmox or unreleased Odorobo formats.
78+
79+
## Examples
80+
81+
Representative JSON fixtures are in [`fixtures/manifest`](fixtures/manifest):
82+
83+
- [`minimal.json`](fixtures/manifest/minimal.json)
84+
- [`storage-backed.json`](fixtures/manifest/storage-backed.json)
85+
- [`networked.json`](fixtures/manifest/networked.json)
86+
- [`cloud-init.json`](fixtures/manifest/cloud-init.json)
87+
- [`vsock.json`](fixtures/manifest/vsock.json)
88+
89+
For example:
90+
91+
```json
92+
{
93+
"api_version": 1,
94+
"id": "01J00000000000000000000005",
95+
"desired": {
96+
"metadata": { "name": "vm", "labels": {}, "annotations": {} },
97+
"compute": { "vcpus": 2, "memory_bytes": 2147483648 },
98+
"storage": [],
99+
"networks": [],
100+
"placement": {},
101+
"boot": { "start": true },
102+
"vsock": { "guest_cid": 42, "socket": "/run/odorobo/vms/vm/vsock.sock" }
103+
}
104+
}
105+
```

odorobo/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
pub mod manifest;
12
pub mod types;

0 commit comments

Comments
 (0)