Skip to content

Add launcher binary#593

Open
fintelia wants to merge 8 commits intomainfrom
user/jbehrens/ula
Open

Add launcher binary#593
fintelia wants to merge 8 commits intomainfrom
user/jbehrens/ula

Conversation

@fintelia
Copy link
Copy Markdown
Contributor

@fintelia fintelia commented Apr 9, 2026

🔍 Description

This PR restores much of the old harpoon code to connect to Omaha servers, and implements a simple launcher binary that queries an Omaha server then connects to trident and does an update with the indicated COSI file

Copilot AI review requested due to automatic review settings April 9, 2026 22:28
@fintelia fintelia requested a review from a team as a code owner April 9, 2026 22:28
@fintelia fintelia force-pushed the user/jbehrens/ula branch from cb15b15 to 4dc818a Compare April 9, 2026 22:29
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR reintroduces an Omaha (Nebraska) client flow by adding a new launcher binary/crate and wiring it into existing provisioning/update pathways so test images can download and (eventually) use it alongside Trident.

Changes:

  • Added a new Rust workspace member crates/launcher implementing basic Omaha request/response handling and a CLI entrypoint.
  • Integrated launcher into netlaunch RCP file injection and the test image systemd unit that downloads binaries.
  • Updated build tooling to produce bin/launcher (via AZL3 Docker build) and copy launcher during Trident self-upgrade provisioning.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tools/pkg/netlaunch/netlaunch.go Injects bin/launcher into the RCP-served files for test/provisioning flows.
tests/images/trident-mos/files/download-trident.service Downloads /usr/bin/launcher alongside existing downloaded binaries.
Makefile Builds launcher in build-azl3, installs to bin/launcher, and adds it to run-netlaunch prerequisites.
crates/trident/src/subsystems/management.rs Copies /usr/bin/launcher into the target OS when SELF_UPGRADE_TRIDENT is enabled.
crates/launcher/src/omaha/xml.rs Adds helper macro for deserializing nested “list” XML elements.
crates/launcher/src/omaha/status.rs Defines Omaha status enums and helpers (+ unit tests).
crates/launcher/src/omaha/response.rs Implements Omaha response parsing/validation (+ unit tests).
crates/launcher/src/omaha/request.rs Implements Omaha request modeling and XML serialization (+ unit tests).
crates/launcher/src/omaha/mod.rs Implements blocking Omaha request execution and event validation (+ unit tests).
crates/launcher/src/omaha/event.rs Defines Omaha event payloads and acknowledgement parsing.
crates/launcher/src/omaha/app.rs Adds AppVersion wrapper around semver::Version (+ unit tests).
crates/launcher/src/main.rs Adds launcher CLI + Trident gRPC update trigger + query logic (+ unit tests).
crates/launcher/src/id.rs Adds machine ID/hostname sourcing for Omaha identifiers.
crates/launcher/src/error.rs Adds HarpoonError error model for launcher/Omaha operations.
crates/launcher/Cargo.toml Defines the new launcher crate dependencies.
Cargo.toml Adds crates/launcher to the workspace members.
Cargo.lock Updates lockfile for the new crate/dependency graph.

Comment on lines +68 to +72
fs::copy(
"/usr/bin/launcher",
path::join_relative(mount_path, "/usr/bin/launcher"),
)
.structured(ServicingError::CopyTridentBinary)?;
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SELF_UPGRADE_TRIDENT provisioning now unconditionally copies /usr/bin/launcher. On systems/images that set this flag but don’t yet have launcher installed, fs::copy will fail and block servicing. Consider making the launcher copy conditional on the source file existing (or otherwise guaranteeing launcher is always present when this flag is enabled), and map the error to a launcher-specific ServicingError variant so failures aren’t reported as “Copy Trident binary”.

Suggested change
fs::copy(
"/usr/bin/launcher",
path::join_relative(mount_path, "/usr/bin/launcher"),
)
.structured(ServicingError::CopyTridentBinary)?;
let launcher_path = Path::new("/usr/bin/launcher");
if launcher_path.exists() {
info!("Copying launcher binary to target OS");
fs::copy(
launcher_path,
path::join_relative(mount_path, "/usr/bin/launcher"),
)
.structured(ServicingError::CopyLauncherBinary)?;
} else {
info!("Skipping launcher copy because source binary is not present");
}

