Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -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=(?<packageName>[^\"]+)\",\\s*\"latestVersion\":\\s*\"(?<currentValue>[^\"]+)\""
],
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^v(?<version>.*)$",
"autoReplaceStringTemplate": "\"renovateTag\": \"github-releases={{{packageName}}}\",\n \"latestVersion\": \"{{{newValue}}}\""
},
{
"customType": "regex",
"description": "update version line in any cse_*.sh",
Expand Down
1 change: 1 addition & 0 deletions e2e/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
52 changes: 52 additions & 0 deletions e2e/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -2005,3 +2006,54 @@ 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
func ValidateWaagentLog(ctx context.Context, s *Scenario) {
s.T.Helper()

versions := components.GetExpectedPackageVersions("walinuxagent", "default", "current")
if len(versions) == 0 || versions[0] == "<SKIP>" {
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 'ExtHandler' %s | grep -iE 'ERROR|CRITICAL' || 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)
}
36 changes: 36 additions & 0 deletions parts/common/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<DO_NOT_UPDATE>",
"latestVersion": "<SKIP>"
}
]
}
},
"azurelinux": {
"OSGUARD/v3.0": {
"versionsV2": [
{
"renovateTag": "<DO_NOT_UPDATE>",
"latestVersion": "<SKIP>"
}
]
}
}
}
}
],
"OCIArtifacts": [
Expand Down
61 changes: 26 additions & 35 deletions vhdbuilder/packer/install_walinuxagent.py
Original file line number Diff line number Diff line change
@@ -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-<version>/.

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 <download_dir> <wireserver_url>
python3 install_walinuxagent.py <download_dir> <wireserver_url> <version>

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
Expand All @@ -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
Expand Down Expand Up @@ -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"<GAFamily>.*?<Version>([^<]+)</Version>",
extensions_config_xml,
re.DOTALL,
)
if not version_match:
raise RuntimeError("No GAFamily version found in extensions config")

uri_match = re.search(
r"<GAFamily>.*?<Uri>([^<]+)</Uri>",
extensions_config_xml,
Expand All @@ -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:
Expand All @@ -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-<version>/
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"
Expand All @@ -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)}")
Expand Down Expand Up @@ -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]} <download_dir> <wireserver_url>", file=sys.stderr)
if len(sys.argv) != 4:
print(f"Usage: {sys.argv[0]} <download_dir> <wireserver_url> <version>", 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
Expand Down
26 changes: 20 additions & 6 deletions vhdbuilder/packer/post-deprovision-walinuxagent.sh
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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}" = "<SKIP>" ]; 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.
Expand Down Expand Up @@ -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
Expand All @@ -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
5 changes: 3 additions & 2 deletions vhdbuilder/packer/test/linux-vhd-content-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1505,8 +1505,9 @@ 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/
Expand Down
Loading