test: reset Windows native runs from clean baseline

This commit is contained in:
2026-09-09 07:13:14 +00:00
parent 665901afcd
commit 985a2c2422
5 changed files with 170 additions and 81 deletions
+19 -17
View File
@@ -192,7 +192,7 @@ scripts/test-e2e [--scenario NAME|all] [--run-id ID] [--resume]
scripts/test-env doctor|coverage|status|logs|collect|recover|reuse|stop|reset|purge|gc ... scripts/test-env doctor|coverage|status|logs|collect|recover|reuse|stop|reset|purge|gc ...
scripts/build build|verify|doctor|recover|clean (pinned toolchain; host-safe) scripts/build build|verify|doctor|recover|clean (pinned toolchain; host-safe)
scripts/windows/build-test-bundle --run-id ID --config FILE [--ca FILE] scripts/windows/build-test-bundle --run-id ID --config FILE [--ca FILE]
scripts/windows/test-host status|prepare|stage|run|collect|stop|reset|recover scripts/windows/test-host status|prepare|stage|install|run|collect|stop|reset|recover
``` ```
Scripts are thin, reviewed orchestration wrappers. `scripts/test-unit` invokes Scripts are thin, reviewed orchestration wrappers. `scripts/test-unit` invokes
@@ -813,8 +813,8 @@ mirror; update both documents when the fixture is reprovisioned.
| Diagnostic VRDE | Enabled at `192.168.50.162:3389`, external/`VBoxAuthSimple` authentication, input/display enabled, audio/USB/clipboard/RDPDR disabled; diagnostic-only because client compatibility is unreliable | | Diagnostic VRDE | Enabled at `192.168.50.162:3389`, external/`VBoxAuthSimple` authentication, input/display enabled, audio/USB/clipboard/RDPDR disabled; diagnostic-only because client compatibility is unreliable |
| Native Windows RDP | Disabled in baseline (`TermService` stopped, `fDenyTSConnections=1`); port 3390 must not be treated as a usable control endpoint | | Native Windows RDP | Disabled in baseline (`TermService` stopped, `fDenyTSConnections=1`); port 3390 must not be treated as a usable control endpoint |
| Test account | Local `rvboxtest`; split-token local administrator; console session 1 observed; Guest Control verified with `whoami`, `whoami /groups`, and `query user` | | Test account | Local `rvboxtest`; split-token local administrator; console session 1 observed; Guest Control verified with `whoami`, `whoami /groups`, and `query user` |
| Baseline | `baseline-clean` (UUID `5e79176a-3e56-4c5d-bb61-a405a6dcdd59`) and child `baseline-disk-first` (UUID `9430a9a4-754a-4b22-beaa-8dfd90043f5b`); both are clean and must be retained | | Baseline | Reset target `baseline-clean` (UUID `5e79176a-3e56-4c5d-bb61-a405a6dcdd59`): no RVBox service, tray registration, state, logs, or staged binary. Retain child `baseline-disk-first` (UUID `9430a9a4-754a-4b22-beaa-8dfd90043f5b`) for diagnostics only. |
| Last checked state | `poweroff`, current snapshot `baseline-disk-first`; the harness must re-check state and leave the VM powered off after cleanup | | Last checked state | `poweroff`, current snapshot `baseline-disk-first`; restore `baseline-clean` before native runs, and leave that reset target selected after cleanup |
The guest password, SSH key, and any host account secret are test secrets. Keep The guest password, SSH key, and any host account secret are test secrets. Keep
them in the operator/CI secret store or a mode-600 password file outside the them in the operator/CI secret store or a mode-600 password file outside the
@@ -826,17 +826,19 @@ overridden with `RVBOX_TEST_GUEST_PASSWORD_FILE`; the password value is never a
default or repository value. The account name and VM metadata above are not default or repository value. The account name and VM metadata above are not
credentials. credentials.
Guest Control uses this split-token administrator's medium-integrity token; its Guest Control uses `rvboxtest`'s split-token, medium-integrity identity; its
Administrators SID is deny-only. Do not bypass UAC to create the service during Administrators SID is deny-only. The reset snapshot contains no RVBox
automation. A one-time interactive elevated fixture bootstrap must install the installation and the harness proves that `RVBoxClient` is absent immediately
test `RVBoxClient` service in the reset baseline and grant `rvboxtest` only after every `prepare`. Do not bypass UAC or turn this active-session test user
query/change-config/start/stop service access plus write access to its dedicated into an always-elevated account. Instead, provision a separate fixture-only
test subtree. Each native run then changes that bootstrapped service's explicit full-token administrator, with its username and a mode-0600 host-side password
image/configuration and starts it through SCM. This is fixture administration, file held outside the repository. `test-host install` uses that identity only
not a second product worker/elevation broker or a Task Scheduler dependency. to execute the staged real `rvbox.exe --install-service` path and proves it by
Keep the exact service SDDL and test-subtree ACL with the fixture record before polling SCM. It is not an RVBox product process, a service/broker, or a Task
taking its replacement baseline snapshot. The production installer/UAC flow is Scheduler dependency, and it never enters the daemon's command-context choice.
tested separately through an interactive elevated lane. The normal active `rvboxtest` session remains the target for execution-role
tests. The consent-prompt branch itself remains an interactive UAC test; an
invisible Guest Control session must never answer it.
For this provisioned lane, the approved host-only credential-file location is For this provisioned lane, the approved host-only credential-file location is
`/home/cabbage/.local/share/rvbox-secrets/rvbox-win10-test.password`. It must `/home/cabbage/.local/share/rvbox-secrets/rvbox-win10-test.password`. It must
@@ -861,7 +863,7 @@ identity in the run manifest:
```sh ```sh
export RVBOX_TEST_VBOX_HOST=helium-remote export RVBOX_TEST_VBOX_HOST=helium-remote
export RVBOX_TEST_VBOX_VM=rvbox-win10-test export RVBOX_TEST_VBOX_VM=rvbox-win10-test
export RVBOX_TEST_VBOX_SNAPSHOT=baseline-disk-first export RVBOX_TEST_VBOX_SNAPSHOT=baseline-clean
export RVBOX_TEST_GUEST_USER=rvboxtest export RVBOX_TEST_GUEST_USER=rvboxtest
export RVBOX_TEST_GUEST_PASSWORD_FILE=/secure/outside-repo/rvbox-win10-test.password export RVBOX_TEST_GUEST_PASSWORD_FILE=/secure/outside-repo/rvbox-win10-test.password
``` ```
@@ -880,7 +882,7 @@ ssh "$RVBOX_TEST_VBOX_HOST" \
# Restore only while powered off, then boot without a GUI. # Restore only while powered off, then boot without a GUI.
ssh "$RVBOX_TEST_VBOX_HOST" \ ssh "$RVBOX_TEST_VBOX_HOST" \
'VBoxManage snapshot "rvbox-win10-test" restore "baseline-disk-first"' 'VBoxManage snapshot "rvbox-win10-test" restore "baseline-clean"'
ssh "$RVBOX_TEST_VBOX_HOST" \ ssh "$RVBOX_TEST_VBOX_HOST" \
'VBoxManage startvm "rvbox-win10-test" --type headless' 'VBoxManage startvm "rvbox-win10-test" --type headless'
@@ -928,7 +930,7 @@ Use this shutdown/reset sequence for every native run:
acpipowerbutton` and poll. Use `controlvm ... poweroff` only for a hung, acpipowerbutton` and poll. Use `controlvm ... poweroff` only for a hung,
disposable test; it intentionally loses guest state. disposable test; it intentionally loses guest state.
3. Collect diagnostics while the VM is still available, then restore 3. Collect diagnostics while the VM is still available, then restore
`baseline-disk-first` and verify the snapshot UUID/current marker. `baseline-clean` and verify the snapshot UUID/current marker.
4. Leave the VM powered off after cleanup. Never delete either baseline 4. Leave the VM powered off after cleanup. Never delete either baseline
snapshot, unregister the VM, or modify `win10_dev` (that name refers to a snapshot, unregister the VM, or modify `win10_dev` (that name refers to a
stale unregistered configuration with a missing disk on this host). stale unregistered configuration with a missing disk on this host).
+50 -33
View File
@@ -6,7 +6,9 @@ Windows release matrix. Keep the values here in sync with the VM before adding
or changing native test automation. or changing native test automation.
Last configuration check: 2026-09-09 UTC. The VM was observed powered off with Last configuration check: 2026-09-09 UTC. The VM was observed powered off with
`baseline-disk-first` selected. A test run must still perform its own identity, `baseline-disk-first` selected. The native harness now targets `baseline-clean`;
the fixture must be rechecked and its current snapshot returned to that clean
baseline before native runs resume. A test run must still perform its own identity,
snapshot, readiness, and exclusive-lease checks rather than relying on that snapshot, readiness, and exclusive-lease checks rather than relying on that
observation. observation.
@@ -25,7 +27,7 @@ observation.
| Guest OS | Windows 10 Pro 22H2, build `19045.2006`, en-US, BIOS boot | | Guest OS | Windows 10 Pro 22H2, build `19045.2006`, en-US, BIOS boot |
| Guest account | Local `rvboxtest`; split-token local administrator; console session 1 was observed during provisioning | | Guest account | Local `rvboxtest`; split-token local administrator; console session 1 was observed during provisioning |
| Guest Additions | `7.2.16r174877`; readiness requires published Guest Additions version and Windows OS-release properties (this build does not publish a RunLevel property) | | Guest Additions | `7.2.16r174877`; readiness requires published Guest Additions version and Windows OS-release properties (this build does not publish a RunLevel property) |
| Last observed state | `poweroff`; current snapshot `baseline-disk-first` | | Last observed state | `poweroff`; current snapshot `baseline-disk-first` (must be restored to `baseline-clean` before native runs) |
The Guest Control credential is test-only. The account name is safe to record, The Guest Control credential is test-only. The account name is safe to record,
but the password value is intentionally not committed to this repository. On but the password value is intentionally not committed to this repository. On
@@ -84,7 +86,9 @@ and collection. Do not expose the VM's RDP endpoints beyond the test LAN.
## Snapshots and reset contract ## Snapshots and reset contract
Two clean snapshots exist and must be retained: Two clean snapshots exist and must be retained. `baseline-clean` is the only
reset target: it contains no `RVBoxClient` SCM service, RVBox tray Run-key
registration, RVBox state, logs, or staged binaries.
| Snapshot | UUID | Description | | Snapshot | UUID | Description |
| --- | --- | --- | | --- | --- | --- |
@@ -95,12 +99,12 @@ Restore only while the VM is powered off. Every destructive or potentially
stateful run must: stateful run must:
1. Acquire the run lease and verify the VM name, UUID, and snapshot UUID. 1. Acquire the run lease and verify the VM name, UUID, and snapshot UUID.
2. Restore `baseline-disk-first` if the current state is not the baseline. 2. Restore `baseline-clean` if the current state is not the baseline.
3. Start headless and wait for `VMState=running` plus Guest Additions readiness. 3. Start headless and wait for `VMState=running` plus Guest Additions readiness.
4. Run the bounded test, collect redacted artifacts, and close every Guest 4. Run the bounded test, collect redacted artifacts, and close every Guest
Control process that was opened by the run. Control process that was opened by the run.
5. Request a graceful guest shutdown and wait for `VMState=poweroff`. 5. Request a graceful guest shutdown and wait for `VMState=poweroff`.
6. Restore `baseline-disk-first` again and leave the VM powered off. 6. Restore `baseline-clean` again and leave the VM powered off.
Use `controlvm ... poweroff` only for a hung, disposable test; it can lose Use `controlvm ... poweroff` only for a hung, disposable test; it can lose
guest state. Never delete either clean snapshot, unregister the VM, or alter guest state. Never delete either clean snapshot, unregister the VM, or alter
@@ -112,7 +116,7 @@ The canonical adapter is the POSIX controller script
[`scripts/windows/test-host`](../scripts/windows/test-host). It runs from the [`scripts/windows/test-host`](../scripts/windows/test-host). It runs from the
Linux controller and invokes `VBoxManage` only through SSH on Helium; the Linux controller and invokes `VBoxManage` only through SSH on Helium; the
fixture host is Arch Linux and does not provide PowerShell. Its actions are fixture host is Arch Linux and does not provide PowerShell. Its actions are
`status`, `prepare`, `stage`, `run`, `collect`, `stop`, `reset`, and `recover`. `status`, `prepare`, `stage`, `install`, `run`, `collect`, `stop`, `reset`, and `recover`.
The legacy [`test-host.ps1`](../scripts/windows/test-host.ps1) is retained only The legacy [`test-host.ps1`](../scripts/windows/test-host.ps1) is retained only
as a reference for a future Windows-hosted fixture and is not the Helium lane. as a reference for a future Windows-hosted fixture and is not the Helium lane.
@@ -122,8 +126,8 @@ The adapter takes identity and credentials only from its environment:
export RVBOX_TEST_VBOX_HOST=helium-remote export RVBOX_TEST_VBOX_HOST=helium-remote
export RVBOX_TEST_VBOX_VM=rvbox-win10-test export RVBOX_TEST_VBOX_VM=rvbox-win10-test
export RVBOX_TEST_VBOX_VM_UUID=6cdc114f-71e5-4167-a394-e922e14e6f5c export RVBOX_TEST_VBOX_VM_UUID=6cdc114f-71e5-4167-a394-e922e14e6f5c
export RVBOX_TEST_VBOX_SNAPSHOT=baseline-disk-first export RVBOX_TEST_VBOX_SNAPSHOT=baseline-clean
export RVBOX_TEST_VBOX_SNAPSHOT_UUID=9430a9a4-754a-4b22-beaa-8dfd90043f5b export RVBOX_TEST_VBOX_SNAPSHOT_UUID=5e79176a-3e56-4c5d-bb61-a405a6dcdd59
export RVBOX_TEST_GUEST_USER=rvboxtest export RVBOX_TEST_GUEST_USER=rvboxtest
export RVBOX_TEST_GUEST_PASSWORD_FILE=/home/cabbage/.local/share/rvbox-secrets/rvbox-win10-test.password export RVBOX_TEST_GUEST_PASSWORD_FILE=/home/cabbage/.local/share/rvbox-secrets/rvbox-win10-test.password
``` ```
@@ -145,34 +149,47 @@ the single `/c` payload are preserved.
`stage` accepts one versioned non-secret test bundle and copies it first to an `stage` accepts one versioned non-secret test bundle and copies it first to an
exact host staging directory, then to exact host staging directory, then to
`C:\\ProgramData\\RVBox\\test-runs\\<run-id>`. `run` never directly executes the `C:\\ProgramData\\RVBox\\test-runs\\<run-id>`. `install` performs the one
GUI-subsystem `rvbox.exe` through Guest Control. It uses `sc.exe` and other purposeful direct Guest Control launch of the staged GUI-subsystem executable,
console-safe management tools to start/query/stop the installed RVBox service, using only the fixture provisioner's high token; because this VirtualBox build
then checks the service's real health endpoint, named-pipe response, durable cannot reliably report that process's exit, SCM `RUNNING` is the completion
state, and agent-server results. Before a WSS scenario it performs a bounded proof. After installation, `run` uses `sc.exe` and other console-safe management
guest-to-nginx connectivity and CA-trust probe. The resulting service and tools to reconfigure/start/query/stop the installed RVBox service, then checks
artifact paths are recorded in the run report and reclaimed by the snapshot the service's real health endpoint, named-pipe response, durable state, and
reset rather than broad guest deletion. agent-server results. Before a WSS scenario it performs a bounded guest-to-nginx
connectivity and CA-trust probe. The resulting service and artifact paths are
recorded in the run report and reclaimed by the snapshot reset rather than broad
guest deletion.
### Required one-time service bootstrap ### Clean baseline and non-interactive installation
Guest Control launches `rvboxtest` with its filtered, medium-integrity UAC `rvboxtest` deliberately remains a split-token administrator. Guest Control
token: the Administrators SID is deny-only. The harness must not bypass UAC to therefore launches it at medium integrity and it must never be used to create
create services. Before native service tests can run, an operator must use a or modify machine-wide SCM state. The reset snapshot has no RVBox installation.
trusted interactive elevated session to install one test-only `RVBoxClient`
service in the baseline and grant only `rvboxtest` the service rights required
by the harness: query status/configuration, change its image/configuration,
start, and stop. The test account also needs write access only to the dedicated
`C:\\ProgramData\\RVBox\\test-runs` subtree; SYSTEM retains ownership of normal
RVBox state and logs. Record the resulting service SDDL and subtree ACL in this
document before taking a new reset snapshot.
Each run then stages an exact bundle, changes the bootstrapped service image to To automate the real install path, provision one separate **fixture-only**
that run's explicit `--service --config` command line, and starts it through full-token local administrator and retain its username/password solely in the
SCM. The one-time bootstrap is fixture administration, not a second RVBox Helium secret store. It must be a genuinely high-integrity Guest Control token;
process, runtime elevation broker, or Task Scheduler mechanism. Native tests do not globally disable UAC or change `rvboxtest` into an always-elevated user.
for the production installer/UAC flow remain a separately interactive test; The normal harness receives it only through these environment variables:
they cannot be automated through this filtered Guest Control token.
```sh
export RVBOX_TEST_PROVISIONER_USER=FIXTURE_ONLY_FULL_ADMIN
export RVBOX_TEST_PROVISIONER_PASSWORD_FILE=/home/cabbage/.local/share/rvbox-secrets/rvbox-win10-test-provisioner.password
```
Both files remain mode `0600` on Helium and neither value is recorded in run
reports or artifacts. `test-host install` first verifies that the reset guest
has no `RVBoxClient`, checks the provisioner's High Mandatory Level, invokes
the actual staged `rvbox.exe --install-service --config ...`, and polls SCM for
`RUNNING`. `run` may then exercise reconfigure/start/restart paths. Snapshot
reset removes the installed service and all RVBox data again.
The provisioner is fixture administration only: it is not shipped with RVBox,
not a product service/broker, not a Task Scheduler dependency, and never
participates in command-context selection. The separately interactive UAC
prompt route remains a small manual test because an invisible Guest Control
session cannot safely approve a consent prompt.
## Scope and known limitations ## Scope and known limitations
+23 -18
View File
@@ -49,6 +49,7 @@ scripts/windows/build-test-bundle \
scripts/windows/test-host prepare --run-id windows-smoke scripts/windows/test-host prepare --run-id windows-smoke
scripts/windows/test-host stage --run-id windows-smoke \ scripts/windows/test-host stage --run-id windows-smoke \
--bundle .test-runs/windows-smoke/windows-bundle --bundle .test-runs/windows-smoke/windows-bundle
scripts/windows/test-host install --run-id windows-smoke
``` ```
The bundle manifest records the source commit and SHA-256 of every bundled The bundle manifest records the source commit and SHA-256 of every bundled
@@ -151,16 +152,17 @@ The provisioned fixture's non-secret identity values, including the host-local
password-file path, are safe defaults in that script and may be overridden for password-file path, are safe defaults in that script and may be overridden for
another documented fixture; the password itself is never embedded. another documented fixture; the password itself is never embedded.
The native lifecycle is `status`, `prepare`, `stage`, `run`, `collect`, The native lifecycle is `status`, `prepare`, `stage`, `install`, `run`,
`stop`, and `reset`. `prepare` verifies the VM and snapshot UUIDs, restores the `collect`, `stop`, and `reset`. `prepare` verifies the VM and snapshot UUIDs,
baseline, starts headless, waits for Guest Additions, and records the selected restores the clean baseline, starts headless, waits for Guest Additions, and
login fixture. `stage` copies a versioned non-secret test bundle through a proves that `RVBoxClient` is absent. `stage` copies a versioned non-secret test
run-specific host directory to a run-specific guest directory. `run` starts bundle through a run-specific host directory to a run-specific guest directory.
the real RVBox SCM service and observes it through SCM, the local health/tray `install` uses the fixture-only high-integrity automation principal to invoke
pipe, durable artifacts, and the test agent endpoint; it must not invoke the the real `rvbox.exe --install-service` path and proves completion through SCM.
GUI-subsystem `rvbox.exe` directly through Guest Control. `collect` obtains `run` is for reconfiguration/restart scenarios after that first installation.
only bounded/redacted artifacts, and `reset` restores the exact baseline and Neither action invokes the GUI-subsystem executable directly with the normal
leaves the VM powered off. Guest Control account. `collect` obtains only bounded/redacted artifacts, and
`reset` restores the exact clean baseline and leaves the VM powered off.
This service-driven protocol is required because VirtualBox Guest Control This service-driven protocol is required because VirtualBox Guest Control
7.2.16 does not reliably complete a direct GUI-subsystem `rvbox.exe` run; 7.2.16 does not reliably complete a direct GUI-subsystem `rvbox.exe` run;
@@ -171,14 +173,17 @@ also performs a bounded guest-to-nginx HTTPS/TCP readiness probe before it
starts the service. Wine and protocol stubs are not equivalent Windows starts the service. Wine and protocol stubs are not equivalent Windows
coverage. coverage.
The fixture needs a one-time operator-approved service bootstrap before the The clean baseline intentionally contains no RVBox service, tray registration,
SCM smoke lane can run: Guest Control supplies the split-token administrator's or RVBox state. Guest Control supplies `rvboxtest` with a filtered medium UAC
medium UAC token, so it cannot safely install services. The bootstrap installs token, so it cannot safely perform the first machine-wide install. The fixture
the test service in the reset baseline and grants the test account only the SCM therefore has a separate test-only full-token automation principal, whose
query/change-config/start/stop rights plus access to its dedicated test subtree. username and mode-600 host-side password-file are provided only as
Each run then reconfigures and starts that one service. This does not add a `RVBOX_TEST_PROVISIONER_USER` and `RVBOX_TEST_PROVISIONER_PASSWORD_FILE` for
product broker or use Task Scheduler; it is fixture setup. See the `install`/machine-mutation actions. It is not an RVBox process, service,
[testing-vm.md](testing-vm.md) for the exact boundary. broker, or Task Scheduler dependency, and it is never used to choose a command
execution context. The normal `rvboxtest` console session remains the subject
of active-user and elevation tests. See [testing-vm.md](testing-vm.md) for the
exact fixture contract.
The VM is the minimum smoke lane, so native multi-session/ambiguous-session, The VM is the minimum smoke lane, so native multi-session/ambiguous-session,
Server Core, and older-build entries remain explicitly blocked until their own Server Core, and older-build entries remain explicitly blocked until their own
+77 -12
View File
@@ -18,7 +18,8 @@ Actions:
status read-only VM/snapshot identity and state check status read-only VM/snapshot identity and state check
prepare restore the declared baseline, boot headless, and verify Guest Additions prepare restore the declared baseline, boot headless, and verify Guest Additions
stage copy a bundle containing rvbox.exe and client.toml into the guest test root stage copy a bundle containing rvbox.exe and client.toml into the guest test root
run create/update and start the RVBox SCM service from the staged bundle install install and start RVBox from the staged bundle through a fixture-only full-admin principal
run start the already-installed RVBox SCM service from the staged bundle
collect copy bounded guest artifacts to the local test-run directory collect copy bounded guest artifacts to the local test-run directory
stop stop RVBox through SCM and request a graceful guest shutdown stop stop RVBox through SCM and request a graceful guest shutdown
reset stop the guest if necessary, restore the declared baseline, and leave it off reset stop the guest if necessary, restore the declared baseline, and leave it off
@@ -29,6 +30,8 @@ Optional environment:
RVBOX_TEST_VBOX_HOST, RVBOX_TEST_VBOX_VM, RVBOX_TEST_VBOX_VM_UUID, RVBOX_TEST_VBOX_HOST, RVBOX_TEST_VBOX_VM, RVBOX_TEST_VBOX_VM_UUID,
RVBOX_TEST_VBOX_SNAPSHOT, RVBOX_TEST_VBOX_SNAPSHOT_UUID, RVBOX_TEST_VBOX_SNAPSHOT, RVBOX_TEST_VBOX_SNAPSHOT_UUID,
RVBOX_TEST_GUEST_USER, RVBOX_TEST_GUEST_PASSWORD_FILE (overrides) RVBOX_TEST_GUEST_USER, RVBOX_TEST_GUEST_PASSWORD_FILE (overrides)
RVBOX_TEST_PROVISIONER_USER, RVBOX_TEST_PROVISIONER_PASSWORD_FILE
(required by install; a fixture-only full-token administrator)
RVBOX_TEST_HOST_STAGE_ROOT (default /home/cabbage/.local/state/rvbox-test-runs) RVBOX_TEST_HOST_STAGE_ROOT (default /home/cabbage/.local/state/rvbox-test-runs)
RVBOX_TEST_RUN_ROOT (default .test-runs/windows-vm) RVBOX_TEST_RUN_ROOT (default .test-runs/windows-vm)
EOF EOF
@@ -77,7 +80,7 @@ while [ "$#" -gt 0 ]; do
esac esac
done done
case $action in status|prepare|stage|run|collect|stop|reset|recover) ;; *) usage >&2; fail "unknown action $action" ;; esac case $action in status|prepare|stage|install|run|collect|stop|reset|recover) ;; *) usage >&2; fail "unknown action $action" ;; esac
if [ "$action" != status ]; then if [ "$action" != status ]; then
[ -n "$run_id" ] || fail "$action requires --run-id" [ -n "$run_id" ] || fail "$action requires --run-id"
safe_id "$run_id" safe_id "$run_id"
@@ -97,10 +100,12 @@ if [ -n "$endpoint" ]; then safe_word endpoint "$endpoint"; fi
: "${RVBOX_TEST_VBOX_HOST:=helium-remote}" : "${RVBOX_TEST_VBOX_HOST:=helium-remote}"
: "${RVBOX_TEST_VBOX_VM:=rvbox-win10-test}" : "${RVBOX_TEST_VBOX_VM:=rvbox-win10-test}"
: "${RVBOX_TEST_VBOX_VM_UUID:=6cdc114f-71e5-4167-a394-e922e14e6f5c}" : "${RVBOX_TEST_VBOX_VM_UUID:=6cdc114f-71e5-4167-a394-e922e14e6f5c}"
: "${RVBOX_TEST_VBOX_SNAPSHOT:=baseline-disk-first}" : "${RVBOX_TEST_VBOX_SNAPSHOT:=baseline-clean}"
: "${RVBOX_TEST_VBOX_SNAPSHOT_UUID:=9430a9a4-754a-4b22-beaa-8dfd90043f5b}" : "${RVBOX_TEST_VBOX_SNAPSHOT_UUID:=5e79176a-3e56-4c5d-bb61-a405a6dcdd59}"
: "${RVBOX_TEST_GUEST_USER:=rvboxtest}" : "${RVBOX_TEST_GUEST_USER:=rvboxtest}"
: "${RVBOX_TEST_GUEST_PASSWORD_FILE:=/home/cabbage/.local/share/rvbox-secrets/rvbox-win10-test.password}" : "${RVBOX_TEST_GUEST_PASSWORD_FILE:=/home/cabbage/.local/share/rvbox-secrets/rvbox-win10-test.password}"
provisioner_user=${RVBOX_TEST_PROVISIONER_USER:-}
provisioner_password_file=${RVBOX_TEST_PROVISIONER_PASSWORD_FILE:-}
for name in RVBOX_TEST_VBOX_HOST RVBOX_TEST_VBOX_VM RVBOX_TEST_VBOX_VM_UUID \ for name in RVBOX_TEST_VBOX_HOST RVBOX_TEST_VBOX_VM RVBOX_TEST_VBOX_VM_UUID \
RVBOX_TEST_VBOX_SNAPSHOT RVBOX_TEST_VBOX_SNAPSHOT_UUID \ RVBOX_TEST_VBOX_SNAPSHOT RVBOX_TEST_VBOX_SNAPSHOT_UUID \
@@ -115,6 +120,8 @@ safe_word RVBOX_TEST_VBOX_SNAPSHOT "$RVBOX_TEST_VBOX_SNAPSHOT"
safe_word RVBOX_TEST_VBOX_SNAPSHOT_UUID "$RVBOX_TEST_VBOX_SNAPSHOT_UUID" safe_word RVBOX_TEST_VBOX_SNAPSHOT_UUID "$RVBOX_TEST_VBOX_SNAPSHOT_UUID"
safe_word RVBOX_TEST_GUEST_USER "$RVBOX_TEST_GUEST_USER" safe_word RVBOX_TEST_GUEST_USER "$RVBOX_TEST_GUEST_USER"
safe_word RVBOX_TEST_GUEST_PASSWORD_FILE "$RVBOX_TEST_GUEST_PASSWORD_FILE" safe_word RVBOX_TEST_GUEST_PASSWORD_FILE "$RVBOX_TEST_GUEST_PASSWORD_FILE"
if [ -n "$provisioner_user" ]; then safe_word RVBOX_TEST_PROVISIONER_USER "$provisioner_user"; fi
if [ -n "$provisioner_password_file" ]; then safe_word RVBOX_TEST_PROVISIONER_PASSWORD_FILE "$provisioner_password_file"; fi
host_stage_root=${RVBOX_TEST_HOST_STAGE_ROOT:-/home/cabbage/.local/state/rvbox-test-runs} host_stage_root=${RVBOX_TEST_HOST_STAGE_ROOT:-/home/cabbage/.local/state/rvbox-test-runs}
run_root=${RVBOX_TEST_RUN_ROOT:-$repo_root/.test-runs/windows-vm} run_root=${RVBOX_TEST_RUN_ROOT:-$repo_root/.test-runs/windows-vm}
@@ -145,6 +152,8 @@ host_stage=$8
guest_root=$9 guest_root=$9
shift 9 shift 9
endpoint=$1 endpoint=$1
provisioner_user=$2
provisioner_password_file=$3
[ "$endpoint" = - ] && endpoint= [ "$endpoint" = - ] && endpoint=
fail() { printf '%s\n' "remote test-host: $*" >&2; exit 2; } fail() { printf '%s\n' "remote test-host: $*" >&2; exit 2; }
@@ -206,6 +215,41 @@ guest_run() {
printf '%s\n' "$output" | tr -d '\r' | grep -qx 'RVBOX_GUEST_OK' printf '%s\n' "$output" | tr -d '\r' | grep -qx 'RVBOX_GUEST_OK'
} }
provisioner_run() {
# The clean baseline deliberately has no RVBox service. Guest Control's
# normal test account has a filtered UAC token, so only the fixture-only
# full-token administrator may perform the first machine-wide install.
[ -n "$provisioner_user" ] || fail "install requires RVBOX_TEST_PROVISIONER_USER"
[ -n "$provisioner_password_file" ] || fail "install requires RVBOX_TEST_PROVISIONER_PASSWORD_FILE"
output=$(VBoxManage guestcontrol "$vm" --username "$provisioner_user" --passwordfile "$provisioner_password_file" \
run "$@" </dev/null 2>&1) || true
printf '%s\n' "$output"
printf '%s\n' "$output" | tr -d '\r' | grep -qx 'RVBOX_GUEST_OK'
}
assert_provisioner_elevated() {
# This fixture is en-US. Check the mandatory label before allowing any
# machine-wide mutation, so an accidentally filtered automation account
# fails closed instead of silently weakening the test contract.
provisioner_run --exe 'C:\\Windows\\System32\\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
/d /s /c 'whoami /groups | findstr /c:"High Mandatory Level" >NUL && echo RVBOX_GUEST_OK' >/dev/null || \
fail "fixture provisioner is not a full high-integrity administrator"
}
assert_clean_guest() {
# A missing service is the authoritative clean-baseline condition. The
# test service name is unique, so do not delete or alter any other service.
guest_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
/d /s /c 'sc.exe query RVBoxClient >NUL 2>&1 & if errorlevel 1060 (echo RVBOX_GUEST_OK) else exit /b 1' >/dev/null || \
fail "reset baseline is not clean: RVBoxClient is already installed"
}
assert_staged_guest() {
guest_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
/d /s /c "if exist \"$guest_root\\rvbox.exe\" if exist \"$guest_root\\client.toml\" echo RVBOX_GUEST_OK" >/dev/null || \
fail "staged guest bundle is missing rvbox.exe or client.toml"
}
wait_guest_additions() { wait_guest_additions() {
attempt=0 attempt=0
while [ "$attempt" -lt 60 ]; do while [ "$attempt" -lt 60 ]; do
@@ -257,7 +301,8 @@ case "$action" in
step prepare-vm-started step prepare-vm-started
wait_guest_additions wait_guest_additions
step prepare-guest-additions-ready step prepare-guest-additions-ready
step prepare-bootstrap-complete assert_clean_guest
step prepare-clean-baseline-verified
;; ;;
probe-identity) probe-identity)
assert_identity assert_identity
@@ -298,10 +343,29 @@ case "$action" in
VBoxManage guestcontrol "$vm" --username "$guest_user" --passwordfile "$password_file" \ VBoxManage guestcontrol "$vm" --username "$guest_user" --passwordfile "$password_file" \
copyto "$host_stage/ca.pem" "$guest_root\\ca.pem" </dev/null copyto "$host_stage/ca.pem" "$guest_root\\ca.pem" </dev/null
;; ;;
install)
assert_identity
require_lease
[ "$(state)" = running ] || fail "install requires a running prepared VM"
assert_clean_guest
assert_staged_guest
assert_provisioner_elevated
# Guest Control cannot reliably wait for GUI-subsystem rvbox.exe. It
# may report a non-zero wrapper result after the process has started,
# therefore SCM state is the completion proof for this exact install.
VBoxManage guestcontrol "$vm" --username "$provisioner_user" --passwordfile "$provisioner_password_file" \
run --exe "$guest_root\\rvbox.exe" --unquoted-args -- \
--install-service --config "$guest_root\\client.toml" </dev/null >/dev/null 2>&1 || true
wait_service RUNNING
step install-scm-service-running
printf 'service=RVBoxClient state=RUNNING install=clean-baseline\n'
;;
run) run)
assert_identity assert_identity
require_lease require_lease
[ "$(state)" = running ] || fail "run requires a running prepared VM" [ "$(state)" = running ] || fail "run requires a running prepared VM"
assert_staged_guest
assert_provisioner_elevated
if [ -n "$endpoint" ]; then if [ -n "$endpoint" ]; then
endpoint_host=${endpoint%:*} endpoint_host=${endpoint%:*}
endpoint_port=${endpoint##*:} endpoint_port=${endpoint##*:}
@@ -309,15 +373,15 @@ case "$action" in
-NoProfile -NonInteractive -Command "if (-not (Test-NetConnection -ComputerName '$endpoint_host' -Port $endpoint_port -InformationLevel Quiet)) { exit 1 }; Write-Output RVBOX_GUEST_OK" >/dev/null -NoProfile -NonInteractive -Command "if (-not (Test-NetConnection -ComputerName '$endpoint_host' -Port $endpoint_port -InformationLevel Quiet)) { exit 1 }; Write-Output RVBOX_GUEST_OK" >/dev/null
fi fi
image="\\\"$guest_root\\rvbox.exe\\\" --service --config \\\"$guest_root\\client.toml\\\"" image="\\\"$guest_root\\rvbox.exe\\\" --service --config \\\"$guest_root\\client.toml\\\""
guest_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \ provisioner_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
/d /s /c "sc.exe query RVBoxClient >NUL 2>&1 && echo RVBOX_GUEST_OK" >/dev/null || \ /d /s /c "sc.exe query RVBoxClient >NUL 2>&1 && echo RVBOX_GUEST_OK" >/dev/null || \
fail "RVBoxClient is not fixture-bootstrapped or the test token lacks SCM query access" fail "RVBoxClient is not installed; run install from the clean baseline first"
guest_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \ provisioner_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
/d /s /c "sc.exe config RVBoxClient binPath= \"$image\" start= demand >NUL 2>&1 && echo RVBOX_GUEST_OK" >/dev/null || \ /d /s /c "sc.exe config RVBoxClient binPath= \"$image\" start= demand >NUL 2>&1 && echo RVBOX_GUEST_OK" >/dev/null || \
fail "fixture service does not grant the test token SERVICE_CHANGE_CONFIG" fail "fixture provisioner could not change RVBoxClient configuration"
guest_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \ provisioner_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
/d /s /c '(sc.exe start RVBoxClient >NUL 2>&1 || sc.exe query RVBoxClient | findstr /c:"RUNNING" >NUL) && echo RVBOX_GUEST_OK' >/dev/null || \ /d /s /c '(sc.exe start RVBoxClient >NUL 2>&1 || sc.exe query RVBoxClient | findstr /c:"RUNNING" >NUL) && echo RVBOX_GUEST_OK' >/dev/null || \
fail "fixture service did not accept SCM start" fail "fixture provisioner could not start RVBoxClient"
wait_service RUNNING wait_service RUNNING
printf 'service=RVBoxClient state=RUNNING\n' printf 'service=RVBoxClient state=RUNNING\n'
;; ;;
@@ -382,7 +446,8 @@ REMOTE
"$1" "$RVBOX_TEST_VBOX_VM" "$RVBOX_TEST_VBOX_VM_UUID" \ "$1" "$RVBOX_TEST_VBOX_VM" "$RVBOX_TEST_VBOX_VM_UUID" \
"$RVBOX_TEST_VBOX_SNAPSHOT" "$RVBOX_TEST_VBOX_SNAPSHOT_UUID" \ "$RVBOX_TEST_VBOX_SNAPSHOT" "$RVBOX_TEST_VBOX_SNAPSHOT_UUID" \
"$RVBOX_TEST_GUEST_USER" "$RVBOX_TEST_GUEST_PASSWORD_FILE" \ "$RVBOX_TEST_GUEST_USER" "$RVBOX_TEST_GUEST_PASSWORD_FILE" \
"$host_stage" "$guest_root" "$remote_endpoint" </dev/null "$host_stage" "$guest_root" "$remote_endpoint" \
"$provisioner_user" "$provisioner_password_file" </dev/null
} }
case $action in case $action in
+1 -1
View File
@@ -7,7 +7,7 @@ param(
[ValidatePattern('^[a-z0-9][a-z0-9-]{0,63}$')] [ValidatePattern('^[a-z0-9][a-z0-9-]{0,63}$')]
[string] $RunId = '', [string] $RunId = '',
[string] $VmName = $(if ($env:RVBOX_WINDOWS_VM) { $env:RVBOX_WINDOWS_VM } elseif ($env:RVBOX_TEST_VBOX_VM) { $env:RVBOX_TEST_VBOX_VM } else { 'rvbox-win10-test' }), [string] $VmName = $(if ($env:RVBOX_WINDOWS_VM) { $env:RVBOX_WINDOWS_VM } elseif ($env:RVBOX_TEST_VBOX_VM) { $env:RVBOX_TEST_VBOX_VM } else { 'rvbox-win10-test' }),
[string] $Snapshot = $(if ($env:RVBOX_WINDOWS_BASELINE_SNAPSHOT) { $env:RVBOX_WINDOWS_BASELINE_SNAPSHOT } elseif ($env:RVBOX_TEST_VBOX_SNAPSHOT) { $env:RVBOX_TEST_VBOX_SNAPSHOT } else { 'baseline-disk-first' }) [string] $Snapshot = $(if ($env:RVBOX_WINDOWS_BASELINE_SNAPSHOT) { $env:RVBOX_WINDOWS_BASELINE_SNAPSHOT } elseif ($env:RVBOX_TEST_VBOX_SNAPSHOT) { $env:RVBOX_TEST_VBOX_SNAPSHOT } else { 'baseline-clean' })
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'