Copilot uses AI. Check for mistakes.
.map_err(|e| HarpoonError::Internal(format!("Failed to serialize request XML: {e}")))?;

debug!("Sending Omaha request to '{url}'",);
trace!("Omaha request body:\n{}", String::from_utf8_lossy(&body));
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trace!("Omaha request body…") logs the full serialized request XML, which includes the machine identifier (machineid). Even at TRACE level, this can end up in collected logs and becomes a sensitive identifier leak. Consider redacting the machine ID (or skipping request-body logging entirely) while keeping higher-level request metadata.

Suggested change
trace!("Omaha request body:\n{}", String::from_utf8_lossy(&body));
trace!("Omaha request prepared ({} bytes)", body.len());

Copilot uses AI. Check for mistakes.
Comment on lines +343 to +348
let document = reqwest::blocking::Client::new()
.get(package_url.clone())
.send()
.map_err(|err| HarpoonError::FetchError(err.to_string()))?
.text()
.map_err(|err| HarpoonError::FetchError(err.to_string()))?;
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

download_document uses response.text() and then validates size/hash over the decoded String. This can change the byte stream (charset decoding, normalization), causing size/hash validation to fail even when the downloaded bytes are correct. Fetch and validate the raw bytes (e.g., bytes()), compute the digest over those bytes, and only decode to String after validation if needed.

Copilot uses AI. Check for mistakes.

.PHONY: run-netlaunch run-netlaunch-stream
run-netlaunch: $(NETLAUNCH_CONFIG) $(TRIDENT_CONFIG) $(NETLAUNCH_ISO) bin/netlaunch validate artifacts/osmodifier $(RUN_NETLAUNCH_TRIDENT_BIN)
run-netlaunch: $(NETLAUNCH_CONFIG) $(TRIDENT_CONFIG) $(NETLAUNCH_ISO) bin/netlaunch artifacts/osmodifier $(RUN_NETLAUNCH_TRIDENT_BIN) bin/launcher
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run-netlaunch now depends on bin/launcher, which (via build-azl3) forces an AZL3 Docker build even when RUN_NETLAUNCH_TRIDENT_BIN resolves to the native bin/trident path. If this wasn’t intended, consider building/using a launcher artifact that matches the selected trident binary path or gating the launcher build the same way. Also, validate was removed from run-netlaunch prerequisites, so config validation no longer runs automatically before launching.

Suggested change
run-netlaunch: $(NETLAUNCH_CONFIG) $(TRIDENT_CONFIG) $(NETLAUNCH_ISO) bin/netlaunch artifacts/osmodifier $(RUN_NETLAUNCH_TRIDENT_BIN) bin/launcher
run-netlaunch: validate $(NETLAUNCH_CONFIG) $(TRIDENT_CONFIG) $(NETLAUNCH_ISO) bin/netlaunch artifacts/osmodifier $(RUN_NETLAUNCH_TRIDENT_BIN)

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +15
[dependencies]
anyhow = { version = "1.0.82", features = ["backtrace"] }
log = "0.4.21"
quick-xml = { version = "0.37.1", features = ["serialize"] }
reqwest = { version = "0.12.4", default-features = false, features = [
"blocking",
"default-tls",
"charset",
] } # "http2" is enabled by default but causes a (false positive) CG alert
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This crate pins many common dependency versions (anyhow, log, reqwest, serde, uuid, etc.) instead of using the workspace versions. In this repo, other crates consistently use { workspace = true }, and pinning here can introduce version skew/duplicate deps and complicate updates/security scanning. Consider switching these to workspace dependencies (and only overriding features where needed).

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 9, 2026 22:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 5 comments.

Comment on lines +418 to +422
data, err := os.ReadFile("bin/launcher")
if err != nil {
return fmt.Errorf("failed to read local launcher binary from 'bin/launcher': %w", err)
}
rcpAgentConfBuilder.registerRcpFile(newRcpAgentFileDownload("launcher", "/usr/bin/launcher", 0755, data))
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

injectRcpAgentConfig now unconditionally reads and injects bin/launcher, even though other injected binaries (trident/osmodifier) are optional and configurable via RcpConfiguration. This makes any RCP-enabled netlaunch fail unless the current working directory has bin/launcher, and there’s no way to override/disable it. Consider adding a LocalLauncherPath (and only injecting when set), or making launcher injection conditional based on an explicit config/flag.

