diff --git a/insights/parsers/date.py b/insights/parsers/date.py index 50b773f9f..e64320410 100644 --- a/insights/parsers/date.py +++ b/insights/parsers/date.py @@ -2,55 +2,21 @@ Date parsers ============ -This module provides processing for the output of the ``date`` command in -various formats. +This module contains the following parsers: Date - command ``date`` ----------------------- - -Class ``Date`` parses the output of the ``date`` command. Sample output of -this command looks like:: - - Fri Jun 24 09:13:34 CST 2016 - DateUTC - command ``date --utc`` -------------------------------- - -Class ``DateUTC`` parses the output of the ``date --utc`` command. Output is -similar to the ``date`` command except that the `Timezone` column uses UTC. - -All classes utilize the same base class ``DateParser`` so the following -examples apply to all classes in this module. - -Examples: - >>> from insights.parsers.date import Date, DateUTC - >>> from insights.tests import context_wrap - >>> date_content = "Mon May 30 10:49:14 CST 2016" - >>> shared = {Date: Date(context_wrap(date_content))} - >>> date_info = shared[Date] - >>> date_info.data - 'Mon May 30 10:49:14 CST 2016' - >>> date_info.datetime is not None - True - >>> date_info.timezone - 'CST' - - >>> date_content = "Mon May 30 10:49:14 UTC 2016" - >>> shared = {DateUTC: DateUTC(context_wrap(date_content))} - >>> date_info = shared[DateUTC] - >>> date_info.data - 'Mon May 30 10:49:14 UTC 2016' - >>> date_info.datetime - datetime.datetime(2016, 5, 30, 10, 49, 14) - >>> date_info.timezone - 'UTC' +TimeDateCtlStatus - command ``timedatectl status`` +-------------------------------------------------- """ - import six import sys from datetime import datetime -from .. import parser, get_active_lines, CommandParser +from insights.parsers import ParseException, SkipException +from insights import parser, get_active_lines, CommandParser from insights.specs import Specs @@ -97,7 +63,22 @@ class Date(DateParser): """ Class to parse ``date`` command output. - Sample: Fri Jun 24 09:13:34 CST 2016 + Sample in:: + + Fri Jun 24 09:13:34 CST 2016 + + Examples: + >>> from insights.parsers.date import Date + >>> from insights.tests import context_wrap + >>> date_content = "Mon May 30 10:49:14 CST 2016" + >>> shared = {Date: Date(context_wrap(date_content))} + >>> date_info = shared[Date] + >>> date_info.data + 'Mon May 30 10:49:14 CST 2016' + >>> date_info.datetime is not None + True + >>> date_info.timezone + 'CST' """ pass @@ -107,6 +88,124 @@ class DateUTC(DateParser): """ Class to parse ``date --utc`` command output. - Sample: Fri Jun 24 09:13:34 UTC 2016 + Sample in:: + + Fri Jun 24 09:13:34 UTC 2016 + + Examples: + >>> from insights.parsers.date import DateUTC + >>> from insights.tests import context_wrap + >>> date_content = "Mon May 30 10:49:14 UTC 2016" + >>> shared = {DateUTC: DateUTC(context_wrap(date_content))} + >>> date_info = shared[DateUTC] + >>> date_info.data + 'Mon May 30 10:49:14 UTC 2016' + >>> date_info.datetime + datetime.datetime(2016, 5, 30, 10, 49, 14) + >>> date_info.timezone + 'UTC' """ pass + + +@parser(Specs.timedatectl_status) +class TimeDateCtlStatus(CommandParser, dict): + """ + Class to parse the ``timedatectl status`` command output. + It saves the infomartion in each line into a dict. + Since the colon in all the lines except warning is aligned, every line + is splited by the same colon index. The key is the lowercase of the first + part joined by underscore after splitting it by colon, and the value is + the left part after the colon. If the next line is continue line, + then append it to the previous key. And also it converts + the value to datetime format for "Local time", "Universal time" and "RTC time". + + Sample in:: + + Local time: Mon 2022-11-14 23:04:06 PST + Universal time: Tue 2022-11-15 07:04:06 UTC + RTC time: Tue 2022-11-15 07:04:05 + Time zone: US/Pacific (PST, -0800) + System clock synchronized: yes + NTP service: active + RTC in local TZ: yes + Last DST change: DST ended at + Sun 2022-11-06 01:59:59 EDT + Sun 2022-11-06 01:00:00 EST + Next DST change: DST begins (the clock jumps one hour forward) at + Sun 2023-03-12 01:59:59 EST + Sun 2023-03-12 03:00:00 EDT + + Warning: The system is configured to read the RTC time in the local time zone. + This mode cannot be fully supported. It will create various problems + with time zone changes and daylight saving time adjustments. The RTC + time is never updated, it relies on external facilities to maintain it. + If at all possible, use RTC in UTC by calling + 'timedatectl set-local-rtc 0'. + + Raises: + DateParseException: when the datetime in "Local time", "Universal time", + "RTC time" are not in expected format. + ParseException: when the colon in each line except warning is not aligned. + + Examples: + >>> ctl_info['ntp_service'] + 'active' + >>> ctl_info['system_clock_synchronized'] + 'yes' + >>> ctl_info['local_time'] + datetime.datetime(2022, 11, 14, 23, 4, 6) + """ + date_format = '%a %Y-%m-%d %H:%M:%S' + + # unify the different names in rhel7 and rhel8 + key_mapping = { + 'ntp_synchronized': 'system_clock_synchronized' + } + + def parse_content(self, content): + dict_key = None + warning_start = False + non_blank_line = None + for line in content: + if line.strip(): + non_blank_line = line + break + if non_blank_line is None: + raise SkipException('No data in the output.') + try: + colon_index = non_blank_line.index(':') + except ValueError: + raise ParseException('No colon found, the line %s is not in expected format.' % line) + for line in content: + if not line.strip(): + continue + if line[colon_index] == ':': + key = line[:colon_index].strip() + value = line[colon_index + 1:].strip() + dict_key = '_'.join(key.lower().split()) + if dict_key in ['local_time', 'universal_time', 'rtc_time']: + if dict_key == 'rtc_time': + final_val = value + else: + final_val = value.rsplit(None, 1)[0] # remove tz info + try: + self[dict_key] = datetime.strptime(final_val, self.date_format) + except Exception: + six.reraise(DateParseException, DateParseException(value), sys.exc_info()[2]) + else: + if dict_key in self.key_mapping: + self[self.key_mapping[dict_key]] = value + else: + self[dict_key] = value + elif not line[:colon_index].strip(): + # this line is also the content of the previous key + self[dict_key] += ' ' + line.strip() + elif line.lstrip().startswith('Warning:'): + warning_start = True + self['warning'] = line.split(':')[1].strip() + else: + if warning_start: + self['warning'] += ' ' + line.strip() + else: + raise ParseException('Unexpected format of line %s.' % line) diff --git a/insights/specs/__init__.py b/insights/specs/__init__.py index 3db842488..a5ee81748 100644 --- a/insights/specs/__init__.py +++ b/insights/specs/__init__.py @@ -734,6 +734,7 @@ class Specs(SpecSet): testparm_v_s = RegistryPoint(filterable=True) thp_enabled = RegistryPoint() thp_use_zero_page = RegistryPoint() + timedatectl_status = RegistryPoint() tmpfilesd = RegistryPoint(multi_output=True) tomcat_server_xml = RegistryPoint(multi_output=True) tomcat_vdc_fallback = RegistryPoint() diff --git a/insights/specs/default.py b/insights/specs/default.py index bea5e6908..4d2b69d88 100644 --- a/insights/specs/default.py +++ b/insights/specs/default.py @@ -639,6 +639,7 @@ class DefaultSpecs(Specs): testparm_v_s = simple_command("/usr/bin/testparm -v -s") thp_enabled = simple_file("/sys/kernel/mm/transparent_hugepage/enabled") thp_use_zero_page = simple_file("/sys/kernel/mm/transparent_hugepage/use_zero_page") + timedatectl_status = simple_command('/usr/bin/timedatectl status') tmpfilesd = glob_file(["/etc/tmpfiles.d/*.conf", "/usr/lib/tmpfiles.d/*.conf", "/run/tmpfiles.d/*.conf"]) tomcat_vdc_fallback = simple_command("/usr/bin/find /usr/share -maxdepth 1 -name 'tomcat*' -exec /bin/grep -R -s 'VirtualDirContext' --include '*.xml' '{}' +") tuned_adm = simple_command("/usr/sbin/tuned-adm list") diff --git a/insights/specs/insights_archive.py b/insights/specs/insights_archive.py index 9013266aa..e53f072e2 100644 --- a/insights/specs/insights_archive.py +++ b/insights/specs/insights_archive.py @@ -274,6 +274,7 @@ class InsightsArchiveSpecs(Specs): systool_b_scsi_v = simple_file("insights_commands/systool_-b_scsi_-v") testparm_s = simple_file("insights_commands/testparm_-s") testparm_v_s = simple_file("insights_commands/testparm_-v_-s") + timedatectl_status = simple_file("insights_commands/timedatectl_status") tomcat_vdc_fallback = simple_file("insights_commands/find_.usr.share_-maxdepth_1_-name_tomcat_-exec_.bin.grep_-R_-s_VirtualDirContext_--include_.xml") tuned_adm = simple_file("insights_commands/tuned-adm_list") uname = simple_file("insights_commands/uname_-a") diff --git a/insights/tests/parsers/test_date.py b/insights/tests/parsers/test_date.py index bd2c23560..d9d76fce2 100644 --- a/insights/tests/parsers/test_date.py +++ b/insights/tests/parsers/test_date.py @@ -1,12 +1,100 @@ import pytest -from insights.parsers.date import Date, DateUTC, DateParseException +import doctest +from datetime import datetime +from insights.parsers import ParseException, SkipException, date +from insights.parsers.date import Date, DateUTC, DateParseException, TimeDateCtlStatus from insights.tests import context_wrap + DATE_OUTPUT1 = "Mon May 30 10:49:14 %s 2016" DATE_OUTPUT2 = "Thu Oct 22 12:59:28 %s 2015" DATE_OUTPUT_TRUNCATED = "Thu Oct 22" DATE_OUTPUT_INVALID = "Fon Pla 34 27:63:89 CST 20-1" +TIMEDATECTL_CONTENT1 = """ + Local time: Mon 2022-11-14 22:37:01 EST + Universal time: Tue 2022-11-15 03:37:01 UTC + RTC time: Tue 2022-11-15 03:37:40 + Time zone: America/New_York (EST, -0500) +System clock synchronized: yes + NTP service: active + RTC in local TZ: no +""" + +TIMEDATECTL_CONTENT2 = """ + Local time: Mon 2022-11-14 23:04:06 PST + Universal time: Tue 2022-11-15 07:04:06 UTC + RTC time: Tue 2022-11-15 07:04:05 + Time zone: US/Pacific (PST, -0800) +System clock synchronized: yes + NTP service: active + RTC in local TZ: yes + +Warning: The system is configured to read the RTC time in the local time zone. + This mode cannot be fully supported. It will create various problems + with time zone changes and daylight saving time adjustments. The RTC + time is never updated, it relies on external facilities to maintain it. + If at all possible, use RTC in UTC by calling + 'timedatectl set-local-rtc 0'. +""" + +TIMEDATECTL_CONTENT3 = """ + Local time: Mon 2022-11-14 02:33:36 EST + Universal time: Mon 2022-11-14 07:33:36 UTC + RTC time: Mon 2022-11-14 07:33:34 + Time zone: America/New_York (EST, -0500) + NTP enabled: yes +NTP synchronized: yes + RTC in local TZ: no + DST active: no + Last DST change: DST ended at + Sun 2022-11-06 01:59:59 EDT + Sun 2022-11-06 01:00:00 EST + Next DST change: DST begins (the clock jumps one hour forward) at + Sun 2023-03-12 01:59:59 EST + Sun 2023-03-12 03:00:00 EDT +""" + +TIMEDATECTL_CONTENT_WRONG_DATE_FORMAT = """ + Local time: Mon 2022-11-14 02:33:36 EST + Universal time: Mon 2022-11-14 07:33:36 UTC + RTC time: 2022-11-14 07:33:34 + Time zone: America/New_York (EST, -0500) + NTP enabled: yes +NTP synchronized: yes + RTC in local TZ: no + DST active: no + Last DST change: DST ended at + Sun 2022-11-06 01:59:59 EDT + Sun 2022-11-06 01:00:00 EST + Next DST change: DST begins (the clock jumps one hour forward) at + Sun 2023-03-12 01:59:59 EST + Sun 2023-03-12 03:00:00 EDT +""" + +TIMEDATECTL_CONTENT_NOT_COLON_ALIGNED = """ + Local time: Mon 2022-11-14 02:33:36 EST + Universal time : Mon 2022-11-14 07:33:36 UTC + RTC time: 2022-11-14 07:33:34 + Time zone: America/New_York (EST, -0500) + NTP enabled: yes +NTP synchronized: yes + RTC in local TZ: no + DST active: no + Last DST change: DST ended at + Sun 2022-11-06 01:59:59 EDT + Sun 2022-11-06 01:00:00 EST + Next DST change: DST begins (the clock jumps one hour forward) at + Sun 2023-03-12 01:59:59 EST + Sun 2023-03-12 03:00:00 EDT +""" + +TIMEDATECTL_CONTENT4_WITHOUT_INFO = """""" + +TIMEDATECTL_CONTENT4_WITHOUT_COLON_OUTPUT = """ +this is just test +""".strip() + def test_get_date1(): DATE = DATE_OUTPUT1 % ('CST') @@ -42,3 +130,48 @@ def test_get_date3(): assert date_info.data == DATE assert date_info.datetime is not None assert date_info.timezone == 'UTC' + + +def test_timedatectl(): + timectl1 = TimeDateCtlStatus(context_wrap(TIMEDATECTL_CONTENT1, strip=False)) + assert timectl1 is not None + assert 'local_time' in timectl1 + local_time_val = datetime.strptime('2022-11-14 22:37:01', '%Y-%m-%d %H:%M:%S') + assert timectl1['local_time'] == local_time_val + assert 'universal_time' in timectl1 + universal_time_val = datetime.strptime('2022-11-15 03:37:01', '%Y-%m-%d %H:%M:%S') + assert timectl1['universal_time'] == universal_time_val + assert 'rtc_time' in timectl1 + rtc_time_val = datetime.strptime('2022-11-15 03:37:40', '%Y-%m-%d %H:%M:%S') + assert timectl1['rtc_time'] == rtc_time_val + assert timectl1['system_clock_synchronized'] == 'yes' + assert timectl1['rtc_in_local_tz'] == 'no' + assert timectl1['ntp_service'] == 'active' + + timectl2 = TimeDateCtlStatus(context_wrap(TIMEDATECTL_CONTENT2, strip=False)) + assert 'warning' in timectl2 + assert 'timedatectl set-local-rtc 0' in timectl2['warning'] + + timectl3 = TimeDateCtlStatus(context_wrap(TIMEDATECTL_CONTENT3, strip=False)) + assert 'last_dst_change' in timectl3 + assert timectl3['system_clock_synchronized'] == 'yes' + assert 'DST ended at Sun 2022-11-06 01:59:59 EDT Sun 2022-11-06 01:00:00 EST' in timectl3['last_dst_change'] + assert 'DST begins (the clock jumps one hour forward) at Sun 2023-03-12 01:59:59 EST Sun 2023-03-12 03:00:00 EDT' in timectl3['next_dst_change'] + + +def test_timedatectl_except(): + with pytest.raises(SkipException): + TimeDateCtlStatus(context_wrap(TIMEDATECTL_CONTENT4_WITHOUT_INFO, strip=False)) + with pytest.raises(ParseException): + TimeDateCtlStatus(context_wrap(TIMEDATECTL_CONTENT4_WITHOUT_COLON_OUTPUT, strip=False)) + with pytest.raises(DateParseException): + TimeDateCtlStatus(context_wrap(TIMEDATECTL_CONTENT_WRONG_DATE_FORMAT, strip=False)) + with pytest.raises(ParseException): + TimeDateCtlStatus(context_wrap(TIMEDATECTL_CONTENT_NOT_COLON_ALIGNED, strip=False)) + + +def test_doc(): + failed_count, _ = doctest.testmod( + date, globs={'ctl_info': TimeDateCtlStatus(context_wrap(TIMEDATECTL_CONTENT2, strip=False))} + ) + assert failed_count == 0