Skip to content

Commit 6c8dcec

Browse files
authored
Role for checking if local time is within required time interval (#167)
Implement check if local time is within safe hours Role will fail if HyperCore server local time is not in specified time_interval.
1 parent 8dee3f7 commit 6c8dcec

File tree

7 files changed

+296
-0
lines changed

7 files changed

+296
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
major_changes:
3+
- Added a role for checking if local time is within required time interval. (https://github.com/ScaleComputing/HyperCoreAnsibleCollection/pull/167)

roles/check_local_time/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# check_local_time
2+
3+
Role check_local_time can be used to:
4+
- check if local time in provided time zone is located within required time interval
5+
- ensure certain tasks/playbooks are executed only during allowed time interval. For example, HyperCore version upgrade might be allowed only outside of business hours.
6+
7+
## Requirements
8+
9+
- NA
10+
11+
## Role Variables
12+
13+
See [argument_specs.yml](../../roles/check_local_time/meta/argument_specs.yml).
14+
15+
## Limitations
16+
17+
- NA
18+
19+
## Dependencies
20+
21+
- NA
22+
23+
## Example Playbook
24+
25+
- NA
26+
27+
## License
28+
29+
GNU General Public License v3.0 or later
30+
31+
See [LICENSE](../../LICENSE) to see the full text.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright: (c) 2022, XLAB Steampunk <steampunk@xlab.si>
3+
#
4+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5+
6+
from __future__ import absolute_import, division, print_function
7+
from __future__ import annotations
8+
9+
__metaclass__ = type
10+
11+
import os
12+
import time
13+
import datetime
14+
import sys
15+
from typing import Tuple
16+
17+
18+
def get_local_time(time_zone: str) -> datetime.datetime:
19+
# https://docs.python.org/3/library/time.html#time.tzset
20+
# There should be no space in zone name.
21+
# We have files like
22+
# /usr/share/zoneinfo/America/New_York
23+
# /usr/share/zoneinfo/America/North_Dakota/New_Salem
24+
time_zone_nospace = time_zone.replace(" ", "_")
25+
26+
orig_tz = os.environ.get("TZ")
27+
os.environ["TZ"] = time_zone_nospace
28+
time.tzset()
29+
# print(f"time tzname={time.tzname} timezone={time.timezone} altzone={time.altzone} daylight={time.daylight}")
30+
31+
local_struct_time = time.localtime()
32+
local_time_str = time.strftime("%a %b %d %Y %H:%M:%S GMT%z", local_struct_time)
33+
local_time = datetime.datetime.strptime(
34+
local_time_str, "%a %b %d %Y %H:%M:%S GMT%z"
35+
)
36+
37+
if orig_tz:
38+
os.environ["TZ"] = orig_tz
39+
else:
40+
os.environ.pop("TZ")
41+
time.tzset()
42+
43+
return local_time
44+
45+
46+
def get_time_interval(
47+
time_interval: str,
48+
) -> Tuple[datetime.datetime, datetime.datetime]:
49+
time_list = time_interval.split("-")
50+
if len(time_list) != 2:
51+
raise AssertionError("Required time interval not correctly formated")
52+
start_time_str = time_list[0].strip()
53+
end_time_str = time_list[1].strip()
54+
55+
# (datatime|time).strptime("22:00", "%H:%M") - does return something near year 1900
56+
start_time = datetime.datetime.strptime(start_time_str, "%H:%M")
57+
end_time = datetime.datetime.strptime(end_time_str, "%H:%M")
58+
59+
return (start_time, end_time)
60+
61+
62+
def is_local_time_in_time_interval(
63+
local_time: datetime.datetime,
64+
start_time: datetime.datetime,
65+
end_time: datetime.datetime,
66+
) -> None: # we have print()
67+
if start_time < end_time:
68+
# needs to be "print" to be able to read it from ansible.builin.script
69+
print(start_time.time() <= local_time.time() < end_time.time())
70+
else: # Over midnight
71+
print(
72+
local_time.time() >= start_time.time()
73+
or local_time.time() < end_time.time()
74+
)
75+
76+
77+
def main(time_zone: str, time_interval: str) -> None: # we have print()
78+
local_time = get_local_time(time_zone)
79+
start_time, end_time = get_time_interval(time_interval)
80+
is_local_time_in_time_interval(local_time, start_time, end_time)
81+
82+
83+
if __name__ == "__main__":
84+
main(sys.argv[1], sys.argv[2])
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
argument_specs:
3+
main:
4+
short_description: Check if local time meets the required time interval
5+
description:
6+
- Check if local time meets the required time interval
7+
options:
8+
time_zone:
9+
description:
10+
- Time zone for which to calculate if local time meets the required time interval
11+
- Must be provided in a form 'Europe/Amsterdam'
12+
required: true
13+
type: str
14+
time_interval:
15+
description:
16+
- Time interval in which local time must be located
17+
- Must be provided in a form '22:00-6:15' or '7:30-12:36'
18+
required: true
19+
type: str
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
- name: Is executable python3
3+
ansible.builtin.command: which python3
4+
register: executable
5+
changed_when: false # to avoid ansible-lint error "no-changed-when: Commands should not change things if nothing needs doing."
6+
ignore_errors: true # if there is no python3, the shell will fail
7+
8+
- name: Is executable python
9+
ansible.builtin.command: which python
10+
register: executable_python
11+
changed_when: false # to avoid ansible-lint error "no-changed-when: Commands should not change things if nothing needs doing."
12+
ignore_errors: true # if there is no python, the shell will fail
13+
14+
- name: If not python3, then set executable to python
15+
ansible.builtin.set_fact: executable="{{ executable_python }}"
16+
when: not executable.stdout_lines
17+
18+
- name: Check if local time is in time interval (run check_local_time.py)
19+
ansible.builtin.script:
20+
executable: "{{ executable.stdout_lines[0] }}"
21+
cmd: check_local_time.py "{{ time_zone }}" "{{ time_interval }}"
22+
register: local_time_output
23+
24+
- name: Assert that local time is in time interval
25+
ansible.builtin.assert:
26+
fail_msg: "Local time for time zone {{ time_zone }} is not in required time interval {{ time_interval }}"
27+
success_msg: "Local time for time zone {{ time_zone }} is in required time interval {{ time_interval }}"
28+
that:
29+
- local_time_output.stdout_lines[0] == "True"
30+
register: result
31+
32+
- name: Set fact to use in tests
33+
ansible.builtin.set_fact: local_time_msg="{{ result.msg }}"
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
- environment:
3+
SC_HOST: "{{ sc_host }}"
4+
SC_USERNAME: "{{ sc_username }}"
5+
SC_PASSWORD: "{{ sc_password }}"
6+
SC_TIMEOUT: "{{ sc_timeout }}"
7+
8+
block:
9+
- name: Check that local time meets required time interval
10+
ansible.builtin.include_role:
11+
name: scale_computing.hypercore.check_local_time
12+
vars:
13+
time_zone: "{{ ansible_date_time.tz }}"
14+
time_interval: "{{ ansible_date_time.hour }}:00-{{ ansible_date_time.hour }}:59"
15+
16+
# it can fail when run near x:59
17+
- ansible.builtin.assert:
18+
that:
19+
- "'Local time for time zone {{ ansible_date_time.tz }} is in required time interval {{ ansible_date_time.hour }}:00-{{ ansible_date_time.hour }}:59' in local_time_msg"
20+
21+
- name: Check that local time doesn't meet required time interval
22+
ansible.builtin.include_role:
23+
name: scale_computing.hypercore.check_local_time
24+
apply:
25+
ignore_errors: True
26+
vars:
27+
time_zone: "{{ ansible_date_time.tz }}"
28+
time_interval: "{{ ansible_date_time.hour }}:00-{{ ansible_date_time.hour }}:01"
29+
30+
# it can fail when run near x:00
31+
- ansible.builtin.assert:
32+
that:
33+
- "'Local time for time zone {{ ansible_date_time.tz }} is not in required time interval {{ ansible_date_time.hour }}:00-{{ ansible_date_time.hour }}:01' in local_time_msg"
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# -*- coding: utf-8 -*-
2+
# # Copyright: (c) 2022, XLAB Steampunk <steampunk@xlab.si>
3+
#
4+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5+
6+
from __future__ import absolute_import, division, print_function
7+
8+
__metaclass__ = type
9+
10+
import sys
11+
import pytest
12+
import datetime
13+
14+
from ansible_collections.scale_computing.hypercore.roles.check_local_time.files import (
15+
check_local_time,
16+
)
17+
18+
# from ansible_collections.scale_computing.hypercore.plugins.module_utils import (
19+
# check_local_time,
20+
# )
21+
22+
from ansible_collections.scale_computing.hypercore.plugins.module_utils.utils import (
23+
MIN_PYTHON_VERSION,
24+
)
25+
26+
pytestmark = pytest.mark.skipif(
27+
sys.version_info < MIN_PYTHON_VERSION,
28+
reason=f"requires python{MIN_PYTHON_VERSION[0]}.{MIN_PYTHON_VERSION[1]} or higher",
29+
)
30+
31+
32+
class TestCheckLocalTime:
33+
def test_get_local_time(self):
34+
local_time = check_local_time.get_local_time("Europe/Ljubljana")
35+
assert type(local_time) == datetime.datetime
36+
37+
def test_get_time_interval(self):
38+
start_time, end_time = check_local_time.get_time_interval("22:31-4:45")
39+
start_time_str = datetime.datetime.strftime(start_time, "%H:%M")
40+
end_time_str = datetime.datetime.strftime(end_time, "%H:%M")
41+
42+
assert start_time_str == "22:31"
43+
assert end_time_str == "04:45"
44+
45+
@pytest.mark.parametrize(
46+
"time_interval, expected_result",
47+
[
48+
("22:30-1:30", "False"),
49+
("6:30-08:30", "False"),
50+
("12:30-13:00", "True"),
51+
("1:00-12:30", "False"),
52+
("1:00-12:31", "True"),
53+
("22:00-12:30", "False"),
54+
("22:00-12:31", "True"),
55+
],
56+
)
57+
def test_is_local_time_in_time_interval(
58+
self, time_interval, expected_result, capfd
59+
):
60+
local_time = datetime.datetime.now()
61+
local_time_constant = local_time.replace(hour=12, minute=30)
62+
63+
start_time, end_time = check_local_time.get_time_interval(time_interval)
64+
check_local_time.is_local_time_in_time_interval(
65+
local_time_constant, start_time, end_time
66+
)
67+
result, err = capfd.readouterr()
68+
69+
assert result.strip() == expected_result # strip removes "\n"
70+
71+
@pytest.mark.parametrize(
72+
"time_interval, expected_result",
73+
[
74+
("22:30-1:30", "False"),
75+
("6:30-08:30", "False"),
76+
("12:30-13:00", "True"),
77+
("1:00-12:30", "False"),
78+
("1:00-12:31", "True"),
79+
("22:00-12:30", "False"),
80+
("22:00-12:31", "True"),
81+
],
82+
)
83+
def test_main(self, mocker, time_interval, expected_result, capfd):
84+
local_time = datetime.datetime.now()
85+
local_time_constant = local_time.replace(hour=12, minute=30)
86+
mocker.patch(
87+
"ansible_collections.scale_computing.hypercore.roles.check_local_time.files.check_local_time.get_local_time"
88+
).return_value = local_time_constant
89+
90+
check_local_time.main("Europe/Ljubljana", time_interval)
91+
result, err = capfd.readouterr()
92+
93+
assert result.strip() == expected_result

0 commit comments

Comments
 (0)