Skip to content

Commit

Permalink
feat: New spec "timedatectl" and the parser (#3592)
Browse files Browse the repository at this point in the history
* feat: New spec "timedatectl" and the parser

Signed-off-by: Huanhuan Li <[email protected]>

* Add "timedatectl" in insights_archive.py

Signed-off-by: Huanhuan Li <[email protected]>

* Append "status" to "timedatectl" to make it more clear

* Move the examples into each parser
* Unifiy the different names on RHEL7 and RHEL8

Signed-off-by: Huanhuan Li <[email protected]>

* Rename the parser

* Get colon index first to avoid checking in for loop

Signed-off-by: Huanhuan Li <[email protected]>

* Adjust import order

Signed-off-by: Huanhuan Li <[email protected]>

* Add more tests to make coverage 100%

Signed-off-by: Huanhuan Li <[email protected]>

Signed-off-by: Huanhuan Li <[email protected]>
  • Loading branch information
huali027 authored Nov 21, 2022
1 parent 3650732 commit 2b89974
Show file tree
Hide file tree
Showing 5 changed files with 277 additions and 42 deletions.
181 changes: 140 additions & 41 deletions insights/parsers/date.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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

Expand All @@ -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)
1 change: 1 addition & 0 deletions insights/specs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions insights/specs/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions insights/specs/insights_archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
135 changes: 134 additions & 1 deletion insights/tests/parsers/test_date.py
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -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

0 comments on commit 2b89974

Please sign in to comment.