diff --git a/.github/renovate.json b/.github/renovate.json index 3d8cfbc7227..93abf3551ce 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -713,6 +713,20 @@ "datasourceTemplate": "rpm", "autoReplaceStringTemplate": "\"renovateTag\": \"RPM_registry={{{registryUrl}}}, name={{{packageName}}}, os=azurelinux, release=3.0\",\n \"latestVersion\": \"{{{newValue}}}\"{{#if depType}},\n \"previousLatestVersion\": \"{{{currentValue}}}\"{{/if}}" }, + { + "customType": "regex", + "description": "auto update GitHub release versions in components.json", + "managerFilePatterns": [ + "/parts/common/components.json/" + ], + "matchStringsStrategy": "any", + "matchStrings": [ + "\"renovateTag\":\\s*\"github-releases=(?[^\"]+)\",\\s*\"latestVersion\":\\s*\"(?[^\"]+)\"" + ], + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.*)$", + "autoReplaceStringTemplate": "\"renovateTag\": \"github-releases={{{packageName}}}\",\n \"latestVersion\": \"{{{newValue}}}\"" + }, { "customType": "regex", "description": "update version line in any cse_*.sh", diff --git a/e2e/validation.go b/e2e/validation.go index f0418f19b2f..f9b7885487f 100644 --- a/e2e/validation.go +++ b/e2e/validation.go @@ -45,6 +45,7 @@ func ValidateCommonLinux(ctx context.Context, s *Scenario) { ValidateIPTablesCompatibleWithCiliumEBPF(ctx, s) ValidateRxBufferDefault(ctx, s) ValidateKernelLogs(ctx, s) + ValidateWaagentLog(ctx, s) ValidateScriptlessCSECmd(ctx, s) ValidateNodeExporter(ctx, s) diff --git a/e2e/validators.go b/e2e/validators.go index 5f4e7d0eb2f..deb04c9590b 100644 --- a/e2e/validators.go +++ b/e2e/validators.go @@ -22,6 +22,7 @@ import ( "github.com/samber/lo" "github.com/tidwall/gjson" + "github.com/Azure/agentbaker/e2e/components" "github.com/Azure/agentbaker/e2e/config" "github.com/Azure/agentbaker/pkg/agent" "github.com/stretchr/testify/assert" @@ -2005,3 +2006,67 @@ func ValidateKernelLogs(ctx context.Context, s *Scenario) { s.T.Logf("No critical kernel issues found") } + +// ValidateWaagentLog checks /var/log/waagent.log for expected agent behavior: +// - AutoUpdate is disabled as expected +// - The correct version is running as ExtHandler +// - No errors from ExtHandler +// Skipped on Flatcar and OSGuard VHDs which manage WALinuxAgent independently. +func ValidateWaagentLog(ctx context.Context, s *Scenario) { + s.T.Helper() + + if s.VHD.Flatcar || strings.Contains(string(s.VHD.Distro), "osguard") { + s.T.Logf("Skipping waagent log validation: not applicable for %s", s.VHD.Distro) + return + } + + // Skip on pinned-version VHDs that predate the waagent installation. + // These VHDs explicitly select a version number and are not updated. + if s.VHD == config.VHDUbuntu2204Gen2ContainerdPrivateKubePkg || s.VHD == config.VHDUbuntu2204Gen2ContainerdNetworkIsolatedK8sNotCached { + s.T.Logf("Skipping waagent log validation: legacy VHD %s predates waagent config changes", s.VHD) + return + } + + versions := components.GetExpectedPackageVersions("walinuxagent", "default", "current") + if len(versions) == 0 || versions[0] == "" { + s.T.Log("Skipping waagent log validation: no walinuxagent version in components.json") + return + } + expectedVersion := versions[0] + + const waagentLogFile = "/var/log/waagent.log" + + logContents := execScriptOnVMForScenarioValidateExitCode(ctx, s, + "sudo cat "+waagentLogFile, 0, + "could not read waagent log").stdout + + // 1. Verify AutoUpdate is disabled + require.Contains(s.T, logContents, "AutoUpdate.UpdateToLatestVersion is set to False, not processing the operation", + "waagent.log should confirm AutoUpdate.UpdateToLatestVersion is set to False") + + // 2. Verify the correct version is running as ExtHandler (PID varies) + expectedRunningPattern := fmt.Sprintf("ExtHandler WALinuxAgent-%s running as process", expectedVersion) + require.Contains(s.T, logContents, expectedRunningPattern, + "waagent.log should confirm WALinuxAgent-%s is running as ExtHandler", expectedVersion) + + // 3. Check for ExtHandler errors + extHandlerErrors := execScriptOnVMForScenarioValidateExitCode(ctx, s, + strings.Join([]string{ + "set -e", + fmt.Sprintf("sudo grep 'ERROR ExtHandler' %s || true", waagentLogFile), + }, "\n"), 0, + "failed to scan waagent log for ExtHandler errors") + + errOutput := strings.TrimSpace(extHandlerErrors.stdout) + if errOutput != "" { + logFileName := "waagent-exthandler-errors.log" + if err := writeToFile(s.T, logFileName, logContents); err != nil { + s.T.Logf("Warning: failed to write waagent log to file: %v", err) + } else { + s.T.Logf("Full waagent log written to: %s/%s", testDir(s.T), logFileName) + } + s.T.Fatalf("ExtHandler errors found in waagent.log:\n%s", errOutput) + } + + s.T.Logf("waagent.log validation passed: WALinuxAgent-%s running correctly with no ExtHandler errors", expectedVersion) +} diff --git a/parts/common/components.json b/parts/common/components.json index 69612cc168e..a406f8e2cd5 100644 --- a/parts/common/components.json +++ b/parts/common/components.json @@ -2097,6 +2097,42 @@ } } } + }, + { + "name": "walinuxagent", + "downloadLocation": "/opt/walinuxagent/downloads", + "downloadURIs": { + "default": { + "current": { + "versionsV2": [ + { + "renovateTag": "github-releases=Azure/WALinuxAgent", + "latestVersion": "2.15.0.1" + } + ] + } + }, + "flatcar": { + "current": { + "versionsV2": [ + { + "renovateTag": "", + "latestVersion": "" + } + ] + } + }, + "azurelinux": { + "OSGUARD/v3.0": { + "versionsV2": [ + { + "renovateTag": "", + "latestVersion": "" + } + ] + } + } + } } ], "OCIArtifacts": [ diff --git a/vhdbuilder/packer/install_walinuxagent.py b/vhdbuilder/packer/install_walinuxagent.py index 7d1beccf458..16c5f47e764 100644 --- a/vhdbuilder/packer/install_walinuxagent.py +++ b/vhdbuilder/packer/install_walinuxagent.py @@ -1,19 +1,20 @@ #!/usr/bin/env python3 -"""Install WALinuxAgent from the Azure wireserver GAFamily manifest. +"""Install WALinuxAgent from the Azure wireserver manifest. -Queries the wireserver to discover the target GAFamily version of WALinuxAgent, -downloads the matching zip from the manifest, and installs it under +Queries the wireserver to discover the manifest URL for WALinuxAgent, +then downloads the zip for the *specified* version and installs it under /var/lib/waagent/WALinuxAgent-/. -This lets the waagent daemon pick up the correct version locally without -downloading from the network at provisioning time. +The target version is passed explicitly (from components.json) rather than +being discovered from the GAFamily block in the extensions config. Usage: - python3 install_walinuxagent.py + python3 install_walinuxagent.py Arguments: download_dir Directory to store the downloaded zip for provenance tracking. wireserver_url Base URL of the Azure wireserver (e.g. http://168.63.129.16:80). + version Target WALinuxAgent version (e.g. 2.15.0.1) from components.json. Exit codes: 0 Success @@ -34,7 +35,7 @@ import xml.etree.ElementTree as ET import zipfile from html import unescape as html_unescape -from typing import Optional, Tuple +from typing import Optional # Retry configuration for wireserver requests MAX_RETRIES = 10 @@ -137,21 +138,12 @@ def extract_extensions_config_url(goalstate_xml: str) -> str: return url -def extract_ga_family_info(extensions_config_xml: str) -> Tuple[str, str]: - """Extract the GAFamily version and first manifest URI from extensions config. +def extract_ga_family_manifest_uri(extensions_config_xml: str) -> str: + """Extract the GAFamily manifest URI from extensions config. Returns: - A tuple of (version, manifest_uri). + The manifest URI string. """ - # Use regex with DOTALL since the GAFamily block spans multiple lines - version_match = re.search( - r".*?([^<]+)", - extensions_config_xml, - re.DOTALL, - ) - if not version_match: - raise RuntimeError("No GAFamily version found in extensions config") - uri_match = re.search( r".*?([^<]+)", extensions_config_xml, @@ -160,9 +152,7 @@ def extract_ga_family_info(extensions_config_xml: str) -> Tuple[str, str]: if not uri_match: raise RuntimeError("No GAFamily manifest URI found in extensions config") - version = version_match.group(1).strip() - manifest_uri = html_unescape(uri_match.group(1).strip()) - return version, manifest_uri + return html_unescape(uri_match.group(1).strip()) def find_zip_url_in_manifest(manifest_xml: str, target_version: str) -> str: @@ -179,20 +169,24 @@ def find_zip_url_in_manifest(manifest_xml: str, target_version: str) -> str: raise RuntimeError(f"Version {target_version} not found in WALinuxAgent manifest") -def install_walinuxagent(download_dir: str, wireserver_url: str) -> None: +def install_walinuxagent(download_dir: str, wireserver_url: str, version: str) -> None: """Main installation logic. 1. Fetch goalstate from wireserver 2. Extract ExtensionsConfig URL from goalstate 3. Fetch extensions config - 4. Extract GAFamily version and manifest URI + 4. Extract GAFamily manifest URI (version comes from components.json) 5. Fetch the manifest 6. Find the zip URL for the target version 7. Download the zip 8. Extract to /var/lib/waagent/WALinuxAgent-/ 9. Copy zip to download_dir for provenance tracking """ - print("Installing WALinuxAgent from wireserver GAFamily manifest...") + # Validate version is a safe string (e.g. "2.15.0.1") before using in paths. + if not re.match(r"^[0-9]+(\.[0-9]+)*$", version): + raise RuntimeError(f"Version contains unexpected characters: {version!r}") + + print(f"Installing WALinuxAgent {version} from wireserver manifest...") # Step 1: Fetch goalstate goalstate_url = f"{wireserver_url}/machine/?comp=goalstate" @@ -206,14 +200,10 @@ def install_walinuxagent(download_dir: str, wireserver_url: str) -> None: print("Fetching extensions config...") extensions_config = fetch_url(extensions_config_url, headers=WIRESERVER_HEADERS, silent=True) - # Step 4: Extract GAFamily version and manifest URI - version, manifest_url = extract_ga_family_info(extensions_config) - # Validate version is a safe string (e.g. "2.9.1.1") before using in paths. - # WALinuxAgent versions are dot-separated digits only. - if not re.match(r"^[0-9]+(\.[0-9]+)*$", version): - raise RuntimeError(f"GAFamily version contains unexpected characters: {version!r}") + # Step 4: Extract manifest URI from GAFamily block + manifest_url = extract_ga_family_manifest_uri(extensions_config) - print(f"GAFamily version: {version}") + print(f"Target version (from components.json): {version}") # Step 5: Fetch the manifest (silent to avoid logging SAS token) print(f"Fetching manifest from {strip_sas_token(manifest_url)}") @@ -257,15 +247,16 @@ def install_walinuxagent(download_dir: str, wireserver_url: str) -> None: def main() -> int: - if len(sys.argv) != 3: - print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + if len(sys.argv) != 4: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) return 1 download_dir = sys.argv[1] wireserver_url = sys.argv[2] + version = sys.argv[3] try: - install_walinuxagent(download_dir, wireserver_url) + install_walinuxagent(download_dir, wireserver_url, version) except Exception as exc: print(f"ERROR: {exc}", file=sys.stderr) return 1 diff --git a/vhdbuilder/packer/post-deprovision-walinuxagent.sh b/vhdbuilder/packer/post-deprovision-walinuxagent.sh index 3683ed31e64..21ed46a0fbb 100755 --- a/vhdbuilder/packer/post-deprovision-walinuxagent.sh +++ b/vhdbuilder/packer/post-deprovision-walinuxagent.sh @@ -1,9 +1,10 @@ #!/bin/bash -eu # Post-deprovision WALinuxAgent install script. # Called by packer inline block AFTER 'waagent -force -deprovision+user', -# which clears /var/lib/waagent/. This script installs the latest -# WALinuxAgent from the wireserver GAFamily manifest so the agent daemon -# can pick it up locally without downloading at provisioning time. +# which clears /var/lib/waagent/. This script reads the target WALinuxAgent +# version from components.json and installs it from the wireserver manifest +# so the agent daemon can pick it up locally without downloading at +# provisioning time. # # NOTE: -x is intentionally omitted to avoid leaking SAS tokens from # wireserver manifest/blob URLs in packer build logs. @@ -60,6 +61,15 @@ if [ "$OS_VARIANT_ID" != "OSGUARD" ]; then # Configuration WALINUXAGENT_DOWNLOAD_DIR="/opt/walinuxagent/downloads" WALINUXAGENT_WIRESERVER_URL="http://168.63.129.16:80" + COMPONENTS_FILEPATH="/opt/azure/components.json" + + # Read WALinuxAgent version from components.json. + WALINUXAGENT_VERSION=$(jq -r '.Packages[] | select(.name == "walinuxagent") | .downloadURIs.default.current.versionsV2[0].latestVersion' "${COMPONENTS_FILEPATH}") + if [ -z "${WALINUXAGENT_VERSION}" ] || [ "${WALINUXAGENT_VERSION}" = "null" ] || [ "${WALINUXAGENT_VERSION}" = "" ]; then + echo "ERROR: Could not read walinuxagent version from ${COMPONENTS_FILEPATH}" >&2 + exit 1 + fi + echo "WALinuxAgent target version from components.json: ${WALINUXAGENT_VERSION}" # DNS will be broken on AzLinux after deprovision because # 'waagent -deprovision' clears /etc/resolv.conf. @@ -90,10 +100,10 @@ if [ "$OS_VARIANT_ID" != "OSGUARD" ]; then echo "Temporarily set DNS to Azure DNS for manifest download" fi - # Install WALinuxAgent from wireserver GAFamily manifest. + # Install WALinuxAgent from wireserver manifest using the version from components.json. # Uses a standalone Python script (stdlib only) for wireserver HTTP, XML parsing, - # and zip extraction — replacing inline python3 one-liners that were in bash. - python3 /opt/azure/containers/install_walinuxagent.py "${WALINUXAGENT_DOWNLOAD_DIR}" "${WALINUXAGENT_WIRESERVER_URL}" + # and zip extraction. + python3 /opt/azure/containers/install_walinuxagent.py "${WALINUXAGENT_DOWNLOAD_DIR}" "${WALINUXAGENT_WIRESERVER_URL}" "${WALINUXAGENT_VERSION}" # Configure waagent.conf to pick up the pre-cached agent from disk: # - AutoUpdate.Enabled=y tells the daemon to look for newer agent versions on disk @@ -109,6 +119,10 @@ if [ "$OS_VARIANT_ID" != "OSGUARD" ]; then echo "WALinuxAgent installed and waagent.conf configured post-deprovision" + # Log the installed version to VHD release notes + VHD_LOGS_FILEPATH=/opt/azure/vhd-install.complete + echo " - WALinuxAgent version ${WALINUXAGENT_VERSION}" >> ${VHD_LOGS_FILEPATH} + else echo "Skipping WALinuxAgent manifest install on AzureLinux OSGuard" fi diff --git a/vhdbuilder/packer/test/linux-vhd-content-test.sh b/vhdbuilder/packer/test/linux-vhd-content-test.sh index f1ff8868f12..8ce7644c6e4 100644 --- a/vhdbuilder/packer/test/linux-vhd-content-test.sh +++ b/vhdbuilder/packer/test/linux-vhd-content-test.sh @@ -1505,45 +1505,52 @@ testBccTools () { return 0 } -# testWALinuxAgentInstalled verifies that the WALinuxAgent GAFamily version was -# installed post-deprovision and that waagent.conf is configured to use it. +# testWALinuxAgentInstalled verifies that the WALinuxAgent version from +# components.json was installed post-deprovision via the wireserver manifest +# and that waagent.conf is configured to use it. # The test runs on a VM booted from the captured VHD image, so the post-deprovision # script has already executed and self-deleted. We verify its *results*: -# 1. At least one WALinuxAgent-* directory exists under /var/lib/waagent/ +# 1. WALinuxAgent- directory exists under /var/lib/waagent/ matching components.json # 2. The directory contains the expected artifacts (bin/, HandlerManifest.json, manifest.xml) # 3. waagent.conf has AutoUpdate.Enabled=y and AutoUpdate.UpdateToLatestVersion=n testWALinuxAgentInstalled() { local test="testWALinuxAgentInstalled" echo "$test:Start" - # Check that at least one WALinuxAgent-* directory was installed - local -a dirs - mapfile -t dirs < <(find /var/lib/waagent -maxdepth 1 -type d -name "WALinuxAgent-*" 2>/dev/null | sort -V) - local dirCount=${#dirs[@]} - if [ "$dirCount" -lt 1 ]; then - err "$test" "Expected at least 1 WALinuxAgent directory under /var/lib/waagent/, found ${dirCount}" + # Read the expected version from components.json + local expectedVersion + expectedVersion=$(jq -r '.Packages[] | select(.name == "walinuxagent") | .downloadURIs.default.current.versionsV2[0].latestVersion' "${COMPONENTS_FILEPATH}") + if [ -z "${expectedVersion}" ] || [ "${expectedVersion}" = "null" ]; then + err "$test" "Could not read walinuxagent version from ${COMPONENTS_FILEPATH}" return 1 fi - echo "$test: Found ${dirCount} WALinuxAgent directories: ${dirs[*]}" + echo "$test: Expected WALinuxAgent version from components.json: ${expectedVersion}" - # Validate the newest directory (highest version) has expected artifacts - local installDir="${dirs[-1]}" - echo "$test: Validating pre-cached agent directory ${installDir}" + # Check that the exact expected version directory exists + local expectedDir="/var/lib/waagent/WALinuxAgent-${expectedVersion}" + if [ ! -d "${expectedDir}" ]; then + local actual + actual=$(find /var/lib/waagent -maxdepth 1 -type d -name "WALinuxAgent-*" 2>/dev/null || true) + err "$test" "Expected directory ${expectedDir} not found. Found: ${actual:-none}" + return 1 + fi + echo "$test: Found expected directory ${expectedDir}" + # Validate the directory has expected artifacts local expectedFiles=("HandlerManifest.json" "manifest.xml") for f in "${expectedFiles[@]}"; do - if [ ! -f "${installDir}/${f}" ]; then - err "$test" "Expected file ${f} not found in ${installDir}, contents: $(ls -al "${installDir}")" + if [ ! -f "${expectedDir}/${f}" ]; then + err "$test" "Expected file ${f} not found in ${expectedDir}, contents: $(ls -al "${expectedDir}")" return 1 fi - echo "$test: Found expected file ${installDir}/${f}" + echo "$test: Found expected file ${expectedDir}/${f}" done - if [ ! -d "${installDir}/bin" ]; then - err "$test" "bin/ directory not found in ${installDir}, contents: $(ls -al "${installDir}")" + if [ ! -d "${expectedDir}/bin" ]; then + err "$test" "bin/ directory not found in ${expectedDir}, contents: $(ls -al "${expectedDir}")" return 1 fi - echo "$test: Found bin/ directory in ${installDir}" + echo "$test: Found bin/ directory in ${expectedDir}" # Verify waagent.conf has the expected AutoUpdate settings if grep -q '^AutoUpdate.Enabled=y' /etc/waagent.conf; then