summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Doran <sdoran@redhat.com>2020-07-30 16:37:19 -0400
committerGitHub <noreply@github.com>2020-07-30 13:37:19 -0700
commit719c40bfdfc233b0554a40d7f38efdeccbe03aab (patch)
treeaf689e0dd3d3c7aad67628ecf04429b9b0225da6
parentb764d381f0e1e31c2e16e742948679e8808ad691 (diff)
downloadansible-719c40bfdfc233b0554a40d7f38efdeccbe03aab.tar.gz
[stable-2.10] facts - fix incorrect time for some date_time_facts (#70665) (#70996)
* [stable-2.10] facts - fix incorrect time for some date_time_facts (#70665) The iso8601_micro and iso8601 facts incorrectly called now.utcnow(), resulting in a new timestamp at the time it was called, not a conversion of the previously stored timestamp. Correct this by capturing the UTC timestamp once then calculating the local time using the UTC offset of the current system. * Use time.time() for getting the current time * Convert from that stored epoch timestamp to local and UTC times * Used existing timestamp for epoch time * Add unit tests that validate the formate of the return value rather than an exact value since mocking time and timezone is non-trivial (cherry picked from commit c4f442ed5a) Co-authored-by: Sam Doran <sdoran@redhat.com> * Remove tests for tz_dst since that only exists in newer versions
-rw-r--r--changelogs/fragments/date-time-facts-fix-utctime.yml2
-rw-r--r--lib/ansible/module_utils/facts/system/date_time.py13
-rw-r--r--test/units/module_utils/facts/test_date_time.py103
3 files changed, 113 insertions, 5 deletions
diff --git a/changelogs/fragments/date-time-facts-fix-utctime.yml b/changelogs/fragments/date-time-facts-fix-utctime.yml
new file mode 100644
index 0000000000..2a5bf8c408
--- /dev/null
+++ b/changelogs/fragments/date-time-facts-fix-utctime.yml
@@ -0,0 +1,2 @@
+bugfixes:
+ - facts - fix incorrect UTC timestamp in ``iso8601_micro`` and ``iso8601``
diff --git a/lib/ansible/module_utils/facts/system/date_time.py b/lib/ansible/module_utils/facts/system/date_time.py
index f4e59705ef..aa59d5bc1b 100644
--- a/lib/ansible/module_utils/facts/system/date_time.py
+++ b/lib/ansible/module_utils/facts/system/date_time.py
@@ -32,7 +32,11 @@ class DateTimeFactCollector(BaseFactCollector):
facts_dict = {}
date_time_facts = {}
- now = datetime.datetime.now()
+ # Store the timestamp once, then get local and UTC versions from that
+ epoch_ts = time.time()
+ now = datetime.datetime.fromtimestamp(epoch_ts)
+ utcnow = datetime.datetime.utcfromtimestamp(epoch_ts)
+
date_time_facts['year'] = now.strftime('%Y')
date_time_facts['month'] = now.strftime('%m')
date_time_facts['weekday'] = now.strftime('%A')
@@ -44,12 +48,11 @@ class DateTimeFactCollector(BaseFactCollector):
date_time_facts['second'] = now.strftime('%S')
date_time_facts['epoch'] = now.strftime('%s')
if date_time_facts['epoch'] == '' or date_time_facts['epoch'][0] == '%':
- # NOTE: in this case, the epoch wont match the rest of the date_time facts? ie, it's a few milliseconds later..? -akl
- date_time_facts['epoch'] = str(int(time.time()))
+ date_time_facts['epoch'] = str(int(epoch_ts))
date_time_facts['date'] = now.strftime('%Y-%m-%d')
date_time_facts['time'] = now.strftime('%H:%M:%S')
- date_time_facts['iso8601_micro'] = now.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ")
- date_time_facts['iso8601'] = now.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
+ date_time_facts['iso8601_micro'] = utcnow.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
+ date_time_facts['iso8601'] = utcnow.strftime("%Y-%m-%dT%H:%M:%SZ")
date_time_facts['iso8601_basic'] = now.strftime("%Y%m%dT%H%M%S%f")
date_time_facts['iso8601_basic_short'] = now.strftime("%Y%m%dT%H%M%S")
date_time_facts['tz'] = time.strftime("%Z")
diff --git a/test/units/module_utils/facts/test_date_time.py b/test/units/module_utils/facts/test_date_time.py
new file mode 100644
index 0000000000..7c92e521db
--- /dev/null
+++ b/test/units/module_utils/facts/test_date_time.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import pytest
+import datetime
+import string
+import time
+
+from ansible.module_utils.facts.system import date_time
+
+EPOCH_TS = 1594449296.123456
+DT = datetime.datetime(2020, 7, 11, 12, 34, 56, 124356)
+DT_UTC = datetime.datetime(2020, 7, 11, 2, 34, 56, 124356)
+
+
+@pytest.fixture
+def fake_now(monkeypatch):
+ """
+ Patch `datetime.datetime.fromtimestamp()`, `datetime.datetime.utcfromtimestamp()`,
+ and `time.time()` to return deterministic values.
+ """
+
+ class FakeNow:
+ @classmethod
+ def fromtimestamp(cls, timestamp):
+ return DT
+
+ @classmethod
+ def utcfromtimestamp(cls, timestamp):
+ return DT_UTC
+
+ def _time():
+ return EPOCH_TS
+
+ monkeypatch.setattr(date_time.datetime, 'datetime', FakeNow)
+ monkeypatch.setattr(time, 'time', _time)
+
+
+@pytest.fixture
+def fake_date_facts(fake_now):
+ """Return a predictable instance of collected date_time facts."""
+
+ collector = date_time.DateTimeFactCollector()
+ data = collector.collect()
+
+ return data
+
+
+@pytest.mark.parametrize(
+ ('fact_name', 'fact_value'),
+ (
+ ('year', '2020'),
+ ('month', '07'),
+ ('weekday', 'Saturday'),
+ ('weekday_number', '6'),
+ ('weeknumber', '27'),
+ ('day', '11'),
+ ('hour', '12'),
+ ('minute', '34'),
+ ('second', '56'),
+ ('date', '2020-07-11'),
+ ('time', '12:34:56'),
+ ('iso8601_basic', '20200711T123456124356'),
+ ('iso8601_basic_short', '20200711T123456'),
+ ('iso8601_micro', '2020-07-11T02:34:56.124356Z'),
+ ('iso8601', '2020-07-11T02:34:56Z'),
+ ),
+)
+def test_date_time_facts(fake_date_facts, fact_name, fact_value):
+ assert fake_date_facts['date_time'][fact_name] == fact_value
+
+
+def test_date_time_epoch(fake_date_facts):
+ """Test that format of returned epoch value is correct"""
+
+ assert fake_date_facts['date_time']['epoch'].isdigit()
+ assert len(fake_date_facts['date_time']['epoch']) == 10 # This length will not change any time soon
+
+
+def test_date_time_tz(fake_date_facts):
+ """
+ Test that the returned value for timezone consists of only uppercase
+ letters and is the expected length.
+ """
+
+ assert fake_date_facts['date_time']['tz'].isupper()
+ assert 2 <= len(fake_date_facts['date_time']['tz']) <= 5
+ assert not set(fake_date_facts['date_time']['tz']).difference(set(string.ascii_uppercase))
+
+
+def test_date_time_tz_offset(fake_date_facts):
+ """
+ Test that the timezone offset begins with a `+` or `-` and ends with a
+ series of integers.
+ """
+
+ assert fake_date_facts['date_time']['tz_offset'][0] in ['-', '+']
+ assert fake_date_facts['date_time']['tz_offset'][1:].isdigit()
+ assert len(fake_date_facts['date_time']['tz_offset']) == 5