diff --git a/DEVELOPER.md b/DEVELOPER.md index c7d6364..1b4dfa0 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -1,11 +1,12 @@ -# UVM Developer Guide +# Nest Developer Guide -This document is the developer handoff for the entire UVM codebase. Read it +This document is the developer handoff for Nest and its UVM backend. Read it before changing lifecycle behavior, host networking, Firecracker launch semantics, persistent state, or the HTTP API. -`uvm` is a local, privileged Firecracker microVM manager. It is an -application with a CLI and optional FastAPI management server. It is not a +Nest owns the workspace dashboard and its server entry point. Its `uvm` +component is a local, privileged Firecracker microVM manager with a CLI and +standalone FastAPI management API. These are application components, not a general-purpose Python library or a multi-host control plane. ## 1. Goals and Boundaries @@ -13,10 +14,10 @@ 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, CPU/RAM/storage resizing, deletion, fixed-target bulk operations, inspection, SSH, -logs, doctor, and explicit port edits/sync. The optional API and local dashboard +logs, doctor, and explicit port edits/sync. The UVM API and Nest dashboard expose health, installation, VM lifecycle controls, and managed port-policy controls, host diagnostics, bounded VM-log tails, and fixed-target bulk -operations. Interactive SSH remains CLI-only. +operations, and bounded guest command execution over vsock. Interactive SSH remains CLI-only. There is no autostart or background reconciler, multi-host control plane, tenancy/quotas, or UVM-provided Jailer/cgroup/namespace hardening. Private port @@ -30,16 +31,18 @@ flag reference, see [USAGE.md](USAGE.md). ## 2. First Run -All UVM-specific files live in this directory. Start here: +All Nest and UVM files live in this project. Start here: ```sh python3 -m pip install -e '.[test]' python3 -B -m unittest discover -s tests -v +./nest.py --help ./uvm.py --help ``` -The `uvm` console script is registered by `pyproject.toml`. During source -development, `./uvm.py` and `python3 -B -m uvm` are equivalent entry points. +The `nest` and `uvm` console scripts are registered by `pyproject.toml`. +During source development, `./nest.py` and `python3 -B -m nest` are equivalent +Nest entry points; `./uvm.py` and `python3 -B -m uvm` launch UVM. Requirements for real host operations: @@ -63,7 +66,15 @@ Packet integration tests must remain inside disposable isolated namespaces. ├── README.md # Project overview ├── USAGE.md # Operator command/deployment guide ├── pyproject.toml # Package metadata and dependencies -├── uvm.py # Executable source-tree launcher +├── nest.py # Executable Nest source-tree launcher +├── nest/ +│ ├── __main__.py # `python -m nest` +│ ├── cli.py # Nest workspace serve command +│ ├── server.py # Nest composition of the dashboard and UVM API +│ └── dashboard/ +│ ├── router.py # Nest dashboard document route +│ └── static/ # Packaged HTML, CSS, JavaScript, and fonts +├── uvm.py # Executable UVM source-tree launcher ├── uvm/ │ ├── __main__.py # `python -m uvm` │ ├── cli.py # argparse, CLI rendering, serve dispatch @@ -78,16 +89,17 @@ Packet integration tests must remain inside disposable isolated namespaces. │ ├── install.py # Apt, Firecracker, kernel, rootfs installation │ ├── images.py # Template lookup, private disks, and guest credentials │ ├── known_hosts.py # Deletion-only cleanup of local SSH host-key caches +│ ├── guest_agent.py # Standalone guest daemon, job scopes, and bounded wire protocol +│ ├── guest_exec.py # Host-side guest sessions and execution results │ ├── network.py # IP/MAC/TAP/bridge/NAT/forward rules │ ├── firewall.py # Owned nft bridge tables and offline conntrack cleanup │ ├── lifecycle.py # Lifecycle, bulk operations, recovery, port intent │ ├── api_models.py # Pydantic HTTP request/response models │ ├── server.py # FastAPI factory and Uvicorn launcher -│ ├── dashboard/ # Packaged local dashboard HTML, CSS, and JS │ ├── routers/ -│ │ ├── dashboard.py # Dashboard document route │ │ ├── diagnostics.py # Host diagnostic endpoint │ │ ├── dependencies.py # API application/token dependencies +│ │ ├── exit.py # Authenticated server shutdown │ │ ├── health.py # Liveness endpoint │ │ ├── installation.py # Installation endpoint │ │ └── vms.py # VM lifecycle endpoints @@ -106,8 +118,10 @@ are intentionally small and operationally focused. ```mermaid flowchart LR - CLI[CLI: uvm] --> APP[app.py composition root] - HTTP[FastAPI server] --> APP + DASH[Nest dashboard] --> NEST[nest/server.py workspace] + NEST --> HTTP[UVM management API] + CLI[CLI: uvm] --> APP[uvm/app.py composition root] + HTTP --> APP APP --> INSTALL[Installer] APP --> LIFE[LifecycleService] APP --> STATE[StateStore] @@ -125,8 +139,9 @@ flowchart LR FC --> KVM[/dev/kvm] ``` -The public entry points are thin adapters. The lifecycle layer is the only -place that coordinates multiple host resources for a VM. Lower-level modules +The public entry points are thin adapters. Nest composes the UVM API and adds +the workspace dashboard; UVM does not import Nest. The lifecycle layer is the +only place that coordinates multiple host resources for a VM. Lower-level modules must not import `cli.py`, `server.py`, or `lifecycle.py`. Leaf modules are `config.py`, `domain.py`, `errors.py`, and `validation.py`. @@ -163,6 +178,11 @@ checks, not per-VM repair or guest application probes. ### HTTP Path +`nest/server.py` owns the workspace application. Its `create_app()` composes +`uvm.server.create_api()` with the dashboard document and static assets from +`nest/dashboard/`. The same-origin API paths and `X-UVM-Token` authentication +are shared with the standalone UVM API. + Routers validate the token and Pydantic request, invoke application services, and render whitelisted response models. Lifecycle, managed port-policy, diagnostics, and bounded log-tail operations have explicit HTTP routes for the @@ -171,9 +191,11 @@ SSH has no HTTP endpoint. The public dashboard shell does not bypass token validation: it stores a supplied token only for the active browser session and sends it with management requests. -Use only `uvm --serve` or `./uvm.py --serve` to launch the API. The -`create_api()` factory requires an explicit host specifically to prevent it -from being used as an insecure generic Uvicorn factory. +Launch the workspace with `nest --serve` or `./nest.py --serve`. The standalone +backend uses `uvm --serve` or `./uvm.py --serve`. Both `create_app()` and +`create_api()` require an explicit host so validation covers the intended bind. +Nest passes its factory to the shared UVM server runner, which enforces token +and TLS requirements and wires graceful server shutdown. ## 6. Application Composition @@ -218,7 +240,8 @@ Default runtime storage is `/var/lib/uvm`: │ ├── rootfs.ext4 │ ├── config.json │ ├── firecracker.log -│ └── firecracker.sock +│ ├── firecracker.sock +│ └── vsock.sock ├── state.json ├── state.lock ├── operations.lock @@ -498,8 +521,9 @@ The `firecracker/` package has three separate responsibilities. 2. `PUT /boot-source` 3. `PUT /drives/rootfs` 4. `PUT /network-interfaces/eth0` -5. `PUT /entropy` with `{}` (a host-backed virtio RNG device) -6. `PUT /actions` with `InstanceStart` +5. `PUT /vsock` with a guest CID and a per-VM Unix socket path +6. `PUT /entropy` with `{}` (a host-backed virtio RNG device) +7. `PUT /actions` with `InstanceStart` Every generated config includes `entropy`, including retained-disk boots. An empty object enables the device with default settings; do not skip it with @@ -587,7 +611,7 @@ fail locally with `Network is unreachable`. `ImageStore.configure_guest_network()` fills in the assigned address/prefix and gateway during creation and stopped-disk resume. It only specializes the exact -shipped profile from recognized UVM Jammy v1/v2/v3 images. Missing, customized, +shipped profile from recognized UVM Jammy v1/v2/v3/v4 images. Missing, customized, symlinked, hard-linked, or unexpectedly owned/mode-set profiles are left intact. An already-specialized profile is a no-op. Custom images retain responsibility for their own guest networking. @@ -597,7 +621,8 @@ For migration, the image layer sparse-copies the private disk and runs committed guest edits cannot be overwritten based on stale offline reads. The updated profile is read back and the filesystem checked before fsync and atomic disk replacement. Failed staging keeps the original disk available for -retry. The built template's recipe marker is v3; cached older templates are +retry. The built template's recipe marker is v4 (v3 introduced the route fix); +cached older templates are also handled by per-VM provisioning without requiring an installation refresh. ### Integrity Model @@ -837,8 +862,9 @@ Python's safe tar extraction filter. ### Server Startup See [USAGE.md](USAGE.md) for dependency installation, loopback deployment, -TLS deployment, and authenticated curl examples. Launch through `uvm --serve` -using the Python environment with the installed API dependencies. Every server +TLS deployment, and authenticated curl examples. Launch the Nest workspace +through `nest --serve`, or the standalone backend through `uvm --serve`, using +the Python environment with the installed API dependencies. Every server requires `UVM_API_TOKEN`, even at `127.0.0.1`. Non-loopback binds additionally require `UVM_API_TLS_CERT` and `UVM_API_TLS_KEY`; paired certificate/key files are loaded into an `ssl.SSLContext` before Uvicorn starts. @@ -851,10 +877,13 @@ or secret manager when that matters. ### API Contract +The root redirect and dashboard document are Nest routes. Both server entry +points expose the remaining management routes below. + | Method | Route | Router | Notes | |---|---|---|---| -| `GET` | `/` | `server.py` | Redirect to `/dashboard` | -| `GET` | `/dashboard` | `dashboard.py` | Public static dashboard shell | +| `GET` | `/` | `nest/server.py` | Redirect to `/dashboard` (Nest only) | +| `GET` | `/dashboard` | `nest/dashboard/router.py` | Public Nest dashboard shell (Nest only) | | `GET` | `/health` | `health.py` | Unauthenticated liveness response | | `POST` | `/exit` | `exit.py` | Request graceful Uvicorn shutdown | | `GET` | `/diagnostics` | `diagnostics.py` | Host readiness checks | @@ -870,6 +899,8 @@ or secret manager when that matters. | `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` | | `POST` | `/vms/{vm_id}/ports/sync` | `vms.py` | Exact ID only; reapply managed policy | +| `POST` | `/vms/{vm_id}/exec` | `vms.py` | Exact ID; `command`, strict `sudo`, bounded `timeout_seconds` | +| `POST` | `/vms/{vm_id}/agent/install` | `vms.py` | Exact ID; install/upgrade the agent on a stopped disk | | `GET` | `/vms/{identifier}/logs` | `vms.py` | Bounded non-following log tail | All management routes use `get_authorized_application()` and require: @@ -878,14 +909,14 @@ All management routes use `get_authorized_application()` and require: X-UVM-Token: ``` -`POST /exit` is a management route. `run_server()` creates the Uvicorn server, -stores it on the FastAPI application state, and runs it in-process. The route -returns `202` before a background task sets `should_exit`, allowing Uvicorn to -deliver the response and perform its normal graceful shutdown. It does not -stop or delete any managed VM. +`POST /exit` is a management route. The shared `uvm.server.run_server()` creates +the Uvicorn server, stores it on the FastAPI application state, and runs it +in-process. The route returns `202` before a background task sets `should_exit`, +allowing Uvicorn to deliver the response and perform its normal graceful +shutdown for Nest or UVM. It does not stop or delete any managed VM. Request schemas reject unexpected fields. `UvmError` instances are rendered as -JSON errors by `server.py`; Pydantic request validation uses FastAPI's normal +JSON errors by `uvm/server.py`; Pydantic request validation uses FastAPI's normal 422 response format. Create accepts `name: str | null`, `ports: list | null`, and strict boolean `no_default_ports` (default false). Port objects contain a strict integer `port` in 1-65535 and uppercase `protocol` TCP/HTTP/HTTPS (TCP @@ -916,10 +947,62 @@ marks a response truncated when an oversized line or that bound prevents a full tail. Bulk bodies use an action plus either explicit identifiers or `all_vms`; the router resolves them before `run_many()` executes the fixed IDs under one operation lock. The API does not expose interactive SSH, raw Firecracker calls, -arbitrary command execution, or host path access. Public `/`, `/dashboard`, +arbitrary host command execution, or host path access. Public `/`, `/dashboard`, `/docs`, `/redoc`, and `/openapi.json` do not perform privileged management requests. +### Guest Execution + +`LifecycleService.execute()` resolves an exact registry ID, validates canonical +paths and an observed running VMM with no pending operation, and connects under +`operations.lock`. `GuestSession` checks the Unix peer PID against the selected +VMM, sends Firecracker's `CONNECT 1070\n`, consumes its `OK` acknowledgement, and +checks the agent's protocol version, VM ID, and configured username. The lock is +released before submission/waiting; the established socket pins the original +VMM and is never reconnected across a concurrent restart or address reuse. + +`guest_agent.py` is both a host-importable protocol module and a standalone +stdlib-only Python 3.10+ guest daemon. Keep it free of imports from the rest of +UVM. Version 1 uses length-prefixed JSON with bounds before allocation, absolute +socket deadlines, a 64 KiB UTF-8 command limit, and 1 MiB combined raw output. +The synchronous HTTP response exposes only exit code, stdout/stderr, timeout, +and truncation. Captured text uses UTF-8 replacement. Nonzero process exits are +HTTP 200 results; agent failures are 502, and failures before submission are 503. +Lost/malformed responses after submission carry `outcome_unknown: true`. There +is no command retry, durable job registry, streaming response, or reconnection. + +The root systemd agent accepts only host-CID connections and limits concurrent +connections/jobs to eight. Each request creates a delegated cgroup v2 job scope. +A fresh child interpreter joins the scope before initializing the chosen user's +supplementary groups, GID, UID, home, and environment, then execs `/bin/sh -c`. +`sudo` selects guest root directly; otherwise the recorded username is used. +Password authentication is not involved, and a recorded `root` login remains +root for both flag values. Input is closed and there is no PTY. + +Both output pipes are drained concurrently. The guest enforces the deadline, +signals the process group, escalates via `cgroup.kill`, and confirms cleanup. +The leader is observed with `waitid(WNOWAIT)` until signaling finishes, preventing +PGID reuse before reap. Normal exits clean up remaining background descendants +too. An unconfirmed cleanup is an unknown outcome. Stopping the agent service +also kills its job scopes; dropping an HTTP connection does not cancel a job. + +The v4 Ubuntu builder installs Python and the service. Creation provisions the +VM ID/username in root-only `/etc/uvm-agent.json` before marking the disk ready. +Older templates are left for an explicit stopped-disk `agent/install` request. +Provisioning and upgrades stage a sparse copy, replay/check ext4, validate guest +path ownership and recorded agent-file hashes, enable the service, verify writes, +and publish atomically. A root-only guest manifest records managed file hashes +so later upgrades preserve guest edits. An older guest missing Python installs +it through apt inside the guest on its next boot; no guest binaries execute on +the host. This bootstrap requires working guest egress until the runtime is ready. + +The guest needs systemd, Python 3.10+, built-in virtio-vsock, and cgroup v2 with +`cgroup.kill` (Linux 5.14+). Host socket paths are derived from Settings, so the +registry schema stays at 3. Each Firecracker instance has an independent vsock +device and uses CID 3. The process adapter retains both sockets while identity +is uncertain and removes them only after confirmed VMM exit. IPv4 port policies +do not govern this host-only management channel. + ## 14. Configuration Reference The environment-variable table is in [README.md](README.md#configuration); @@ -944,6 +1027,8 @@ Expected failures inherit from `UvmError`: | `ConflictError` | Ambiguous identifier, allocation conflict, or unsafe/pending operation | | `CommandError` | Required host command failure | | `FirecrackerError` | VMM process or Unix API failure | +| `GuestAgentUnavailableError` | Agent connection/readiness failure before submission (HTTP 503) | +| `GuestExecutionError` | Execution failure or unknown outcome after submission (HTTP 502) | | `TapCreationError` | TAP setup failed after interface creation | CLI code catches `UvmError` and prints `uvm: error: ...` without a traceback. @@ -996,11 +1081,13 @@ Ordinary tests require no root, KVM, Firecracker, or host network mutations. They cover validation, schema migration/locking/permissions, artifact trust, private disks/credentials, network/firewall rendering and readback, conntrack semantics, pidfd/legacy identity, lifecycle recovery/resume/bulk targeting, -CLI parsing/output, and server security. Mock/fixture policy checks do not -prove the kernel's actual packet behavior. +CLI parsing/output, Nest dashboard delivery, and server security. Mock/fixture +policy checks do not prove the kernel's actual packet behavior. FastAPI route tests are conditionally enabled when both `fastapi` and `httpx` are installed. They should be enabled in CI using the `test` extra. +`test_nest_server.py` checks the workspace launcher, relocated frontend assets, +authenticated machine controls, API documentation, and graceful shutdown. When `nft` and `conntrack` are installed, command-parser tests append an invalid final command/option so parsing is checked without executing any mutation. These checks supplement fixture-based readback and cleanup tests; they do not @@ -1024,6 +1111,15 @@ addresses/gateways, custom subnets, idempotent migration, journal-replay rechecks, and retention of guest data, credentials and keys on failures. Lifecycle tests require confirmed process exit before network-profile repair. +Guest execution tests use real child processes with fake cgroup scopes, local +Unix sockets emulating Firecracker's vsock handshake, API validation fixtures, +and real temporary ext4 upgrades. They cover credential selection, concurrent +output draining, timeout cleanup, connection loss, VM targeting/locking, and +upgrade rollback. `tests/integration/test_guest_exec.py` is an opt-in smoke test +against a prepared disposable VM with a non-root login and all TCP ports closed; +see [USAGE.md](USAGE.md#18-verification). Unit tests do not validate the real +guest vsock device or cgroup kernel path. + ### Isolated Packet Tests The opt-in packet harness must run **only in disposable isolated user/network @@ -1075,8 +1171,10 @@ on an isolated Linux host: `get_authorized_application()`, and register new routers in `create_api()`. Add API tests and update all route tables. New CLI commands do not implicitly authorize new HTTP endpoints; `/health` remains the only unauthenticated - operational API route, while `/` and `/dashboard` only serve the public - dashboard shell. + operational API route. Nest owns the public `/` and `/dashboard` routes. +- Dashboard: keep the document route and frontend assets in `nest/dashboard/`. + Compose workspace-level routes in `nest/server.py`; machine-management + operations continue through the UVM API and its application services. - Host primitives: use the specific network/firewall/install/process module and `CommandRunner` for external commands. Define ownership, idempotence, and compensation before invoking a primitive from lifecycle code. Use fake @@ -1137,7 +1235,7 @@ Before submitting a change: 1. Run the complete unit suite. 2. Run `git diff --check`. -3. Confirm a new feature stays inside the `uvm/` project directory. +3. Keep workspace/dashboard features in `nest/` and machine-management features in `uvm/`. 4. Verify command/API errors use `UvmError` rather than raw tracebacks. 5. Verify host mutations have a lock owner and cleanup path. 6. Verify persistent state remains readable across upgrades. diff --git a/README.md b/README.md index a0a8b92..ce6e31c 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,17 @@ -# uvm +# Nest -`uvm` is a small, local Firecracker microVM command-line manager for Ubuntu. -It creates one Firecracker process per VM, attaches it to a host TAP device, +Nest is a local infrastructure workspace with a browser dashboard for your +machines, network, and host health. Launch it with `nest --serve`. + +Its `uvm` component is a small, local Firecracker microVM manager for Ubuntu. +UVM creates one Firecracker process per VM, attaches it to a host TAP device, and keeps a private disk and local registry for named VMs. The CLI supports create, start, stop, restart, resize, delete, inspection, SSH, logs, diagnostics, host-shared IPv4 internet access, and enforced private-IPv4 TCP port allowlists. -This directory is a self-contained project. All UVM code, tests, packaging -metadata, launcher scripts, and documentation live here. +This directory is a self-contained project. Nest owns the dashboard and +workspace server; UVM provides the machine-management CLI and backend services. +All code, tests, packaging metadata, launchers, and documentation live here. For architecture, lifecycle invariants, extension guidance, and contributor workflows, see [DEVELOPER.md](DEVELOPER.md). @@ -24,11 +28,13 @@ feature contract; these guides describe the implementation in the workspace. Run commands from this project root: ```sh +./nest.py --help +python3 -B -m nest --help ./uvm.py --help python3 -B -m uvm --help ``` -The `pyproject.toml` file also defines an installable `uvm` console command. +The `pyproject.toml` file defines installable `nest` and `uvm` console commands. The project is an application, not a reusable Python SDK. ## Requirements @@ -271,7 +277,7 @@ DNS publication, reverse proxies, or guest-service TLS. UDP and IPv6 service pol yet supported. Guest-initiated IPv4 TCP/UDP egress, its replies, and required ARP/ICMP traffic remain allowed; managed TAPs block IPv6. -## Management API +## Nest Dashboard and Management API Install the project dependencies before serving the API: @@ -279,28 +285,33 @@ Install the project dependencies before serving the API: python3 -m pip install . ``` -Launch the FastAPI management server using the environment where the project +Launch the Nest workspace server using the environment where the project dependencies were installed (use the full console-script path if needed): ```sh export UVM_API_TOKEN='replace-with-a-long-random-secret' sudo env UVM_API_TOKEN="$UVM_API_TOKEN" \ - uvm --serve --host 127.0.0.1 --port 8000 + nest --serve --host 127.0.0.1 --port 8000 # Equivalent when running the source checkout directly. sudo env UVM_API_TOKEN="$UVM_API_TOKEN" \ - ./uvm.py --serve --host 127.0.0.1 --port 8000 + ./nest.py --serve --host 127.0.0.1 --port 8000 ``` The default bind address is `127.0.0.1` and the default port is `8000`. OpenAPI documentation is available at `/docs` once the server is running. The -root URL redirects to the local dashboard at `/dashboard`; enter the API token +root URL redirects to the Nest dashboard at `/dashboard`; enter the API token there for the current browser session. -The dashboard includes a fleet overview, searchable machine collection, private -network and port-policy view, host diagnostics, and machine provisioning. Open a -machine to manage its lifecycle, edit or sync its port rules, copy its SSH command, -or read recent logs. Select machines in the collection for bulk operations. +Nest serves the dashboard and UVM-backed management endpoints together, using +the existing `UVM_API_TOKEN` and `UVM_*` host configuration. The standalone UVM +API is available through `uvm --serve`, with its reference at `/docs`; the `/` +and `/dashboard` routes belong to the Nest server. + +The Nest dashboard includes a fleet overview, searchable machine collection, +private network and port-policy view, host diagnostics, and machine provisioning. +Open a machine to manage its lifecycle, edit or sync its port rules, copy its SSH +command, or read recent logs. Select machines in the collection for bulk operations. Use `Ctrl+K` / `⌘K` (or `/`) to find a machine or page. Fleet data refreshes every five seconds while the tab is visible; host checks refresh every thirty seconds and can also be run manually. Charts show observed machine states and allocated @@ -311,10 +322,10 @@ The server exposes these routes: | Method | Route | Purpose | |---|---|---| -| `GET` | `/` | Redirect to the local dashboard | -| `GET` | `/dashboard` | Serve the local management dashboard | +| `GET` | `/` | Redirect to the dashboard (Nest server) | +| `GET` | `/dashboard` | Serve the Nest workspace dashboard (Nest server) | | `GET` | `/health` | Unauthenticated liveness response | -| `POST` | `/exit` | Gracefully stop the UVM API server | +| `POST` | `/exit` | Gracefully stop the management server | | `GET` | `/diagnostics` | Run host readiness checks for the dashboard/system view | | `POST` | `/install` | Install or refresh host assets; body: `{ "force": false }` | | `GET` | `/vms` | List VM records and observed status | @@ -328,6 +339,8 @@ The server exposes these routes: | `DELETE` | `/vms/{identifier}` | Delete a VM and release its resources; optional `timeout` query parameter | | `POST` | `/vms/{vm_id}/ports` | Set, open, or close private port rules for an exact VM ID | | `POST` | `/vms/{vm_id}/ports/sync` | Reapply a managed private port policy for an exact VM ID | +| `POST` | `/vms/{vm_id}/exec` | Execute a command/script inside a running VM over vsock, with optional root privileges | +| `POST` | `/vms/{vm_id}/agent/install` | Install/upgrade the execution agent on a stopped VM's private disk | | `GET` | `/vms/{identifier}/logs` | Read a bounded, non-following tail of a VM log | API port rules are objects such as `{"port": 80, "protocol": "HTTP"}`; @@ -346,6 +359,39 @@ Bulk requests use an action (`start`, `stop`, `restart`, `delete`, or for each fixed target and do not prompt for confirmation. Interactive SSH remains CLI-only. The API and dashboard are not background reconcilers. +### Execute Commands Without SSH + +Use an exact VM ID from create/list: + +```sh +curl -X POST \ + -H "X-UVM-Token: $UVM_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"command":"whoami","sudo":true,"timeout_seconds":60}' \ + "http://127.0.0.1:8000/vms//exec" +``` + +The response contains `exit_code`, `stdout`, `stderr`, `timed_out`, and +`truncated`. Commands and multiline scripts run through `/bin/sh` inside the +guest. `sudo: false` (the default) uses the VM's configured username; `true` +uses guest root without a password prompt. A VM configured with username `root` +still runs as root when `sudo` is false. + +Execution uses a guest systemd agent over Firecracker vsock. It works with SSH +disabled and with an empty TCP allowlist. Rebuild the default Ubuntu template +with `uvm install --force` using your configured checksums to include the agent +and Python in new VMs. Restart the management server to load these routes. + +For an existing VM, call **stop → `POST /vms/{vm_id}/agent/install` → start**. +Installation stages and verifies a private disk copy, preserving guest data and +credentials. If Python is missing, the installed bootstrap service acquires it +inside the guest on the next boot; this one-time step needs guest internet access. +The guest needs Python 3.10+, systemd with cgroup v2, and a vsock-capable Linux +5.14+ kernel; UVM's default Linux 6.1 kernel provides the required drivers. + +See [execution examples and limits](USAGE.md#execute-commands-without-ssh) for +programmatic calls, local script submission, timeout behavior, and upgrade steps. + ### API Security The server controls privileged host operations. `UVM_API_TOKEN` is required @@ -365,7 +411,7 @@ sudo env \ UVM_API_TOKEN="$UVM_API_TOKEN" \ UVM_API_TLS_CERT="$UVM_API_TLS_CERT" \ UVM_API_TLS_KEY="$UVM_API_TLS_KEY" \ - ./uvm.py --serve --host 0.0.0.0 --port 8000 + ./nest.py --serve --host 0.0.0.0 --port 8000 ``` Send the token on management requests: @@ -385,8 +431,8 @@ curl -X POST \ ``` The endpoint returns `202 {"status":"stopping"}` and then gracefully terminates -the UVM API process. It is a protected management endpoint; it does not stop or -delete managed VMs. +the serving Nest or UVM process. It is a protected management endpoint; it does +not stop or delete managed VMs. Do not expose this server directly to an untrusted network. For remote access, keep TLS enabled and consider placing it behind an additional authenticated @@ -570,7 +616,7 @@ Host and API settings can be overridden through environment variables: | `UVM_ROOTFS_SHA256` | Trusted prebuilt rootfs SHA-256; use with `UVM_ROOTFS_URL` | | `UVM_FIRECRACKER_BINARY_SHA256` | Optional trusted local Firecracker binary SHA-256 | | `UVM_ALLOW_UNVERIFIED_DOWNLOADS` | Set to `1` only for unverified local development | -| `UVM_API_TOKEN` | Required for every API server; sent as `X-UVM-Token` | +| `UVM_API_TOKEN` | Required for Nest and standalone UVM servers; sent as `X-UVM-Token` | | `UVM_API_TLS_CERT` | TLS certificate path required for a non-loopback API bind | | `UVM_API_TLS_KEY` | TLS private-key path required for a non-loopback API bind | @@ -584,14 +630,18 @@ Selected files: ├── USAGE.md ├── DEVELOPER.md ├── pyproject.toml -├── uvm.py # Executable compatibility launcher -├── uvm/ # Private application implementation +├── nest.py # Executable Nest workspace launcher +├── nest/ # Nest workspace application +│ ├── cli.py # nest --serve entry point +│ ├── server.py # Workspace server, composing the UVM management API +│ └── dashboard/ # Nest document route and local HTML, CSS, JS, and fonts +├── uvm.py # Executable UVM compatibility launcher +├── uvm/ # Machine-management implementation │ ├── cli.py # argparse, terminal output, SSH execution │ ├── lifecycle.py # Lifecycle, bulk operations, recovery, port-policy intent │ ├── network.py # IP/MAC/TAP/bridge/NAT management │ ├── firewall.py # Owned nftables bridge policies and offline conntrack cleanup -│ ├── dashboard/ # Local dashboard HTML, CSS, and JavaScript -│ ├── routers/ # FastAPI routes, including the dashboard document +│ ├── routers/ # UVM management API routes │ └── firecracker/ # Firecracker config, API, and process adapters └── tests/ # Unit tests and opt-in isolated namespace packet tests ``` @@ -639,7 +689,7 @@ guest connectivity on an isolated Linux host. - No quotas or cgroup CPU, memory, I/O, or process limits are applied. - Firecracker is launched directly by this local CLI. Jailer/cgroup hardening is not implemented by the current command set. -- No VM autostart, background reconciliation, guest readiness probing, +- No VM autostart, background reconciliation, guest application health probing, snapshots, DHCP, public-port publishing, or multi-host scheduling. - The API uses one shared token, not users, roles, or tenants. UVM is not a hardened multi-tenant platform. diff --git a/USAGE.md b/USAGE.md index 27a7508..cefc9d5 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1,8 +1,8 @@ -# UVM Usage Guide +# Nest and UVM Usage Guide -This guide explains how to install, operate, and troubleshoot UVM as an -operator. It covers the command-line tool, the optional FastAPI server, common -scenarios, and expected limitations. +This guide explains how to install, operate, and troubleshoot Nest and its UVM +machine-management backend. It covers the Nest dashboard, UVM command-line +tool, FastAPI server, common scenarios, and expected limitations. For the project architecture and contributor guidance, see [DEVELOPER.md](DEVELOPER.md). For a concise overview, see [README.md](README.md). @@ -68,20 +68,23 @@ lscpu | grep -i virtualization From this project root: ```sh +./nest.py --help ./uvm.py --help python3 -B -m uvm --help ``` -To install the optional HTTP API dependencies and the console command: +To install the HTTP server dependencies and console commands: ```sh python3 -m pip install . +nest --help uvm --help ``` The rest of this guide uses `./uvm.py` so it works from a source checkout. If -you installed the package, replace `./uvm.py` with `uvm`. For the API, use the -Python environment containing the installed dependencies; under `sudo`, use +you installed the package, replace `./uvm.py` with `uvm` and `./nest.py` with +`nest`. For the server, use the Python environment containing the installed +dependencies; under `sudo`, use the full path to that environment's console script when necessary. ### Privilege Rules @@ -107,6 +110,7 @@ Examples elsewhere are separate scenarios; substitute your VM identifiers and do not run every destructive example as a single script. ```text +nest --serve [--host HOST] [--port PORT] uvm install [--force] uvm create [--name NAME] [--cpu CPU] [--ram RAM] [--storage STORAGE] [--guest-ip GUEST_IP] [--username USER] [--password PASSWORD | --password-stdin] @@ -144,7 +148,8 @@ not disable confirmations, and errors may still be plain text on stderr. ### Stop the Management API The API can be stopped remotely through its authenticated shutdown endpoint. -This stops only the UVM API process; it does not stop or delete any VM: +This stops the serving Nest or standalone UVM process; it does not stop or +delete any VM: ```sh curl -X POST \ @@ -766,31 +771,38 @@ UVM will not blindly signal that PID or delete a possibly in-use disk. Listing does not perform adoption. Inspect such failures rather than manually inventing identity fields in `state.json`. -## 10. Use the FastAPI Server +## 10. Use the Nest Workspace and FastAPI Server The API uses the same local lifecycle services and exposes health, installation, VM lifecycle, diagnostics, bounded VM-log tails, and managed private-port -controls. The local dashboard at `/dashboard` uses those same endpoints. There -API also supports fixed-target bulk actions; interactive SSH remains CLI-only. +controls. Nest owns the local dashboard at `/dashboard` and serves it alongside +these UVM-backed endpoints. The API also supports fixed-target bulk actions; +interactive SSH remains CLI-only. The server does not autostart VMs or reconcile rules, and is not safe to expose directly to the public internet. ### Start a Local Server -After installing the project dependencies, start the server as root: +After installing the project dependencies, start the Nest workspace as root: ```sh export UVM_API_TOKEN='replace-with-a-long-random-visible-ascii-token' sudo env \ UVM_API_TOKEN="$UVM_API_TOKEN" \ - ./uvm.py --serve --host 127.0.0.1 --port 8000 + ./nest.py --serve --host 127.0.0.1 --port 8000 ``` Liveness is at `http://127.0.0.1:8000/health`; the dashboard is at `http://127.0.0.1:8000/dashboard`; OpenAPI docs are at `http://127.0.0.1:8000/docs`. The default bind is `127.0.0.1:8000`. +The installed command is `nest --serve`; `python3 -B -m nest --serve` also +works from the source checkout. Nest uses `UVM_API_TOKEN` and the `UVM_*` host +settings for its machine-management backend. To launch the standalone UVM API, +use `./uvm.py --serve` with the same host/port options and configuration. That +server exposes the management endpoints and `/docs`; Nest serves the dashboard. + The API token is required even for a loopback server. All VM, diagnostics, and installation routes require it. The dashboard shell, liveness route, and FastAPI's informational `/docs`, `/redoc`, and `/openapi.json` endpoints are available @@ -814,7 +826,7 @@ sudo env \ UVM_API_TOKEN="$UVM_API_TOKEN" \ UVM_API_TLS_CERT="$UVM_API_TLS_CERT" \ UVM_API_TLS_KEY="$UVM_API_TLS_KEY" \ - ./uvm.py --serve --host 0.0.0.0 --port 8443 + ./nest.py --serve --host 0.0.0.0 --port 8443 ``` For real remote use, use a certificate trusted by clients and restrict network @@ -845,8 +857,8 @@ curl --cacert /path/to/ca.pem \ | Method | Path | Operation | |---|---|---| -| `GET` | `/` | Redirect to `/dashboard` | -| `GET` | `/dashboard` | Serve the local management dashboard | +| `GET` | `/` | Redirect to `/dashboard` (Nest server) | +| `GET` | `/dashboard` | Serve the Nest workspace dashboard (Nest server) | | `GET` | `/health` | Unauthenticated liveness | | `GET` | `/diagnostics` | Run host readiness checks | | `POST` | `/install` | Install/refresh with a `force` boolean | @@ -861,6 +873,8 @@ curl --cacert /path/to/ca.pem \ | `DELETE` | `/vms/{identifier}` | Delete VM and private disk; accepts `timeout` query parameter | | `POST` | `/vms/{vm_id}/ports` | Set, open, or close private ports for an exact VM ID | | `POST` | `/vms/{vm_id}/ports/sync` | Reapply the policy for an exact VM ID | +| `POST` | `/vms/{vm_id}/exec` | Execute a guest command/script over vsock, with optional root privileges | +| `POST` | `/vms/{vm_id}/agent/install` | Install/upgrade the agent on a stopped VM's disk | | `GET` | `/vms/{identifier}/logs` | Read a bounded, non-following log tail | Individual lifecycle requests do not expose a CLI dry-run flag and do not prompt @@ -1034,6 +1048,137 @@ the other policy operations. `open`, `close`, and `sync` reject a legacy policy. Mutating a live policy is subject to the same containment and confirmation rules as the CLI. +### Execute Commands Without SSH + +`POST /vms/{vm_id}/exec` accepts an **exact VM ID** and a JSON body: + +| Field | Contract | +|---|---| +| `command` | Required, non-empty command or multiline script; at most 65,536 UTF-8 bytes; no NUL characters | +| `sudo` | Strict boolean, default `false`; select guest root instead of the VM's configured login user | +| `timeout_seconds` | Finite JSON number greater than 0 and at most 3600; default 60 | + +```sh +export VM_ID='' +curl --fail-with-body -X POST \ + -H "X-UVM-Token: $UVM_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"command":"whoami","sudo":true,"timeout_seconds":60}' \ + "$UVM_API_URL/vms/$VM_ID/exec" +``` + +Example response: + +```json +{"exit_code":0,"stdout":"root\n","stderr":"","timed_out":false,"truncated":false} +``` + +The agent runs `/bin/sh -c COMMAND` in the selected user's home directory, with +that user's UID and groups and a fresh `HOME`/`USER`/`PATH` environment. Stdin is +closed and no TTY is allocated. The entire script, including pipes and +redirections, uses the selected identity. For a Bash script already in the VM, +use a command such as `bash /opt/jobs/process.sh`. The flag selects an execution +identity directly; it does not invoke an interactive `sudo` password prompt. +For VMs whose configured username is `root`, both flag values run as root. + +Submit a local script's contents using `jq` to handle JSON quoting: + +```sh +jq -n --rawfile command ./script.sh \ + '{command:$command,sudo:false,timeout_seconds:300}' | + curl --fail-with-body -X POST \ + -H "X-UVM-Token: $UVM_API_TOKEN" \ + -H "Content-Type: application/json" \ + --data-binary @- "$UVM_API_URL/vms/$VM_ID/exec" +``` + +Or call it from Python using the standard library: + +```python +import json +import os +from urllib.request import Request, urlopen + +request = Request( + f"{os.environ['UVM_API_URL']}/vms/{os.environ['VM_ID']}/exec", + data=json.dumps({"command": "id && uname -a", "sudo": False}).encode(), + headers={"X-UVM-Token": os.environ["UVM_API_TOKEN"], + "Content-Type": "application/json"}, + method="POST", +) +with urlopen(request, timeout=75) as response: + result = json.load(response) +print(result["stdout"]) +print(result["exit_code"]) +``` + +**Behavior and limits:** + +- The VM must be running with a known process identity and no pending lifecycle + operation. Guest readiness is checked separately by connecting to its agent. +- The call waits for completion. Nonzero command exits still return HTTP 200; + inspect `exit_code`. Negative codes indicate a terminating signal. +- The deadline is enforced inside the guest. On timeout the agent terminates + the job, including descendants in its cgroup, and returns partial output with + `timed_out: true` and `exit_code: null`. Connection and cleanup time are additional + to `timeout_seconds`; give the HTTP client about 15 extra seconds. +- The agent captures up to **1 MiB combined stdout/stderr**, then drains and + discards excess data. `truncated` reports this limit; invalid UTF-8 is replaced. +- Up to eight connections/jobs per VM are admitted. Excess connections fail + before submission. Execution does not hold the global lifecycle lock while + waiting, so another request can stop or restart the VM. +- A normal script exit also cleans up background descendants in the job's + cgroup. Use a guest service manager for workloads that should outlive the call. +- A lost connection after submission returns HTTP 502 with + `error.outcome_unknown: true`. The command may already have produced effects; + it is never automatically resubmitted. An HTTP client disconnect does not + itself cancel the guest job; the guest deadline still applies. +- Results are returned in the response, not stored as durable jobs. Commands + and their captured output are not copied to the VM registry or host traces. + +#### Prepare New or Existing VMs + +The default Ubuntu template recipe v4 includes Python 3 and `uvm-agent.service`. +Rebuild it with `uvm install --force` using the checksum settings from the +installation section, then create new VMs. Cached older templates and custom +images require the explicit agent installation below. Restart a running Nest or +UVM API server to load the new code before making these requests. + +For an existing VM, run these requests in order, checking each succeeds: + +```sh +curl --fail-with-body -X POST -H "X-UVM-Token: $UVM_API_TOKEN" \ + "$UVM_API_URL/vms/$VM_ID/stop" +curl --fail-with-body -X POST -H "X-UVM-Token: $UVM_API_TOKEN" \ + "$UVM_API_URL/vms/$VM_ID/agent/install" +curl --fail-with-body -X POST -H "X-UVM-Token: $UVM_API_TOKEN" \ + "$UVM_API_URL/vms/$VM_ID/start" +``` + +Installation returns `{"id":"...","status":"installed","restart_required":true}`. +It requires a cleanly stopped, proven-offline private disk and does not start the +VM itself. Updates are checked on a sparse copy before publication. Guest files, +credentials, and SSH keys are retained; customized agent files are preserved +rather than overwritten. Repeating installation also upgrades unedited managed +agent files using their recorded checksums. + +The guest image must provide systemd and apt (Ubuntu 22.04 or newer is the +supported baseline). If Python 3.10+ is missing, a bootstrap service installs +`python3` inside the VM on its next boot. This one-time install needs guest DNS, +internet access, and working apt repositories. The service retries failed startup; +execution returns 503 until it can connect. New v4 templates already include +the runtime and do not need this download. + +The guest kernel needs built-in `CONFIG_VSOCKETS=y`, `CONFIG_VIRTIO_VSOCKETS=y`, +and `CONFIG_CGROUPS=y`, plus cgroup v2 and `cgroup.kill` (Linux 5.14+). UVM's +default Linux 6.1 kernel supports these. Refresh an older installed kernel as +described in the installation/entropy recovery sections before booting. + +All transport is via a per-VM Unix socket and Firecracker vsock port 1070. This +management channel is independent of the IPv4 port policy, including TCP/22; +the guest agent accepts connections from the host CID only. VM execution does +not need SSH credentials, SSH host-key caches, or an open guest TCP port. + ### Diagnostics and Log Tails The dashboard's System view calls the protected diagnostics endpoint. It runs @@ -1083,6 +1228,8 @@ Use `{"force": true}` to refresh all downloaded artifacts. | `409` | Name/identifier conflict, incompatible retained state, or an unsafe/pending operation. | | `422` | Invalid JSON body, extra request fields, or invalid UVM input. | | `500` | Host command, Firecracker, state, or unexpected operational failure. | +| `502` | Guest execution failure or lost/invalid execution response; inspect `error.outcome_unknown`. | +| `503` | Guest agent unavailable, not ready, or at its connection limit before command submission. | UVM operation errors use this envelope: @@ -1091,6 +1238,8 @@ UVM operation errors use this envelope: ``` FastAPI request-schema errors use FastAPI's normal `detail` response format. +Guest execution errors additionally include a boolean `outcome_unknown` in the +`error` object; `true` means the caller must not assume the command did not run. ## 13. Configuration Scenarios @@ -1318,7 +1467,7 @@ matching checksum with the required drivers instead. For an explicitly unverified development installation, retain the existing `UVM_ALLOW_UNVERIFIED_DOWNLOADS=1` setting as described in the installation section. -Restart a running UVM API server to load the updated code before using dashboard +Restart a running Nest server to load the updated code before using dashboard start/restart actions. Look for `Started OpenBSD Secure Shell server` in the new boot log and confirm an actual SSH connection. `start` on an already-running VM only refreshes networking; use a stopped VM or `restart` to attach the device. @@ -1335,7 +1484,8 @@ Keep these constraints in mind when deciding whether UVM fits a scenario: DNS publication, guest reverse proxy/TLS setup, DHCP, load balancing, snapshots, multi-network tenancy, or multi-host scheduling. - The API uses one shared bearer token, not individual users, roles, or tenants. -- There is no guest readiness probe beyond Firecracker process state. +- Execution probes guest-agent readiness; VM list/status still describes the + Firecracker process rather than guest application health. - UVM is not a hardened multi-tenant platform. ## 17. Safe Operating Checklist @@ -1377,6 +1527,24 @@ upstreams to check NAT, offline startup, reconnects and uplink changes. It does not boot Firecracker or validate the host's real TAP setup. Run actual VM/SSH/egress checks on an isolated Linux host before relying on the deployment. +The execution smoke tests target an already-created **disposable VM on an +isolated host**, with a non-root login, an installed/running agent, and an empty +TCP allowlist. They check both privilege modes, output and exit status, bounded +output, and cgroup cleanup of a descendant that starts its own session. They do +not create, stop, or delete VMs: + +```sh +UVM_EXEC_INTEGRATION=1 UVM_EXEC_VM_ID='' \ + python3 -B -m unittest discover -s tests/integration -p test_guest_exec.py -v +``` + +Set `UVM_API_URL` and `UVM_API_TOKEN` as in the API examples. For TLS, the Python +client uses the system trust store (or `SSL_CERT_FILE`). Without the explicit +integration flag these tests skip and make no requests. The ordinary unit tests +exercise real child processes with fake cgroup scopes, local Unix-socket protocol +fixtures, and temporary ext4 upgrades; they do not prove the real vsock/cgroup +path inside a VM. + ## 19. Get Help ```sh diff --git a/nest.py b/nest.py new file mode 100755 index 0000000..de235bb --- /dev/null +++ b/nest.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +"""Source-tree launcher for the Nest workspace.""" + +from nest.cli import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/nest/__init__.py b/nest/__init__.py new file mode 100644 index 0000000..8bcf631 --- /dev/null +++ b/nest/__init__.py @@ -0,0 +1 @@ +"""Nest workspace application.""" diff --git a/nest/__main__.py b/nest/__main__.py new file mode 100644 index 0000000..1e7cb3b --- /dev/null +++ b/nest/__main__.py @@ -0,0 +1,7 @@ +"""Run the Nest workspace with ``python -m nest``.""" + +from .cli import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/nest/cli.py b/nest/cli.py new file mode 100644 index 0000000..535f7be --- /dev/null +++ b/nest/cli.py @@ -0,0 +1,44 @@ +"""Command-line entry point for the Nest workspace server.""" + +from __future__ import annotations + +import argparse +import sys +from collections.abc import Sequence + +from uvm.app import build_application +from uvm.errors import UvmError + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="nest", + description="Nest dashboard and local machine management workspace.", + allow_abbrev=False, + ) + parser.add_argument("--serve", action="store_true", help="launch the Nest workspace server") + parser.add_argument("--host", default="127.0.0.1", help="bind address (default: 127.0.0.1)") + parser.add_argument("--port", type=int, default=8000, help="bind port (default: 8000)") + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + if not args.serve: + parser.error("--serve is required to launch the Nest workspace") + if not 1 <= args.port <= 65535: + parser.error("--port must be between 1 and 65535") + + try: + from .server import run_server + + application = build_application(emit=lambda message: print(message, file=sys.stderr)) + run_server(application, host=args.host, port=args.port) + return 0 + except UvmError as error: + print(f"nest: error: {error}", file=sys.stderr) + return 1 + except KeyboardInterrupt: + print("nest: interrupted", file=sys.stderr) + return 130 diff --git a/nest/dashboard/__init__.py b/nest/dashboard/__init__.py new file mode 100644 index 0000000..abeb727 --- /dev/null +++ b/nest/dashboard/__init__.py @@ -0,0 +1 @@ +"""Packaged frontend assets for the Nest workspace.""" diff --git a/uvm/routers/dashboard.py b/nest/dashboard/router.py similarity index 70% rename from uvm/routers/dashboard.py rename to nest/dashboard/router.py index f512852..dfb5592 100644 --- a/uvm/routers/dashboard.py +++ b/nest/dashboard/router.py @@ -1,4 +1,4 @@ -"""Dashboard frontend routes.""" +"""Nest dashboard document route.""" from __future__ import annotations @@ -9,7 +9,7 @@ from fastapi.responses import HTMLResponse router = APIRouter() -_DASHBOARD_HTML = Path(__file__).parent.parent / "dashboard" / "static" / "index.html" +_DASHBOARD_HTML = Path(__file__).parent / "static" / "index.html" @router.get("", response_class=HTMLResponse) diff --git a/uvm/dashboard/static/css/dashboard.css b/nest/dashboard/static/css/dashboard.css similarity index 100% rename from uvm/dashboard/static/css/dashboard.css rename to nest/dashboard/static/css/dashboard.css diff --git a/uvm/dashboard/static/fonts/OFL.txt b/nest/dashboard/static/fonts/OFL.txt similarity index 100% rename from uvm/dashboard/static/fonts/OFL.txt rename to nest/dashboard/static/fonts/OFL.txt diff --git a/uvm/dashboard/static/fonts/manrope-latin.woff2 b/nest/dashboard/static/fonts/manrope-latin.woff2 similarity index 100% rename from uvm/dashboard/static/fonts/manrope-latin.woff2 rename to nest/dashboard/static/fonts/manrope-latin.woff2 diff --git a/uvm/dashboard/static/index.html b/nest/dashboard/static/index.html similarity index 92% rename from uvm/dashboard/static/index.html rename to nest/dashboard/static/index.html index 9c093ef..a635bf0 100644 --- a/uvm/dashboard/static/index.html +++ b/nest/dashboard/static/index.html @@ -5,11 +5,12 @@ - UVM Control - + Nest + + - - + +
- uvm.workspace -
A little space for big ideas

Your compute.
A little calmer.

A considered workspace for your machines,
your network, and everything in between.

+ nest.workspace +
A little space for big ideas

Your workspace.
Right at home.

A home for your machines,
your network, and your next idea.

- +
Your local workspace -

Welcome to UVM

+

Welcome to Nest

Bring your machines into view.
Connect with your management token to begin.

@@ -80,8 +81,8 @@