diff --git a/changelogs/fragments/hostname-use-detection.yml b/changelogs/fragments/hostname-use-detection.yml new file mode 100644 index 00000000000..29b213d5384 --- /dev/null +++ b/changelogs/fragments/hostname-use-detection.yml @@ -0,0 +1,2 @@ +bugfixes: + - hostname - Fixed use=generic and use=debian and automatic detection for minimal Debian install without dbus (https://github.com/ansible/ansible/issues/85069). diff --git a/lib/ansible/modules/hostname.py b/lib/ansible/modules/hostname.py index 0f30ddf0f16..3efb57f023f 100644 --- a/lib/ansible/modules/hostname.py +++ b/lib/ansible/modules/hostname.py @@ -81,9 +81,9 @@ from ansible.module_utils.common.text.converters import to_native, to_text STRATS = { 'alpine': 'Alpine', - 'debian': 'Systemd', + 'debian': 'File', 'freebsd': 'FreeBSD', - 'generic': 'Base', + 'generic': 'File', 'macos': 'Darwin', 'macosx': 'Darwin', 'darwin': 'Darwin', @@ -109,7 +109,7 @@ class BaseStrategy(object): def update_current_hostname(self): name = self.module.params['name'] current_name = self.get_current_hostname() - if current_name != name: + if current_name is not None and current_name != name: if not self.module.check_mode: self.set_current_hostname(name) self.changed = True @@ -122,7 +122,7 @@ class BaseStrategy(object): self.set_permanent_hostname(name) self.changed = True - def get_current_hostname(self): + def get_current_hostname(self) -> None | str: return self.get_permanent_hostname() def set_current_hostname(self, name): @@ -171,9 +171,9 @@ class UnimplementedStrategy(BaseStrategy): class CommandStrategy(BaseStrategy): COMMAND = 'hostname' - def __init__(self, module): + def __init__(self, module: AnsibleModule, cmd_required: bool = True): super(CommandStrategy, self).__init__(module) - self.hostname_cmd = self.module.get_bin_path(self.COMMAND, True) + self.hostname_cmd = self.module.get_bin_path(self.COMMAND, cmd_required) def get_current_hostname(self): cmd = [self.hostname_cmd] @@ -195,9 +195,32 @@ class CommandStrategy(BaseStrategy): pass -class FileStrategy(BaseStrategy): +def requires_hostname_cmd(strategy_method): + def wrapper(self, *args, **kwargs): + if self.hostname_cmd is None: + self.module.warn( + f"The command '{self.COMMAND}' is not in the PATH, " + f"falling back to use the file {self.FILE} exclusively." + ) + return + return strategy_method(self, *args, **kwargs) + return wrapper + + +class FileStrategy(CommandStrategy): FILE = '/etc/hostname' + def __init__(self, module: AnsibleModule, cmd_required: bool = False): + super().__init__(module, cmd_required=cmd_required) + + @requires_hostname_cmd + def set_current_hostname(self, name): + return super().set_current_hostname(name) + + @requires_hostname_cmd + def get_current_hostname(self): + return super().get_current_hostname() + def get_permanent_hostname(self): if not os.path.isfile(self.FILE): return '' @@ -296,6 +319,11 @@ class SystemdStrategy(BaseStrategy): super(SystemdStrategy, self).__init__(module) self.hostnamectl_cmd = self.module.get_bin_path(self.COMMAND, True) + @property + def has_hostnamectl(self): + rc, out, err = self.module.run_command(self.hostnamectl_cmd) + return bool(rc == 0) + def get_current_hostname(self): cmd = [self.hostnamectl_cmd, '--transient', 'status'] rc, out, err = self.module.run_command(cmd) @@ -612,7 +640,10 @@ class Hostname(object): self.strategy = strategy(module) elif platform.system() == 'Linux' and ServiceMgrFactCollector.is_systemd_managed(module): # This is Linux and systemd is active - self.strategy = SystemdStrategy(module) + if (strategy := SystemdStrategy(module)) and strategy.has_hostnamectl: + self.strategy = strategy + else: + self.strategy = self.strategy_class(module) else: self.strategy = self.strategy_class(module) @@ -867,7 +898,7 @@ def main(): changed = hostname.update_current_and_permanent_hostname() - if name != current_hostname: + if current_hostname is not None and name != current_hostname: name_before = current_hostname else: name_before = permanent_hostname diff --git a/test/integration/targets/hostname/tasks/Alpine.yml b/test/integration/targets/hostname/tasks/Alpine.yml new file mode 100644 index 00000000000..2f8afebbdb3 --- /dev/null +++ b/test/integration/targets/hostname/tasks/Alpine.yml @@ -0,0 +1,20 @@ +--- +- name: Test AlpineStrategy by setting hostname + become: 'yes' + hostname: + use: alpine + name: "{{ ansible_distribution_release }}-bebop.ansible.example.com" + +- name: Test AlpineStrategy by getting current hostname + command: hostname + register: get_hostname + +- name: Test AlpineStrategy by verifying /etc/hostname content + command: grep -v '^#' /etc/hostname + register: grep_hostname + +- name: Test AlpineStrategy using assertions + assert: + that: + - "ansible_distribution_release ~ '-bebop.ansible.example.com' in get_hostname.stdout" + - "ansible_distribution_release ~ '-bebop.ansible.example.com' in grep_hostname.stdout" diff --git a/test/integration/targets/hostname/tasks/Debian.yml b/test/integration/targets/hostname/tasks/Debian.yml index a39280e21b6..7162366fb49 100644 --- a/test/integration/targets/hostname/tasks/Debian.yml +++ b/test/integration/targets/hostname/tasks/Debian.yml @@ -18,3 +18,50 @@ that: - "ansible_distribution_release ~ '-bebop.ansible.example.com' in get_hostname.stdout" - "ansible_distribution_release ~ '-bebop.ansible.example.com' in grep_hostname.stdout" + +- name: Test DebianStrategy without hostname + block: + - name: Get hostname command + shell: which hostname + register: hostname + ignore_errors: True + + - name: Move hostname command + shell: "mv {{ hostname.stdout }} {{ hostname.stdout }}.bak" + when: hostname.stdout != "" + become: True + register: moved_hostname + + - name: Test no change is made without hostname in the PATH + hostname: + name: "{{ ansible_distribution_release }}-bebop.ansible.example.com" + use: debian + register: debian_no_change + + - assert: + that: debian_no_change is not changed + + - name: Test updating /etc/hostname + hostname: + name: "{{ ansible_distribution_release }}-bop.ansible.example.com" + use: debian + register: debian_changed + + - name: Verify /etc/hostname content + command: grep -v '^#' /etc/hostname + register: grep_hostname + + - name: Assert /etc/hostname has been modified + assert: + that: + - debian_changed is changed + - "ansible_distribution_release ~ '-bop.ansible.example.com' in grep_hostname.stdout" + always: + - command: "mv {{ hostname.stdout }}.bak {{ hostname.stdout }}" + when: moved_hostname is defined and moved_hostname is changed + become: True + + - name: Verify the current hostname is unchanged + command: hostname + register: current_hostname + failed_when: "ansible_distribution_release ~ '-bop.ansible.example.com' in current_hostname.stdout" diff --git a/test/integration/targets/hostname/tasks/test_normal.yml b/test/integration/targets/hostname/tasks/test_normal.yml index 42e49b55d96..c8e81a39139 100644 --- a/test/integration/targets/hostname/tasks/test_normal.yml +++ b/test/integration/targets/hostname/tasks/test_normal.yml @@ -24,3 +24,13 @@ - hn3 is not changed - current_after_hn2.stdout == 'crocodile.ansible.test.doesthiswork.net.example.com' - current_after_hn2.stdout == current_after_hn2.stdout + +- name: Test use=generic + become: True + hostname: + name: crocodile.ansible.test.doesthiswork.net.example.com + use: generic + register: hn4 + +- assert: + that: hn4 is not changed