diff --git a/salt/modules/timezone.py b/salt/modules/timezone.py index a01e3cedf6..e9cd979843 100644 --- a/salt/modules/timezone.py +++ b/salt/modules/timezone.py @@ -6,8 +6,10 @@ from __future__ import absolute_import # Import python libs import os +import errno import logging import re +import string # Import salt libs import salt.utils @@ -25,6 +27,67 @@ def __virtual__(): return True +def _get_zone_solaris(): + tzfile = '/etc/TIMEZONE' + with salt.utils.fopen(tzfile, 'r') as fp_: + for line in fp_: + if 'TZ=' in line: + zonepart = line.rstrip('\n').split('=')[-1] + return zonepart.strip('\'"') or 'UTC' + raise CommandExecutionError('Unable to get timezone from ' + tzfile) + + +def _get_zone_sysconfig(): + tzfile = '/etc/sysconfig/clock' + with salt.utils.fopen(tzfile, 'r') as fp_: + for line in fp_: + if re.match(r'^\s*#', line): + continue + if 'ZONE' in line and '=' in line: + zonepart = line.rstrip('\n').split('=')[-1] + return zonepart.strip('\'"') or 'UTC' + raise CommandExecutionError('Unable to get timezone from ' + tzfile) + + +def _get_zone_etc_localtime(): + tzfile = '/etc/localtime' + tzdir = '/usr/share/zoneinfo/' + tzdir_len = len(tzdir) + try: + olson_name = os.path.normpath( + os.path.join('/etc', os.readlink(tzfile)) + ) + if olson_name.startswith(tzdir): + return olson_name[tzdir_len:] + except OSError as exc: + if exc.errno == errno.ENOENT: + raise CommandExecutionError(tzfile + ' does not exist') + elif exc.errno == errno.EINVAL: + log.warning( + tzfile + ' is not a symbolic link, attempting to match ' + + tzfile + ' to zoneinfo files' + ) + # Regular file. Try to match the hash. + hash_type = __opts__.get('hash_type', 'md5') + tzfile_hash = salt.utils.get_hash(tzfile, hash_type) + # Not a link, just a copy of the tzdata file + for root, dirs, files in os.walk(tzdir): + for filename in files: + full_path = os.path.join(root, filename) + olson_name = full_path[tzdir_len:] + if olson_name[0] in string.ascii_lowercase: + continue + if tzfile_hash == \ + salt.utils.get_hash(full_path, hash_type): + return olson_name + raise CommandExecutionError('Unable to determine timezone') + + +def _get_zone_etc_timezone(): + with salt.utils.fopen('/etc/timezone', 'r') as fp_: + return fp_.read().strip() + + def get_zone(): ''' Get current timezone (i.e. America/Denver) @@ -37,7 +100,7 @@ def get_zone(): ''' cmd = '' if salt.utils.which('timedatectl'): - out = __salt__['cmd.run']('timedatectl') + out = __salt__['cmd.run'](['timedatectl'], python_shell=False) for line in (x.strip() for x in out.splitlines()): try: return re.match(r'Time ?zone:\s+(\S+)', line).group(1) @@ -46,23 +109,21 @@ def get_zone(): raise CommandExecutionError( 'Failed to parse timedatectl output, this is likely a bug' ) - elif 'RedHat' in __grains__['os_family']: - cmd = 'grep ZONE /etc/sysconfig/clock | grep -vE "^#"' - elif 'Suse' in __grains__['os_family']: - cmd = 'grep ZONE /etc/sysconfig/clock | grep -vE "^#"' - elif 'Debian' in __grains__['os_family']: - with salt.utils.fopen('/etc/timezone', 'r') as ofh: - return ofh.read().strip() - elif 'Gentoo' in __grains__['os_family']: - with salt.utils.fopen('/etc/timezone', 'r') as ofh: - return ofh.read().strip() - elif __grains__['os_family'] in ('FreeBSD', 'OpenBSD', 'NetBSD'): - return os.readlink('/etc/localtime').lstrip('/usr/share/zoneinfo/') - elif 'Solaris' in __grains__['os_family']: - cmd = 'grep "TZ=" /etc/TIMEZONE' - out = __salt__['cmd.run'](cmd, python_shell=True).split('=') - ret = out[1].replace('"', '') - return ret + else: + if __grains__['os'].lower() == 'centos': + return _get_zone_etc_localtime() + os_family = __grains__['os_family'] + for family in ('RedHat', 'Suse'): + if family in os_family: + return _get_zone_sysconfig() + for family in ('Debian', 'Gentoo'): + if family in os_family: + return _get_zone_etc_timezone() + if os_family in ('FreeBSD', 'OpenBSD', 'NetBSD'): + return _get_zone_etc_localtime() + elif 'Solaris' in os_family: + return _get_zone_solaris() + raise CommandExecutionError('Unable to get timezone') def get_zonecode(): @@ -75,9 +136,7 @@ def get_zonecode(): salt '*' timezone.get_zonecode ''' - cmd = 'date +%Z' - out = __salt__['cmd.run'](cmd) - return out + return __salt__['cmd.run'](['date', '+%Z'], python_shell=False) def get_offset(): @@ -90,9 +149,7 @@ def get_offset(): salt '*' timezone.get_offset ''' - cmd = 'date +%z' - out = __salt__['cmd.run'](cmd) - return out + return __salt__['cmd.run'](['date', '+%z'], python_shell=False) def set_zone(timezone): @@ -101,7 +158,8 @@ def set_zone(timezone): The timezone is crucial to several system processes, each of which SHOULD be restarted (for instance, whatever you system uses as its cron and - syslog daemons). This will not be magically done for you! + syslog daemons). This will not be automagically done and must be done + manually! CLI Example: @@ -195,7 +253,7 @@ def get_hwclock(): ''' cmd = '' if salt.utils.which('timedatectl'): - out = __salt__['cmd.run']('timedatectl') + out = __salt__['cmd.run'](['timedatectl'], python_shell=False) for line in (x.strip() for x in out.splitlines()): if 'rtc in local tz' in line.lower(): try: @@ -208,39 +266,64 @@ def get_hwclock(): raise CommandExecutionError( 'Failed to parse timedatectl output, this is likely a bug' ) - elif 'RedHat' in __grains__['os_family']: - cmd = 'tail -n 1 /etc/adjtime' - return __salt__['cmd.run'](cmd) - elif 'Suse' in __grains__['os_family']: - cmd = 'tail -n 1 /etc/adjtime' - return __salt__['cmd.run'](cmd) - elif 'Debian' in __grains__['os_family']: - #Original way to look up hwclock on Debian-based systems - cmd = 'grep "UTC=" /etc/default/rcS | grep -vE "^#"' - out = __salt__['cmd.run']( - cmd, ignore_retcode=True, python_shell=True).split('=') - if len(out) > 1: - if out[1] == 'yes': - return 'UTC' - else: - return 'localtime' - else: - #Since Wheezy - cmd = 'tail -n 1 /etc/adjtime' - return __salt__['cmd.run'](cmd) - elif 'Gentoo' in __grains__['os_family']: - cmd = 'grep "^clock=" /etc/conf.d/hwclock | grep -vE "^#"' - out = __salt__['cmd.run'](cmd, python_shell=True).split('=') - return out[1].replace('"', '') - elif 'Solaris' in __grains__['os_family']: - if os.path.isfile('/etc/rtc_config'): - with salt.utils.fopen('/etc/rtc_config', 'r') as fp_: - for line in fp_: - if line.startswith('zone_info=GMT'): - return 'UTC' - return 'localtime' - else: - return 'UTC' + else: + os_family = __grains__['os_family'] + for family in ('RedHat', 'Suse'): + if family in os_family: + cmd = ['tail', '-n', '1', '/etc/adjtime'] + return __salt__['cmd.run'](cmd, python_shell=False) + if 'Debian' in __grains__['os_family']: + # Original way to look up hwclock on Debian-based systems + try: + with salt.utils.fopen('/etc/default/rcS', 'r') as fp_: + for line in fp_: + if re.match(r'^\s*#', line): + continue + if 'UTC=' in line: + is_utc = line.rstrip('\n').split('=')[-1].lower() + if is_utc == 'yes': + return 'UTC' + else: + return 'localtime' + except IOError as exc: + pass + # Since Wheezy + cmd = ['tail', '-n', '1', '/etc/adjtime'] + return __salt__['cmd.run'](cmd, python_shell=False) + elif 'Gentoo' in __grains__['os_family']: + offset_file = '/etc/conf.d/hwclock' + try: + with salt.utils.fopen(offset_file, 'r') as fp_: + for line in fp_: + if line.startswith('clock='): + line = line.rstrip('\n') + return line.split('=')[-1].strip('\'"') + raise CommandExecutionError( + 'Offset information not found in {0}'.format( + offset_file + ) + ) + except IOError as exc: + raise CommandExecutionError( + 'Problem reading offset file {0}: {1}' + .format(offset_file, exc.strerror) + ) + elif 'Solaris' in __grains__['os_family']: + offset_file = '/etc/rtc_config' + try: + with salt.utils.fopen(offset_file, 'r') as fp_: + for line in fp_: + if line.startswith('zone_info=GMT'): + return 'UTC' + return 'localtime' + except IOError as exc: + if exc.errno == errno.ENOENT: + # offset file does not exist + return 'UTC' + raise CommandExecutionError( + 'Problem reading offset file {0}: {1}' + .format(offset_file, exc.strerror) + ) def set_hwclock(clock): @@ -256,33 +339,31 @@ def set_hwclock(clock): timezone = get_zone() if 'Solaris' in __grains__['os_family']: + if clock.lower() not in ('localtime', 'utc'): + raise SaltInvocationError( + 'localtime and UTC are the only permitted values' + ) if 'sparc' in __grains__['cpuarch']: - return 'UTC is the only choice for SPARC architecture' - if clock == 'localtime': - cmd = 'rtc -z {0}'.format(timezone) - __salt__['cmd.run'](cmd) - return True - elif clock == 'UTC': - cmd = 'rtc -z GMT' - __salt__['cmd.run'](cmd) - return True - else: - zonepath = '/usr/share/zoneinfo/{0}'.format(timezone) + raise SaltInvocationError( + 'UTC is the only choice for SPARC architecture' + ) + cmd = ['rtc', '-z', 'GMT' if clock.lower() == 'utc' else timezone] + return __salt__['cmd.retcode'](cmd, python_shell=False) == 0 + + zonepath = '/usr/share/zoneinfo/{0}'.format(timezone) if not os.path.exists(zonepath): - return 'Zone does not exist: {0}'.format(zonepath) + raise CommandExecutionError( + 'Zone \'{0}\' does not exist'.format(zonepath) + ) - if 'Solaris' not in __grains__['os_family']: - os.unlink('/etc/localtime') - os.symlink(zonepath, '/etc/localtime') + os.unlink('/etc/localtime') + os.symlink(zonepath, '/etc/localtime') if 'Arch' in __grains__['os_family']: - if clock == 'localtime': - cmd = 'timezonectl set-local-rtc true' - __salt__['cmd.run'](cmd) - else: - cmd = 'timezonectl set-local-rtc false' - __salt__['cmd.run'](cmd) + cmd = ['timezonectl', 'set-local-rtc', + 'true' if clock == 'localtime' else 'false'] + return __salt__['cmd.retcode'](cmd, python_shell=False) == 0 elif 'RedHat' in __grains__['os_family']: __salt__['file.sed']( '/etc/sysconfig/clock', '^ZONE=.*', 'ZONE="{0}"'.format(timezone)) diff --git a/tests/unit/modules/timezone_test.py b/tests/unit/modules/timezone_test.py index 941e730576..e28a3c0fc1 100644 --- a/tests/unit/modules/timezone_test.py +++ b/tests/unit/modules/timezone_test.py @@ -22,8 +22,7 @@ ensure_in_syspath('../../') import salt.utils from salt.modules import timezone import os -from salt.exceptions import CommandExecutionError -from salt.exceptions import SaltInvocationError +from salt.exceptions import CommandExecutionError, SaltInvocationError # Globals @@ -53,22 +52,28 @@ class TimezoneTestCase(TestCase): with patch('salt.utils.fopen', mock_open(read_data=file_data), create=True) as mfile: mfile.return_value.__iter__.return_value = file_data.splitlines() - with patch.dict(timezone.__grains__, {'os_family': 'Debian'}): + with patch.dict(timezone.__grains__, {'os_family': 'Debian', + 'os': 'Debian'}): self.assertEqual(timezone.get_zone(), '#\nA') - with patch.dict(timezone.__grains__, {'os_family': 'Gentoo'}): + with patch.dict(timezone.__grains__, {'os_family': 'Gentoo', + 'os': 'Gentoo'}): self.assertEqual(timezone.get_zone(), '') - with patch.dict(timezone.__grains__, {'os_family': 'FreeBSD'}): - with patch.object(os, 'readlink', - return_value='/usr/share/zoneinfo/'): - self.assertEqual(timezone.get_zone(), '') + with patch.dict(timezone.__grains__, {'os_family': 'FreeBSD', + 'os': 'FreeBSD'}): + zone = 'America/Denver' + linkpath = '/usr/share/zoneinfo/' + zone + with patch.object(os, 'readlink', return_value=linkpath): + self.assertEqual(timezone.get_zone(), zone) - with patch.dict(timezone.__grains__, {'os_family': 'Solaris'}): - with patch.dict(timezone.__salt__, - {'cmd.run': - MagicMock(return_value='A=B')}): - self.assertEqual(timezone.get_zone(), 'B') + with patch.dict(timezone.__grains__, {'os_family': 'Solaris', + 'os': 'Solaris'}): + fl_data = 'TZ=Foo\n' + with patch('salt.utils.fopen', + mock_open(read_data=fl_data)) as mfile: + mfile.return_value.__iter__.return_value = [fl_data] + self.assertEqual(timezone.get_zone(), 'Foo') def test_get_zonecode(self): ''' @@ -171,26 +176,24 @@ class TimezoneTestCase(TestCase): self.assertEqual(timezone.get_hwclock(), 'A') with patch.dict(timezone.__grains__, {'os_family': 'Debian'}): - with patch.dict(timezone.__salt__, - {'cmd.run': - MagicMock(return_value='A=yes')}): + fl_data = 'UTC=yes\n' + with patch('salt.utils.fopen', + mock_open(read_data=fl_data)) as mfile: + mfile.return_value.__iter__.return_value = [fl_data] self.assertEqual(timezone.get_hwclock(), 'UTC') - with patch.dict(timezone.__salt__, - {'cmd.run': - MagicMock(return_value='A=no')}): + fl_data = 'UTC=no\n' + with patch('salt.utils.fopen', + mock_open(read_data=fl_data)) as mfile: + mfile.return_value.__iter__.return_value = [fl_data] self.assertEqual(timezone.get_hwclock(), 'localtime') - with patch.dict(timezone.__salt__, - {'cmd.run': - MagicMock(return_value='A')}): - self.assertEqual(timezone.get_hwclock(), 'A') - with patch.dict(timezone.__grains__, {'os_family': 'Gentoo'}): - with patch.dict(timezone.__salt__, - {'cmd.run': - MagicMock(return_value='A=B')}): - self.assertEqual(timezone.get_hwclock(), 'B') + fl_data = 'clock=UTC\n' + with patch('salt.utils.fopen', + mock_open(read_data=fl_data)) as mfile: + mfile.return_value.__iter__.return_value = [fl_data] + self.assertEqual(timezone.get_hwclock(), 'UTC') mock = MagicMock(return_value=True) with patch.object(os.path, 'isfile', mock): @@ -226,31 +229,30 @@ class TimezoneTestCase(TestCase): ''' Test to sets the hardware clock to be either UTC or localtime ''' - ret = ('UTC is the only choice for SPARC architecture') - ret1 = ('Zone does not exist: /usr/share/zoneinfo/America/Denver') - with patch.object(timezone, 'get_zone', - return_value='America/Denver'): + zone = 'America/Denver' + with patch.object(timezone, 'get_zone', return_value=zone): with patch.dict(timezone.__grains__, {'os_family': 'Solaris', 'cpuarch': 'sparc'}): - self.assertEqual(timezone.set_hwclock('clock'), ret) + self.assertRaises( + SaltInvocationError, + timezone.set_hwclock, + 'clock' + ) + self.assertRaises( + SaltInvocationError, + timezone.set_hwclock, + 'localtime' + ) - with patch.dict(timezone.__salt__, - {'cmd.run': - MagicMock(return_value=None)}): - self.assertTrue(timezone.set_hwclock('localtime')) - - self.assertTrue(timezone.set_hwclock('UTC')) - - with patch.dict(timezone.__grains__, {'os_family': 'Sola'}): + with patch.dict(timezone.__grains__, + {'os_family': 'DoesNotMatter'}): with patch.object(os.path, 'exists', return_value=False): - self.assertEqual(timezone.set_hwclock('clock'), ret1) - - with patch.object(os.path, 'exists', return_value=True): - with patch.object(os, 'unlink', return_value=None): - with patch.object(os, 'symlink', return_value=None): - self.assertTrue(timezone.set_hwclock('clock')) - + self.assertRaises( + CommandExecutionError, + timezone.set_hwclock, + 'UTC' + ) if __name__ == '__main__': from integration import run_tests