From bb73f28bf51888671fffea4b6f92d9e2eec61b75 Mon Sep 17 00:00:00 2001 From: kurokobo Date: Sat, 18 May 2024 22:41:34 +0900 Subject: [PATCH] feat: implement timestamp callback plugin to show simple timestamp for each header (#8308) * feat: add community.general.timestamp callback plugin * feat: add minimal integration tests for timestamp callback plugin * feat: add maintainers for timestamp callback plugin * fix: correct license * fix: remove type annotation for the older python environment * fix: remove unnecessary comment Co-authored-by: Felix Fontein * fix: add trailing period Co-authored-by: Felix Fontein * fix: split long description into list Co-authored-by: Felix Fontein * fix: remove default and add type Co-authored-by: Felix Fontein * fix; add type Co-authored-by: Felix Fontein * fix: split long description into list Co-authored-by: Felix Fontein * fix: improve description for format_string to describe usable format codes * fix: clarify the original codes and add copyright from that * fix: shorten long lines * fix: correct link format * fix: add seealso section * fix: add ignore entries for EOL CI * fix: update seealso to correctly associate with related plugin Co-authored-by: Felix Fontein --------- Co-authored-by: Felix Fontein --- .github/BOTMETA.yml | 2 + plugins/callback/timestamp.py | 127 ++++++++++++++++++ .../targets/callback_timestamp/aliases | 6 + .../targets/callback_timestamp/tasks/main.yml | 66 +++++++++ tests/sanity/ignore-2.13.txt | 1 + tests/sanity/ignore-2.14.txt | 1 + 6 files changed, 203 insertions(+) create mode 100644 plugins/callback/timestamp.py create mode 100644 tests/integration/targets/callback_timestamp/aliases create mode 100644 tests/integration/targets/callback_timestamp/tasks/main.yml diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 5e674628f4..add3249355 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -91,6 +91,8 @@ files: maintainers: ryancurrah $callbacks/syslog_json.py: maintainers: imjoseangel + $callbacks/timestamp.py: + maintainers: kurokobo $callbacks/unixy.py: labels: unixy maintainers: akatch diff --git a/plugins/callback/timestamp.py b/plugins/callback/timestamp.py new file mode 100644 index 0000000000..07cd8d239c --- /dev/null +++ b/plugins/callback/timestamp.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, kurokobo +# Copyright (c) 2014, Michael DeHaan +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r""" + name: timestamp + type: stdout + short_description: Adds simple timestamp for each header + version_added: 9.0.0 + description: + - This callback adds simple timestamp for each header. + author: kurokobo (@kurokobo) + options: + timezone: + description: + - Timezone to use for the timestamp in IANA time zone format. + - For example C(America/New_York), C(Asia/Tokyo)). Ignored on Python < 3.9. + ini: + - section: callback_timestamp + key: timezone + env: + - name: ANSIBLE_CALLBACK_TIMESTAMP_TIMEZONE + type: string + format_string: + description: + - Format of the timestamp shown to user in 1989 C standard format. + - > + Refer to L(the Python documentation,https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) + for the available format codes. + ini: + - section: callback_timestamp + key: format_string + env: + - name: ANSIBLE_CALLBACK_TIMESTAMP_FORMAT_STRING + default: "%H:%M:%S" + type: string + seealso: + - plugin: ansible.posix.profile_tasks + plugin_type: callback + description: > + You can use P(ansible.posix.profile_tasks#callback) callback plugin to time individual tasks and overall execution time + with detailed timestamps. + extends_documentation_fragment: + - ansible.builtin.default_callback + - ansible.builtin.result_format_callback +""" + + +from ansible.plugins.callback.default import CallbackModule as Default +from ansible.utils.display import get_text_width +from ansible.module_utils.common.text.converters import to_text +from datetime import datetime +import types +import sys + +# Store whether the zoneinfo module is available +_ZONEINFO_AVAILABLE = sys.version_info >= (3, 9) + + +def get_datetime_now(tz): + """ + Returns the current timestamp with the specified timezone + """ + return datetime.now(tz=tz) + + +def banner(self, msg, color=None, cows=True): + """ + Prints a header-looking line with cowsay or stars with length depending on terminal width (3 minimum) with trailing timestamp + + Based on the banner method of Display class from ansible.utils.display + + https://github.com/ansible/ansible/blob/4403519afe89138042108e237aef317fd5f09c33/lib/ansible/utils/display.py#L511 + """ + timestamp = get_datetime_now(self.timestamp_tzinfo).strftime(self.timestamp_format_string) + timestamp_len = get_text_width(timestamp) + 1 # +1 for leading space + + msg = to_text(msg) + if self.b_cowsay and cows: + try: + self.banner_cowsay("%s @ %s" % (msg, timestamp)) + return + except OSError: + self.warning("somebody cleverly deleted cowsay or something during the PB run. heh.") + + msg = msg.strip() + try: + star_len = self.columns - get_text_width(msg) - timestamp_len + except EnvironmentError: + star_len = self.columns - len(msg) - timestamp_len + if star_len <= 3: + star_len = 3 + stars = "*" * star_len + self.display("\n%s %s %s" % (msg, stars, timestamp), color=color) + + +class CallbackModule(Default): + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = "stdout" + CALLBACK_NAME = "community.general.timestamp" + + def __init__(self): + super(CallbackModule, self).__init__() + + # Replace the banner method of the display object with the custom one + self._display.banner = types.MethodType(banner, self._display) + + def set_options(self, task_keys=None, var_options=None, direct=None): + super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct) + + # Store zoneinfo for specified timezone if available + tzinfo = None + if _ZONEINFO_AVAILABLE and self.get_option("timezone"): + from zoneinfo import ZoneInfo + + tzinfo = ZoneInfo(self.get_option("timezone")) + + # Inject options into the display object + setattr(self._display, "timestamp_tzinfo", tzinfo) + setattr(self._display, "timestamp_format_string", self.get_option("format_string")) diff --git a/tests/integration/targets/callback_timestamp/aliases b/tests/integration/targets/callback_timestamp/aliases new file mode 100644 index 0000000000..124adcfb8c --- /dev/null +++ b/tests/integration/targets/callback_timestamp/aliases @@ -0,0 +1,6 @@ +# Copyright (c) 2024, kurokobo +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or ) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/posix/1 +needs/target/callback diff --git a/tests/integration/targets/callback_timestamp/tasks/main.yml b/tests/integration/targets/callback_timestamp/tasks/main.yml new file mode 100644 index 0000000000..5e0acc15f0 --- /dev/null +++ b/tests/integration/targets/callback_timestamp/tasks/main.yml @@ -0,0 +1,66 @@ +--- +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# Copyright (c) 2024, kurokobo +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Run tests + include_role: + name: callback + vars: + tests: + - name: Enable timestamp in the default length + environment: + ANSIBLE_NOCOLOR: 'true' + ANSIBLE_FORCE_COLOR: 'false' + ANSIBLE_STDOUT_CALLBACK: community.general.timestamp + ANSIBLE_CALLBACK_TIMESTAMP_FORMAT_STRING: "15:04:05" + playbook: | + - hosts: testhost + gather_facts: false + tasks: + - name: Sample task name + debug: + msg: sample debug msg + expected_output: [ + "", + "PLAY [testhost] ******************************************************* 15:04:05", + "", + "TASK [Sample task name] *********************************************** 15:04:05", + "ok: [testhost] => {", + " \"msg\": \"sample debug msg\"", + "}", + "", + "PLAY RECAP ************************************************************ 15:04:05", + "testhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 " + ] + + - name: Enable timestamp in the longer length + environment: + ANSIBLE_NOCOLOR: 'true' + ANSIBLE_FORCE_COLOR: 'false' + ANSIBLE_STDOUT_CALLBACK: community.general.timestamp + ANSIBLE_CALLBACK_TIMESTAMP_FORMAT_STRING: "2006-01-02T15:04:05" + playbook: | + - hosts: testhost + gather_facts: false + tasks: + - name: Sample task name + debug: + msg: sample debug msg + expected_output: [ + "", + "PLAY [testhost] ******************************************** 2006-01-02T15:04:05", + "", + "TASK [Sample task name] ************************************ 2006-01-02T15:04:05", + "ok: [testhost] => {", + " \"msg\": \"sample debug msg\"", + "}", + "", + "PLAY RECAP ************************************************* 2006-01-02T15:04:05", + "testhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 " + ] diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt index 954a8afebf..cfeaff7c31 100644 --- a/tests/sanity/ignore-2.13.txt +++ b/tests/sanity/ignore-2.13.txt @@ -1,4 +1,5 @@ .azure-pipelines/scripts/publish-codecov.py replace-urlopen +plugins/callback/timestamp.py validate-modules:invalid-documentation plugins/lookup/etcd.py validate-modules:invalid-documentation plugins/lookup/etcd3.py validate-modules:invalid-documentation plugins/modules/consul_session.py validate-modules:parameter-state-invalid-choice diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt index 01b195e9f5..247d43fe37 100644 --- a/tests/sanity/ignore-2.14.txt +++ b/tests/sanity/ignore-2.14.txt @@ -1,4 +1,5 @@ .azure-pipelines/scripts/publish-codecov.py replace-urlopen +plugins/callback/timestamp.py validate-modules:invalid-documentation plugins/lookup/etcd.py validate-modules:invalid-documentation plugins/lookup/etcd3.py validate-modules:invalid-documentation plugins/modules/consul_session.py validate-modules:parameter-state-invalid-choice