Repository: ansible-collections/community.general

Issue:
timezone module does not work on Gentoo
##### SUMMARY
Note: This was initially reported as ansible/ansible#47891.

The timezone module fails on a non-systemd Gentoo target.
The module checks for Debian and SuSE and then treats everything else as RedHat, i.e. do something to `/etc/sysconfig/clock`. This does not work on, at least, Gentoo, since `/etc/sysconfig` does not exist. The Gentoo way is described in [the Handbook](https://wiki.gentoo.org/wiki/Handbook:AMD64/Installation/Base#Timezone).

The TL;DR is:

```sh
echo 'Europe/Berlin' > /etc/timezone
emerge --config sys-libs/timezone-data
```

##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME
timezone

##### ANSIBLE VERSION
```
ansible 2.9.10
  config file = None
  configured module search path = ['/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.7/site-packages/ansible
  executable location = /usr/lib/python-exec/python3.7/ansible
  python version = 3.7.8 (default, Jul 24 2020, 18:32:17) [GCC 9.3.0]
```

##### CONFIGURATION

not relevant

##### OS / ENVIRONMENT

Target OS is Gentoo Linux.

##### STEPS TO REPRODUCE

```yaml
- tasks:
  - name: 'set time zone'
    timezone:
      name: 'Europe/Berlin'
```

##### EXPECTED RESULTS

The timezone on the target system is set correctly.

##### ACTUAL RESULTS

Ansible fails with the following error:

```
The full traceback is:
  File "/tmp/ansible_xxpihswr/ansible_module_timezone.py", line 456, in _edit_file
    file = open(filename, 'w')

fatal: [localhost]: FAILED! => {
    "changed": false,
    "invocation": {
        "module_args": {
            "hwclock": null,
            "name": "Europe/Berlin"
        }
    },
    "msg": "Error message:\ntried to configure name using a file \"/etc/sysconfig/clock\", but could not write to it"
}
```



Discussion:
Files identified in the description:
* [`plugins/modules/system/timezone.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/system/timezone.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @MorrisA @bcoca @d-little @flynn1973 @gforster @indrajitr @jasperla @kairoaraujo @marvin-sinister @mator @molekuul @ramooncamacho @tmshn @wtcross
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
This issue also happens on Alpine, for the same reasons: Lack of `/etc/sysconfig`.

Alpine is running in a VM, if that contributes to anything.
i failed with installing gentoo, but quickly installed alpine (like 5-10 minutes)... ansible current timezone code with my alpine installation is: 
```
$ ansible -m timezone alpine
alpine | FAILED! => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3.8"
    },
    "changed": false,
    "msg": "one of the following is required: hwclock, name"
}
```
but hwclock is installed
```
alpine:~$ hwclock 
hwclock: Cannot access the Hardware Clock via any known method.
```
> i failed with installing gentoo

I did not try it, but you might be able to reproduce the issue in a [Gentoo container](https://hub.docker.com/u/gentoo/). Try `gentoo/stage3-amd64` or something like that.
@mator 

> but hwclock is installed

That is not a reproducer. Your command didn't actually invoke the module timezone detection code because you failed to pass any mandatory arguments (that is at least one of `hwclock` or `name` — as the error message says). It didn't go past the module helper instance constructor: https://github.com/ansible-collections/community.general/blob/8afdd23/plugins/modules/system/timezone.py#L872-L874.

The correct syntax would be
```console
$ ansible -m timezone -a 'name=Europe/Berlin' alpine
```
Here's a container-based repro demo:
[![asciicast](https://asciinema.org/a/388463.svg)](https://asciinema.org/a/388463)
> That is not a reproducer. Your command didn't actually invoke the module timezone detection code because you failed to pass any mandatory arguments (that is at least one of `hwclock` or `name` — as the error message says). 

1. the error message is so confusing for me, i thought that i miss some packages installed. We probably need to change it to  add "missing arguments", so it would be clearer.
2. i was under impression that if there's no arguments, timezone module would return current server timezone


running with arguments:
```
$ ansible -m timezone alpine -a 'name=europe/moscow'
alpine | FAILED! => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3.8"
    },
    "changed": false,
    "msg": "Error message:\ngiven timezone \"europe/moscow\" is not available"
}

