From 304be0559f6e6e471b648e559175408b59d15909 Mon Sep 17 00:00:00 2001 From: Emanuele Giuseppe Esposito Date: Thu, 2 Jun 2022 15:56:02 +0200 Subject: [PATCH] cc_set_hostname: do not write "localhost" when no hostname is given (#1453) RH-Author: Emanuele Giuseppe Esposito RH-MergeRequest: 77: cc_set_hostname: do not write "localhost" when no hostname is given (#1453) RH-Commit: [1/1] 9b54be1a52eff87bf2b9e396edbf998bb9d68d49 (eesposit/cloud-init) RH-Bugzilla: 2092909 RH-Acked-by: Miroslav Rezanina RH-Acked-by: Mohamed Gamal Morsy commit 74e43496f353db52e15d96abeb54ad63baac5be9 Author: Emanuele Giuseppe Esposito Date: Tue May 31 16:03:44 2022 +0200 cc_set_hostname: do not write "localhost" when no hostname is given (#1453) Systemd used to sometimes ignore localhost in /etc/hostnames, and many programs like cloud-init used this as a workaround to set a default hostname. From https://github.com/systemd/systemd/commit/d39079fcaa05e23540d2b1f0270fa31c22a7e9f1: We would sometimes ignore localhost-style names in /etc/hostname. That is brittle. If the user configured some hostname, it's most likely because they want to use that as the hostname. If they don't want to use such a hostname, they should just not create the config. Everything becomes simples if we just use the configured hostname as-is. This behaviour seems to have been a workaround for Anaconda installer and other tools writing out /etc/hostname with the default of "localhost.localdomain". Anaconda PR to stop doing that: rhinstaller/anaconda#3040. That might have been useful as a work-around for other programs misbehaving if /etc/hostname was not present, but nowadays it's not useful because systemd mostly controls the hostname and it is perfectly happy without that file. Apart from making things simpler, this allows users to set a hostname like "localhost" and have it honoured, if such a whim strikes them. As also suggested by the Anaconda PR, we need to stop writing default "localhost" in /etc/hostnames, and let the right service (networking, user) do that if they need to. Otherwise, "localhost" will permanently stay as hostname and will prevent other tools like NetworkManager from setting the right one. Signed-off-by: Emanuele Giuseppe Esposito RHBZ: 1980403 Conflicts: almost all test files had conflicts because they use '' instead of "" Signed-off-by: Emanuele Giuseppe Esposito --- cloudinit/cmd/main.py | 5 ++-- cloudinit/config/cc_apt_configure.py | 2 +- cloudinit/config/cc_debug.py | 2 +- cloudinit/config/cc_phone_home.py | 6 ++-- cloudinit/config/cc_set_hostname.py | 14 ++++++++- cloudinit/config/cc_spacewalk.py | 11 ++++--- cloudinit/config/cc_update_etc_hosts.py | 6 ++-- cloudinit/config/cc_update_hostname.py | 14 ++++++++- cloudinit/sources/DataSourceAliYun.py | 8 ++++- cloudinit/sources/DataSourceCloudSigma.py | 9 ++++-- cloudinit/sources/DataSourceGCE.py | 6 +++- cloudinit/sources/DataSourceScaleway.py | 3 +- cloudinit/sources/__init__.py | 28 ++++++++++++++---- cloudinit/util.py | 29 +++++++++++++++---- .../unittests/test_datasource/test_aliyun.py | 5 ++-- .../test_datasource/test_cloudsigma.py | 14 +++++---- .../unittests/test_datasource/test_hetzner.py | 2 +- .../unittests/test_datasource/test_vmware.py | 4 ++- 18 files changed, 125 insertions(+), 43 deletions(-) diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py index baf1381f..ebd3719d 100644 --- a/cloudinit/cmd/main.py +++ b/cloudinit/cmd/main.py @@ -701,8 +701,9 @@ def _maybe_set_hostname(init, stage, retry_stage): @param retry_stage: String represented logs upon error setting hostname. """ cloud = init.cloudify() - (hostname, _fqdn) = util.get_hostname_fqdn( - init.cfg, cloud, metadata_only=True) + (hostname, _fqdn, _) = util.get_hostname_fqdn( + init.cfg, cloud, metadata_only=True + ) if hostname: # meta-data or user-data hostname content try: cc_set_hostname.handle('set-hostname', init.cfg, cloud, LOG, None) diff --git a/cloudinit/config/cc_apt_configure.py b/cloudinit/config/cc_apt_configure.py index bb8a1278..8a2b57b5 100644 --- a/cloudinit/config/cc_apt_configure.py +++ b/cloudinit/config/cc_apt_configure.py @@ -926,7 +926,7 @@ def search_for_mirror_dns(configured, mirrortype, cfg, cloud): raise ValueError("unknown mirror type") # if we have a fqdn, then search its domain portion first - (_, fqdn) = util.get_hostname_fqdn(cfg, cloud) + fqdn = util.get_hostname_fqdn(cfg, cloud).fqdn mydom = ".".join(fqdn.split(".")[1:]) if mydom: doms.append(".%s" % mydom) diff --git a/cloudinit/config/cc_debug.py b/cloudinit/config/cc_debug.py index 4d5a6aa2..7ca4ea0f 100644 --- a/cloudinit/config/cc_debug.py +++ b/cloudinit/config/cc_debug.py @@ -88,7 +88,7 @@ def handle(name, cfg, cloud, log, args): to_print.write("Datasource: %s\n" % (type_utils.obj_name(cloud.datasource))) to_print.write("Distro: %s\n" % (type_utils.obj_name(cloud.distro))) - to_print.write("Hostname: %s\n" % (cloud.get_hostname(True))) + to_print.write("Hostname: %s\n" % (cloud.get_hostname(True).hostname)) to_print.write("Instance ID: %s\n" % (cloud.get_instance_id())) to_print.write("Locale: %s\n" % (cloud.get_locale())) to_print.write("Launch IDX: %s\n" % (cloud.launch_index)) diff --git a/cloudinit/config/cc_phone_home.py b/cloudinit/config/cc_phone_home.py index 733c3910..a71237dc 100644 --- a/cloudinit/config/cc_phone_home.py +++ b/cloudinit/config/cc_phone_home.py @@ -99,9 +99,9 @@ def handle(name, cfg, cloud, log, args): post_list = POST_LIST_ALL all_keys = {} - all_keys['instance_id'] = cloud.get_instance_id() - all_keys['hostname'] = cloud.get_hostname() - all_keys['fqdn'] = cloud.get_hostname(fqdn=True) + all_keys["instance_id"] = cloud.get_instance_id() + all_keys["hostname"] = cloud.get_hostname().hostname + all_keys["fqdn"] = cloud.get_hostname(fqdn=True).hostname pubkeys = { 'pub_key_dsa': '/etc/ssh/ssh_host_dsa_key.pub', diff --git a/cloudinit/config/cc_set_hostname.py b/cloudinit/config/cc_set_hostname.py index 1d23d80d..37e5ac1d 100644 --- a/cloudinit/config/cc_set_hostname.py +++ b/cloudinit/config/cc_set_hostname.py @@ -62,7 +62,15 @@ def handle(name, cfg, cloud, log, _args): log.debug(("Configuration option 'preserve_hostname' is set," " not setting the hostname in module %s"), name) return - (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud) + + # Set prefer_fqdn_over_hostname value in distro + hostname_fqdn = util.get_cfg_option_bool( + cfg, "prefer_fqdn_over_hostname", None + ) + if hostname_fqdn is not None: + cloud.distro.set_option("prefer_fqdn_over_hostname", hostname_fqdn) + + (hostname, fqdn, is_default) = util.get_hostname_fqdn(cfg, cloud) # Check for previous successful invocation of set-hostname # set-hostname artifact file accounts for both hostname and fqdn @@ -79,6 +87,10 @@ def handle(name, cfg, cloud, log, _args): if not hostname_changed: log.debug('No hostname changes. Skipping set-hostname') return + if is_default and hostname == "localhost": + # https://github.com/systemd/systemd/commit/d39079fcaa05e23540d2b1f0270fa31c22a7e9f1 + log.debug("Hostname is localhost. Let other services handle this.") + return log.debug("Setting the hostname to %s (%s)", fqdn, hostname) try: cloud.distro.set_hostname(hostname, fqdn) diff --git a/cloudinit/config/cc_spacewalk.py b/cloudinit/config/cc_spacewalk.py index 95083607..29bf8415 100644 --- a/cloudinit/config/cc_spacewalk.py +++ b/cloudinit/config/cc_spacewalk.py @@ -79,10 +79,13 @@ def handle(name, cfg, cloud, log, _args): # Need to have this installed before further things will work. cloud.distro.install_packages(required_packages) if not is_registered(): - do_register(spacewalk_server, - cloud.datasource.get_hostname(fqdn=True), - proxy=cfg.get("proxy"), log=log, - activation_key=cfg.get('activation_key')) + do_register( + spacewalk_server, + cloud.datasource.get_hostname(fqdn=True).hostname, + proxy=cfg.get("proxy"), + log=log, + activation_key=cfg.get("activation_key"), + ) else: log.debug("Skipping module named %s, 'spacewalk/server' key" " was not found in configuration", name) diff --git a/cloudinit/config/cc_update_etc_hosts.py b/cloudinit/config/cc_update_etc_hosts.py index 03fffb96..69c5a2f6 100644 --- a/cloudinit/config/cc_update_etc_hosts.py +++ b/cloudinit/config/cc_update_etc_hosts.py @@ -59,8 +59,8 @@ frequency = PER_ALWAYS def handle(name, cfg, cloud, log, _args): manage_hosts = util.get_cfg_option_str(cfg, "manage_etc_hosts", False) - if util.translate_bool(manage_hosts, addons=['template']): - (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud) + if util.translate_bool(manage_hosts, addons=["template"]): + (hostname, fqdn, _) = util.get_hostname_fqdn(cfg, cloud) if not hostname: log.warning(("Option 'manage_etc_hosts' was set," " but no hostname was found")) @@ -78,7 +78,7 @@ def handle(name, cfg, cloud, log, _args): {'hostname': hostname, 'fqdn': fqdn}) elif manage_hosts == "localhost": - (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud) + (hostname, fqdn, _) = util.get_hostname_fqdn(cfg, cloud) if not hostname: log.warning(("Option 'manage_etc_hosts' was set," " but no hostname was found")) diff --git a/cloudinit/config/cc_update_hostname.py b/cloudinit/config/cc_update_hostname.py index d5f4eb5a..61496525 100644 --- a/cloudinit/config/cc_update_hostname.py +++ b/cloudinit/config/cc_update_hostname.py @@ -45,7 +45,19 @@ def handle(name, cfg, cloud, log, _args): " not updating the hostname in module %s"), name) return - (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud) + # Set prefer_fqdn_over_hostname value in distro + hostname_fqdn = util.get_cfg_option_bool( + cfg, "prefer_fqdn_over_hostname", None + ) + if hostname_fqdn is not None: + cloud.distro.set_option("prefer_fqdn_over_hostname", hostname_fqdn) + + (hostname, fqdn, is_default) = util.get_hostname_fqdn(cfg, cloud) + if is_default and hostname == "localhost": + # https://github.com/systemd/systemd/commit/d39079fcaa05e23540d2b1f0270fa31c22a7e9f1 + log.debug("Hostname is localhost. Let other services handle this.") + return + try: prev_fn = os.path.join(cloud.get_cpath('data'), "previous-hostname") log.debug("Updating hostname to %s (%s)", fqdn, hostname) diff --git a/cloudinit/sources/DataSourceAliYun.py b/cloudinit/sources/DataSourceAliYun.py index 09052873..365ce176 100644 --- a/cloudinit/sources/DataSourceAliYun.py +++ b/cloudinit/sources/DataSourceAliYun.py @@ -3,6 +3,7 @@ from cloudinit import dmi from cloudinit import sources from cloudinit.sources import DataSourceEc2 as EC2 +from cloudinit.sources import DataSourceHostname ALIYUN_PRODUCT = "Alibaba Cloud ECS" @@ -17,7 +18,12 @@ class DataSourceAliYun(EC2.DataSourceEc2): extended_metadata_versions = [] def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False): - return self.metadata.get('hostname', 'localhost.localdomain') + hostname = self.metadata.get("hostname") + is_default = False + if hostname is None: + hostname = "localhost.localdomain" + is_default = True + return DataSourceHostname(hostname, is_default) def get_public_ssh_keys(self): return parse_public_keys(self.metadata.get('public-keys', {})) diff --git a/cloudinit/sources/DataSourceCloudSigma.py b/cloudinit/sources/DataSourceCloudSigma.py index f63baf74..9f6f1d42 100644 --- a/cloudinit/sources/DataSourceCloudSigma.py +++ b/cloudinit/sources/DataSourceCloudSigma.py @@ -12,6 +12,8 @@ from cloudinit.cs_utils import Cepko, SERIAL_PORT from cloudinit import dmi from cloudinit import log as logging from cloudinit import sources +from cloudinit.sources import DataSourceHostname +from cloudinit.sources.helpers.cloudsigma import SERIAL_PORT, Cepko LOG = logging.getLogger(__name__) @@ -89,10 +91,11 @@ class DataSourceCloudSigma(sources.DataSource): Cleans up and uses the server's name if the latter is set. Otherwise the first part from uuid is being used. """ - if re.match(r'^[A-Za-z0-9 -_\.]+$', self.metadata['name']): - return self.metadata['name'][:61] + if re.match(r"^[A-Za-z0-9 -_\.]+$", self.metadata["name"]): + ret = self.metadata["name"][:61] else: - return self.metadata['uuid'].split('-')[0] + ret = self.metadata["uuid"].split("-")[0] + return DataSourceHostname(ret, False) def get_public_ssh_keys(self): return [self.ssh_public_key] diff --git a/cloudinit/sources/DataSourceGCE.py b/cloudinit/sources/DataSourceGCE.py index 746caddb..ac5d3e5b 100644 --- a/cloudinit/sources/DataSourceGCE.py +++ b/cloudinit/sources/DataSourceGCE.py @@ -13,6 +13,8 @@ from cloudinit import log as logging from cloudinit import sources from cloudinit import url_helper from cloudinit import util +from cloudinit.net.dhcp import EphemeralDHCPv4 +from cloudinit.sources import DataSourceHostname LOG = logging.getLogger(__name__) @@ -100,7 +102,9 @@ class DataSourceGCE(sources.DataSource): def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False): # GCE has long FDQN's and has asked for short hostnames. - return self.metadata['local-hostname'].split('.')[0] + return DataSourceHostname( + self.metadata["local-hostname"].split(".")[0], False + ) @property def availability_zone(self): diff --git a/cloudinit/sources/DataSourceScaleway.py b/cloudinit/sources/DataSourceScaleway.py index 41be7665..875b5bb3 100644 --- a/cloudinit/sources/DataSourceScaleway.py +++ b/cloudinit/sources/DataSourceScaleway.py @@ -33,6 +33,7 @@ from cloudinit import util from cloudinit import net from cloudinit.net.dhcp import EphemeralDHCPv4, NoDHCPLeaseError from cloudinit.event import EventType +from cloudinit.sources import DataSourceHostname LOG = logging.getLogger(__name__) @@ -271,7 +272,7 @@ class DataSourceScaleway(sources.DataSource): return ssh_keys def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False): - return self.metadata['hostname'] + return DataSourceHostname(self.metadata["hostname"], False) @property def availability_zone(self): diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py index 7d74f8d9..6f5d7ea6 100644 --- a/cloudinit/sources/__init__.py +++ b/cloudinit/sources/__init__.py @@ -134,6 +134,11 @@ def redact_sensitive_keys(metadata, redact_value=REDACT_SENSITIVE_VALUE): URLParams = namedtuple( 'URLParms', ['max_wait_seconds', 'timeout_seconds', 'num_retries']) +DataSourceHostname = namedtuple( + "DataSourceHostname", + ["hostname", "is_default"], +) + class DataSource(CloudInitPickleMixin, metaclass=abc.ABCMeta): @@ -233,7 +238,7 @@ class DataSource(CloudInitPickleMixin, metaclass=abc.ABCMeta): def _get_standardized_metadata(self, instance_data): """Return a dictionary of standardized metadata keys.""" - local_hostname = self.get_hostname() + local_hostname = self.get_hostname().hostname instance_id = self.get_instance_id() availability_zone = self.availability_zone # In the event of upgrade from existing cloudinit, pickled datasource @@ -593,22 +598,33 @@ class DataSource(CloudInitPickleMixin, metaclass=abc.ABCMeta): @param metadata_only: Boolean, set True to avoid looking up hostname if meta-data doesn't have local-hostname present. - @return: hostname or qualified hostname. Optionally return None when + @return: a DataSourceHostname namedtuple + , (str, bool). + is_default is a bool and + it's true only if hostname is localhost and was + returned by util.get_hostname() as a default. + This is used to differentiate with a user-defined + localhost hostname. + Optionally return (None, False) when metadata_only is True and local-hostname data is not available. """ defdomain = "localdomain" defhost = "localhost" domain = defdomain + is_default = False if not self.metadata or not self.metadata.get('local-hostname'): if metadata_only: - return None + return DataSourceHostname(None, is_default) # this is somewhat questionable really. # the cloud datasource was asked for a hostname # and didn't have one. raising error might be more appropriate # but instead, basically look up the existing hostname toks = [] hostname = util.get_hostname() + if hostname == "localhost": + # default hostname provided by socket.gethostname() + is_default = True hosts_fqdn = util.get_fqdn_from_hosts(hostname) if hosts_fqdn and hosts_fqdn.find(".") > 0: toks = str(hosts_fqdn).split(".") @@ -641,9 +657,9 @@ class DataSource(CloudInitPickleMixin, metaclass=abc.ABCMeta): hostname = toks[0] if fqdn and domain != defdomain: - return "%s.%s" % (hostname, domain) - else: - return hostname + hostname = "%s.%s" % (hostname, domain) + + return DataSourceHostname(hostname, is_default) def get_package_mirror_info(self): return self.distro.get_package_mirror_info(data_source=self) diff --git a/cloudinit/util.py b/cloudinit/util.py index fe37ae89..8a2b13ba 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -32,7 +32,8 @@ import subprocess import sys import time from base64 import b64decode, b64encode -from errno import ENOENT +from collections import deque, namedtuple +from errno import EACCES, ENOENT from functools import lru_cache from urllib import parse from typing import List @@ -967,6 +968,12 @@ def dos2unix(contents): return contents.replace('\r\n', '\n') +HostnameFqdnInfo = namedtuple( + "HostnameFqdnInfo", + ["hostname", "fqdn", "is_default"], +) + + def get_hostname_fqdn(cfg, cloud, metadata_only=False): """Get hostname and fqdn from config if present and fallback to cloud. @@ -974,9 +981,17 @@ def get_hostname_fqdn(cfg, cloud, metadata_only=False): @param cloud: Cloud instance from init.cloudify(). @param metadata_only: Boolean, set True to only query cloud meta-data, returning None if not present in meta-data. - @return: a Tuple of strings , . Values can be none when + @return: a namedtuple of + , , (str, str, bool). + Values can be none when metadata_only is True and no cfg or metadata provides hostname info. + is_default is a bool and + it's true only if hostname is localhost and was + returned by util.get_hostname() as a default. + This is used to differentiate with a user-defined + localhost hostname. """ + is_default = False if "fqdn" in cfg: # user specified a fqdn. Default hostname then is based off that fqdn = cfg['fqdn'] @@ -990,12 +1005,16 @@ def get_hostname_fqdn(cfg, cloud, metadata_only=False): else: # no fqdn set, get fqdn from cloud. # get hostname from cfg if available otherwise cloud - fqdn = cloud.get_hostname(fqdn=True, metadata_only=metadata_only) + fqdn = cloud.get_hostname( + fqdn=True, metadata_only=metadata_only + ).hostname if "hostname" in cfg: hostname = cfg['hostname'] else: - hostname = cloud.get_hostname(metadata_only=metadata_only) - return (hostname, fqdn) + hostname, is_default = cloud.get_hostname( + metadata_only=metadata_only + ) + return HostnameFqdnInfo(hostname, fqdn, is_default) def get_fqdn_from_hosts(hostname, filename="/etc/hosts"): diff --git a/tests/unittests/test_datasource/test_aliyun.py b/tests/unittests/test_datasource/test_aliyun.py index cab1ac2b..acdee0c9 100644 --- a/tests/unittests/test_datasource/test_aliyun.py +++ b/tests/unittests/test_datasource/test_aliyun.py @@ -127,8 +127,9 @@ class TestAliYunDatasource(test_helpers.HttprettyTestCase): self.ds.get_instance_id()) def _test_host_name(self): - self.assertEqual(self.default_metadata['hostname'], - self.ds.get_hostname()) + self.assertEqual( + self.default_metadata["hostname"], self.ds.get_hostname().hostname + ) @mock.patch("cloudinit.sources.DataSourceAliYun._is_aliyun") def test_with_mock_server(self, m_is_aliyun): diff --git a/tests/unittests/test_datasource/test_cloudsigma.py b/tests/unittests/test_datasource/test_cloudsigma.py index 7aa3b1d1..79d52725 100644 --- a/tests/unittests/test_datasource/test_cloudsigma.py +++ b/tests/unittests/test_datasource/test_cloudsigma.py @@ -57,12 +57,14 @@ class DataSourceCloudSigmaTest(test_helpers.CiTestCase): def test_get_hostname(self): self.datasource.get_data() - self.assertEqual("test_server", self.datasource.get_hostname()) - self.datasource.metadata['name'] = '' - self.assertEqual("65b2fb23", self.datasource.get_hostname()) - utf8_hostname = b'\xd1\x82\xd0\xb5\xd1\x81\xd1\x82'.decode('utf-8') - self.datasource.metadata['name'] = utf8_hostname - self.assertEqual("65b2fb23", self.datasource.get_hostname()) + self.assertEqual( + "test_server", self.datasource.get_hostname().hostname + ) + self.datasource.metadata["name"] = "" + self.assertEqual("65b2fb23", self.datasource.get_hostname().hostname) + utf8_hostname = b"\xd1\x82\xd0\xb5\xd1\x81\xd1\x82".decode("utf-8") + self.datasource.metadata["name"] = utf8_hostname + self.assertEqual("65b2fb23", self.datasource.get_hostname().hostname) def test_get_public_ssh_keys(self): self.datasource.get_data() diff --git a/tests/unittests/test_datasource/test_hetzner.py b/tests/unittests/test_datasource/test_hetzner.py index eadb92f1..0ccb07c9 100644 --- a/tests/unittests/test_datasource/test_hetzner.py +++ b/tests/unittests/test_datasource/test_hetzner.py @@ -97,7 +97,7 @@ class TestDataSourceHetzner(CiTestCase): self.assertTrue(m_readmd.called) - self.assertEqual(METADATA.get('hostname'), ds.get_hostname()) + self.assertEqual(METADATA.get("hostname"), ds.get_hostname().hostname) self.assertEqual(METADATA.get('public-keys'), ds.get_public_ssh_keys()) diff --git a/tests/unittests/test_datasource/test_vmware.py b/tests/unittests/test_datasource/test_vmware.py index 597db7c8..3bffd136 100644 --- a/tests/unittests/test_datasource/test_vmware.py +++ b/tests/unittests/test_datasource/test_vmware.py @@ -356,7 +356,9 @@ class TestDataSourceVMwareGuestInfo_InvalidPlatform(FilesystemMockingTestCase): def assert_metadata(test_obj, ds, metadata): test_obj.assertEqual(metadata.get("instance-id"), ds.get_instance_id()) - test_obj.assertEqual(metadata.get("local-hostname"), ds.get_hostname()) + test_obj.assertEqual( + metadata.get("local-hostname"), ds.get_hostname().hostname + ) expected_public_keys = metadata.get("public_keys") if not isinstance(expected_public_keys, list): -- 2.31.1