From 05e53957e59727bed7690ac4e514157a85f7e141 Mon Sep 17 00:00:00 2001 From: eabdullin Date: Mon, 15 Sep 2025 11:48:53 +0000 Subject: [PATCH] import CS cloud-init-24.4-7.el9 --- ...n-exit-code-in-cloud-init-status-for.patch | 68 ++ ...source-support-crawl-metadata-at-onc.patch | 1000 +++++++++++++++++ ...t-to-identify-non-x86-OpenStack-inst.patch | 177 +++ ...d-bring-up-individual-network-conns-.patch | 67 ++ ...le-in-ds-identify-on-no-datasources-.patch | 90 ++ SPECS/cloud-init.spec | 30 +- 6 files changed, 1431 insertions(+), 1 deletion(-) create mode 100644 SOURCES/0003-downstream-Retain-exit-code-in-cloud-init-status-for.patch create mode 100644 SOURCES/ci-feat-aliyun-datasource-support-crawl-metadata-at-onc.patch create mode 100644 SOURCES/ci-fix-Don-t-attempt-to-identify-non-x86-OpenStack-inst.patch create mode 100644 SOURCES/ci-fix-NM-reload-and-bring-up-individual-network-conns-.patch create mode 100644 SOURCES/ci-fix-strict-disable-in-ds-identify-on-no-datasources-.patch diff --git a/SOURCES/0003-downstream-Retain-exit-code-in-cloud-init-status-for.patch b/SOURCES/0003-downstream-Retain-exit-code-in-cloud-init-status-for.patch new file mode 100644 index 0000000..264715a --- /dev/null +++ b/SOURCES/0003-downstream-Retain-exit-code-in-cloud-init-status-for.patch @@ -0,0 +1,68 @@ +From d211a3a03b548a759c4a64e63044b2ea034f2999 Mon Sep 17 00:00:00 2001 +From: Ani Sinha +Date: Tue, 12 Mar 2024 12:52:10 +0530 +Subject: [PATCH] downstream: Retain exit code in cloud-init status for + recoverable errors + +RH-Author: Ani Sinha +RH-MergeRequest: 71: Retain exit code in cloud-init status for recoverable errors +RH-Jira: RHEL-28549 +RH-Acked-by: Emanuele Giuseppe Esposito +RH-Acked-by: Cathy Avery +RH-Commit: [1/1] 00934ade88c481c012bc1947fa44e5ed59f82858 (anisinha/cloud-init) + +Version 23.4 of cloud-init changed the status code reported by cloud-init for +recoverable errors from 0 to 2. Please see the commit +70acb7f2a30d58 ("Add support for cloud-init "degraded" state (#4500)") + +This change has the potential to break customers who are expecting a 0 status +and where warnings can be expected. Hence, revert the status code from 2 to 0 +even in case of recoverable errors. This retains the old behavior and hence +avoids breaking scripts and software stack that expects 0 on the end user side. + +Cannonical has made a similar change downstream for similar reasons. Please see +https://bugs.launchpad.net/ubuntu/+source/cloud-init/+bug/2048522 +and the corresponding downstream patch: +https://github.com/canonical/cloud-init/pull/4747/commits/adce34bfd214e4eecdf87329486f30f0898dd303 + +This patch has limited risk as it narrowly only restores the old status +code for recoverable errors and does not modify anything else. + +X-downstream-only: true +Signed-off-by: Ani Sinha + +Patch-name: ci-Retain-exit-code-in-cloud-init-status-for-recoverabl.patch +Patch-id: 12 +Patch-present-in-specfile: True +(cherry picked from commit 424eb97cff0bd97967c82214308693481f17a50a) +--- + cloudinit/cmd/status.py | 2 +- + tests/unittests/cmd/test_status.py | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/cloudinit/cmd/status.py b/cloudinit/cmd/status.py +index 98084a492..0dfb9b2f7 100644 +--- a/cloudinit/cmd/status.py ++++ b/cloudinit/cmd/status.py +@@ -251,7 +251,7 @@ def handle_status_args(name, args) -> int: + return 1 + # Recoverable error + elif details.condition_status == ConditionStatus.DEGRADED: +- return 2 ++ return 0 + return 0 + + +diff --git a/tests/unittests/cmd/test_status.py b/tests/unittests/cmd/test_status.py +index 022e4034c..da41fa98f 100644 +--- a/tests/unittests/cmd/test_status.py ++++ b/tests/unittests/cmd/test_status.py +@@ -664,7 +664,7 @@ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin + }, + None, + MyArgs(long=False, wait=False, format="json"), +- 2, ++ 0, + { + "boot_status_code": "enabled-by-kernel-command-line", + "datasource": "nocloud", diff --git a/SOURCES/ci-feat-aliyun-datasource-support-crawl-metadata-at-onc.patch b/SOURCES/ci-feat-aliyun-datasource-support-crawl-metadata-at-onc.patch new file mode 100644 index 0000000..dad8230 --- /dev/null +++ b/SOURCES/ci-feat-aliyun-datasource-support-crawl-metadata-at-onc.patch @@ -0,0 +1,1000 @@ +From 75eaa90b0581e0533fa50b59b9088f99f738e3cf Mon Sep 17 00:00:00 2001 +From: jinkangkang <1547182170@qq.com> +Date: Thu, 20 Feb 2025 10:55:05 +0800 +Subject: [PATCH] feat: aliyun datasource support crawl metadata at once + (#5942) + +RH-Author: xiachen +RH-MergeRequest: 128: feat: aliyun datasource support crawl metadata at once (#5942) +RH-Jira: RHEL-88658 +RH-Acked-by: Ani Sinha +RH-Acked-by: Emanuele Giuseppe Esposito +RH-Commit: [1/1] 284ed9fb0516bca2d852014ad9da7b48b07f451b (xiachen/cloud-init-centos) + +Obtain metadata information from the new metadata path for better +performance. Fall back to old directory tree format on failure. + +Also: + +- Separate the Alibaba Cloud data source from ec2 and make it independent +- Use network card names to sort routing priorities +- Add vendor data support +- Streamline logic, made possible by separating the datasources + +Based on discussion in: GH-5838 + +(cherry picked from commit 27adc8e598991e0861f45274f91d9fb97cdec636) +Signed-off-by: Amy Chen +--- + cloudinit/sources/DataSourceAliYun.py | 338 ++++++++++++++++++++++++- + cloudinit/sources/DataSourceEc2.py | 9 +- + cloudinit/sources/helpers/aliyun.py | 211 +++++++++++++++ + tests/unittests/sources/test_aliyun.py | 213 ++++++++++++---- + tests/unittests/sources/test_ec2.py | 10 - + 5 files changed, 704 insertions(+), 77 deletions(-) + create mode 100644 cloudinit/sources/helpers/aliyun.py + +diff --git a/cloudinit/sources/DataSourceAliYun.py b/cloudinit/sources/DataSourceAliYun.py +index d674e1fc0..000e15a84 100644 +--- a/cloudinit/sources/DataSourceAliYun.py ++++ b/cloudinit/sources/DataSourceAliYun.py +@@ -2,27 +2,43 @@ + + import copy + import logging +-from typing import List ++from typing import List, Union + + from cloudinit import dmi, sources ++from cloudinit import url_helper as uhelp ++from cloudinit import util + from cloudinit.event import EventScope, EventType +-from cloudinit.sources import DataSourceEc2 as EC2 +-from cloudinit.sources import DataSourceHostname, NicOrder ++from cloudinit.net.dhcp import NoDHCPLeaseError ++from cloudinit.net.ephemeral import EphemeralIPNetwork ++from cloudinit.sources import DataSourceHostname ++from cloudinit.sources.helpers import aliyun, ec2 + + LOG = logging.getLogger(__name__) + + ALIYUN_PRODUCT = "Alibaba Cloud ECS" + + +-class DataSourceAliYun(EC2.DataSourceEc2): ++class DataSourceAliYun(sources.DataSource): + + dsname = "AliYun" + metadata_urls = ["http://100.100.100.200"] + +- # The minimum supported metadata_version from the ec2 metadata apis ++ # The minimum supported metadata_version from the ecs metadata apis + min_metadata_version = "2016-01-01" + extended_metadata_versions: List[str] = [] + ++ # Setup read_url parameters per get_url_params. ++ url_max_wait = 240 ++ url_timeout = 50 ++ ++ # API token for accessing the metadata service ++ _api_token = None ++ # Used to cache calculated network cfg v1 ++ _network_config: Union[str, dict] = sources.UNSET ++ ++ # Whether we want to get network configuration from the metadata service. ++ perform_dhcp_setup = False ++ + # Aliyun metadata server security enhanced mode overwrite + @property + def imdsv2_token_put_header(self): +@@ -32,11 +48,9 @@ class DataSourceAliYun(EC2.DataSourceEc2): + super(DataSourceAliYun, self).__init__(sys_cfg, distro, paths) + self.default_update_events = copy.deepcopy(self.default_update_events) + self.default_update_events[EventScope.NETWORK].add(EventType.BOOT) +- self._fallback_nic_order = NicOrder.NIC_NAME + + def _unpickle(self, ci_pkl_version: int) -> None: + super()._unpickle(ci_pkl_version) +- self._fallback_nic_order = NicOrder.NIC_NAME + + def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False): + hostname = self.metadata.get("hostname") +@@ -51,9 +65,315 @@ class DataSourceAliYun(EC2.DataSourceEc2): + + def _get_cloud_name(self): + if _is_aliyun(): +- return EC2.CloudNames.ALIYUN ++ return self.dsname.lower() ++ return "NO_ALIYUN_METADATA" ++ ++ @property ++ def platform(self): ++ return self.dsname.lower() ++ ++ # IMDSv2 related parameters from the ecs metadata api document ++ @property ++ def api_token_route(self): ++ return "latest/api/token" ++ ++ @property ++ def imdsv2_token_ttl_seconds(self): ++ return "21600" ++ ++ @property ++ def imdsv2_token_redact(self): ++ return [self.imdsv2_token_put_header, self.imdsv2_token_req_header] ++ ++ @property ++ def imdsv2_token_req_header(self): ++ return self.imdsv2_token_put_header + "-ttl-seconds" ++ ++ @property ++ def network_config(self): ++ """Return a network config dict for rendering ENI or netplan files.""" ++ if self._network_config != sources.UNSET: ++ return self._network_config ++ ++ result = {} ++ iface = self.distro.fallback_interface ++ net_md = self.metadata.get("network") ++ if isinstance(net_md, dict): ++ result = aliyun.convert_ecs_metadata_network_config( ++ net_md, ++ fallback_nic=iface, ++ full_network_config=util.get_cfg_option_bool( ++ self.ds_cfg, "apply_full_imds_network_config", True ++ ), ++ ) + else: +- return EC2.CloudNames.NO_EC2_METADATA ++ LOG.warning("Metadata 'network' key not valid: %s.", net_md) ++ return result ++ self._network_config = result ++ return self._network_config ++ ++ def _maybe_fetch_api_token(self, mdurls): ++ """Get an API token for ECS Instance Metadata Service. ++ ++ On ECS. IMDS will always answer an API token, set ++ HttpTokens=optional (default) when create instance will not forcefully ++ use the security-enhanced mode (IMDSv2). ++ ++ https://api.alibabacloud.com/api/Ecs/2014-05-26/RunInstances ++ """ ++ ++ urls = [] ++ url2base = {} ++ url_path = self.api_token_route ++ request_method = "PUT" ++ for url in mdurls: ++ cur = "{0}/{1}".format(url, url_path) ++ urls.append(cur) ++ url2base[cur] = url ++ ++ # use the self._imds_exception_cb to check for Read errors ++ LOG.debug("Fetching Ecs IMDSv2 API Token") ++ ++ response = None ++ url = None ++ url_params = self.get_url_params() ++ try: ++ url, response = uhelp.wait_for_url( ++ urls=urls, ++ max_wait=url_params.max_wait_seconds, ++ timeout=url_params.timeout_seconds, ++ status_cb=LOG.warning, ++ headers_cb=self._get_headers, ++ exception_cb=self._imds_exception_cb, ++ request_method=request_method, ++ headers_redact=self.imdsv2_token_redact, ++ connect_synchronously=False, ++ ) ++ except uhelp.UrlError: ++ # We use the raised exception to interupt the retry loop. ++ # Nothing else to do here. ++ pass ++ ++ if url and response: ++ self._api_token = response ++ return url2base[url] ++ ++ # If we get here, then wait_for_url timed out, waiting for IMDS ++ # or the IMDS HTTP endpoint is disabled ++ return None ++ ++ def wait_for_metadata_service(self): ++ mcfg = self.ds_cfg ++ mdurls = mcfg.get("metadata_urls", self.metadata_urls) ++ ++ # try the api token path first ++ metadata_address = self._maybe_fetch_api_token(mdurls) ++ ++ if metadata_address: ++ self.metadata_address = metadata_address ++ LOG.debug("Using metadata source: '%s'", self.metadata_address) ++ else: ++ LOG.warning("IMDS's HTTP endpoint is probably disabled") ++ return bool(metadata_address) ++ ++ def crawl_metadata(self): ++ """Crawl metadata service when available. ++ ++ @returns: Dictionary of crawled metadata content containing the keys: ++ meta-data, user-data, vendor-data and dynamic. ++ """ ++ if not self.wait_for_metadata_service(): ++ return {} ++ redact = self.imdsv2_token_redact ++ crawled_metadata = {} ++ exc_cb = self._refresh_stale_aliyun_token_cb ++ exc_cb_ud = self._skip_or_refresh_stale_aliyun_token_cb ++ skip_cb = None ++ exe_cb_whole_meta = self._skip_json_path_meta_path_aliyun_cb ++ try: ++ crawled_metadata["user-data"] = aliyun.get_instance_data( ++ self.min_metadata_version, ++ self.metadata_address, ++ headers_cb=self._get_headers, ++ headers_redact=redact, ++ exception_cb=exc_cb_ud, ++ item_name="user-data", ++ ) ++ crawled_metadata["vendor-data"] = aliyun.get_instance_data( ++ self.min_metadata_version, ++ self.metadata_address, ++ headers_cb=self._get_headers, ++ headers_redact=redact, ++ exception_cb=exc_cb_ud, ++ item_name="vendor-data", ++ ) ++ try: ++ result = aliyun.get_instance_meta_data( ++ self.min_metadata_version, ++ self.metadata_address, ++ headers_cb=self._get_headers, ++ headers_redact=redact, ++ exception_cb=exe_cb_whole_meta, ++ ) ++ crawled_metadata["meta-data"] = result ++ except Exception: ++ util.logexc( ++ LOG, ++ "Faild read json meta-data from %s " ++ "fall back directory tree style", ++ self.metadata_address, ++ ) ++ crawled_metadata["meta-data"] = ec2.get_instance_metadata( ++ self.min_metadata_version, ++ self.metadata_address, ++ headers_cb=self._get_headers, ++ headers_redact=redact, ++ exception_cb=exc_cb, ++ retrieval_exception_ignore_cb=skip_cb, ++ ) ++ except Exception: ++ util.logexc( ++ LOG, ++ "Failed reading from metadata address %s", ++ self.metadata_address, ++ ) ++ return {} ++ return crawled_metadata ++ ++ def _refresh_stale_aliyun_token_cb(self, msg, exception): ++ """Exception handler for Ecs to refresh token if token is stale.""" ++ if isinstance(exception, uhelp.UrlError) and exception.code == 401: ++ # With _api_token as None, _get_headers will _refresh_api_token. ++ LOG.debug("Clearing cached Ecs API token due to expiry") ++ self._api_token = None ++ return True # always retry ++ ++ def _skip_retry_on_codes(self, status_codes, cause): ++ """Returns False if cause.code is in status_codes.""" ++ return cause.code not in status_codes ++ ++ def _skip_or_refresh_stale_aliyun_token_cb(self, msg, exception): ++ """Callback will not retry on SKIP_USERDATA_VENDORDATA_CODES or ++ if no token is available.""" ++ retry = self._skip_retry_on_codes(ec2.SKIP_USERDATA_CODES, exception) ++ if not retry: ++ return False # False raises exception ++ return self._refresh_stale_aliyun_token_cb(msg, exception) ++ ++ def _skip_json_path_meta_path_aliyun_cb(self, msg, exception): ++ """Callback will not retry of whole meta_path is not found""" ++ if isinstance(exception, uhelp.UrlError) and exception.code == 404: ++ LOG.warning("whole meta_path is not found, skipping") ++ return False ++ return self._refresh_stale_aliyun_token_cb(msg, exception) ++ ++ def _get_data(self): ++ if self.cloud_name != self.dsname.lower(): ++ return False ++ if self.perform_dhcp_setup: # Setup networking in init-local stage. ++ if util.is_FreeBSD(): ++ LOG.debug("FreeBSD doesn't support running dhclient with -sf") ++ return False ++ try: ++ with EphemeralIPNetwork( ++ self.distro, ++ self.distro.fallback_interface, ++ ipv4=True, ++ ipv6=False, ++ ) as netw: ++ self._crawled_metadata = self.crawl_metadata() ++ LOG.debug( ++ "Crawled metadata service%s", ++ f" {netw.state_msg}" if netw.state_msg else "", ++ ) ++ ++ except NoDHCPLeaseError: ++ return False ++ else: ++ self._crawled_metadata = self.crawl_metadata() ++ if not self._crawled_metadata or not isinstance( ++ self._crawled_metadata, dict ++ ): ++ return False ++ self.metadata = self._crawled_metadata.get("meta-data", {}) ++ self.userdata_raw = self._crawled_metadata.get("user-data", {}) ++ self.vendordata_raw = self._crawled_metadata.get("vendor-data", {}) ++ return True ++ ++ def _refresh_api_token(self, seconds=None): ++ """Request new metadata API token. ++ @param seconds: The lifetime of the token in seconds ++ ++ @return: The API token or None if unavailable. ++ """ ++ ++ if seconds is None: ++ seconds = self.imdsv2_token_ttl_seconds ++ ++ LOG.debug("Refreshing Ecs metadata API token") ++ request_header = {self.imdsv2_token_req_header: seconds} ++ token_url = "{}/{}".format(self.metadata_address, self.api_token_route) ++ try: ++ response = uhelp.readurl( ++ token_url, ++ headers=request_header, ++ headers_redact=self.imdsv2_token_redact, ++ request_method="PUT", ++ ) ++ except uhelp.UrlError as e: ++ LOG.warning( ++ "Unable to get API token: %s raised exception %s", token_url, e ++ ) ++ return None ++ return response.contents ++ ++ def _get_headers(self, url=""): ++ """Return a dict of headers for accessing a url. ++ ++ If _api_token is unset on AWS, attempt to refresh the token via a PUT ++ and then return the updated token header. ++ """ ++ ++ request_token_header = { ++ self.imdsv2_token_req_header: self.imdsv2_token_ttl_seconds ++ } ++ if self.api_token_route in url: ++ return request_token_header ++ if not self._api_token: ++ # If we don't yet have an API token, get one via a PUT against ++ # api_token_route. This _api_token may get unset by a 403 due ++ # to an invalid or expired token ++ self._api_token = self._refresh_api_token() ++ if not self._api_token: ++ return {} ++ return {self.imdsv2_token_put_header: self._api_token} ++ ++ def _imds_exception_cb(self, msg, exception=None): ++ """Fail quickly on proper AWS if IMDSv2 rejects API token request ++ ++ Guidance from Amazon is that if IMDSv2 had disabled token requests ++ by returning a 403, or cloud-init malformed requests resulting in ++ other 40X errors, we want the datasource detection to fail quickly ++ without retries as those symptoms will likely not be resolved by ++ retries. ++ ++ Exceptions such as requests.ConnectionError due to IMDS being ++ temporarily unroutable or unavailable will still retry due to the ++ callsite wait_for_url. ++ """ ++ if isinstance(exception, uhelp.UrlError): ++ # requests.ConnectionError will have exception.code == None ++ if exception.code and exception.code >= 400: ++ if exception.code == 403: ++ LOG.warning( ++ "Ecs IMDS endpoint returned a 403 error. " ++ "HTTP endpoint is disabled. Aborting." ++ ) ++ else: ++ LOG.warning( ++ "Fatal error while requesting Ecs IMDSv2 API tokens" ++ ) ++ raise exception + + + def _is_aliyun(): +diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py +index 10837df6a..0b763b52b 100644 +--- a/cloudinit/sources/DataSourceEc2.py ++++ b/cloudinit/sources/DataSourceEc2.py +@@ -34,7 +34,6 @@ STRICT_ID_DEFAULT = "warn" + + + class CloudNames: +- ALIYUN = "aliyun" + AWS = "aws" + BRIGHTBOX = "brightbox" + ZSTACK = "zstack" +@@ -54,7 +53,7 @@ def skip_404_tag_errors(exception): + + + # Cloud platforms that support IMDSv2 style metadata server +-IDMSV2_SUPPORTED_CLOUD_PLATFORMS = [CloudNames.AWS, CloudNames.ALIYUN] ++IDMSV2_SUPPORTED_CLOUD_PLATFORMS = [CloudNames.AWS] + + # Only trigger hook-hotplug on NICs with Ec2 drivers. Avoid triggering + # it on docker virtual NICs and the like. LP: #1946003 +@@ -768,11 +767,6 @@ def warn_if_necessary(cfgval, cfg): + warnings.show_warning("non_ec2_md", cfg, mode=True, sleep=sleep) + + +-def identify_aliyun(data): +- if data["product_name"] == "Alibaba Cloud ECS": +- return CloudNames.ALIYUN +- +- + def identify_aws(data): + # data is a dictionary returned by _collect_platform_data. + uuid_str = data["uuid"] +@@ -821,7 +815,6 @@ def identify_platform(): + identify_zstack, + identify_e24cloud, + identify_outscale, +- identify_aliyun, + lambda x: CloudNames.UNKNOWN, + ) + for checker in checks: +diff --git a/cloudinit/sources/helpers/aliyun.py b/cloudinit/sources/helpers/aliyun.py +new file mode 100644 +index 000000000..201ceb04b +--- /dev/null ++++ b/cloudinit/sources/helpers/aliyun.py +@@ -0,0 +1,211 @@ ++# This file is part of cloud-init. See LICENSE file for license information. ++ ++import logging ++from typing import MutableMapping ++ ++from cloudinit import net, url_helper, util ++from cloudinit.sources.helpers import ec2 ++ ++LOG = logging.getLogger(__name__) ++ ++ ++def get_instance_meta_data( ++ api_version="latest", ++ metadata_address="http://100.100.100.200", ++ ssl_details=None, ++ timeout=5, ++ retries=5, ++ headers_cb=None, ++ headers_redact=None, ++ exception_cb=None, ++): ++ ud_url = url_helper.combine_url(metadata_address, api_version) ++ ud_url = url_helper.combine_url(ud_url, "meta-data/all") ++ response = url_helper.read_file_or_url( ++ ud_url, ++ ssl_details=ssl_details, ++ timeout=timeout, ++ retries=retries, ++ exception_cb=exception_cb, ++ headers_cb=headers_cb, ++ headers_redact=headers_redact, ++ ) ++ meta_data_raw: object = util.load_json(response.contents) ++ ++ # meta_data_raw is a json object with the following format get ++ # by`meta-data/all` ++ # { ++ # "sub-private-ipv4-list": "", ++ # "dns-conf": { ++ # "nameservers": "100.100.2.136\r\n100.100.2.138" ++ # }, ++ # "zone-id": "cn-hangzhou-i", ++ # "instance": { ++ # "instance-name": "aliyun_vm_test", ++ # "instance-type": "ecs.g7.xlarge" ++ # }, ++ # "disks": { ++ # "bp1cikh4di1xxxx": { ++ # "name": "disk_test", ++ # "id": "d-bp1cikh4di1lf7pxxxx" ++ # } ++ # }, ++ # "instance-id": "i-bp123", ++ # "eipv4": "47.99.152.7", ++ # "private-ipv4": "192.168.0.9", ++ # "hibernation": { ++ # "configured": "false" ++ # }, ++ # "vpc-id": "vpc-bp1yeqg123", ++ # "mac": "00:16:3e:30:3e:ca", ++ # "source-address": "http://mirrors.cloud.aliyuncs.com", ++ # "vswitch-cidr-block": "192.168.0.0/24", ++ # "network": { ++ # "interfaces": { ++ # "macs": { ++ # "00:16:3e:30:3e:ca": { ++ # "vpc-cidr-block": "192.168.0.0/16", ++ # "netmask": "255.255.255.0" ++ # } ++ # } ++ # } ++ # }, ++ # "network-type": "vpc", ++ # "hostname": "aliyun_vm_test", ++ # "region-id": "cn-hangzhou", ++ # "ntp-conf": { ++ # "ntp-servers": "ntp1.aliyun.com\r\nntp2.aliyun.com" ++ # }, ++ # } ++ # Note: For example, in the values of dns conf: the `nameservers` ++ # key is a string, the format is the same as the response from the ++ # `meta-data/dns-conf/nameservers` endpoint. we use the same ++ # serialization method to ensure consistency between ++ # the two methods (directory tree and json path). ++ def _process_dict_values(d): ++ if isinstance(d, dict): ++ return {k: _process_dict_values(v) for k, v in d.items()} ++ elif isinstance(d, list): ++ return [_process_dict_values(item) for item in d] ++ else: ++ return ec2.MetadataLeafDecoder()("", d) ++ ++ return _process_dict_values(meta_data_raw) ++ ++ ++def get_instance_data( ++ api_version="latest", ++ metadata_address="http://100.100.100.200", ++ ssl_details=None, ++ timeout=5, ++ retries=5, ++ headers_cb=None, ++ headers_redact=None, ++ exception_cb=None, ++ item_name=None, ++): ++ ud_url = url_helper.combine_url(metadata_address, api_version) ++ ud_url = url_helper.combine_url(ud_url, item_name) ++ data = b"" ++ support_items_list = ["user-data", "vendor-data"] ++ if item_name not in support_items_list: ++ LOG.error( ++ "aliyun datasource not support the item %s", ++ item_name, ++ ) ++ return data ++ try: ++ response = url_helper.read_file_or_url( ++ ud_url, ++ ssl_details=ssl_details, ++ timeout=timeout, ++ retries=retries, ++ exception_cb=exception_cb, ++ headers_cb=headers_cb, ++ headers_redact=headers_redact, ++ ) ++ data = response.contents ++ except Exception: ++ util.logexc(LOG, "Failed fetching %s from url %s", item_name, ud_url) ++ return data ++ ++ ++def convert_ecs_metadata_network_config( ++ network_md, ++ macs_to_nics=None, ++ fallback_nic=None, ++ full_network_config=True, ++): ++ """Convert ecs metadata to network config version 2 data dict. ++ ++ @param: network_md: 'network' portion of ECS metadata. ++ generally formed as {"interfaces": {"macs": {}} where ++ 'macs' is a dictionary with mac address as key: ++ @param: macs_to_nics: Optional dict of mac addresses and nic names. If ++ not provided, get_interfaces_by_mac is called to get it from the OS. ++ @param: fallback_nic: Optionally provide the primary nic interface name. ++ This nic will be guaranteed to minimally have a dhcp4 configuration. ++ @param: full_network_config: Boolean set True to configure all networking ++ presented by IMDS. This includes rendering secondary IPv4 and IPv6 ++ addresses on all NICs and rendering network config on secondary NICs. ++ If False, only the primary nic will be configured and only with dhcp ++ (IPv4/IPv6). ++ ++ @return A dict of network config version 2 based on the metadata and macs. ++ """ ++ netcfg: MutableMapping = {"version": 2, "ethernets": {}} ++ if not macs_to_nics: ++ macs_to_nics = net.get_interfaces_by_mac() ++ macs_metadata = network_md["interfaces"]["macs"] ++ ++ if not full_network_config: ++ for mac, nic_name in macs_to_nics.items(): ++ if nic_name == fallback_nic: ++ break ++ dev_config: MutableMapping = { ++ "dhcp4": True, ++ "dhcp6": False, ++ "match": {"macaddress": mac.lower()}, ++ "set-name": nic_name, ++ } ++ nic_metadata = macs_metadata.get(mac) ++ if nic_metadata.get("ipv6s"): # Any IPv6 addresses configured ++ dev_config["dhcp6"] = True ++ netcfg["ethernets"][nic_name] = dev_config ++ return netcfg ++ nic_name_2_mac_map = dict() ++ for mac, nic_name in macs_to_nics.items(): ++ nic_metadata = macs_metadata.get(mac) ++ if not nic_metadata: ++ continue # Not a physical nic represented in metadata ++ nic_name_2_mac_map[nic_name] = mac ++ ++ # sorted by nic_name ++ orderd_nic_name_list = sorted( ++ nic_name_2_mac_map.keys(), key=net.natural_sort_key ++ ) ++ for nic_idx, nic_name in enumerate(orderd_nic_name_list): ++ nic_mac = nic_name_2_mac_map[nic_name] ++ nic_metadata = macs_metadata.get(nic_mac) ++ dhcp_override = {"route-metric": (nic_idx + 1) * 100} ++ dev_config = { ++ "dhcp4": True, ++ "dhcp4-overrides": dhcp_override, ++ "dhcp6": False, ++ "match": {"macaddress": nic_mac.lower()}, ++ "set-name": nic_name, ++ } ++ if nic_metadata.get("ipv6s"): # Any IPv6 addresses configured ++ dev_config["dhcp6"] = True ++ dev_config["dhcp6-overrides"] = dhcp_override ++ ++ netcfg["ethernets"][nic_name] = dev_config ++ # Remove route-metric dhcp overrides and routes / routing-policy if only ++ # one nic configured ++ if len(netcfg["ethernets"]) == 1: ++ for nic_name in netcfg["ethernets"].keys(): ++ netcfg["ethernets"][nic_name].pop("dhcp4-overrides") ++ netcfg["ethernets"][nic_name].pop("dhcp6-overrides", None) ++ netcfg["ethernets"][nic_name].pop("routes", None) ++ netcfg["ethernets"][nic_name].pop("routing-policy", None) ++ return netcfg +diff --git a/tests/unittests/sources/test_aliyun.py b/tests/unittests/sources/test_aliyun.py +index 2639302b2..2d61ff8af 100644 +--- a/tests/unittests/sources/test_aliyun.py ++++ b/tests/unittests/sources/test_aliyun.py +@@ -9,46 +9,93 @@ import responses + + from cloudinit import helpers + from cloudinit.sources import DataSourceAliYun as ay +-from cloudinit.sources.DataSourceEc2 import convert_ec2_metadata_network_config ++from cloudinit.sources.helpers.aliyun import ( ++ convert_ecs_metadata_network_config, ++) ++from cloudinit.util import load_json + from tests.unittests import helpers as test_helpers + +-DEFAULT_METADATA = { +- "instance-id": "aliyun-test-vm-00", +- "eipv4": "10.0.0.1", +- "hostname": "test-hostname", +- "image-id": "m-test", +- "launch-index": "0", +- "mac": "00:16:3e:00:00:00", +- "network-type": "vpc", +- "private-ipv4": "192.168.0.1", +- "serial-number": "test-string", +- "vpc-cidr-block": "192.168.0.0/16", +- "vpc-id": "test-vpc", +- "vswitch-id": "test-vpc", +- "vswitch-cidr-block": "192.168.0.0/16", +- "zone-id": "test-zone-1", +- "ntp-conf": { +- "ntp_servers": [ +- "ntp1.aliyun.com", +- "ntp2.aliyun.com", +- "ntp3.aliyun.com", +- ] +- }, +- "source-address": [ +- "http://mirrors.aliyun.com", +- "http://mirrors.aliyuncs.com", +- ], +- "public-keys": { +- "key-pair-1": {"openssh-key": "ssh-rsa AAAAB3..."}, +- "key-pair-2": {"openssh-key": "ssh-rsa AAAAB3..."}, ++DEFAULT_METADATA_RAW = r"""{ ++ "disks": { ++ "bp15spwwhlf8bbbn7xxx": { ++ "id": "d-bp15spwwhlf8bbbn7xxx", ++ "name": "" ++ } ++ }, ++ "dns-conf": { ++ "nameservers": [ ++ "100.100.2.136", ++ "100.100.2.138" ++ ] ++ }, ++ "hibernation": { ++ "configured": "false" ++ }, ++ "instance": { ++ "instance-name": "aliyun-test-vm-00", ++ "instance-type": "ecs.g8i.large", ++ "last-host-landing-time": "2024-11-17 10:02:41", ++ "max-netbw-egress": "2560000", ++ "max-netbw-ingress": "2560000", ++ "virtualization-solution": "ECS Virt", ++ "virtualization-solution-version": "2.0" ++ }, ++ "network": { ++ "interfaces": { ++ "macs": { ++ "00:16:3e:14:59:58": { ++ "gateway": "172.16.101.253", ++ "netmask": "255.255.255.0", ++ "network-interface-id": "eni-bp13i3ed90icgdgaxxxx" ++ } ++ } ++ } ++ }, ++ "ntp-conf": { ++ "ntp-servers": [ ++ "ntp1.aliyun.com", ++ "ntp1.cloud.aliyuncs.com" ++ ] ++ }, ++ "public-keys": { ++ "0": { ++ "openssh-key": "ssh-rsa AAAAB3Nza" + }, +-} ++ "skp-bp1test": { ++ "openssh-key": "ssh-rsa AAAAB3Nza" ++ } ++ }, ++ "eipv4": "121.66.77.88", ++ "hostname": "aliyun-test-vm-00", ++ "image-id": "ubuntu_24_04_x64_20G_alibase_20241016.vhd", ++ "instance-id": "i-bp15ojxppkmsnyjxxxxx", ++ "mac": "00:16:3e:14:59:58", ++ "network-type": "vpc", ++ "owner-account-id": "123456", ++ "private-ipv4": "172.16.111.222", ++ "region-id": "cn-hangzhou", ++ "serial-number": "3ca05955-a892-46b3-a6fc-xxxxxx", ++ "source-address": "http://mirrors.cloud.aliyuncs.com", ++ "sub-private-ipv4-list": "172.16.101.215", ++ "vpc-cidr-block": "172.16.0.0/12", ++ "vpc-id": "vpc-bp1uwvjta7txxxxxxx", ++ "vswitch-cidr-block": "172.16.101.0/24", ++ "vswitch-id": "vsw-bp12cibmw6078qv123456", ++ "zone-id": "cn-hangzhou-j" ++}""" ++ ++DEFAULT_METADATA = load_json(DEFAULT_METADATA_RAW) + + DEFAULT_USERDATA = """\ + #cloud-config + + hostname: localhost""" + ++DEFAULT_VENDORDATA = """\ ++#cloud-config ++bootcmd: ++- echo hello world > /tmp/vendor""" ++ + + class TestAliYunDatasource(test_helpers.ResponsesTestCase): + def setUp(self): +@@ -67,6 +114,10 @@ class TestAliYunDatasource(test_helpers.ResponsesTestCase): + def default_userdata(self): + return DEFAULT_USERDATA + ++ @property ++ def default_vendordata(self): ++ return DEFAULT_VENDORDATA ++ + @property + def metadata_url(self): + return ( +@@ -78,12 +129,29 @@ class TestAliYunDatasource(test_helpers.ResponsesTestCase): + + "/" + ) + ++ @property ++ def metadata_all_url(self): ++ return ( ++ os.path.join( ++ self.metadata_address, ++ self.ds.min_metadata_version, ++ "meta-data", ++ ) ++ + "/all" ++ ) ++ + @property + def userdata_url(self): + return os.path.join( + self.metadata_address, self.ds.min_metadata_version, "user-data" + ) + ++ @property ++ def vendordata_url(self): ++ return os.path.join( ++ self.metadata_address, self.ds.min_metadata_version, "vendor-data" ++ ) ++ + # EC2 provides an instance-identity document which must return 404 here + # for this test to pass. + @property +@@ -133,9 +201,17 @@ class TestAliYunDatasource(test_helpers.ResponsesTestCase): + register = functools.partial(self.responses.add, responses.GET) + register_helper(register, base_url, data) + +- def regist_default_server(self): ++ def regist_default_server(self, register_json_meta_path=True): + self.register_mock_metaserver(self.metadata_url, self.default_metadata) ++ if register_json_meta_path: ++ self.register_mock_metaserver( ++ self.metadata_all_url, DEFAULT_METADATA_RAW ++ ) + self.register_mock_metaserver(self.userdata_url, self.default_userdata) ++ self.register_mock_metaserver( ++ self.vendordata_url, self.default_userdata ++ ) ++ + self.register_mock_metaserver(self.identity_url, self.default_identity) + self.responses.add(responses.PUT, self.token_url, "API-TOKEN") + +@@ -175,7 +251,25 @@ class TestAliYunDatasource(test_helpers.ResponsesTestCase): + self._test_get_iid() + self._test_host_name() + self.assertEqual("aliyun", self.ds.cloud_name) +- self.assertEqual("ec2", self.ds.platform) ++ self.assertEqual("aliyun", self.ds.platform) ++ self.assertEqual( ++ "metadata (http://100.100.100.200)", self.ds.subplatform ++ ) ++ ++ @mock.patch("cloudinit.sources.DataSourceEc2.util.is_resolvable") ++ @mock.patch("cloudinit.sources.DataSourceAliYun._is_aliyun") ++ def test_with_mock_server_without_json_path(self, m_is_aliyun, m_resolv): ++ m_is_aliyun.return_value = True ++ self.regist_default_server(register_json_meta_path=False) ++ ret = self.ds.get_data() ++ self.assertEqual(True, ret) ++ self.assertEqual(1, m_is_aliyun.call_count) ++ self._test_get_data() ++ self._test_get_sshkey() ++ self._test_get_iid() ++ self._test_host_name() ++ self.assertEqual("aliyun", self.ds.cloud_name) ++ self.assertEqual("aliyun", self.ds.platform) + self.assertEqual( + "metadata (http://100.100.100.200)", self.ds.subplatform + ) +@@ -221,7 +315,7 @@ class TestAliYunDatasource(test_helpers.ResponsesTestCase): + self._test_get_iid() + self._test_host_name() + self.assertEqual("aliyun", self.ds.cloud_name) +- self.assertEqual("ec2", self.ds.platform) ++ self.assertEqual("aliyun", self.ds.platform) + self.assertEqual( + "metadata (http://100.100.100.200)", self.ds.subplatform + ) +@@ -272,31 +366,28 @@ class TestAliYunDatasource(test_helpers.ResponsesTestCase): + public_keys["key-pair-0"]["openssh-key"], + ) + +- def test_route_metric_calculated_without_device_number(self): +- """Test that route-metric code works without `device-number` +- +- `device-number` is part of EC2 metadata, but not supported on aliyun. +- Attempting to access it will raise a KeyError. +- +- LP: #1917875 +- """ +- netcfg = convert_ec2_metadata_network_config( ++ def test_route_metric_calculated_with_multiple_network_cards(self): ++ """Test that route-metric code works with multiple network cards""" ++ netcfg = convert_ecs_metadata_network_config( + { + "interfaces": { + "macs": { +- "06:17:04:d7:26:09": { +- "interface-id": "eni-e44ef49e", ++ "00:16:3e:14:59:58": { ++ "ipv6-gateway": "2408:xxxxx", ++ "ipv6s": "[2408:xxxxxx]", ++ "network-interface-id": "eni-bp13i1xxxxx", + }, +- "06:17:04:d7:26:08": { +- "interface-id": "eni-e44ef49f", ++ "00:16:3e:39:43:27": { ++ "gateway": "172.16.101.253", ++ "netmask": "255.255.255.0", ++ "network-interface-id": "eni-bp13i2xxxx", + }, + } + } + }, +- mock.Mock(), + macs_to_nics={ +- "06:17:04:d7:26:09": "eth0", +- "06:17:04:d7:26:08": "eth1", ++ "00:16:3e:14:59:58": "eth0", ++ "00:16:3e:39:43:27": "eth1", + }, + ) + +@@ -314,6 +405,28 @@ class TestAliYunDatasource(test_helpers.ResponsesTestCase): + netcfg["ethernets"]["eth1"].keys() + ) + ++ # eth0 network meta-data have ipv6s info, ipv6 should True ++ met0_dhcp6 = netcfg["ethernets"]["eth0"]["dhcp6"] ++ assert met0_dhcp6 is True ++ ++ netcfg = convert_ecs_metadata_network_config( ++ { ++ "interfaces": { ++ "macs": { ++ "00:16:3e:14:59:58": { ++ "gateway": "172.16.101.253", ++ "netmask": "255.255.255.0", ++ "network-interface-id": "eni-bp13ixxxx", ++ } ++ } ++ } ++ }, ++ macs_to_nics={"00:16:3e:14:59:58": "eth0"}, ++ ) ++ met0 = netcfg["ethernets"]["eth0"] ++ # single network card would have no dhcp4-overrides ++ assert "dhcp4-overrides" not in met0 ++ + + class TestIsAliYun(test_helpers.CiTestCase): + ALIYUN_PRODUCT = "Alibaba Cloud ECS" +diff --git a/tests/unittests/sources/test_ec2.py b/tests/unittests/sources/test_ec2.py +index b28afc52f..c3d33dfc9 100644 +--- a/tests/unittests/sources/test_ec2.py ++++ b/tests/unittests/sources/test_ec2.py +@@ -1709,16 +1709,6 @@ class TestIdentifyPlatform: + ) + assert ec2.CloudNames.AWS == ec2.identify_platform() + +- @mock.patch("cloudinit.sources.DataSourceEc2._collect_platform_data") +- def test_identify_aliyun(self, m_collect): +- """aliyun should be identified if product name equals to +- Alibaba Cloud ECS +- """ +- m_collect.return_value = self.collmock( +- product_name="Alibaba Cloud ECS" +- ) +- assert ec2.CloudNames.ALIYUN == ec2.identify_platform() +- + @mock.patch("cloudinit.sources.DataSourceEc2._collect_platform_data") + def test_identify_zstack(self, m_collect): + """zstack should be identified if chassis-asset-tag +-- +2.48.1 + diff --git a/SOURCES/ci-fix-Don-t-attempt-to-identify-non-x86-OpenStack-inst.patch b/SOURCES/ci-fix-Don-t-attempt-to-identify-non-x86-OpenStack-inst.patch new file mode 100644 index 0000000..c9622ea --- /dev/null +++ b/SOURCES/ci-fix-Don-t-attempt-to-identify-non-x86-OpenStack-inst.patch @@ -0,0 +1,177 @@ +From cce7faf49aeb3b53433b9b7bcc48ca2e7dbaee64 Mon Sep 17 00:00:00 2001 +From: Brett Holman +Date: Thu, 22 Aug 2024 16:54:53 -0600 +Subject: [PATCH 1/2] fix: Don't attempt to identify non-x86 OpenStack + instances + +RH-Author: Ani Sinha +RH-MergeRequest: 129: CVE-2024-6174: fix: Don't attempt to identify non-x86 OpenStack instances +RH-Jira: RHEL-100615 +RH-Acked-by: xiachen +RH-Acked-by: Miroslav Rezanina +RH-Commit: [1/2] 71a7a1b189d33ff43dc439ecf73c996a9cca3494 (anisinha/cloud-init) + +This causes cloud-init to attempt to reach out to the OpenStack Nova +datasource in non-Nova deployments on non-x86 architectures. + +Change default policy of ds-identify to disallow discovery of datasources +without strict identifiable artifacts in either kernel cmdline, DMI +platform information or system configuration files. This prevents +cloud-init from attempting to reach out to well-known hard-codded link-local +IP addresses for configuration information unless the platform strictly +identifies as a specific datasource. + +CVE-2024-6174 +LP: #2069607 +BREAKING_CHANGE: This may break non-x86 OpenStack Nova users. Affected users + may wish to use ConfigDrive as a workaround. + +(cherry picked from commit 8c3ae1bb9f1d80fbf217b41a222ee434e7f58900) +Signed-off-by: Ani Sinha +--- + doc/rtd/reference/breaking_changes.rst | 49 ++++++++++++++++++++++++++ + tests/unittests/test_ds_identify.py | 13 ++++--- + tools/ds-identify | 8 ++--- + 3 files changed, 59 insertions(+), 11 deletions(-) + +diff --git a/doc/rtd/reference/breaking_changes.rst b/doc/rtd/reference/breaking_changes.rst +index ce54e1c95..cd425a304 100644 +--- a/doc/rtd/reference/breaking_changes.rst ++++ b/doc/rtd/reference/breaking_changes.rst +@@ -11,6 +11,54 @@ releases. + many operating system vendors patch out breaking changes in + cloud-init to ensure consistent behavior on their platform. + ++25.1.3 ++====== ++ ++Strict datasource identity before network ++----------------------------------------- ++Affects detection of Ec2, OpenStack or AltCloud datasources for non-x86 ++architectures where DMI may not be accessible. ++ ++Datasource detection provided by ds-identify in cloud-init now requires strict ++identification based on DMI platform information, kernel command line or ++`datasource_list:` system configuration in /etc/cloud/cloud.cfg.d. ++ ++Prior to this change, ds-identify would allow non-x86 architectures without ++strict identifying platform information to run in a discovery mode which would ++attempt to reach out to well known static link-local IPs to attempt to ++retrieve configuration once system networking is up. ++ ++To mitigate the potential of a bad-actor in a local network responding ++to such provisioning requests from cloud-init clients, ds-identify will no ++longer allow this late discovery mode for platforms unable to expose clear ++identifying characteristics of a known cloud-init datasource. ++ ++The most likely affected cloud platforms are AltCloud, Ec2 and OpenStack for ++non-x86 architectures where DMI data is not exposed by the kernel. ++ ++If your non-x86 architecture or images no longer detect the proper datasource, ++any of the following steps can ensure proper detection of cloud-init config: ++ ++- Provide kernel commandline containing ``ds=`` ++ which forces ds-identify to discover a specific datasource. ++- Image creators: provide a config file part such as ++ :file:`/etc/cloud/cloud.cfg.d/*.cfg` containing the ++ case-sensitive ``datasource_list: [ ]`` to force cloud-init ++ to use a specific datasource without performing discovery. ++ ++For example, to force OpenStack discovery in cloud-init any of the following ++approaches work: ++ ++- OpenStack: `attach a ConfigDrive`_ as an alternative config source ++- Kernel command line containing ``ds=openstack`` ++- Custom images provide :file:`/etc/cloud/cloud.cfg.d/91-set-datasource.cfg` ++ containing: ++ ++.. code-block:: yaml ++ ++ datasource_list: [ OpenStack ] ++ ++ + 24.3 + ==== + +@@ -148,5 +196,6 @@ Workarounds include updating the kernel command line and optionally configuring + a ``datasource_list`` in ``/etc/cloud/cloud.cfg.d/*.cfg``. + + ++.. _attach a ConfigDrive: https://docs.openstack.org/nova/2024.1/admin/config-drive.html + .. _this patch: https://github.com/canonical/cloud-init/blob/ubuntu/noble/debian/patches/no-single-process.patch + .. _Python3 equivalent: https://github.com/canonical/cloud-init/pull/5489#issuecomment-2408210561 +diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py +index 5d47e552b..9b3828ce6 100644 +--- a/tests/unittests/test_ds_identify.py ++++ b/tests/unittests/test_ds_identify.py +@@ -208,9 +208,9 @@ system_info: + """ + + POLICY_FOUND_ONLY = "search,found=all,maybe=none,notfound=disabled" +-POLICY_FOUND_OR_MAYBE = "search,found=all,maybe=all,notfound=disabled" +-DI_DEFAULT_POLICY = "search,found=all,maybe=all,notfound=disabled" +-DI_DEFAULT_POLICY_NO_DMI = "search,found=all,maybe=all,notfound=enabled" ++POLICY_FOUND_OR_MAYBE = "search,found=all,maybe=none,notfound=disabled" ++DI_DEFAULT_POLICY = "search,found=all,maybe=none,notfound=disabled" ++DI_DEFAULT_POLICY_NO_DMI = "search,found=all,maybe=none,notfound=enabled" + DI_EC2_STRICT_ID_DEFAULT = "true" + OVF_MATCH_STRING = "http://schemas.dmtf.org/ovf/environment/1" + +@@ -937,7 +937,7 @@ class TestDsIdentify(DsIdentifyBase): + self._test_ds_found("OpenStack-AssetTag-Compute") + + def test_openstack_on_non_intel_is_maybe(self): +- """On non-Intel, openstack without dmi info is maybe. ++ """On non-Intel, openstack without dmi info is none. + + nova does not identify itself on platforms other than intel. + https://bugs.launchpad.net/cloud-init/+bugs?field.tag=dsid-nova""" +@@ -957,10 +957,9 @@ class TestDsIdentify(DsIdentifyBase): + + # updating the uname to ppc64 though should get a maybe. + data.update({"mocks": [MOCK_VIRT_IS_KVM, MOCK_UNAME_IS_PPC64]}) +- (_, _, err, _, _) = self._check_via_dict( +- data, RC_FOUND, dslist=["OpenStack", "None"] +- ) ++ (_, _, err, _, _) = self._check_via_dict(data, RC_NOT_FOUND) + self.assertIn("check for 'OpenStack' returned maybe", err) ++ self.assertIn("No ds found", err) + + def test_default_ovf_is_found(self): + """OVF is identified found when ovf/ovf-env.xml seed file exists.""" +diff --git a/tools/ds-identify b/tools/ds-identify +index e00b05e80..5644b1e39 100755 +--- a/tools/ds-identify ++++ b/tools/ds-identify +@@ -14,7 +14,7 @@ + # The format is: + # ,found=value,maybe=value,notfound=value + # default setting is: +-# search,found=all,maybe=all,notfound=disabled ++# search,found=all,maybe=none,notfound=disabled + # + # kernel command line option: ci.di.policy= + # example line in /etc/cloud/ds-identify.cfg: +@@ -40,7 +40,7 @@ + # first: use the first found do no further checking + # all: enable all DS_FOUND + # +-# maybe: (default=all) ++# maybe: (default=none) + # if nothing returned 'found', then how to handle maybe. + # no network sources are allowed to return 'maybe'. + # all: enable all DS_MAYBE +@@ -100,8 +100,8 @@ DI_MAIN=${DI_MAIN:-main} + + DI_BLKID_EXPORT_OUT="" + DI_GEOM_LABEL_STATUS_OUT="" +-DI_DEFAULT_POLICY="search,found=all,maybe=all,notfound=${DI_DISABLED}" +-DI_DEFAULT_POLICY_NO_DMI="search,found=all,maybe=all,notfound=${DI_ENABLED}" ++DI_DEFAULT_POLICY="search,found=all,maybe=none,notfound=${DI_DISABLED}" ++DI_DEFAULT_POLICY_NO_DMI="search,found=all,maybe=none,notfound=${DI_ENABLED}" + DI_DMI_BOARD_NAME="" + DI_DMI_CHASSIS_ASSET_TAG="" + DI_DMI_PRODUCT_NAME="" +-- +2.39.3 + diff --git a/SOURCES/ci-fix-NM-reload-and-bring-up-individual-network-conns-.patch b/SOURCES/ci-fix-NM-reload-and-bring-up-individual-network-conns-.patch new file mode 100644 index 0000000..a3c2f6c --- /dev/null +++ b/SOURCES/ci-fix-NM-reload-and-bring-up-individual-network-conns-.patch @@ -0,0 +1,67 @@ +From 95e9d95a1bd0b995cdb505395b761896739d1476 Mon Sep 17 00:00:00 2001 +From: Ani Sinha +Date: Thu, 13 Mar 2025 19:46:35 +0530 +Subject: [PATCH] fix: NM reload and bring up individual network conns (#6073) + +RH-Author: Ani Sinha +RH-MergeRequest: 125: fix: NM reload and bring up individual network conns (#6073) +RH-Jira: RHEL-81703 +RH-Commit: [1/1] 7d9b3575e1807360c40a1f45620060d1843a65b7 (anisinha/cloud-init) + +Reloading the network manager service is equivalent to "nmcli reload" and this +command only reloads the global .conf files and DNS config, not connections. +This means changes to connection files will not take effect. For those to take +effect, we need "nmcli conn load/reload" and then "nmcli conn up". Thus, +reloading network manager as well as reloading the connections are required to +cover all cases. + +Also see https://github.com/canonical/cloud-init/issues/5512#issuecomment-2298371744 + +While at it, rename "reload-or-try-restart" -> "try-reload-or-restart" since +the former is legacy and the later is the officially documented sub-command. + +Fixes: GH-6064 +Fixes: bde913ae242 ("fix(NetworkManager): Fix network activator") + +Signed-off-by: Ani Sinha +(cherry picked from commit 671baf22df846bcc2cfecf3d2c0e09a816fbf240) +Signed-off-by: Ani Sinha +--- + cloudinit/net/activators.py | 4 ++-- + tests/unittests/test_net_activators.py | 4 ++-- + 2 files changed, 4 insertions(+), 4 deletions(-) + +diff --git a/cloudinit/net/activators.py b/cloudinit/net/activators.py +index de9a1d3c9..942128941 100644 +--- a/cloudinit/net/activators.py ++++ b/cloudinit/net/activators.py +@@ -206,9 +206,9 @@ class NetworkManagerActivator(NetworkActivator): + state, + ) + return _alter_interface( +- ["systemctl", "reload-or-try-restart", "NetworkManager.service"], ++ ["systemctl", "try-reload-or-restart", "NetworkManager.service"], + "all", +- ) ++ ) and all(cls.bring_up_interface(device) for device in device_names) + + + class NetplanActivator(NetworkActivator): +diff --git a/tests/unittests/test_net_activators.py b/tests/unittests/test_net_activators.py +index a720ada81..84876b73b 100644 +--- a/tests/unittests/test_net_activators.py ++++ b/tests/unittests/test_net_activators.py +@@ -247,8 +247,8 @@ NETWORK_MANAGER_BRING_UP_ALL_CALL_LIST: list = [ + ), + {}, + ), +- ((["systemctl", "reload-or-try-restart", "NetworkManager.service"],), {}), +-] ++ ((["systemctl", "try-reload-or-restart", "NetworkManager.service"],), {}), ++] + NETWORK_MANAGER_BRING_UP_CALL_LIST + + NETWORKD_BRING_UP_CALL_LIST: list = [ + ((["ip", "link", "set", "dev", "eth0", "up"],), {}), +-- +2.48.1 + diff --git a/SOURCES/ci-fix-strict-disable-in-ds-identify-on-no-datasources-.patch b/SOURCES/ci-fix-strict-disable-in-ds-identify-on-no-datasources-.patch new file mode 100644 index 0000000..097b9cd --- /dev/null +++ b/SOURCES/ci-fix-strict-disable-in-ds-identify-on-no-datasources-.patch @@ -0,0 +1,90 @@ +From 9c2c1169ba2d11b22ff054583583cd4298a5ba81 Mon Sep 17 00:00:00 2001 +From: Chad Smith +Date: Tue, 24 Jun 2025 09:12:52 -0600 +Subject: [PATCH 2/2] fix: strict disable in ds-identify on no datasources + found + +RH-Author: Ani Sinha +RH-MergeRequest: 129: CVE-2024-6174: fix: Don't attempt to identify non-x86 OpenStack instances +RH-Jira: RHEL-100615 +RH-Acked-by: xiachen +RH-Acked-by: Miroslav Rezanina +RH-Commit: [2/2] f941ad029982aa5b1aecd569380ae47a6d727d9b (anisinha/cloud-init) + +Take the CVE-2024-6174 strict detection fix one step further. + +Commit 8c3ae1b took a step to ignore DS_MAYBE datasource discovery. +But, if no datasources are met the DS_FOUND conditions, ds-identify was +still leaving cloud-init enabled. This resulted in cloud-init python +code attempting to discover all datasources later in boot based on +the default datasource_list. + +ds-identify will now assert that at least one datasource is found. If +no datasources, ds-identify will exit 1 which disables cloud-init boot +stages and results in no boot configuration operations from cloud-init. + +OpenStack images which cannot identify a valid datasource with DMI-data +or kernel command line ci.ds=OpenStack parameter will need to either: +- provide image-based configuration in either /etc/cloud/cloud.cfg.* to set + datasource_list: [ OpenStack ] +- provide --config-drive true to openstack server create +- attach a nocloud disk labelled CIDATA containing user-data and + meta-data files + +CVE-2024-6174 +LP: #2069607 + +(cherry picked from commit e3f42adc2674a38fb29e414cfbf96f884934b2d2) +Signed-off-by: Ani Sinha +--- + tests/unittests/test_ds_identify.py | 6 ++++-- + tools/ds-identify | 2 +- + 2 files changed, 5 insertions(+), 3 deletions(-) + +diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py +index 9b3828ce6..2d6306c2f 100644 +--- a/tests/unittests/test_ds_identify.py ++++ b/tests/unittests/test_ds_identify.py +@@ -210,7 +210,7 @@ system_info: + POLICY_FOUND_ONLY = "search,found=all,maybe=none,notfound=disabled" + POLICY_FOUND_OR_MAYBE = "search,found=all,maybe=none,notfound=disabled" + DI_DEFAULT_POLICY = "search,found=all,maybe=none,notfound=disabled" +-DI_DEFAULT_POLICY_NO_DMI = "search,found=all,maybe=none,notfound=enabled" ++DI_DEFAULT_POLICY_NO_DMI = "search,found=all,maybe=none,notfound=disabled" + DI_EC2_STRICT_ID_DEFAULT = "true" + OVF_MATCH_STRING = "http://schemas.dmtf.org/ovf/environment/1" + +@@ -947,7 +947,7 @@ class TestDsIdentify(DsIdentifyBase): + data.update( + { + "policy_dmi": POLICY_FOUND_OR_MAYBE, +- "policy_no_dmi": POLICY_FOUND_OR_MAYBE, ++ "policy_no_dmi": DI_DEFAULT_POLICY_NO_DMI, + } + ) + +@@ -960,6 +960,8 @@ class TestDsIdentify(DsIdentifyBase): + (_, _, err, _, _) = self._check_via_dict(data, RC_NOT_FOUND) + self.assertIn("check for 'OpenStack' returned maybe", err) + self.assertIn("No ds found", err) ++ self.assertIn("Disabled cloud-init", err) ++ self.assertIn("returning 1", err) + + def test_default_ovf_is_found(self): + """OVF is identified found when ovf/ovf-env.xml seed file exists.""" +diff --git a/tools/ds-identify b/tools/ds-identify +index 5644b1e39..9bd9c9bbb 100755 +--- a/tools/ds-identify ++++ b/tools/ds-identify +@@ -101,7 +101,7 @@ DI_MAIN=${DI_MAIN:-main} + DI_BLKID_EXPORT_OUT="" + DI_GEOM_LABEL_STATUS_OUT="" + DI_DEFAULT_POLICY="search,found=all,maybe=none,notfound=${DI_DISABLED}" +-DI_DEFAULT_POLICY_NO_DMI="search,found=all,maybe=none,notfound=${DI_ENABLED}" ++DI_DEFAULT_POLICY_NO_DMI="search,found=all,maybe=none,notfound=${DI_DISABLED}" + DI_DMI_BOARD_NAME="" + DI_DMI_CHASSIS_ASSET_TAG="" + DI_DMI_PRODUCT_NAME="" +-- +2.39.3 + diff --git a/SPECS/cloud-init.spec b/SPECS/cloud-init.spec index d468b28..d7ec486 100644 --- a/SPECS/cloud-init.spec +++ b/SPECS/cloud-init.spec @@ -1,6 +1,6 @@ Name: cloud-init Version: 24.4 -Release: 4%{?dist} +Release: 7%{?dist} Summary: Cloud instance init scripts License: ASL 2.0 or GPLv3 URL: http://launchpad.net/cloud-init @@ -21,6 +21,15 @@ Patch7: ci-Use-log_with_downgradable_level-for-user-password-wa.patch Patch8: ci-downstream-set-deprecation-boundary-version.patch # For RHEL-76361 - [c9s] cloud-init remove 'NOZEROCONF=yes' from /etc/sysconfig/network Patch9: ci-net-sysconfig-do-not-remove-all-existing-settings-of.patch +# For RHEL-81703 - DataSourceNoCloudNet network configuration is ineffective - c9s +Patch10: ci-fix-NM-reload-and-bring-up-individual-network-conns-.patch +# For RHEL-88658 - Cloud-Init Backport Optimization Features on Alibaba Cloud +Patch11: ci-feat-aliyun-datasource-support-crawl-metadata-at-onc.patch +# For RHEL-100615 - CVE-2024-6174 cloud-init: From CVEorg collector [rhel-9.7] +Patch12: ci-fix-Don-t-attempt-to-identify-non-x86-OpenStack-inst.patch +# For RHEL-100615 - CVE-2024-6174 cloud-init: From CVEorg collector [rhel-9.7] +Patch13: ci-fix-strict-disable-in-ds-identify-on-no-datasources-.patch +Patch14: 0003-downstream-Retain-exit-code-in-cloud-init-status-for.patch BuildArch: noarch @@ -235,6 +244,25 @@ fi %config(noreplace) %{_sysconfdir}/rsyslog.d/21-cloudinit.conf %changelog +* Thu Jul 03 2025 Miroslav Rezanina - 24.4-7 +- ci-fix-Don-t-attempt-to-identify-non-x86-OpenStack-inst.patch [RHEL-100615] +- ci-fix-strict-disable-in-ds-identify-on-no-datasources-.patch [RHEL-100615] +- Fix missing patch [RHEL-101692] +- Resolves: RHEL-100615 + (CVE-2024-6174 cloud-init: From CVEorg collector [rhel-9.7]) +- Resolves: RHEL-101692 + (c9s dist-git missing patch "downstream: Retain exit code in cloud-init status for recoverable errors") + +* Wed May 14 2025 Jon Maloy - 24.4-6 +- ci-feat-aliyun-datasource-support-crawl-metadata-at-onc.patch [RHEL-88658] +- Resolves: RHEL-88658 + (Cloud-Init Backport Optimization Features on Alibaba Cloud) + +* Tue Mar 18 2025 Jon Maloy - 24.4-5 +- ci-fix-NM-reload-and-bring-up-individual-network-conns-.patch [RHEL-81703] +- Resolves: RHEL-81703 + (DataSourceNoCloudNet network configuration is ineffective - c9s) + * Mon Feb 17 2025 Jon Maloy - 24.4-4 - ci-net-sysconfig-do-not-remove-all-existing-settings-of.patch [RHEL-76361] - Resolves: RHEL-76361