Repository: ansible/ansible

Issue:
failed to run "timezone" module
##### ISSUE TYPE
  - Bug Report
 

##### COMPONENT NAME
module: timezone

##### ANSIBLE VERSION
ansible 2.3

##### CONFIGURATION
default config

##### OS / ENVIRONMENT
Centos7 


##### SUMMARY

TASK [common : set timezone] **********************************************************************************
ok: [10.53.16.10]
ok: [10.53.16.22]
ok: [10.53.16.23]
ok: [10.53.16.11]
fatal: [10.53.16.12]: FAILED! => {"changed": false, "failed": true, "msg": "Error message:\ncannot read configuration file \"/etc/sysconfig/clock\" for name"}

##### STEPS TO REPRODUCE
unknown, but ofter appeared on my servers .

<!--- Paste example playbooks or commands between quotes below -->
```yaml
- name: set timezone
  timezone:
    name: Asia/Shanghai

```
##### EXPECTED RESULTS
return OK

##### ACTUAL RESULTS
fatal: [10.53.16.12]: FAILED! => {"changed": false, "failed": true, "msg": "Error message:\ncannot read configuration file \"/etc/sysconfig/clock\" for name"}

broken html on: timezone - Configure timezone setting 
##### ISSUE TYPE
Documentation Report

##### COMPONENT NAME
timezone module

##### ANSIBLE VERSION
2.3

##### CONFIGURATION


##### OS / ENVIRONMENT


##### SUMMARY
The site has broken html formatting on the right column, comments section.

