Skip to content

Commit 4a5ae2d

Browse files
committed
Make upgrade E2E production-faithful
1 parent 777749f commit 4a5ae2d

15 files changed

Lines changed: 1582 additions & 1122 deletions

File tree

dstack/verifier/src/verification.rs

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,31 @@ impl CvmVerifier {
392392
.is_some_and(|digest| digest == expected))
393393
}
394394

395+
fn prune_unlisted_image_files(extracted_dir: &Path, files_doc: &str) -> Result<()> {
396+
let listed_files: Vec<&OsStr> = files_doc
397+
.lines()
398+
.flat_map(|line| line.split_whitespace().nth(1))
399+
.map(|s| s.as_ref())
400+
.collect();
401+
let files = fs_err::read_dir(extracted_dir).context("Failed to read directory")?;
402+
for file in files {
403+
let file = file.context("Failed to read directory entry")?;
404+
let filename = file.file_name();
405+
// sha256sum.txt is the content-addressed OS identity and is needed
406+
// again when a legacy TDX quote is verified from the cache.
407+
if filename != OsStr::new("sha256sum.txt")
408+
&& !listed_files.contains(&filename.as_os_str())
409+
{
410+
if file.path().is_dir() {
411+
fs_err::remove_dir_all(file.path()).context("Failed to remove directory")?;
412+
} else {
413+
fs_err::remove_file(file.path()).context("Failed to remove file")?;
414+
}
415+
}
416+
}
417+
Ok(())
418+
}
419+
395420
fn tdx_acpi_hashes_from_event_log(event_log: &[TdxEvent]) -> Result<TdxRtmr0AcpiHashes> {
396421
let rtmr0_events = event_log
397422
.iter()
@@ -1108,23 +1133,7 @@ impl CvmVerifier {
11081133
let sha256sum_path = extracted_dir.join("sha256sum.txt");
11091134
let files_doc =
11101135
fs_err::read_to_string(&sha256sum_path).context("Failed to read sha256sum.txt")?;
1111-
let listed_files: Vec<&OsStr> = files_doc
1112-
.lines()
1113-
.flat_map(|line| line.split_whitespace().nth(1))
1114-
.map(|s| s.as_ref())
1115-
.collect();
1116-
let files = fs_err::read_dir(&extracted_dir).context("Failed to read directory")?;
1117-
for file in files {
1118-
let file = file.context("Failed to read directory entry")?;
1119-
let filename = file.file_name();
1120-
if !listed_files.contains(&filename.as_os_str()) {
1121-
if file.path().is_dir() {
1122-
fs_err::remove_dir_all(file.path()).context("Failed to remove directory")?;
1123-
} else {
1124-
fs_err::remove_file(file.path()).context("Failed to remove file")?;
1125-
}
1126-
}
1127-
}
1136+
Self::prune_unlisted_image_files(&extracted_dir, &files_doc)?;
11281137

11291138
// All image modes are addressed by sha256(sha256sum.txt). Extra
11301139
// measurement CBOR files are ordinary sha256sum.txt entries and do not
@@ -1247,6 +1256,21 @@ mod tests {
12471256
assert!(decode_key_provider_info(b"not json").is_none());
12481257
}
12491258

1259+
#[test]
1260+
fn image_cache_pruning_keeps_checksum_identity() {
1261+
let dir = tempfile::tempdir().expect("temp image directory");
1262+
let files_doc = "00 metadata.json\n";
1263+
fs_err::write(dir.path().join("sha256sum.txt"), files_doc).unwrap();
1264+
fs_err::write(dir.path().join("metadata.json"), "{}").unwrap();
1265+
fs_err::write(dir.path().join("unmeasured"), "remove me").unwrap();
1266+
1267+
CvmVerifier::prune_unlisted_image_files(dir.path(), files_doc).unwrap();
1268+
1269+
assert!(dir.path().join("sha256sum.txt").exists());
1270+
assert!(dir.path().join("metadata.json").exists());
1271+
assert!(!dir.path().join("unmeasured").exists());
1272+
}
1273+
12501274
#[tokio::test]
12511275
async fn verifies_sev_snp_attestation_fixture_without_image_download() {
12521276
let request: VerificationRequest =
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Focused regression tests for vmm-cli update request construction."""
5+
6+
from __future__ import annotations
7+
8+
import contextlib
9+
import importlib.util
10+
import io
11+
import sys
12+
import unittest
13+
from pathlib import Path
14+
15+
16+
def load_vmm_cli():
17+
"""Load the executable vmm-cli.py as a normal Python module."""
18+
path = Path(__file__).resolve().parents[1] / "vmm-cli.py"
19+
spec = importlib.util.spec_from_file_location("dstack_vmm_cli", path)
20+
if spec is None or spec.loader is None:
21+
raise RuntimeError(f"cannot load {path}")
22+
module = importlib.util.module_from_spec(spec)
23+
sys.modules[spec.name] = module
24+
spec.loader.exec_module(module)
25+
return module
26+
27+
28+
VMM_CLI = load_vmm_cli()
29+
30+
31+
class UpdateVmTests(unittest.TestCase):
32+
"""Verify update flags are not silently discarded by the CLI."""
33+
34+
def test_kms_urls_are_sent_to_upgrade_app(self) -> None:
35+
"""Send both the update flag and requested KMS URL list."""
36+
cli = VMM_CLI.VmmCLI("http://127.0.0.1:8080")
37+
calls: list[tuple[str, dict]] = []
38+
cli.rpc_call = lambda method, params=None: calls.append((method, params)) or {}
39+
40+
with contextlib.redirect_stdout(io.StringIO()):
41+
cli.update_vm("vm-id", kms_urls=["https://kms.example:8000"])
42+
43+
self.assertEqual(
44+
calls,
45+
[
46+
(
47+
"UpgradeApp",
48+
{
49+
"id": "vm-id",
50+
"update_kms_urls": True,
51+
"kms_urls": ["https://kms.example:8000"],
52+
},
53+
)
54+
],
55+
)
56+
57+
58+
if __name__ == "__main__":
59+
unittest.main()

dstack/vmm/src/vmm-cli.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,11 @@ def update_vm(
11271127
upgrade_params["user_config"] = user_config
11281128
updates.append("user config")
11291129

1130+
if kms_urls is not None:
1131+
upgrade_params["update_kms_urls"] = True
1132+
upgrade_params["kms_urls"] = kms_urls
1133+
updates.append(f"KMS URLs ({len(kms_urls)})")
1134+
11301135
# handle port updates - only update if --port or --no-ports is specified
11311136
if no_ports or ports is not None:
11321137
if no_ports:
Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,63 @@
11
# SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>
22
# SPDX-License-Identifier: Apache-2.0
33

4-
# Directory containing unpacked dstack guest images: <store>/<image-name>/{bzImage,initramfs...,rootfs...,digest.txt}
5-
# In meta-dstack this is usually ../../../build/images from this directory, or an absolute path.
6-
DSTACK_E2E_IMAGE_STORE=../../../build/images
4+
# Directory containing unpacked dstack guest images:
5+
# <store>/<image-name>/{bzImage,initramfs...,rootfs...,digest.txt,sha256sum.txt}
6+
DSTACK_E2E_IMAGE_STORE=../../../meta-dstack/build/images
77
DSTACK_E2E_IMAGE_NAME=dstack-0.6.0
88
DSTACK_E2E_PLATFORM=tdx
99

10-
# App image used inside the CVMs launched by this suite.
10+
# Exact released upgrade sources. Both are digest-pinned on Docker Hub and
11+
# checked by their in-image --version output before any CVM is launched.
12+
DSTACK_E2E_OLD_KMS_IMAGE=dstacktee/dstack-kms:0.5.8@sha256:9650dcb47dad0065470f432f00e78e012912214ef1a5b1d7272918817e61a26d
13+
DSTACK_E2E_OLD_GATEWAY_IMAGE=dstacktee/dstack-gateway:0.5.8@sha256:6eb1dc1a5000f37cc5b0322d3fdb71e7f2e31859b5e3a611634919278cee2411
1114
DSTACK_E2E_APP_IMAGE=nginx:alpine
1215

13-
# Released images used by run-upgrade-e2e.sh. The KMS case intentionally pins
14-
# exactly 0.5.7; the script fails before starting the stack if Docker Hub does
15-
# not provide that tag. Gateway 0.5.8 is the default published upgrade source.
16-
DSTACK_E2E_OLD_KMS_IMAGE=dstacktee/dstack-kms:0.5.7
17-
DSTACK_E2E_OLD_GATEWAY_IMAGE=dstacktee/dstack-gateway:0.5.8
18-
1916
# Mock ACME/DNS + externally visible test domain for Gateway SNI.
2017
DSTACK_E2E_BASE_DOMAIN=e2e.test
2118
DSTACK_E2E_PEBBLE_IMAGE=kvin/pebble:latest
2219

23-
# Host ports. Defaults are chosen to avoid the common dstackup ports.
20+
# Host ports. Defaults are chosen to avoid common dstackup ports.
2421
DSTACK_E2E_VMM_PORT=29080
2522
DSTACK_E2E_AUTH_PORT=28011
26-
DSTACK_E2E_KMS_HOST_PORT=28082
27-
# 0.5.x KMS encodes rpc-domain as a DNS SAN even when it looks like an IP.
28-
# Use a DNS name that resolves to QEMU's 10.0.2.2 host gateway.
29-
DSTACK_E2E_KMS_RPC_DOMAIN=10.0.2.2.nip.io
30-
DSTACK_E2E_GATEWAY_RPC_HOST_PORT=28000
31-
DSTACK_E2E_GATEWAY_ADMIN_HOST_PORT=28001
32-
DSTACK_E2E_GATEWAY_PROXY_HOST_PORT=28443
33-
DSTACK_E2E_GATEWAY_WG_HOST_PORT=28120
23+
DSTACK_E2E_ARTIFACT_PORT=38081
24+
DSTACK_E2E_KMS_OLD_HOST_PORT=28082
25+
DSTACK_E2E_KMS_LATEST_HOST_PORT=28083
26+
# KMS 0.5.8 encodes this value as a DNS SAN. It must resolve to the test host
27+
# from BOTH the host-side deployment client and each CVM. If omitted, the
28+
# renderer derives <host-default-route-ip>.nip.io (for example below).
29+
# DSTACK_E2E_KMS_RPC_DOMAIN=192.168.1.90.nip.io
30+
DSTACK_E2E_GATEWAY1_RPC_HOST_PORT=28000
31+
DSTACK_E2E_GATEWAY1_ADMIN_HOST_PORT=28001
32+
DSTACK_E2E_GATEWAY1_PROXY_HOST_PORT=28443
33+
DSTACK_E2E_GATEWAY1_WG_HOST_PORT=28120
34+
DSTACK_E2E_GATEWAY2_RPC_HOST_PORT=28100
35+
DSTACK_E2E_GATEWAY2_ADMIN_HOST_PORT=28101
36+
DSTACK_E2E_GATEWAY2_PROXY_HOST_PORT=28543
37+
DSTACK_E2E_GATEWAY2_WG_HOST_PORT=28121
3438
DSTACK_E2E_KEY_PROVIDER_PORT=13443
3539
DSTACK_E2E_HOST_API_PORT=20011
3640
DSTACK_E2E_MOCK_CF_HTTP_PORT=38080
3741
DSTACK_E2E_PEBBLE_HTTP_PORT=34000
3842
DSTACK_E2E_PEBBLE_MGMT_PORT=35000
3943

40-
# Dev-compose gateway policy. "any" matches the tdxlab host-side gateway mode:
41-
# app CVMs skip gateway app-id pinning but still use KMS authorization for apps.
42-
DSTACK_E2E_GATEWAY_APP_ID=any
43-
DSTACK_E2E_GATEWAY_WG_INTERFACE=wg-ds-e2e
44-
DSTACK_E2E_GATEWAY_WG_IP=10.8.0.1/16
45-
DSTACK_E2E_GATEWAY_WG_RESERVED_NET=10.8.0.1/32
46-
DSTACK_E2E_GATEWAY_WG_CLIENT_RANGE=10.8.0.0/18
44+
# Optional exact 20-byte Gateway application ID. If omitted, the renderer
45+
# deterministically derives one. "any" is intentionally rejected.
46+
# DSTACK_E2E_GATEWAY_APP_ID=0123456789abcdef0123456789abcdef01234567
4747

4848
# VMM/QEMU knobs.
4949
DSTACK_E2E_CID_START=15000
5050
DSTACK_E2E_QGS_PORT=4050
5151
DSTACK_E2E_QEMU_PATH=/usr/bin/qemu-system-x86_64
5252

53-
# SGX QCNL config used by AESMD. On hosts with a local PCCS, point this at the
54-
# host config, e.g. /etc/sgx_default_qcnl.conf.
53+
# SGX QCNL config used by the Local-Key-Provider that seals each KMS CVM disk.
5554
DSTACK_E2E_QCNL_CONF=../../dstack/key-provider-build/sgx_default_qcnl.conf
5655

5756
# Runner behaviour.
5857
DSTACK_E2E_CLEAN_START=true
5958
DSTACK_E2E_CLEANUP_AFTER=false
60-
61-
# Upgrade-driver behaviour. Keep the completed stack by default for inspection.
6259
DSTACK_E2E_UPGRADE_CLEAN_STATE=true
6360
DSTACK_E2E_KEEP_STACK=true
61+
62+
# Set only when the current x86_64-musl KMS/Gateway binaries were already built.
63+
DSTACK_E2E_SKIP_CURRENT_BUILD=false

0 commit comments

Comments
 (0)