CBL-Mariner/SPECS/cloud-init/0003-feat-azure-add-support...

409 строки
15 KiB
Diff

From 8932242a65bae5504ba45134091767f215a441fa Mon Sep 17 00:00:00 2001
From: Ksenija Stanojevic <ksenija.stanojevic@gmail.com>
Date: Mon, 15 Jul 2024 18:48:19 -0700
Subject: [PATCH 3/3] feat(azure): add support for azure-proxy-agent
NOTE change for azurelinux :
This patch has some addtional modifications in the test_azure.py. Those changes remove
some tests which would fail to run:
1. removing unit tests for imds.headers_cb in test_no_pps, which is not implement
in our current version of cloud-init
2. remove checking for distro=self.azure_ds.distro in test_no_pps, this is due to
a behavior difference in mock_azure_get_metadata_from_fabric which would not return
self.azure_ds.distro
3. remove mock_report_dmesg_to_kvp test in test_no_pps, as mock_report_dmesg_to_kvp is not
implemented in our current version of cloud-init
---
cloudinit/sources/DataSourceAzure.py | 40 ++++
cloudinit/sources/azure/errors.py | 19 +-
tests/unittests/sources/test_azure.py | 254 ++++++++++++++++++++++++++
3 files changed, 312 insertions(+), 1 deletion(-)
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index dc2b79a3a..c2f74e173 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -483,6 +483,41 @@ class DataSourceAzure(sources.DataSource):
or self._ephemeral_dhcp_ctx.lease is None
)
+ def _check_azure_proxy_agent_status(self) -> None:
+ """Check if azure-proxy-agent is ready for communication with WS/IMDS.
+ If ProvisionGuestProxyAgent is true, query azure-proxy-agent status,
+ waiting up to 120 seconds for the proxy to negotiate with Wireserver
+ and configure an eBPF proxy. Once azure-proxy-agent is ready,
+ it will exit with code 0 and cloud-init can then expect to be able to
+ communicate with these services.
+ Fail deployment if azure-proxy-agent is not found or otherwise returns
+ an error.
+ For more information, check out:
+ https://github.com/azure/guestproxyagent
+ """
+ try:
+ cmd = [
+ "azure-proxy-agent",
+ "--status",
+ "--wait",
+ "120",
+ ]
+ out, err = subp.subp(cmd)
+ report_diagnostic_event(
+ "Running azure-proxy-agent %s resulted"
+ "in stderr output: %s with stdout: %s" % (cmd, err, out),
+ logger_func=LOG.debug,
+ )
+ except subp.ProcessExecutionError as error:
+ if isinstance(error.reason, FileNotFoundError):
+ report_error = errors.ReportableErrorProxyAgentNotFound()
+ self._report_failure(report_error)
+ else:
+ reportable_error = (
+ errors.ReportableErrorProxyAgentStatusFailure(error)
+ )
+ self._report_failure(reportable_error)
+
@azure_ds_telemetry_reporter
def crawl_metadata(self):
"""Walk all instance metadata sources returning a dict on success.
@@ -566,6 +601,11 @@ class DataSourceAzure(sources.DataSource):
imds_md = {}
if self._is_ephemeral_networking_up():
+ # check if azure-proxy-agent is enabled in the ovf-env.xml file.
+ # azure-proxy-agent feature is opt-in and disabled by default.
+ if cfg.get("ProvisionGuestProxyAgent"):
+ self._check_azure_proxy_agent_status()
+
imds_md = self.get_metadata_from_imds(report_failure=True)
if not imds_md and ovf_source is None:
diff --git a/cloudinit/sources/azure/errors.py b/cloudinit/sources/azure/errors.py
index 966725b00..b331cd686 100644
--- a/cloudinit/sources/azure/errors.py
+++ b/cloudinit/sources/azure/errors.py
@@ -12,7 +12,7 @@ from typing import Any, Dict, List, Optional
import requests
-from cloudinit import version
+from cloudinit import subp, version
from cloudinit.sources.azure import identity
from cloudinit.url_helper import UrlError
@@ -151,3 +151,20 @@ class ReportableErrorUnhandledException(ReportableError):
self.supporting_data["exception"] = repr(exception)
self.supporting_data["traceback_base64"] = trace_base64
+
+
+class ReportableErrorProxyAgentNotFound(ReportableError):
+ def __init__(self) -> None:
+ super().__init__(
+ "Unable to activate Azure Guest Proxy Agent."
+ "azure-proxy-agent not found"
+ )
+
+
+class ReportableErrorProxyAgentStatusFailure(ReportableError):
+ def __init__(self, exception: subp.ProcessExecutionError) -> None:
+ super().__init__("azure-proxy-agent status failure")
+
+ self.supporting_data["exit_code"] = exception.exit_code
+ self.supporting_data["stdout"] = exception.stdout
+ self.supporting_data["stderr"] = exception.stderr
diff -ruN a/tests/unittests/sources/test_azure.py b/tests/unittests/sources/test_azure.py
--- a/tests/unittests/sources/test_azure.py 2023-08-28 09:20:24.000000000 -0700
+++ b/tests/unittests/sources/test_azure.py 2024-09-06 11:30:27.992040291 -0700
@@ -1,6 +1,7 @@
# This file is part of cloud-init. See LICENSE file for license information.
import copy
+import datetime
import json
import os
import stat
@@ -49,6 +50,16 @@
@pytest.fixture
+def mock_wrapping_report_failure(azure_ds):
+ with mock.patch.object(
+ azure_ds,
+ "_report_failure",
+ wraps=azure_ds._report_failure,
+ ) as m:
+ yield m
+
+
+@pytest.fixture
def mock_azure_helper_readurl():
with mock.patch(
"cloudinit.sources.helpers.azure.url_helper.readurl", autospec=True
@@ -254,6 +265,14 @@
@pytest.fixture
+def mock_timestamp():
+ timestamp = datetime.datetime.utcnow()
+ with mock.patch.object(errors, "datetime", autospec=True) as m:
+ m.utcnow.return_value = timestamp
+ yield timestamp
+
+
+@pytest.fixture
def mock_util_ensure_dir():
with mock.patch(
MOCKPATH + "util.ensure_dir",
@@ -3649,6 +3668,76 @@
}
def test_no_pps(self):
+ ovf = construct_ovf_env(provision_guest_proxy_agent=False)
+ md, ud, cfg = dsaz.read_azure_ovf(ovf)
+ self.mock_util_mount_cb.return_value = (md, ud, cfg, {})
+ self.mock_readurl.side_effect = [
+ mock.MagicMock(contents=json.dumps(self.imds_md).encode()),
+ ]
+ self.mock_azure_get_metadata_from_fabric.return_value = []
+
+ self.azure_ds._check_and_get_data()
+
+ assert self.mock_subp_subp.mock_calls == []
+
+ # Verify DHCP is setup once.
+ assert self.mock_wrapping_setup_ephemeral_networking.mock_calls == [
+ mock.call(timeout_minutes=20)
+ ]
+ assert self.mock_net_dhcp_maybe_perform_dhcp_discovery.mock_calls == [
+ mock.call(
+ self.azure_ds.distro,
+ None,
+ dsaz.dhcp_log_cb,
+ )
+ ]
+ assert self.azure_ds._wireserver_endpoint == "10.11.12.13"
+ assert self.azure_ds._is_ephemeral_networking_up() is False
+
+ # Verify DMI usage.
+ assert self.mock_dmi_read_dmi_data.mock_calls == [
+ mock.call("chassis-asset-tag"),
+ mock.call("system-uuid"),
+ ]
+ assert (
+ self.azure_ds.metadata["instance-id"]
+ == "50109936-ef07-47fe-ac82-890c853f60d5"
+ )
+
+ # Verify IMDS metadata.
+ assert self.azure_ds.metadata["imds"] == self.imds_md
+
+ # Verify reporting ready once.
+ assert self.mock_azure_get_metadata_from_fabric.mock_calls == [
+ mock.call(
+ endpoint="10.11.12.13",
+ iso_dev="/dev/sr0",
+ pubkey_info=None,
+ )
+ ]
+
+ # Verify netlink.
+ assert self.mock_netlink.mock_calls == []
+
+ # Verify no reported_ready marker written.
+ assert self.wrapped_util_write_file.mock_calls == []
+ assert self.patched_reported_ready_marker_path.exists() is False
+
+ # Verify reports via KVP.
+ assert len(self.mock_kvp_report_failure_to_host.mock_calls) == 0
+ assert len(self.mock_azure_report_failure_to_fabric.mock_calls) == 0
+ assert len(self.mock_kvp_report_success_to_host.mock_calls) == 1
+
+
+ def test_no_pps_gpa(self):
+ """test full provisioning scope when azure-proxy-agent
+ is enabled and running."""
+ self.mock_subp_subp.side_effect = [
+ subp.SubpResult("Guest Proxy Agent running", ""),
+ ]
+ ovf = construct_ovf_env(provision_guest_proxy_agent=True)
+ md, ud, cfg = dsaz.read_azure_ovf(ovf)
+ self.mock_util_mount_cb.return_value = (md, ud, cfg, {})
self.mock_readurl.side_effect = [
mock.MagicMock(contents=json.dumps(self.imds_md).encode()),
]
@@ -3656,6 +3745,11 @@
self.azure_ds._check_and_get_data()
+ assert self.mock_subp_subp.mock_calls == [
+ mock.call(
+ ["azure-proxy-agent", "--status", "--wait", "120"],
+ ),
+ ]
assert self.mock_readurl.mock_calls == [
mock.call(
"http://169.254.169.254/metadata/instance?"
@@ -3713,6 +3807,92 @@
# Verify reports via KVP.
assert len(self.mock_kvp_report_failure_to_host.mock_calls) == 0
+ assert len(self.mock_azure_report_failure_to_fabric.mock_calls) == 0
+ assert len(self.mock_kvp_report_success_to_host.mock_calls) == 1
+
+ def test_no_pps_gpa_fail(self):
+ """test full provisioning scope when azure-proxy-agent is enabled and
+ throwing an exception during provisioning."""
+ self.mock_subp_subp.side_effect = [
+ subp.ProcessExecutionError(
+ cmd=["failed", "azure-proxy-agent"],
+ stdout="test_stdout",
+ stderr="test_stderr",
+ exit_code=4,
+ ),
+ ]
+ ovf = construct_ovf_env(provision_guest_proxy_agent=True)
+ md, ud, cfg = dsaz.read_azure_ovf(ovf)
+ self.mock_util_mount_cb.return_value = (md, ud, cfg, {})
+ self.mock_readurl.side_effect = [
+ mock.MagicMock(contents=json.dumps(self.imds_md).encode()),
+ ]
+ self.mock_azure_get_metadata_from_fabric.return_value = []
+
+ self.azure_ds._check_and_get_data()
+ assert self.mock_subp_subp.mock_calls == [
+ mock.call(
+ ["azure-proxy-agent", "--status", "--wait", "120"],
+ ),
+ ]
+ assert self.mock_readurl.mock_calls == [
+ mock.call(
+ "http://169.254.169.254/metadata/instance?"
+ "api-version=2021-08-01&extended=true",
+ timeout=30,
+ headers={"Metadata": "true"},
+ exception_cb=mock.ANY,
+ infinite=True,
+ log_req_resp=True,
+ ),
+ ]
+
+ # Verify DHCP is setup once.
+ assert self.mock_wrapping_setup_ephemeral_networking.mock_calls == [
+ mock.call(timeout_minutes=20)
+ ]
+ assert self.mock_net_dhcp_maybe_perform_dhcp_discovery.mock_calls == [
+ mock.call(
+ self.azure_ds.distro,
+ None,
+ dsaz.dhcp_log_cb,
+ )
+ ]
+ assert self.azure_ds._wireserver_endpoint == "10.11.12.13"
+ assert self.azure_ds._is_ephemeral_networking_up() is False
+
+ # Verify DMI usage.
+ assert self.mock_dmi_read_dmi_data.mock_calls == [
+ mock.call("chassis-asset-tag"),
+ mock.call("system-uuid"),
+ mock.call("system-uuid"),
+ ]
+ assert (
+ self.azure_ds.metadata["instance-id"]
+ == "50109936-ef07-47fe-ac82-890c853f60d5"
+ )
+
+ # Verify IMDS metadata.
+ assert self.azure_ds.metadata["imds"] == self.imds_md
+
+ ### BACKPORT NOTE: 23.3 _will_ report ready later after failure.
+ ### In newer versions there will be no call to report ready after failure.
+ assert self.mock_azure_get_metadata_from_fabric.mock_calls == [
+ mock.call(
+ endpoint="10.11.12.13", iso_dev="/dev/sr0", pubkey_info=None
+ )
+ ]
+
+ # Verify netlink.
+ assert self.mock_netlink.mock_calls == []
+
+ # Verify no reported_ready marker written.
+ assert self.wrapped_util_write_file.mock_calls == []
+ assert self.patched_reported_ready_marker_path.exists() is False
+
+ # Verify reports via KVP.
+ assert len(self.mock_kvp_report_failure_to_host.mock_calls) == 1
+ assert len(self.mock_azure_report_failure_to_fabric.mock_calls) == 1
assert len(self.mock_kvp_report_success_to_host.mock_calls) == 1
def test_running_pps(self):
@@ -4292,6 +4472,64 @@
assert len(self.mock_kvp_report_success_to_host.mock_calls) == 1
+class TestCheckAzureProxyAgent:
+ @pytest.fixture(autouse=True)
+ def proxy_setup(
+ self,
+ azure_ds,
+ mock_subp_subp,
+ caplog,
+ mock_wrapping_report_failure,
+ mock_timestamp,
+ ):
+ self.azure_ds = azure_ds
+ self.mock_subp_subp = mock_subp_subp
+ self.caplog = caplog
+ self.mock_wrapping_report_failure = mock_wrapping_report_failure
+ self.mock_timestamp = mock_timestamp
+
+ def test_check_azure_proxy_agent_status(self):
+ self.mock_subp_subp.side_effect = [
+ subp.SubpResult("Guest Proxy Agent running", ""),
+ ]
+ self.azure_ds._check_azure_proxy_agent_status()
+ assert "Running azure-proxy-agent" in self.caplog.text
+ assert self.mock_wrapping_report_failure.mock_calls == []
+
+ def test_check_azure_proxy_agent_status_notfound(self):
+ exception = subp.ProcessExecutionError(reason=FileNotFoundError())
+ self.mock_subp_subp.side_effect = [
+ exception,
+ ]
+ self.azure_ds._check_azure_proxy_agent_status()
+ assert "azure-proxy-agent not found" in self.caplog.text
+ assert self.mock_wrapping_report_failure.mock_calls == [
+ mock.call(
+ errors.ReportableErrorProxyAgentNotFound(),
+ ),
+ ]
+
+ def test_check_azure_proxy_agent_status_failure(self):
+ exception = subp.ProcessExecutionError(
+ cmd=["failed", "azure-proxy-agent"],
+ stdout="test_stdout",
+ stderr="test_stderr",
+ exit_code=4,
+ )
+ self.mock_subp_subp.side_effect = [
+ exception,
+ ]
+ self.azure_ds._check_azure_proxy_agent_status()
+ assert "azure-proxy-agent status failure" in self.caplog.text
+ assert self.mock_wrapping_report_failure.mock_calls == [
+ mock.call(
+ errors.ReportableErrorProxyAgentStatusFailure(
+ exception=exception
+ ),
+ ),
+ ]
+
+
class TestGetMetadataFromImds:
@pytest.mark.parametrize("report_failure", [False, True])
@pytest.mark.parametrize(
--
2.34.1