feat: implement resource resizing for VMs with storage management
- Added KnownHostsManager to handle cleanup of SSH known_hosts entries upon VM deletion. - Introduced disk_size_bytes method in ImageStore to read logical disk capacity. - Enhanced LifecycleService to manage VM resource resizing, including CPU, RAM, and storage. - Updated application configuration to support known_hosts_files for SSH management. - Modified dashboard to include resource resizing options in the UI. - Improved error handling and validation for resource inputs in the dashboard. - Removed obsolete .pyc files from the repository.
This commit is contained in:
+64
-14
@@ -12,7 +12,7 @@ general-purpose Python library or a multi-host control plane.
|
||||
|
||||
UVM installs host/guest assets, provisions private disks, and manages named
|
||||
VMs and private IPv4 TCP allowlists on one host. The CLI offers retained-disk
|
||||
start/stop/restart, deletion, fixed-target bulk operations, inspection, SSH,
|
||||
start/stop/restart, CPU/RAM/storage resizing, deletion, fixed-target bulk operations, inspection, SSH,
|
||||
logs, doctor, and explicit port edits/sync. The optional API and local dashboard
|
||||
expose health, installation, VM lifecycle controls, and managed port-policy
|
||||
controls, host diagnostics, bounded VM-log tails, and fixed-target bulk
|
||||
@@ -47,7 +47,7 @@ Requirements for real host operations:
|
||||
- Linux with KVM enabled and `/dev/kvm` accessible
|
||||
- Linux >= 5.3 with native bridge conntrack and safe pidfd support
|
||||
- Root privileges for installation and lifecycle mutations
|
||||
- `iproute2`, `iptables`, `nftables`, `conntrack`, `e2fsprogs`, `util-linux`,
|
||||
- `iproute2`, `iptables`, `nftables`, `conntrack`, `coreutils`, `e2fsprogs`, `util-linux`,
|
||||
`openssl`, and OpenSSH client tools
|
||||
- An x86_64 or ARM64 (`aarch64`) host, including NVIDIA DGX Spark
|
||||
|
||||
@@ -140,7 +140,7 @@ services. It owns confirmation, terminal/JSON output, exit handling, SSH
|
||||
`execvp`, local log streaming from a validated path, and `--serve` dispatch.
|
||||
It must not coordinate state files, interfaces, or Firecracker sockets itself.
|
||||
|
||||
The local surface is install/create/list/inspect/start/stop/restart/delete
|
||||
The local surface is install/create/list/inspect/start/stop/restart/resize/delete
|
||||
(`destroy` alias)/ssh/logs/doctor and ports list/open/close/set/sync. Identifiers
|
||||
resolve by exact ID, name, or guest IP, never an ID prefix. Generated-ID-shaped
|
||||
inputs resolve only by ID; names cannot impersonate generated IDs. Ambiguous
|
||||
@@ -156,7 +156,7 @@ Bulk lifecycle actions and ports sync resolve/deduplicate IDs before prompting.
|
||||
Delete and mutating `--all` selections require a TTY confirmation or `--yes`/`-y`.
|
||||
Port set/close-all and removal of TCP/22 also prompt, except empty managed
|
||||
no-ops. `--dry-run` previews without lifecycle/policy mutations or confirmation;
|
||||
it is not a full preflight. Stop/restart/delete accept `--timeout`; start does
|
||||
it is not a full preflight. Stop/restart/resize/delete accept `--timeout`; start does
|
||||
not. Logs follow does not hold the operation lock. Doctor runs readiness
|
||||
checks, not per-VM repair or guest application probes.
|
||||
|
||||
@@ -230,11 +230,11 @@ state file.
|
||||
### State Schema
|
||||
|
||||
[`domain.py`](uvm/domain.py) is the schema authority. The default registry is
|
||||
`/var/lib/uvm/state.json`; its current schema version is **2**. An empty registry:
|
||||
`/var/lib/uvm/state.json`; its current schema version is **3**. An empty registry:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": 2,
|
||||
"schema_version": 3,
|
||||
"next_mac_index": 1,
|
||||
"vms": {}
|
||||
}
|
||||
@@ -245,7 +245,8 @@ record. Do not make API models or CLI namespaces the persistence format.
|
||||
|
||||
| Record Fields | Purpose |
|
||||
|---|---|
|
||||
| `id`, `name`, `cpu`, `ram_mib` | Stable identity, optional unique name, resource request |
|
||||
| `id`, `name`, `cpu`, `ram_mib`, `storage_mib` | Stable identity, name, applied resources; storage is null for older disks |
|
||||
| `pending_resize` | Null or a validated `VmResources` target with CPU, RAM MiB, and storage MiB |
|
||||
| `guest_ip`, `gateway`, `network`, `bridge`, `tap`, `mac` | Retained network identity/configuration |
|
||||
| `socket`, `config`, `log`, `disk` | Canonical per-VM runtime paths |
|
||||
| `username`, `password` | Guest credentials; password stays out of public output |
|
||||
@@ -271,8 +272,8 @@ mode. There is no persisted boolean asserting that a policy is enforced.
|
||||
- `operations.lock` serializes long lifecycle, bulk, policy, and installation operations.
|
||||
- State reads are lock-free because atomic replacement gives readers either a
|
||||
complete old file or a complete new file.
|
||||
- Legacy versions 0 (including unversioned state) and 1 are loaded into schema
|
||||
2 in memory and written as schema 2 on the next transaction. Newer schemas
|
||||
- Legacy versions 0 (including unversioned state), 1, and 2 are loaded into schema
|
||||
3 in memory and written as schema 3 on the next transaction. Newer schemas
|
||||
are rejected. Reads do not reconcile VMs or adopt process identities.
|
||||
- Missing `name`, `ports`, and `boot_id` remain null. No policy or default SSH
|
||||
rule is silently imposed on a legacy VM. Use stop, explicit `ports set`,
|
||||
@@ -285,6 +286,10 @@ mode. There is no persisted boolean asserting that a policy is enforced.
|
||||
explicit `runtime_owned=False` on a new interrupted creation stays authoritative.
|
||||
- Records created before credential provisioning default to username `root`
|
||||
and password `null`; UVM does not apply a password retroactively.
|
||||
- Missing `storage_mib` and `pending_resize` remain null. The first explicit
|
||||
resize infers an older disk's capacity and enforces the 30 GiB minimum; reads
|
||||
and schema migration never resize a disk. Schema 3 prevents older UVM versions
|
||||
from ignoring pending resource changes during a boot.
|
||||
- `next_mac_index` is reconstructed from older locally administered MACs when
|
||||
absent.
|
||||
|
||||
@@ -326,8 +331,9 @@ a ruleset/attachment snapshot, not application health or a reachability test.
|
||||
1. Validate the request/root access and acquire `operations.lock`.
|
||||
2. Check KVM, verified Firecracker, firewall capability, and installed templates.
|
||||
3. Reserve ID/name/IP/MAC/ports with `status=starting`, `operation=preparing`.
|
||||
4. Create the owned mode-0700 runtime directory, copy a mode-0600 private disk,
|
||||
provision credentials/host keys, and persist `disk_ready=True`.
|
||||
4. Create the owned mode-0700 runtime directory, sparse-copy a mode-0600 private
|
||||
disk, resize ext4 to the requested storage capacity, provision credentials/host
|
||||
keys, and persist `disk_ready=True`.
|
||||
5. Ensure bridge/egress networking and prepare an owned, attached TAP **down**.
|
||||
6. Apply/read back the nftables bridge policy, then activate the TAP.
|
||||
7. Write Firecracker config; persist `operation=spawning`, `spawn_pending=True`,
|
||||
@@ -370,6 +376,35 @@ successful start. Failed resume retains the private disk for retry. Restart
|
||||
performs stop and start under one operation lock, not two independent public
|
||||
calls with a race between them.
|
||||
|
||||
### Resource Resizing
|
||||
|
||||
`resize()` accepts a partial CPU/RAM/storage update for one VM. New disks default
|
||||
to 30720 MiB (30 GiB), also the minimum accepted storage. Storage parsing uses
|
||||
GiB for bare numbers, binary B/K/M/G/T suffixes, and rounds up to MiB; RAM retains
|
||||
its MiB-default parsing. Capacities must fit the host's signed 64-bit file offsets.
|
||||
|
||||
One `operations.lock` spans preflight, durable `pending_resize` publication,
|
||||
`_stop_locked()`, offline resource application, and `_start_locked()`. Stop must
|
||||
prove process exit before any disk work. The complete target is a validated
|
||||
`VmResources`, while the record's CPU/RAM/storage fields remain the applied values.
|
||||
Only the resource fields change; identity, networking, credentials, ports, and
|
||||
guest SSH keys are retained. Even an initially stopped VM boots after resize.
|
||||
|
||||
`ImageStore.resize_vm_disk()` sparse-copies the private disk to a mode-0600 sibling,
|
||||
runs `e2fsck -f -p`, resizes ext4, verifies its geometry using `dumpe2fs`, fsyncs,
|
||||
then atomically replaces the original and fsyncs the directory. Growth extends
|
||||
the image before `resize2fs`; shrinking truncates it only after ext4 fits and its
|
||||
geometry matches. Failed staging leaves the original disk intact. CPU/RAM-only
|
||||
resizes avoid disk work when its configured capacity is already known.
|
||||
|
||||
After disk publication, one state write applies all resource fields and clears
|
||||
`pending_resize`; boot then follows the normal start path. A crash between disk
|
||||
publication and that state write is recoverable: the target is durable and the
|
||||
image layer recognizes an already-completed capacity. A stopped VM's `start`
|
||||
retries a pending resize before boot; a live pending resize requires `resize`
|
||||
or `stop` first. Stop retains the target and deletion never applies it. A failed
|
||||
boot after successful resizing retains the new resources for retry.
|
||||
|
||||
### Stop and Delete
|
||||
|
||||
```text
|
||||
@@ -470,6 +505,9 @@ make recovery convenient. Proven-gone legacy processes can be cleaned up.
|
||||
the VM runtime directory before UVM modifies the requested existing account's
|
||||
shadow entry and OpenSSH password policy with `debugfs`. Firecracker only sees
|
||||
the resulting private copy. Never change that to a shared writable rootfs.
|
||||
Copies use coreutils `cp --reflink=auto --sparse=always`; the new disk is resized
|
||||
to the requested capacity before credential provisioning. Existing disk resizes
|
||||
do not reprovision credentials or host keys.
|
||||
|
||||
Guest images must provide `/etc/passwd`, `/etc/shadow`, OpenSSH's
|
||||
`/etc/ssh/sshd_config`, and the requested account. UVM does not create missing
|
||||
@@ -498,7 +536,7 @@ writes a manifest marked `verified: false`; a later strict run will not treat
|
||||
that manifest as publisher trust. Use `install --force` with trusted digests to
|
||||
refresh an existing artifact without manually deleting managed files.
|
||||
|
||||
Retain the explicit opt-out on create/start/restart (or the API server) when
|
||||
Retain the explicit opt-out on create/start/restart/resize (or the API server) when
|
||||
using an unverified development installation. It is not appropriate for
|
||||
untrusted workloads. A bare strict install requires the trusted digests.
|
||||
|
||||
@@ -675,11 +713,12 @@ or secret manager when that matters.
|
||||
| `GET` | `/diagnostics` | `diagnostics.py` | Host readiness checks |
|
||||
| `POST` | `/install` | `installation.py` | `{ "force": false }` body |
|
||||
| `GET` | `/vms` | `vms.py` | List persisted VM records and observed status |
|
||||
| `POST` | `/vms` | `vms.py` | `cpu`, `ram`, `guest_ip`, `username`, `password`, `name`, `ports`, `no_default_ports` |
|
||||
| `POST` | `/vms` | `vms.py` | `cpu`, `ram`, `storage`, `guest_ip`, `username`, `password`, `name`, `ports`, `no_default_ports` |
|
||||
| `POST` | `/vms/actions` | `vms.py` | Fixed-ID snapshot bulk lifecycle action |
|
||||
| `GET` | `/vms/{identifier}` | `vms.py` | Inspect by exact ID, name, or guest IP |
|
||||
| `POST` | `/vms/{identifier}/start` | `vms.py` | Boot a retained private disk |
|
||||
| `POST` | `/vms/{identifier}/restart` | `vms.py` | Restart; optional `timeout` query parameter |
|
||||
| `POST` | `/vms/{identifier}/resize` | `vms.py` | Partial `cpu`/`ram`/`storage` update; stop, resize, boot; optional `timeout` |
|
||||
| `POST` | `/vms/{identifier}/stop` | `vms.py` | Stop; optional `timeout` query parameter |
|
||||
| `DELETE` | `/vms/{identifier}` | `vms.py` | Remove runtime files/state; optional `timeout` query parameter |
|
||||
| `POST` | `/vms/{vm_id}/ports` | `vms.py` | Exact ID only; body has `mode` and `ports` |
|
||||
@@ -701,12 +740,17 @@ default). The router normalizes/deduplicates ports and adds TCP/22 unless
|
||||
default ports are disabled, even for null/empty input. It never creates an
|
||||
unmanaged VM from an omitted policy. Names share CLI validation.
|
||||
|
||||
Create accepts `storage` (default `"30G"`). Resize requires at least one non-null
|
||||
`cpu`, `ram`, or `storage`; identity/credential fields are rejected by its schema.
|
||||
Responses expose applied `storage_mib` and a nullable, resource-only
|
||||
`pending_resize` object so interrupted requests are visible without secrets.
|
||||
|
||||
VM responses include `name`, `ports`, `policy_status`, and `operation` alongside
|
||||
resource/status fields and username. The schema excludes password and
|
||||
disk/socket/config/log path fields; error messages may still contain paths.
|
||||
`ports: null` in a legacy response still means unmanaged. Start/restart/stop
|
||||
and port-policy mutations report the resulting policy status. Restart, stop,
|
||||
and delete accept an optional finite non-negative `timeout` query parameter;
|
||||
resize, and delete accept an optional finite non-negative `timeout` query parameter;
|
||||
there is no dry-run HTTP schema. Port mutation bodies use
|
||||
`{"mode":"set"|"open"|"close", "ports":[...]}` and always resolve the
|
||||
target by exact ID to prevent name/IP reuse. A legacy policy must be set while
|
||||
@@ -809,6 +853,12 @@ final command/option so parsing is checked without executing any mutation.
|
||||
These checks supplement fixture-based readback and cleanup tests; they do not
|
||||
exercise the kernel packet path.
|
||||
|
||||
`test_storage.py` also exercises real e2fsprogs against temporary unmounted,
|
||||
sparse ext4 images when the tools are installed: default 30 GiB creation,
|
||||
growth, shrinking, geometry, and retained data/password-hash/host-key fixtures.
|
||||
Lifecycle resize tests use fake processes/networking to verify ordering,
|
||||
single-lock ownership, identity preservation, pending intent, and failure recovery.
|
||||
|
||||
### Isolated Packet Tests
|
||||
|
||||
The opt-in packet harness must run **only in disposable isolated user/network
|
||||
|
||||
Reference in New Issue
Block a user