Suggested change
data, err := os.ReadFile("bin/launcher")
if err != nil {
return fmt.Errorf("failed to read local launcher binary from 'bin/launcher': %w", err)
}
rcpAgentConfBuilder.registerRcpFile(newRcpAgentFileDownload("launcher", "/usr/bin/launcher", 0755, data))
launcherPath := "bin/launcher"
if _, err := os.Stat(launcherPath); err == nil {
data, err := os.ReadFile(launcherPath)
if err != nil {
return fmt.Errorf("failed to read local launcher binary from '%s': %w", launcherPath, err)
}
rcpAgentConfBuilder.registerRcpFile(newRcpAgentFileDownload("launcher", "/usr/bin/launcher", 0755, data))
} else if !os.IsNotExist(err) {
return fmt.Errorf("failed to stat local launcher binary at '%s': %w", launcherPath, err)
}

Copilot uses AI. Check for mistakes.
Comment on lines 194 to +197
bin/trident-azl3: build-azl3
@cp -u target/azl3/release/trident bin/trident-azl3
bin/launcher: build-azl3
@cp -u target/azl3/release/launcher bin/launcher
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bin/launcher depends on build-azl3, which always builds the full Trident release binary first. That means make bin/launcher (and anything that depends on it, like run-netlaunch) does extra work and slows iteration. Consider splitting out a build-launcher-azl3 target so building/copying launcher doesn’t force a full trident build.

Copilot uses AI. Check for mistakes.
Comment on lines 574 to 576
.PHONY: run-netlaunch run-netlaunch-stream
run-netlaunch: $(NETLAUNCH_CONFIG) $(TRIDENT_CONFIG) $(NETLAUNCH_ISO) bin/netlaunch validate artifacts/osmodifier $(RUN_NETLAUNCH_TRIDENT_BIN)
run-netlaunch: $(NETLAUNCH_CONFIG) $(TRIDENT_CONFIG) $(NETLAUNCH_ISO) bin/netlaunch artifacts/osmodifier $(RUN_NETLAUNCH_TRIDENT_BIN) bin/launcher
@echo "Using trident binary: $(RUN_NETLAUNCH_TRIDENT_BIN)"
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run-netlaunch no longer depends on the validate target, so invalid $(TRIDENT_CONFIG) files won’t be caught early (previously bin/trident validate ... ran before launching). If this was removed to avoid using bin/trident on Ubuntu 24+, consider updating validate to use $(RUN_NETLAUNCH_TRIDENT_BIN) and keeping validation as a dependency here.

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +86
let r = query_and_fetch_yaml_document(
&args.url,
"b0ec8f0d-1c13-4bf4-9efd-ea54464a7098",
"west-us",
&Version::new(0, 0, 0),
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main hardcodes the app_id, track, and starting version when querying Omaha (only the server URL is configurable). This doesn’t match the PR description of a launcher that queries for a “specific app and track”, and makes the binary hard to reuse across environments. Consider adding CLI args for app_id/track/current version (and possibly IdSource) instead of embedding constants.

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +127
config: match hash {
Some(hash) => format!("image:\n url: {url}\n sha384: {hash}"),
None => {
format!("image:\n url: {url}\n sha384: ignored")
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trigger() formats the HostConfiguration as sha384: {hash} when a hash is provided. Trident’s image.sha384 expects either ignored or a 96-hex SHA384 value (Sha384Hash); passing a 64-hex SHA256 (as used elsewhere in this crate) will fail config parsing and block updates. Consider either wiring through the actual COSI metadata SHA384, or changing the type to Option (and/or falling back to ignored if you don’t have a SHA384).

Copilot uses AI. Check for mistakes.
publish = false

[dependencies]
anyhow = { version = "1.0.82", features = ["backtrace"] }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit workspace = true ?

)
.structured(ServicingError::CopyTridentBinary)?;

fs::copy(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps do it conditionally so we skip this if the launcher doesn't exist?

/// look at the first package returned by the omaha server to fetch the
/// document. The function expects the document to be a single file with `.yaml`
/// extension.
pub fn query_and_fetch_yaml_document(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(happy to address in follow ups to make smaller changes) many strings/names/functions assumed we'd be pulling a HC from nebraska, but we are really really just pulling the image in this new iteration

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants