From 8221017b652eac387ed095580f7cb290ccb36f91 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 2 Jul 2026 14:47:11 -0500 Subject: [PATCH 1/2] serve full stamped system_version read from zone file Read the full stamped version string (N.P.C-pre+gitHHHH) from a well-known path inside the deployed zone (/var/oxide/system-version), falling back to the compile-time SYSTEM_VERSION core when absent (dev/test, unstamped packages). /v1/version now serves this via omicron_common::system_version(). --- common/src/lib.rs | 80 ++++++++++++++++++++++++++++++++++- nexus/external-api/src/lib.rs | 2 +- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/common/src/lib.rs b/common/src/lib.rs index c2c0b50945d..90ada7740a3 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -111,9 +111,46 @@ pub fn format_time_delta(time_delta: chrono::TimeDelta) -> String { /// the future. /// /// The releng build process appends build metadata to this to produce the full -/// version string; the `/v1/version` API endpoint returns it as-is. +/// version string. It is the compile-time fallback for [`system_version`], +/// which prefers the full stamped string when running in a deployed zone. pub const SYSTEM_VERSION: semver::Version = semver::Version::new(22, 0, 0); +/// Path, inside a deployed zone, of the file holding the full stamped system +/// version string (e.g. `21.0.0-0.ci+git0abc1234def`). +/// +/// `omicron-package stamp` writes the version into each zone image's top-level +/// `oxide.json` metadata, but that metadata is only readable from the global +/// zone (by re-opening the image tarball); a process running *inside* a zone +/// cannot see it. To make the full version available to Nexus at runtime, the +/// stamp step also writes it to this path within the zone filesystem. The file +/// is absent in dev/test builds and in unstamped packages. +const SYSTEM_VERSION_PATH: &str = "/var/oxide/system-version"; + +/// The full version of the running system software, as served by `/v1/version`. +/// +/// Returns the stamped version string from [`SYSTEM_VERSION_PATH`] when present +/// (a deployed, stamped zone), otherwise the compile-time [`SYSTEM_VERSION`] +/// core. The result is read once and cached: a system update replaces the zone +/// image and restarts the process, so the file cannot change under a running +/// server. +pub fn system_version() -> &'static semver::Version { + static VERSION: std::sync::OnceLock = + std::sync::OnceLock::new(); + VERSION.get_or_init(|| { + read_system_version(std::path::Path::new(SYSTEM_VERSION_PATH)) + }) +} + +/// Read and parse the stamped system version from `path`, falling back to +/// [`SYSTEM_VERSION`] when the file is absent or does not contain a valid +/// semver. Split out from [`system_version`] (which caches) to be testable. +fn read_system_version(path: &std::path::Path) -> semver::Version { + match std::fs::read_to_string(path) { + Ok(contents) => contents.trim().parse().unwrap_or(SYSTEM_VERSION), + Err(_) => SYSTEM_VERSION, + } +} + pub const OMICRON_DPD_TAG: &str = "omicron"; /// A wrapper struct that does nothing other than elide the inner value from @@ -187,3 +224,44 @@ impl std::fmt::Debug for BytesToHexDebug<'_> { Ok(()) } } + +#[cfg(test)] +mod test { + use super::{SYSTEM_VERSION, read_system_version}; + use camino_tempfile::NamedUtf8TempFile; + use std::io::Write; + + fn write_version_file(contents: &str) -> NamedUtf8TempFile { + let mut file = NamedUtf8TempFile::new().unwrap(); + file.write_all(contents.as_bytes()).unwrap(); + file.flush().unwrap(); + file + } + + #[test] + fn system_version_falls_back_when_file_absent() { + let path = camino::Utf8Path::new("/nonexistent/system-version"); + assert_eq!(read_system_version(path.as_std_path()), SYSTEM_VERSION); + } + + #[test] + fn system_version_reads_full_stamped_string() { + // The stamped string carries prerelease and build metadata that the + // compile-time SYSTEM_VERSION core lacks. The trailing newline tests + // that whitespace around the version is trimmed. + const STAMPED_VERSION: &str = "21.0.0-0.ci+git0abc1234def"; + let expected: semver::Version = STAMPED_VERSION.parse().unwrap(); + let file = write_version_file(&format!("{STAMPED_VERSION}\n")); + let version = read_system_version(file.path().as_std_path()); + assert_eq!(version, expected); + } + + #[test] + fn system_version_falls_back_on_unparseable_file() { + let file = write_version_file("not a version"); + assert_eq!( + read_system_version(file.path().as_std_path()), + SYSTEM_VERSION + ); + } +} diff --git a/nexus/external-api/src/lib.rs b/nexus/external-api/src/lib.rs index b95ec65cdef..06e50fcb276 100644 --- a/nexus/external-api/src/lib.rs +++ b/nexus/external-api/src/lib.rs @@ -433,7 +433,7 @@ pub trait NexusExternalApi { ) -> Result, HttpError> { Ok(HttpResponseOk(latest::system::Version { api_version: latest_version(), - system_version: omicron_common::SYSTEM_VERSION, + system_version: omicron_common::system_version().clone(), })) } From f3118d41647e9bec06ef8eca567e050bc28662c4 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 2 Jul 2026 19:26:11 -0500 Subject: [PATCH 2/2] patch omicron-zone-package, try to test it in helios/deploy --- .github/buildomat/jobs/deploy.sh | 14 ++++++++++++++ .github/buildomat/jobs/package.sh | 12 ++++++++++++ Cargo.lock | 3 +-- Cargo.toml | 5 +++++ 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/.github/buildomat/jobs/deploy.sh b/.github/buildomat/jobs/deploy.sh index 336b50d2eba..f2bc123601c 100755 --- a/.github/buildomat/jobs/deploy.sh +++ b/.github/buildomat/jobs/deploy.sh @@ -503,6 +503,20 @@ while ! curl -sSf "$OXIDE_HOST/v1/ping" --resolve "$OXIDE_RESOLVE" --cacert "$E2 retry=$((retry + 1)) done +EXPECTED_SYSTEM_VERSION=$(out/deploy-system-version + mapfile -t packages \ < <(cargo run --locked --release --bin omicron-package -- -t test list-outputs) @@ -55,6 +66,7 @@ mkdir tests files=( .github/buildomat/ci-env.sh + out/deploy-system-version out/target/test out/npuzone/* package-manifest.toml diff --git a/Cargo.lock b/Cargo.lock index 01f409c3482..cbcf045bc17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9951,8 +9951,7 @@ dependencies = [ [[package]] name = "omicron-zone-package" version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8bd7aa2335195256a00e62ea85724df4582d7c4ddcc88f9d2349e2a0dd19cc" +source = "git+https://github.com/oxidecomputer/omicron-package.git?rev=b3a1c66d2abf1c68f1ecf500a5de6df6515b7f5e#b3a1c66d2abf1c68f1ecf500a5de6df6515b7f5e" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 528468ea589..30eb8bf0b9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1088,6 +1088,11 @@ opt-level = 3 # dependencies. If you want to use those, uncomment one of these blocks. # [patch.crates-io] +# PROTOTYPE ONLY: use the unpublished in-zone system-version change +# end-to-end (see .claude/notes/2026-07-02-full-system-version-in-zone.md). +# Revert before merge; the real change ships as a published +# omicron-zone-package release + version bump above. +omicron-zone-package = { git = "https://github.com/oxidecomputer/omicron-package.git", rev = "b3a1c66d2abf1c68f1ecf500a5de6df6515b7f5e" } # diesel = { path = "../../diesel/diesel" } # drift = { path = "../drift" } # dropshot = { path = "../dropshot/dropshot" }