diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8944a75..3fdc83d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -228,6 +228,30 @@ jobs: build-mode: ${{ matrix.build-mode }} binary-path: ${{ matrix.binary }} + - name: Build Linux pipette + if: matrix.build-mode == 'musl' + shell: bash + run: | + set -euo pipefail + rustup target add "${{ matrix.target }}" + cargo build \ + --manifest-path openvmm/Cargo.toml \ + --release \ + --target "${{ matrix.target }}" \ + --package pipette \ + --bin pipette + + - name: Upload Linux pipette + if: matrix.build-mode == 'musl' + uses: actions/upload-artifact@v7 + with: + name: openvmm-linux-pipette + path: openvmm/target/${{ matrix.target }}/release/pipette + if-no-files-found: error + compression-level: 0 + overwrite: true + retention-days: 1 + - name: Upload OpenVMM executable uses: actions/upload-artifact@v7 with: @@ -250,7 +274,7 @@ jobs: openvmm-tests: name: OpenVMM tests / ${{ matrix.name }} - needs: [openvmm-changes] + needs: [openvmm-changes, openvmm-binaries] if: >- ${{ always() && @@ -263,6 +287,7 @@ jobs: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository ) && + needs.openvmm-binaries.result == 'success' && ( needs.openvmm-changes.result != 'success' || needs.openvmm-changes.outputs.run-tests == 'true' @@ -306,6 +331,13 @@ jobs: - name: Set up retrying curl uses: ./.github/actions/setup-curl + - name: Download Linux pipette + if: runner.os == 'Windows' + uses: actions/download-artifact@v8 + with: + name: openvmm-linux-pipette + path: ${{ runner.temp }}/openvmm-linux-pipette + - name: Prepare Windows cache tools if: runner.os == 'Windows' shell: powershell @@ -418,6 +450,7 @@ jobs: shell: powershell env: GH_TOKEN: ${{ github.token }} + OPENVMM_LINUX_PIPETTE: ${{ runner.temp }}\openvmm-linux-pipette\pipette run: python scripts\nvx.py test-openvmm --backend "${{ matrix.backend }}" - name: Save OpenVMM inputs on Linux diff --git a/doc/build.md b/doc/build.md index 3133842..feaf82c 100644 --- a/doc/build.md +++ b/doc/build.md @@ -16,9 +16,10 @@ The OpenVMM restore step excludes the compatibility IGVM artifact, which NVX does not build or package, so builds do not depend on unrelated upstream workflow artifacts. -OpenVMM's microVM tests build their own minimal Xen PVH guest from source in -the OpenVMM checkout. They do not consume `build/vmlinux` or -`build/initramfs.cpio.gz`. NVX uses those two artifacts only for its Linux and +OpenVMM's tests build their own minimal Xen PVH guest and Linux pipette agent +from source in the OpenVMM checkout. They also restore OpenVMM's packaged +Linux-direct test kernel and initrd. They do not consume `build/vmlinux` or +`build/initramfs.cpio.gz`; NVX uses those two artifacts only for its Linux and device correctness tests, benchmarks, and packaged runtime. On a Linux host, build the guest directly: @@ -51,8 +52,11 @@ python3 scripts/nvx.py test-openvmm --backend kvm python3 scripts/nvx.py test-microvm --backend kvm ``` -The first command needs only the OpenVMM checkout. The second needs the -standard build outputs above and writes complete per-scenario logs under +On Linux, the first command needs only the initialized OpenVMM checkout and the +Rust targets provisioned by the host setup. A local Windows/WHP run additionally +needs `OPENVMM_LINUX_PIPETTE` to identify a same-revision Linux pipette; see +[Usage](usage.md#test-openvmm). The second command needs the standard build +outputs above and writes complete per-scenario logs under `build/test-results/microvm` by default. The initramfs includes the sandbox PID-1 bootstrap, its container namespace diff --git a/doc/ci.md b/doc/ci.md index c765c10..44940f8 100644 --- a/doc/ci.md +++ b/doc/ci.md @@ -6,8 +6,16 @@ three runners labeled by operating system, backend, and `virtual-machine`. Jobs target the shared backend labels so any available matching runner can execute them. This allows the backend lanes to execute concurrently without binding a workload to a specific host. `openvmm-tests` builds its Xen PVH probe -entirely from the OpenVMM checkout and exercises OpenVMM lifecycle, TTRPC, and -snapshot contracts without restoring NVX guest artifacts. +entirely from the OpenVMM checkout and exercises OpenVMM lifecycle, TTRPC, +snapshot, and the canonical `multiarch::openvmm_linux_x64_boot` Linux-direct +boot contract without restoring NVX guest artifacts. The selector uses an exact +nextest name matcher so similarly named boot variants are not included. +Linux runners build the static musl pipette locally. The Linux musl OpenVMM +binary job also uploads a same-revision pipette for WHP; Windows prepares the +remaining test bundle, adds that pipette, and runs the complete selector with +`vmm-tests-run-target`. Linux runner provisioning installs both +`x86_64-unknown-none` and `x86_64-unknown-linux-musl` because the protected +Rustup state is read-only to workflow jobs. `nvx-microvm-tests` consumes the NVX Linux kernel and Alpine initramfs and exercises Linux, SMP, virtio, sandbox, and snapshot behavior through the public OpenVMM CLI. Failure logs from the NVX layer are uploaded per backend. diff --git a/doc/usage.md b/doc/usage.md index 199178f..5f9686e 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -28,7 +28,7 @@ python3 scripts/nvx.py performance gate --help | `build-initramfs` | Build the Alpine initramfs natively. | | `build-openvmm` | Build the OpenVMM release binary. | | `setup-cross-os-cache` | Install GNU tar and zstd for GitHub Actions cross-OS caches. | -| `test-openvmm` | Run self-contained OpenVMM microVM control-plane tests. | +| `test-openvmm` | Run checkout-owned OpenVMM control-plane and Linux-direct boot tests. | | `test-microvm` | Run NVX Linux and device correctness tests through OpenVMM. | | `build` | Build the guest artifacts and OpenVMM. | | `download` | Download and install the latest matching GitHub release. | @@ -127,7 +127,33 @@ python3 scripts/nvx.py test-openvmm --backend {kvm,mshv,whp} ``` Builds and runs OpenVMM's checkout-owned microVM tests. The test artifacts are -produced by OpenVMM itself; NVX's kernel and initramfs are not required. +produced from the pinned OpenVMM source; NVX's kernel and initramfs are not +required. The selector includes only the canonical Linux-direct boot test +`multiarch::openvmm_linux_x64_boot`, not similarly named boot variants. + +Linux hosts build the required static Linux pipette guest agent locally and +therefore need the `x86_64-unknown-linux-musl` Rust target. On Windows, set +`OPENVMM_LINUX_PIPETTE` to a same-revision Linux pipette before running the WHP +test: + +```powershell +$env:OPENVMM_LINUX_PIPETTE = "C:\path\to\pipette" +python scripts\nvx.py test-openvmm --backend whp +``` + +Build that artifact from the OpenVMM checkout on Linux: + +```bash +cd openvmm +cargo xflowey restore-packages --no-compat-igvm +rustup target add x86_64-unknown-linux-musl +cargo build --release --target x86_64-unknown-linux-musl \ + --package pipette --bin pipette +``` + +The resulting file is +`openvmm/target/x86_64-unknown-linux-musl/release/pipette`. GitHub Actions +builds, uploads, and downloads this artifact automatically for the WHP lane. ### `test-microvm` diff --git a/openvmm b/openvmm index 0bc357b..79eb629 160000 --- a/openvmm +++ b/openvmm @@ -1 +1 @@ -Subproject commit 0bc357bbcf3a654b63dfb51f1103c5751bf3d31f +Subproject commit 79eb6298118ed7fbd111f72bacb9ea45f18cb54f diff --git a/scripts/nvx_tools/ci.py b/scripts/nvx_tools/ci.py index b222914..c7469bd 100644 --- a/scripts/nvx_tools/ci.py +++ b/scripts/nvx_tools/ci.py @@ -24,10 +24,15 @@ ZSTD_SHA256 = "acb4e8111511749dc7a3ebedca9b04190e37a17afeb73f55d4425dbf0b90fad9" OPENVMM_TEST_BACKENDS = ("kvm", "mshv", "whp") OPENVMM_GUEST_RUST_TARGET = "x86_64-unknown-none" -OPENVMM_MICROVM_TEST_FILTER = ( +OPENVMM_LINUX_RUST_TARGET = "x86_64-unknown-linux-musl" +OPENVMM_MICROVM_BASE_TEST_FILTER = ( "test(openvmm_microvm_test_pvh_x64_phase_1_lifecycle) + " "test(test_ttrpc_microvm_pvh_snapshot)" ) +OPENVMM_LINUX_X64_BOOT_TEST_FILTER = "test(=multiarch::openvmm_linux_x64_boot)" +OPENVMM_MICROVM_TEST_FILTER = ( + f"{OPENVMM_MICROVM_BASE_TEST_FILTER} + {OPENVMM_LINUX_X64_BOOT_TEST_FILTER}" +) def validate_openvmm_test_backend(backend: str) -> None: @@ -59,11 +64,15 @@ def run_openvmm_tests(backend: str) -> None: cargo = require_tool("cargo") rustup = require_tool("rustup") - run_checked([rustup, "target", "add", OPENVMM_GUEST_RUST_TARGET]) + targets = [OPENVMM_GUEST_RUST_TARGET] + if os.name != "nt": + targets.append(OPENVMM_LINUX_RUST_TARGET) + run_checked([rustup, "target", "add", *targets]) run_checked( [cargo, "xflowey", "restore-packages", "--no-compat-igvm"], cwd=OPENVMM_DIR, ) + test_dir = Path(os.environ.get("RUNNER_TEMP", "C:/ovm-tests")) / backend command = [ cargo, "xflowey", @@ -75,12 +84,36 @@ def run_openvmm_tests(backend: str) -> None: OPENVMM_MICROVM_TEST_FILTER, ] if os.name == "nt": + pipette = os.environ.get("OPENVMM_LINUX_PIPETTE") + if not pipette: + raise ScriptError("OPENVMM_LINUX_PIPETTE is required for WHP OpenVMM tests") + pipette_path = require_file(Path(pipette), "OpenVMM Linux pipette") + build_command = [ + *command, + "--build-only", + "--dir", + os.fspath(test_dir), + ] + build_command[build_command.index("--filter") + 1] = ( + OPENVMM_MICROVM_BASE_TEST_FILTER + ) + run_checked(build_command, cwd=OPENVMM_DIR) + test_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(pipette_path, test_dir / "pipette") + command = [ + cargo, + "xflowey", + "vmm-tests-run-target", + "--needs-whp", + "--ci-profile", + "--skip-vhd-prompt", + "--filter", + OPENVMM_MICROVM_TEST_FILTER, + ] command.extend( ( "--dir", - os.fspath( - Path(os.environ.get("RUNNER_TEMP", "C:/ovm-tests")) / backend - ), + os.fspath(test_dir), ) ) run_checked(command, cwd=OPENVMM_DIR) diff --git a/scripts/setup/setup-linux-runner.sh b/scripts/setup/setup-linux-runner.sh index 1ab85ed..e6d4d84 100644 --- a/scripts/setup/setup-linux-runner.sh +++ b/scripts/setup/setup-linux-runner.sh @@ -448,13 +448,11 @@ install_rust_tools() { RUSTUP_HOME="$trusted_rustup_home" \ RUSTUP_TOOLCHAIN="$RUST_TOOLCHAIN" \ "$rustup" target add x86_64-unknown-none - if [ "$backend" = mshv ]; then - run_as_root env \ - CARGO_HOME="$trusted_cargo_home" \ - RUSTUP_HOME="$trusted_rustup_home" \ - RUSTUP_TOOLCHAIN="$RUST_TOOLCHAIN" \ - "$rustup" target add x86_64-unknown-linux-musl - fi + run_as_root env \ + CARGO_HOME="$trusted_cargo_home" \ + RUSTUP_HOME="$trusted_rustup_home" \ + RUSTUP_TOOLCHAIN="$RUST_TOOLCHAIN" \ + "$rustup" target add x86_64-unknown-linux-musl rust_version=$(run_as_root env \ CARGO_HOME="$trusted_cargo_home" \ diff --git a/scripts/test_nvx_tools.py b/scripts/test_nvx_tools.py index d30e7ef..ad57d03 100644 --- a/scripts/test_nvx_tools.py +++ b/scripts/test_nvx_tools.py @@ -640,6 +640,8 @@ def test_openvmm_tests_are_independent_of_nvx_guest_artifacts(self): openvmm = root / "openvmm" openvmm.mkdir() (openvmm / "Cargo.toml").touch() + pipette = root / "pipette" + pipette.write_bytes(b"pipette") backend = "whp" if os.name == "nt" else "kvm" with ( @@ -655,36 +657,71 @@ def test_openvmm_tests_are_independent_of_nvx_guest_artifacts(self): "OPENVMM_MICROVM_PVH_INITRD": "nvx-initrd", "PETRI_CAPABILITIES": "vpci", "RUNNER_TEMP": os.fspath(root), + "OPENVMM_LINUX_PIPETTE": os.fspath(pipette), }, ), ): ci.run_openvmm_tests(backend) - self.assertEqual(run_checked.call_count, 3) - install_target, restore, tests = run_checked.call_args_list + self.assertEqual(run_checked.call_count, 4 if os.name == "nt" else 3) + install_target, restore, *test_commands = run_checked.call_args_list self.assertEqual( install_target.args[0], - ["rustup", "target", "add", "x86_64-unknown-none"], + [ + "rustup", + "target", + "add", + "x86_64-unknown-none", + *([] if os.name == "nt" else ["x86_64-unknown-linux-musl"]), + ], ) self.assertEqual( restore.args[0], ["cargo", "xflowey", "restore-packages", "--no-compat-igvm"], ) - command = tests.args[0] - self.assertEqual(command[:3], ["cargo", "xflowey", "vmm-tests-run"]) + command = test_commands[-1].args[0] + self.assertEqual( + command[:3], + [ + "cargo", + "xflowey", + "vmm-tests-run-target" if os.name == "nt" else "vmm-tests-run", + ], + ) + self.assertIn("--ci-profile", command) + self.assertIn("--skip-vhd-prompt", command) + if os.name == "nt": + self.assertIn("--needs-whp", command) + self.assertNotIn("--needs-hyperv", command) + else: + self.assertIn("--release", command) filter_index = command.index("--filter") self.assertEqual(command[filter_index + 1], ci.OPENVMM_MICROVM_TEST_FILTER) - self.assertIn( - "test_ttrpc_microvm_pvh_snapshot", + self.assertEqual( ci.OPENVMM_MICROVM_TEST_FILTER, + "test(openvmm_microvm_test_pvh_x64_phase_1_lifecycle) + " + "test(test_ttrpc_microvm_pvh_snapshot) + " + "test(=multiarch::openvmm_linux_x64_boot)", ) - self.assertEqual(tests.kwargs["cwd"], openvmm) - self.assertNotIn("env", tests.kwargs) + self.assertEqual( + ci.OPENVMM_LINUX_X64_BOOT_TEST_FILTER, + "test(=multiarch::openvmm_linux_x64_boot)", + ) + self.assertEqual(test_commands[-1].kwargs["cwd"], openvmm) + self.assertNotIn("env", test_commands[-1].kwargs) if os.name == "nt": + build = test_commands[0].args[0] + self.assertIn("--build-only", build) + self.assertIn("--release", build) + self.assertEqual( + build[build.index("--filter") + 1], + ci.OPENVMM_MICROVM_BASE_TEST_FILTER, + ) self.assertEqual( command[command.index("--dir") + 1], os.fspath(root / backend), ) + self.assertEqual((root / backend / "pipette").read_bytes(), b"pipette") def test_openvmm_tests_reject_unknown_backend(self): with self.assertRaisesRegex(common.ScriptError, "unsupported.*backend"): @@ -692,6 +729,19 @@ def test_openvmm_tests_reject_unknown_backend(self): class CiConfigurationTests(unittest.TestCase): + def test_openvmm_tests_require_binary_handoff(self): + workflow = (common.REPO_ROOT / ".github" / "workflows" / "ci.yml").read_text( + encoding="utf-8" + ) + openvmm_tests = workflow.split(" openvmm-tests:", maxsplit=1)[1].split( + "\n nvx-microvm-tests:", maxsplit=1 + )[0] + + self.assertIn( + "needs.openvmm-binaries.result == 'success'", + openvmm_tests, + ) + def test_flowey_downloads_use_retrying_curl(self): action = ( common.REPO_ROOT / ".github" / "actions" / "setup-curl" / "action.yml"