$ ansible -m timezone alpine -a 'name=Europe/Moscow'
alpine | FAILED! => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3.8"
    },
    "changed": false,
    "msg": "Error message:\ntried to configure name using a file \"/etc/sysconfig/clock\", but could not write to it"
} 
```

The error message comes from here: https://github.com/ansible/ansible/blob/devel/lib/ansible/module_utils/common/validation.py#L95

Resolving patch:
diff --git a/plugins/modules/system/timezone.py b/plugins/modules/system/timezone.py
--- a/plugins/modules/system/timezone.py
+++ b/plugins/modules/system/timezone.py
@@ -355,20 +355,26 @@ def __init__(self, module):
         # Validate given timezone
         if 'name' in self.value:
             tzfile = self._verify_timezone()
+            planned_tz = self.value['name']['planned']
             # `--remove-destination` is needed if /etc/localtime is a symlink so
             # that it overwrites it instead of following it.
             self.update_timezone = ['%s --remove-destination %s /etc/localtime' % (self.module.get_bin_path('cp', required=True), tzfile)]
         self.update_hwclock = self.module.get_bin_path('hwclock', required=True)
+        distribution = get_distribution()
+        self.conf_files['name'] = '/etc/timezone'
+        self.regexps['name'] = re.compile(r'^([^\s]+)', re.MULTILINE)
+        self.tzline_format = '%s\n'
         # Distribution-specific configurations
         if self.module.get_bin_path('dpkg-reconfigure') is not None:
             # Debian/Ubuntu
             if 'name' in self.value:
                 self.update_timezone = ['%s -sf %s /etc/localtime' % (self.module.get_bin_path('ln', required=True), tzfile),
                                         '%s --frontend noninteractive tzdata' % self.module.get_bin_path('dpkg-reconfigure', required=True)]
-            self.conf_files['name'] = '/etc/timezone'
             self.conf_files['hwclock'] = '/etc/default/rcS'
-            self.regexps['name'] = re.compile(r'^([^\s]+)', re.MULTILINE)
-            self.tzline_format = '%s\n'
+        elif distribution == 'Alpine' or distribution == 'Gentoo':
+            self.conf_files['hwclock'] = '/etc/conf.d/hwclock'
+            if distribution == 'Alpine':
+                self.update_timezone = ['%s -z %s' % (self.module.get_bin_path('setup-timezone', required=True), planned_tz)]
         else:
             # RHEL/CentOS/SUSE
             if self.module.get_bin_path('tzdata-update') is not None:
@@ -386,7 +392,6 @@ def __init__(self, module):
             except IOError as err:
                 if self._allow_ioerror(err, 'name'):
                     # If the config file doesn't exist detect the distribution and set regexps.
-                    distribution = get_distribution()
                     if distribution == 'SuSE':
                         # For SUSE
                         self.regexps['name'] = self.dist_regexps['SuSE']
@@ -536,7 +541,9 @@ def get(self, key, phase):
                         # to other zone files, so it's hard to get which TZ is actually set
                         # if we follow the symlink.
                         path = os.readlink('/etc/localtime')
-                        linktz = re.search(r'/usr/share/zoneinfo/(.*)', path, re.MULTILINE)
+                        # most linuxes has it in /usr/share/zoneinfo
+                        # alpine linux links under /etc/zoneinfo
+                        linktz = re.search(r'(?:/(?:usr/share|etc)/zoneinfo/)(.*)', path, re.MULTILINE)
                         if linktz:
                             valuelink = linktz.group(1)
                             if valuelink != planned:


Test patch: