Skip to content

Commit defd823

Browse files
ashraffoudaclaude
andcommitted
feat(deployment): move a zmachine + zmount + rootfs between nodes (transfer/prepare/start)
Adds the node-side building blocks to relocate a Grade-1 deployment to another node while preserving zmount data and rootfs changes, over caller-provided presigned S3 URLs (no credentials on nodes): - storage: S3 streaming primitives (DiskUpload/DiskDownload, VolumeUpload/VolumeDownload) on the storage module + zbus stub, with a shared helper (pkg/storagetransfer) that streams seekable bodies with a correct Content-Length (S3 rejects chunked presigned PUTs). Unit tested against an httptest server. - provision engine: `Prepare` (provision network + zmount + rootfs, VM left unbooted) and `Start` (boot the prepared VM) ops, added to the Engine + pkg.Provision interfaces and the zbus stub; contract validation still applies to Prepare. - RMB API (zos_api + zos_api_light): `zos.deployment.transfer` (pause source, upload zmount(s)+rootfs to presigned PUTs) and `zos.deployment.prepare` (provision + pull bytes from presigned GETs, optionally start); routes registered. - client SDK: DeploymentTransfer / DeploymentPrepare. - docs/deployment-move-testing.md: two-node dev runbook; scripts/migration: standalone operator CLI used to exercise the flow (binary gitignored). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ebc5a36 commit defd823

21 files changed

Lines changed: 2536 additions & 0 deletions

File tree

‎.gitignore‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,8 @@ qemu/*.efi
2020

2121
# c object
2222
*.o
23+
24+
# session + migration dev cruft
25+
.claude/
26+
scripts/migration.bak/
27+
scripts/migration/zosmigration

‎client/node.go‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,56 @@ func (n *NodeClient) DeploymentDelete(ctx context.Context, contractID uint64) er
107107
return n.bus.Call(ctx, n.nodeTwin, cmd, in, nil)
108108
}
109109

110+
// TransferItem maps a workload (a zmount or a zmachine, by name) to a presigned
111+
// S3 URL used to move its bytes between nodes.
112+
type TransferItem struct {
113+
WorkloadName gridtypes.Name `json:"workload_name"`
114+
URL string `json:"url"`
115+
// Size is the volume size to (re)create on the target for a rootfs volume
116+
// download; ignored for uploads and for zmount downloads.
117+
Size gridtypes.Unit `json:"size,omitempty"`
118+
}
119+
120+
// DeploymentTransfer asks the (old) node to upload the persistent bytes of the
121+
// given contract's workloads (zmount disks and/or the VM rootfs writable layer)
122+
// to the provided presigned S3 URLs. The node pauses the deployment first for a
123+
// consistent copy.
124+
func (n *NodeClient) DeploymentTransfer(ctx context.Context, contractID uint64, uploads []TransferItem) error {
125+
const cmd = "zos.deployment.transfer"
126+
in := args{
127+
"contract_id": contractID,
128+
"uploads": uploads,
129+
}
130+
131+
return n.bus.Call(ctx, n.nodeTwin, cmd, in, nil)
132+
}
133+
134+
// DeploymentPrepare asks the (new) node to stage the deployment without starting
135+
// the zmachine (provision network + zmount(s)) and to pull each listed workload's
136+
// bytes from the provided presigned S3 URLs into the new zmount disk / rootfs
137+
// volume. The VM is started by a follow-up call once data is in place.
138+
func (n *NodeClient) DeploymentPrepare(ctx context.Context, dl gridtypes.Deployment, downloads []TransferItem, start bool) error {
139+
const cmd = "zos.deployment.prepare"
140+
in := args{
141+
"deployment": dl,
142+
"downloads": downloads,
143+
"start": start,
144+
}
145+
146+
return n.bus.Call(ctx, n.nodeTwin, cmd, in, nil)
147+
}
148+
149+
// DeploymentStart boots the zmachine(s) of a deployment previously staged on the
150+
// (new) node via DeploymentPrepare, completing a contract move.
151+
func (n *NodeClient) DeploymentStart(ctx context.Context, contractID uint64) error {
152+
const cmd = "zos.deployment.start"
153+
in := args{
154+
"contract_id": contractID,
155+
}
156+
157+
return n.bus.Call(ctx, n.nodeTwin, cmd, in, nil)
158+
}
159+
110160
// Counters (statistics) of the node
111161
type Counters struct {
112162
// Total system capacity

‎docs/deployment-move-testing.md‎

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# Testing a deployment move between two nodes (zmount + rootfs)
2+
3+
This is a dev runbook for the `zos.deployment.transfer` / `prepare` / `start` feature
4+
(branch `feat/deployment-transfer`). It moves a zmachine (mycelium network + zmount)
5+
from **node A** to **node B**, preserving zmount data *and* rootfs changes, via S3.
6+
7+
## The flow at a glance
8+
9+
```
10+
node A S3 (MinIO) node B
11+
│ transfer (PUT) ──────────▶ zmount.raw │
12+
│ transfer (PUT) ──────────▶ rootfs.tar │
13+
│ │ prepare (GET) ◀────────┤ creates net+zmount+rootfs, pulls data
14+
│ └───────────────────────▶ │ (VM NOT booted yet)
15+
│ start ─────────┤ boots the VM on B
16+
▼ cancel old contract ▼ VM running on B
17+
```
18+
19+
`transfer` pauses the source VM first, so the copy is consistent.
20+
21+
## Prerequisites
22+
23+
1. **Two zos-light nodes** A and B on the same farm/network, both reachable over RMB,
24+
both running a node image **built from this branch** (see "Build the node image").
25+
2. **A twin + mnemonic** you control (the deployment owner), KYC-verified on the target
26+
chain env — `prepare` runs the same validation as `deploy` (twin match, signature,
27+
contract hash). Dev/QA net is fine.
28+
3. **A MinIO (or S3) bucket** reachable from both nodes, plus the `mc` client to mint
29+
presigned URLs.
30+
4. **A running source deployment on A**: a mycelium network + a zmount + a zmachine that
31+
mounts the zmount. Write a marker file into both the rootfs and the zmount so you can
32+
prove they survived the move.
33+
34+
## Build the node image
35+
36+
zoslight is already wired to this branch via a local replace (added during implementation):
37+
38+
```
39+
# in the zoslight repo
40+
grep 'zos_base =>' go.mod
41+
# replace github.com/threefoldtech/zos_base => /home/afouda/code/github/threefoldtech/zosbase
42+
go build ./... # sanity: compiles against the branch
43+
```
44+
45+
Build/flash the node image the way you normally do for zoslight (the replace makes the
46+
image include these RMB endpoints). For a throwaway test you can also run the affected
47+
daemons (api_gateway, provisiond, storaged) from source on a dev node.
48+
49+
> Before pushing the branch, drop the local `replace` and pin a real `zos_base`
50+
> pseudo-version instead — the path replace only works on your machine.
51+
52+
## Mint presigned URLs (MinIO example)
53+
54+
```bash
55+
mc alias set m http://MINIO:9000 ACCESS SECRET
56+
mc mb m/zos-move
57+
58+
# uploads (used by node A on transfer) — PUT
59+
ZMOUNT_PUT=$(mc share upload --expire 12h m/zos-move/zmount.raw | awk '/share:/{print $2}')
60+
ROOTFS_PUT=$(mc share upload --expire 12h m/zos-move/rootfs.tar | awk '/share:/{print $2}')
61+
# downloads (used by node B on prepare) — GET
62+
ZMOUNT_GET=$(mc share download --expire 12h m/zos-move/zmount.raw | awk '/share:/{print $2}')
63+
ROOTFS_GET=$(mc share download --expire 12h m/zos-move/rootfs.tar | awk '/share:/{print $2}')
64+
```
65+
66+
The node streams a definite Content-Length, so plain presigned PUT/GET work (no chunked).
67+
68+
## Driver program (uses the client SDK added on this branch)
69+
70+
`client.NodeClient` gained `DeploymentTransfer`, `DeploymentPrepare`, `DeploymentStart`.
71+
Workload names below (`"data"`, `"vm"`) are the `Name` fields in *your* deployment.
72+
73+
```go
74+
package main
75+
76+
import (
77+
"context"
78+
"time"
79+
80+
"github.com/threefoldtech/zos_base/client"
81+
"github.com/threefoldtech/zos_base/pkg/gridtypes"
82+
"github.com/threefoldtech/zos_sdk_go/rmb-sdk-go"
83+
)
84+
85+
func main() {
86+
cl, err := rmb.Default() // or a peer-backed rmb.Client for your env
87+
if err != nil { panic(err) }
88+
89+
const (
90+
twin = 1234 // your twin id
91+
nodeATwin = 1111 // node A twin id
92+
nodeBTwin = 2222 // node B twin id
93+
contractA = 100 // running contract on A
94+
)
95+
96+
nodeA := client.NewNodeClient(nodeATwin, cl)
97+
nodeB := client.NewNodeClient(nodeBTwin, cl)
98+
ctx := context.Background()
99+
100+
// 0) grab the running deployment from A (you'll reuse it for prepare on B)
101+
dl, err := nodeA.DeploymentGet(ctx, contractA)
102+
if err != nil { panic(err) }
103+
104+
// 1) A: pause + upload zmount + rootfs to S3
105+
must(nodeA.DeploymentTransfer(ctx, contractA, []client.TransferItem{
106+
{WorkloadName: "data", URL: ZMOUNT_PUT}, // zmount
107+
{WorkloadName: "vm", URL: ROOTFS_PUT}, // zmachine rootfs
108+
}))
109+
110+
// 2) stage a contract on B with the SAME deployment hash (see note below),
111+
// set dl.ContractID = <new B contract>, dl.Version = 0.
112+
dl.ContractID = 200
113+
dl.Version = 0
114+
115+
// 3) B: prepare (provision net+zmount, skip VM) and pull data back
116+
must(nodeB.DeploymentPrepare(ctx, dl, []client.TransferItem{
117+
{WorkloadName: "data", URL: ZMOUNT_GET},
118+
{WorkloadName: "vm", URL: ROOTFS_GET, Size: rootfsSize}, // rootfs volume size
119+
}))
120+
121+
// 4) B: boot the VM
122+
time.Sleep(2 * time.Second)
123+
must(nodeB.DeploymentStart(ctx, dl.ContractID))
124+
125+
// 5) A: cancel the old contract on chain to decommission the source
126+
}
127+
128+
func must(err error) { if err != nil { panic(err) } }
129+
```
130+
131+
### The on-chain contract for B (the manual bit)
132+
133+
`prepare` validates against a node contract for **B** whose `DeploymentHash` matches the
134+
deployment. In production the grid/tfchain orchestration creates it; for a manual test,
135+
create a node contract on B for the same hash (`dl.ChallengeHash()`) using your usual tool
136+
(tfchain client / `tfcmd` / grid client), then use that contract id as `dl.ContractID`
137+
in step 2. Same-contract migration (updating the existing contract's node id on chain) is
138+
a later refinement — for now use a fresh contract on B.
139+
140+
## Verify
141+
142+
- `nodeB.DeploymentGet(ctx, 200)` → zmount + network `StateOk`, zmachine `StateOk` after start.
143+
- On node B host: the zmount file exists at `/mnt/<pool>/vdisks/<twin>-200-data` and the
144+
rootfs volume at `/mnt/<pool>/rootfs:<twin>-200-vm/rw`.
145+
- Console into the VM (mycelium IP is derived from the deployment seed, so it's the same as
146+
on A) and confirm your **marker files** in both the rootfs and the mounted zmount.
147+
148+
## Cleanup
149+
150+
Cancel the source contract on A (normal decommission path); the node tears down the paused
151+
deployment. Delete the S3 objects.
152+
153+
## Known limitations (this phase)
154+
155+
- `transfer` pauses vCPUs but does not `fsync` the guest — tiny consistency risk; for a
156+
clean test, quiesce the app inside the VM before transferring.
157+
- The rootfs volume quota on B is set at `prepare` time from the `Size` you pass; make it
158+
match the VM's rootfs size.
159+
- Same-contract (in-place node reassignment) orchestration is not implemented — use a new
160+
contract on B as above.
161+
```

‎pkg/provision.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ type Provision interface {
1515
// GetWorkloadStatus: returns status, bool(true if workload exits otherwise it is false), error
1616
GetWorkloadStatus(id string) (gridtypes.ResultState, bool, error)
1717
CreateOrUpdate(twin uint32, deployment gridtypes.Deployment, update bool) error
18+
// PrepareDeployment stages a deployment (provisions all but the zmachine) for a contract move
19+
PrepareDeployment(twin uint32, deployment gridtypes.Deployment) error
20+
// PauseDeployment pauses a deployment (used to freeze a source VM before transferring its data)
21+
PauseDeployment(twin uint32, id uint64) error
22+
// StartDeployment boots the zmachine(s) of a deployment previously staged via PrepareDeployment
23+
StartDeployment(twin uint32, id uint64) error
1824
Get(twin uint32, contractID uint64) (gridtypes.Deployment, error)
1925
List(twin uint32) ([]gridtypes.Deployment, error)
2026
Changes(twin uint32, contractID uint64) ([]gridtypes.Workload, error)

0 commit comments

Comments
 (0)