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