##### STEPS TO REPRODUCE
[timezone_module on docs.ansible.com](http://docs.ansible.com/ansible/timezone_module.html)

##### EXPECTED RESULTS


##### ACTUAL RESULTS





Discussion:
The issue is often appeared, in fact the file "/etc/sysconfig/clock"  on Centos7.3  is always not existed on all my servers, 
I don't know why it happened only on 10.53.16.12. 
I found at that time  failed to run "timedatectl" on the server .
# timedatectl 
Failed to query server: Connection timed out

on the other severs which didn't happen the issue,  it is ok to run "timedatectl".

cc @tmshn @jasperla
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@todaygood Thanks for the report!

As I believe, on CentOS 7, the absence of `"/etc/sysconfig/clock"` is **correct** state; this file used to be used in older version like CentOS 6.

Let me ask some questions to clarify the situation:

1. The problem does **not** happen every time you try to provision?
2. When the problem happens, it is always on `10.53.16.12`?
3. When "Connection timed out" happens, it is always on the `timezone` module?
4. What will happen when you run `timedatectl` command in the server, not through Ansible but via SSH? (I want to see: return code, outputs on stdout and stderr, if any)

1. yes, not every time, but appeared with high frequency.
2.not always.
3. yes.
4. maybe manually run "timedatectl"  will fail too, because  I found an issue 
https://github.com/systemd/systemd/issues/6135

I forgot systemd issue is happened the server whether is too happened on one server, I thought this issue and that issue  have no relationship. 
when this issue appeared next time , I will notice it  and report here.

Okay, so you mean "Connection timed out" happened...

```
                                    here ↓
[Controll machine]--SSH--[Target host]--NTP--[NTP server]
```
right?
I found the similar issue for SaltStack:
https://github.com/saltstack/salt/issues/26313

IMO, the problem that `timedatectl` fails  is out of the scope of Ansible.
Similarly to that issue, all I can do here is to modify the error handling to give a better message.
What do you think? @todaygood 
Note that I got this when running my `geerlingguy.ntp` role on a minimal CentOS 6 install with one VPS host, and adding a missing `/etc/sysconfig/clock` file as root with the contents `ZONE="America/Chicago"` (or any other valid timezone) fixed the issue for me.

I haven't had any issues on CentOS 7 minimal installs, nor have I encountered this on any other hosting environment running CentOS 6, so I'll chalk it up to a bad CentOS 6 minimal image with the particular VPS host I used for that server.
Actually, I take that back, since switching to using Ansible's `timezone` module, my NTP role is now failing on all CentOS 6 minimal install hosts with:

```
fatal: [server]: FAILED! => {"changed": false, "failed": true, "msg": "Error message:\ncannot read configuration file \"/etc/sysconfig/clock\" for name"}
```

Maybe in my case there's some package that sets up the clock file that's missing?
@tmshn , good. 
The module is failing when trying to build an AWS AMI with Hashicorp Packer based off the one with id `ami-402f1a33` but with a different message:
```
amazon-ebs: fatal: [127.0.0.1]: FAILED! => {
    amazon-ebs:     "changed": false,
    amazon-ebs:     "failed": true,
    amazon-ebs:     "invocation": {
    amazon-ebs:         "module_args": {
    amazon-ebs:             "hwclock": null,
    amazon-ebs:             "name": "Europe/Zurich"
    amazon-ebs:         }
    amazon-ebs:     },
    amazon-ebs:     "msg": "Error message:\nstill not desired state, though changes have made\nOther message(s):\nAdded 1 line and deleted 1 line(s) on /etc/timezone"
    amazon-ebs: }
```

The task code is simply:
```
---
- name: "Set server timezone to Europe/Zurich"
  timezone:
    name: "Europe/Zurich"
  become: yes
  notify: restart crond
```

I am also using @geerlingguy 's NTP role with its default values and the operating system under which this fails is a Debian OS.

Note that this does NOT fail when building a VM locally with test-kitchen.
Update, on the EC2 instance where this fails, I get the following:
```
root# timedatectl
Failed to create bus connection: No such file or directory
```

I also saw #19280, could they be related (Debian is used there as well as in my instance)?
Another update. Looks like the AWS instance was missing the `dbus` package whereby the VM used in test-kitchen has it (*sigh*). Installing it (with Ansible) on the builder EC2 instance solved my problem. 


cc @tmshn @jasperla
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@rt711 Thanks for the report! Just to confirm, are you talking about the table on the "*Return Values*" section? Or elsewhere?
@tmshn Hi, both the "options" and "return values" actually. On mobile view options adds an extra column to the end, on PC it's a row.
I'm  adding a screenshot and you can see the "return values" contains section looks like as a table inside a table. Not entirely sure how should I understand that part.
![screenshot from 2017-04-03 08-14-55](https://cloud.githubusercontent.com/assets/1867519/24598887/ad0ce8d0-1845-11e7-935d-13b3ad6a8266.png)

@rt711 Okay I can see the extra buggy table inside of the table on the "return values" section.

But about the table on the "options", I can't see any extra rows or columns.

Here's my screenshot (the table is colored blue because I'm using Chrome's dev tool).
![timezone_doc_options](https://cloud.githubusercontent.com/assets/3760893/24653161/a3c3705c-196f-11e7-8abd-51ffb784ad02.png)

@tmshn no problem, here is the screenshot. Browser: Firefox
![screenshot from 2017-04-05 07-50-02](https://cloud.githubusercontent.com/assets/1867519/24693256/9a2e360c-19d4-11e7-81e7-ef584a535d8b.png)

@rt711 
#### An extra row/col on table
This is not because of my docs, but because of the size mismatch between `<table>` and `<tbody>` in css. This mismatch, I guess, is intended to support scroll on narrow screen.

But showing up extra/rows on wider screen might be a bug?
Hello @sivel, do you have any idea about this?

#### Table-in-table on "return" section
As I look through other modules' docs, I found this is not a bug but a correct state for return values of `dict` type to show the items in it.
You can see 
E.g.:
- http://docs.ansible.com/ansible/redshift_module.html
- http://docs.ansible.com/ansible/win_updates_module.html

But I agreed that they looks weird and hard to understand, so here's what I tried. IMO this look better, isn't it?
<img width="776" alt="2017-04-13 14 02 14" src="https://cloud.githubusercontent.com/assets/3760893/24990862/ea21989c-2051-11e7-8cf9-de5d3759aa11.png">


Resolving patch:
diff --git a/lib/ansible/modules/system/timezone.py b/lib/ansible/modules/system/timezone.py
--- a/lib/ansible/modules/system/timezone.py
+++ b/lib/ansible/modules/system/timezone.py
@@ -70,6 +70,7 @@
     name: Asia/Tokyo
 '''
 
+import errno
 import os
 import platform
 import random
@@ -99,8 +100,13 @@ def __new__(cls, module):
         """
         if get_platform() == 'Linux':
             timedatectl = module.get_bin_path('timedatectl')
-            if timedatectl is not None and module.run_command(timedatectl)[0] == 0:
-                return super(Timezone, SystemdTimezone).__new__(SystemdTimezone)
+            if timedatectl is not None:
+                rc, stdout, stderr = module.run_command(timedatectl)
+                if rc == 0:
+                    return super(Timezone, SystemdTimezone).__new__(SystemdTimezone)
+                else:
+                    module.warn('timedatectl command was found but not usable: %s. using other method.' % stderr)
+                    return super(Timezone, NosystemdTimezone).__new__(NosystemdTimezone)
             else:
                 return super(Timezone, NosystemdTimezone).__new__(NosystemdTimezone)
         elif re.match('^joyent_.*Z', platform.version()):
@@ -311,6 +317,8 @@ class NosystemdTimezone(Timezone):
         adjtime='/etc/adjtime'
     )
 
+    allow_no_file = dict()
+
     regexps = dict(
         name   =None,  # To be set in __init__
         hwclock=re.compile(r'^UTC\s*=\s*([^\s]+)', re.MULTILINE),
@@ -325,28 +333,41 @@ def __init__(self, module):
             self.update_timezone  = self.module.get_bin_path('cp', required=True)
             self.update_timezone += ' %s /etc/localtime' % tzfile
         self.update_hwclock = self.module.get_bin_path('hwclock', required=True)
+        self.allow_no_file['hwclock'] = True  # Since this is only used for get values, file absense does not metter
         # Distribution-specific configurations
         if self.module.get_bin_path('dpkg-reconfigure') is not None:
             # Debian/Ubuntu
             self.update_timezone       = self.module.get_bin_path('dpkg-reconfigure', required=True)
             self.update_timezone      += ' --frontend noninteractive tzdata'
             self.conf_files['name']    = '/etc/timezone'
+            self.allow_no_file['name'] = True
             self.conf_files['hwclock'] = '/etc/default/rcS'
             self.regexps['name']       = re.compile(r'^([^\s]+)', re.MULTILINE)
             self.tzline_format         = '%s\n'
         else:
             # RHEL/CentOS
             if self.module.get_bin_path('tzdata-update') is not None:
-                self.update_timezone   = self.module.get_bin_path('tzdata-update', required=True)
+                self.update_timezone       = self.module.get_bin_path('tzdata-update', required=True)
+                self.allow_no_file['name'] = True
             # else:
-            #   self.update_timezone   = 'cp ...' <- configured above
+            #   self.update_timezone       = 'cp ...' <- configured above
+            #   self.allow_no_file['name'] = False <- this is default behavior
             self.conf_files['name']    = '/etc/sysconfig/clock'
             self.conf_files['hwclock'] = '/etc/sysconfig/clock'
             self.regexps['name']       = re.compile(r'^ZONE\s*=\s*"?([^"\s]+)"?', re.MULTILINE)
             self.tzline_format         = 'ZONE="%s"\n'
-        self.update_hwclock  = self.module.get_bin_path('hwclock', required=True)
 
-    def _edit_file(self, filename, regexp, value):
+    def _allow_ioerror(self, err, key):
+        # In some cases, even if the target file does not exist,
+        # simply creating it may solve the problem.
+        # In such cases, we should continue the configuration rather than aborting.
+        if err.errno != errno.ENOENT:
+            # If the error is not ENOENT ("No such file or directory"),
+            # (e.g., permission error, etc), we should abort.
+            return False
+        return self.allow_no_file.get(key, False)
+
+    def _edit_file(self, filename, regexp, value, key):
         """Replace the first matched line with given `value`.
 
         If `regexp` matched more than once, other than the first line will be deleted.
@@ -355,12 +376,16 @@ def _edit_file(self, filename, regexp, value):
             filename: The name of the file to edit.
             regexp:   The regular expression to search with.
             value:    The line which will be inserted.
+            key:      For what key the file is being editted.
         """
         # Read the file
         try:
             file = open(filename, 'r')
-        except IOError:
-            self.abort('cannot read "%s"' % filename)
+        except IOError as err:
+            if self._allow_ioerror(err, key):
+                lines = []
+            else:
+                self.abort('tried to configure %s using a file "%s", but could not read it' % (key, filename))
         else:
             lines = file.readlines()
             file.close()
@@ -382,7 +407,7 @@ def _edit_file(self, filename, regexp, value):
         try:
             file = open(filename, 'w')
         except IOError:
-            self.abort('cannot write to "%s"' % filename)
+            self.abort('tried to configure %s using a file "%s", but could not write to it' % (key, filename))
         else:
             file.writelines(lines)
             file.close()
@@ -397,15 +422,18 @@ def get(self, key, phase):
 
         try:
             file = open(filename, mode='r')
-        except IOError:
-            self.abort('cannot read configuration file "%s" for %s' % (filename, key))
+        except IOError as err:
+            if self._allow_ioerror(err, key):
+                return None
+            else:
+                self.abort('tried to configure %s using a file "%s", but could not read it' % (key, filename))
         else:
             status = file.read()
             file.close()
             try:
                 value = self.regexps[key].search(status).group(1)
             except AttributeError:
-                self.abort('cannot find the valid value from configuration file "%s" for %s' % (filename, key))
+                self.abort('tried to configure %s using a file "%s", but could not find a valid value in it' % (key, filename))
             else:
                 if key == 'hwclock':
                     # For key='hwclock'; convert yes/no -> UTC/local
@@ -422,7 +450,8 @@ def get(self, key, phase):
     def set_timezone(self, value):
         self._edit_file(filename=self.conf_files['name'],
                         regexp=self.regexps['name'],
-                        value=self.tzline_format % value)
+                        value=self.tzline_format % value,
+                        key='name')
         self.execute(self.update_timezone)
 
     def set_hwclock(self, value):


Test patch: