From e9038d8dc16f3f42c5c22f9be9d25842d74c4145 Mon Sep 17 00:00:00 2001 From: Jasper Lievisse Adriaanse Date: Fri, 13 Jan 2017 13:10:46 +0100 Subject: [PATCH] Add support for OpenBSD and SmartOS to timezone module --- lib/ansible/modules/system/timezone.py | 136 +++++++++++++++++++++++-- 1 file changed, 128 insertions(+), 8 deletions(-) diff --git a/lib/ansible/modules/system/timezone.py b/lib/ansible/modules/system/timezone.py index 6791590732..b1d65a9ea4 100644 --- a/lib/ansible/modules/system/timezone.py +++ b/lib/ansible/modules/system/timezone.py @@ -19,7 +19,11 @@ # along with Ansible. If not, see . import os +import platform +import random import re +import string + from ansible.module_utils.basic import AnsibleModule, get_platform from ansible.module_utils.six import iteritems @@ -34,12 +38,15 @@ module: timezone short_description: Configure timezone setting description: - This module configures the timezone setting, both of the system clock - and of the hardware clock. I(Currently only the Linux platform is supported). + and of the hardware clock. I(Currently only Linux, OpenBSD and SmartOS + instances are supported.) It is recommended to restart C(crond) after changing the timezone, otherwise the jobs may run at the wrong time. - It uses the C(timedatectl) command if available. Otherwise, it edits - C(/etc/sysconfig/clock) or C(/etc/timezone) for the system clock, + On Linux it uses the C(timedatectl) command if available. Otherwise, + it edits C(/etc/sysconfig/clock) or C(/etc/timezone) for the system clock, and uses the C(hwclock) command for the hardware clock. + On SmartOS the C(sm-set-timezone) utility is used to set the zone timezone, + and on OpenBSD C(/etc/localtime) is modified. If you want to set up the NTP, use M(service) module. version_added: "2.2" options: @@ -56,9 +63,12 @@ options: Note that this option is recommended not to change and may fail to configure, especially on virtual environments such as AWS. B(At least one of name and hwclock are required.) + I(Only used on Linux.) required: false aliases: ['rtc'] -author: "Shinichi TAMURA (@tmshn)" +author: + - "Shinichi TAMURA (@tmshn)" + - "Jasper Lievisse Adriaanse (@jasperla)" ''' RETURN = ''' @@ -93,8 +103,8 @@ class Timezone(object): def __new__(cls, module): """Return the platform-specific subclass. - It does not use load_platform_subclass() because it need to judge based - on whether the `timedatectl` command exists and available. + It does not use load_platform_subclass() because it needs to judge based + on whether the `timedatectl` command exists and is available. Args: module: The AnsibleModule. @@ -105,6 +115,14 @@ class Timezone(object): return super(Timezone, SystemdTimezone).__new__(SystemdTimezone) else: return super(Timezone, NosystemdTimezone).__new__(NosystemdTimezone) + elif re.match('^joyent_.*Z', platform.version()): + # get_platform() returns SunOS, which is too broad. So look at the + # platform version instead. + return super(Timezone, SmartOSTimezone).__new__(SmartOSTimezone) + elif re.match('^OpenBSD', platform.platform()): + # This might be too specific for now, however it can then serve as + # a generic base for /etc/localtime honoring Unix-like systems. + return super(Timezone, OpenBSDTimezone).__new__(OpenBSDTimezone) else: # Not supported yet return super(Timezone, Timezone).__new__(Timezone) @@ -430,12 +448,114 @@ class NosystemdTimezone(Timezone): self.abort('unknown parameter "%s"' % key) +class SmartOSTimezone(Timezone): + """This is a Timezone manipulation class for SmartOS instances. + + It uses the C(sm-set-timezone) utility to set the timezone, and + inspects C(/etc/default/init) to determine the current timezone. + + NB: A zone needs to be rebooted in order for the change to be + activated. + """ + + def __init__(self, module): + super(SmartOSTimezone, self).__init__(module) + self.settimezone = self.module.get_bin_path('sm-set-timezone', required=True) + + def get(self, key, phase): + """Lookup the current timezone name in `/etc/default/init`. If anything else + is requested, or if the TZ field is not set we fail. + """ + if key == 'name': + try: + f = open('/etc/default/init', 'r') + for line in f: + m = re.match('^TZ=(.*)$', line.strip()) + if m: + return m.groups()[0] + except: + self.module.fail_json(msg='Failed to read /etc/default/init') + else: + self.module.fail_json(msg='{0} is not a supported option on target platform'.format(key)) + + def set(self, key, value): + """Set the requested timezone through sm-set-timezone, an invalid timezone name + will be rejected and we have no further input validation to perform. + """ + if key == 'name': + cmd = 'sm-set-timezone {0}'.format(value) + + (rc, stdout, stderr) = self.module.run_command(cmd) + + if rc != 0: + self.module.fail_json(msg=stderr) + + # sm-set-timezone knows no state and will always set the timezone. + # XXX: https://github.com/joyent/smtools/pull/2 + m = re.match('^\* Changed (to)? timezone (to)? ({0}).*'.format(value), stdout.splitlines()[1]) + if not (m and m.groups()[-1] == value): + self.module.fail_json(msg='Failed to set timezone') + else: + self.module.fail_json(msg='{0} is not a supported option on target platform'. + format(key)) + + +class OpenBSDTimezone(Timezone): + """This is the timezone implementation for OpenBSD which works simply through + updating the `/etc/localtime` symlink to point to a valid timezone name under + `/usr/share/zoneinfo`. + """ + + def __init__(self, module): + super(OpenBSDTimezone, self).__init__(module) + + def get(self, key, phase): + """Lookup the current timezone by resolving `/etc/localtime`.""" + if key == 'name': + try: + tz = os.readlink('/etc/localtime') + return tz.replace('/usr/share/zoneinfo/', '') + except: + self.module.fail_json(msg='Could not read /etc/localtime') + else: + self.module.fail_json(msg='{0} is not a supported option on target platform'. + format(key)) + + def set(self, key, value): + if key == 'name': + # First determine if the requested timezone is valid by looking in + # the zoneinfo directory. + zonefile = '/usr/share/zoneinfo/' + value + try: + if not os.path.isfile(zonefile): + self.module.fail_json(msg='{0} is not a recognized timezone'.format(value)) + except: + self.module.fail_json(msg='Failed to stat {0}'.format(zonefile)) + + # Now (somewhat) atomically update the symlink by creating a new + # symlink and move it into place. Otherwise we have to remove the + # original symlink and create the new symlink, however that would + # create a race condition in case another process tries to read + # /etc/localtime between removal and creation. + suffix = "".join([random.choice(string.ascii_letters + string.digits) for x in range(0, 10)]) + new_localtime = '/etc/localtime.' + suffix + + try: + os.symlink(zonefile, new_localtime) + os.rename(new_localtime, '/etc/localtime') + except: + os.remove(new_localtime) + self.module.fail_json(msg='Could not update /etc/localtime') + else: + self.module.fail_json(msg='{0} is not a supported option on target platform'.format(key)) + + def main(): # Construct 'module' and 'tz' module = AnsibleModule( argument_spec=dict( - hwclock=dict(default=None, choices=['UTC', 'local'], aliases=['rtc']), - name=dict(default=None), + hwclock=dict(choices=['UTC', 'local'], aliases=['rtc']), + name=dict(), ), required_one_of=[['hwclock', 'name']], supports_check_mode=True