Compare commits

...

5 commits

Author SHA1 Message Date
Axionize
a3408f354b
Merge 6e7c537956 into 1994b2cf1c 2025-04-28 17:22:59 +00:00
softwarefactory-project-zuul[bot]
1994b2cf1c
Merge pull request #626 from pablomh/datetime_format
plugins/callback/profile_tasks.py: Add option to provide a different date/time format

SUMMARY
The new datetime_format key will offer the possibility of providing a different date/time format than the default one ('%A %d %B %Y  %H:%M:%S %z').

The iso8601 value can be used as an '%Y-%m-%dT%H:%M:%S.%f' alias (format of the ISO 8601 date/time standard).

The code has changed from using the time API to the datetime one in order to support sub-second precision (needed by the ISO 8601 format, for example).
Fixes: #279
ISSUE TYPE

Feature Pull Request

COMPONENT NAME
plugins/callback/profile_tasks.py
ADDITIONAL INFORMATION

Output with no key which keeps current behavior:

TASK [Import subscription manifest] *******************************************************************************************************************************************************************************************************************************************************************
Thursday 10 April 2025  00:52:11 +0200 (0:00:17.416)       0:00:17.453 ******** 
changed: [localhost]


Output with datetime_format = 'iso8601':

TASK [Import subscription manifest] *******************************************************************************************************************************************************************************************************************************************************************
2025-04-10T00:55:19.967718 (0:00:15.664)       0:00:15.691 ******************** 
changed: [localhost]


Output with datetime_format = '%Y-%m-%dT%H:%M:%S.%f%z' (ISO 8601 with UTC offset information):

TASK [Import subscription manifest] *******************************************************************************************************************************************************************************************************************************************************************
2025-04-10T00:57:49.290347+0200 (0:00:16.265)       0:00:16.293 *************** 
changed: [localhost]

Reviewed-by: Hideki Saito <saito@fgrep.org>
2025-04-25 06:44:25 +00:00
Pablo Méndez Hernández
2f224e6a6a Add option to provide a different date/time format
The new `datetime_format` key will offer the possibility of
providing a different date/time format than the default one
(`'%A %d %B %Y  %H:%M:%S %z'`).

- The `iso8601` value can be used as an `'%Y-%m-%dT%H:%M:%S.%f'`
  alias (format of the ISO 8601 date/time standard).

The code has changed from using the `time` API to the `datetime`
one in order to support sub-second precision (needed by the ISO
8601 format, for example).

Signed-off-by: Pablo Mendez Hernandez <pablomh@redhat.com>
2025-04-17 15:57:00 +02:00
Axionize
6e7c537956 Add example to documentation 2023-12-25 00:58:33 -05:00
Axionize
d0ea1143ee Make synchronize work with multiple src paths 2023-12-25 00:54:16 -05:00
4 changed files with 58 additions and 23 deletions

View file

@ -0,0 +1,3 @@
---
minor_changes:
- profile_tasks - Add option to provide a different date/time format (https://github.com/ansible-collections/ansible.posix/issues/279).

View file

@ -339,6 +339,8 @@ class ActionModule(ActionBase):
dest = _tmp_args.get('dest', None) dest = _tmp_args.get('dest', None)
if src is None or dest is None: if src is None or dest is None:
return dict(failed=True, msg="synchronize requires both src and dest parameters are set") return dict(failed=True, msg="synchronize requires both src and dest parameters are set")
if isinstance(src, str):
src = [src]
# Determine if we need a user@ and a password # Determine if we need a user@ and a password
user = None user = None
@ -365,11 +367,11 @@ class ActionModule(ActionBase):
# use the mode to define src and dest's url # use the mode to define src and dest's url
if _tmp_args.get('mode', 'push') == 'pull': if _tmp_args.get('mode', 'push') == 'pull':
# src is a remote path: <user>@<host>, dest is a local path # src is a remote path: <user>@<host>, dest is a local path
src = self._process_remote(_tmp_args, src_host, src, user, inv_port in localhost_ports) src = [self._process_remote(_tmp_args, src_host, e, user, inv_port in localhost_ports) for e in src]
dest = self._process_origin(dest_host, dest, user) dest = self._process_origin(dest_host, dest, user)
else: else:
# src is a local path, dest is a remote path: <user>@<host> # src is a local path, dest is a remote path: <user>@<host>
src = self._process_origin(src_host, src, user) src = [self._process_origin(src_host, e, user) for e in src]
dest = self._process_remote(_tmp_args, dest_host, dest, user, inv_port in localhost_ports) dest = self._process_remote(_tmp_args, dest_host, dest, user, inv_port in localhost_ports)
password = dest_host_inventory_vars.get('ansible_ssh_pass', None) or dest_host_inventory_vars.get('ansible_password', None) password = dest_host_inventory_vars.get('ansible_ssh_pass', None) or dest_host_inventory_vars.get('ansible_password', None)
@ -378,7 +380,7 @@ class ActionModule(ActionBase):
else: else:
# Still need to munge paths (to account for roles) even if we aren't # Still need to munge paths (to account for roles) even if we aren't
# copying files between hosts # copying files between hosts
src = self._get_absolute_path(path=src) src = [self._get_absolute_path(path=e) for e in src]
dest = self._get_absolute_path(path=dest) dest = self._get_absolute_path(path=dest)
_tmp_args['_local_rsync_password'] = password _tmp_args['_local_rsync_password'] = password

View file

@ -52,6 +52,17 @@ DOCUMENTATION = '''
- section: callback_profile_tasks - section: callback_profile_tasks
key: summary_only key: summary_only
version_added: 1.5.0 version_added: 1.5.0
datetime_format:
description:
- Datetime format, as expected by the C(strftime) and C(strptime) methods.
An C(iso8601) alias will be translated to C('%Y-%m-%dT%H:%M:%S.%f') if that datetime standard wants to be used.
default: '%A %d %B %Y %H:%M:%S %z'
env:
- name: PROFILE_TASKS_DATETIME_FORMAT
ini:
- section: callback_profile_tasks
key: datetime_format
version_added: 3.0.0
''' '''
EXAMPLES = ''' EXAMPLES = '''
@ -72,14 +83,15 @@ sample output: >
''' '''
import collections import collections
import time
from datetime import datetime
from ansible.module_utils.six.moves import reduce from ansible.module_utils.six.moves import reduce
from ansible.plugins.callback import CallbackBase from ansible.plugins.callback import CallbackBase
# define start time # define start time
t0 = tn = time.time() dt0 = dtn = datetime.now().astimezone()
def secondsToStr(t): def secondsToStr(t):
@ -104,17 +116,18 @@ def filled(msg, fchar="*"):
def timestamp(self): def timestamp(self):
if self.current is not None: if self.current is not None:
elapsed = time.time() - self.stats[self.current]['started'] elapsed = (datetime.now().astimezone() - self.stats[self.current]['started']).total_seconds()
self.stats[self.current]['elapsed'] += elapsed self.stats[self.current]['elapsed'] += elapsed
def tasktime(): def tasktime(self):
global tn global dtn
time_current = time.strftime('%A %d %B %Y %H:%M:%S %z') cdtn = datetime.now().astimezone()
time_elapsed = secondsToStr(time.time() - tn) datetime_current = cdtn.strftime(self.datetime_format)
time_total_elapsed = secondsToStr(time.time() - t0) time_elapsed = secondsToStr((cdtn - dtn).total_seconds())
tn = time.time() time_total_elapsed = secondsToStr((cdtn - dt0).total_seconds())
return filled('%s (%s)%s%s' % (time_current, time_elapsed, ' ' * 7, time_total_elapsed)) dtn = cdtn
return filled('%s (%s)%s%s' % (datetime_current, time_elapsed, ' ' * 7, time_total_elapsed))
class CallbackModule(CallbackBase): class CallbackModule(CallbackBase):
@ -134,6 +147,7 @@ class CallbackModule(CallbackBase):
self.sort_order = None self.sort_order = None
self.summary_only = None self.summary_only = None
self.task_output_limit = None self.task_output_limit = None
self.datetime_format = None
super(CallbackModule, self).__init__() super(CallbackModule, self).__init__()
@ -159,9 +173,14 @@ class CallbackModule(CallbackBase):
else: else:
self.task_output_limit = int(self.task_output_limit) self.task_output_limit = int(self.task_output_limit)
self.datetime_format = self.get_option('datetime_format')
if self.datetime_format is not None:
if self.datetime_format == 'iso8601':
self.datetime_format = '%Y-%m-%dT%H:%M:%S.%f'
def _display_tasktime(self): def _display_tasktime(self):
if not self.summary_only: if not self.summary_only:
self._display.display(tasktime()) self._display.display(tasktime(self))
def _record_task(self, task): def _record_task(self, task):
""" """
@ -176,10 +195,11 @@ class CallbackModule(CallbackBase):
# with the same UUID is executed when `serial` is specified in a playbook. # with the same UUID is executed when `serial` is specified in a playbook.
# elapsed: Elapsed time since the first serialized task was started # elapsed: Elapsed time since the first serialized task was started
self.current = task._uuid self.current = task._uuid
dtn = datetime.now().astimezone()
if self.current not in self.stats: if self.current not in self.stats:
self.stats[self.current] = {'started': time.time(), 'elapsed': 0.0, 'name': task.get_name()} self.stats[self.current] = {'started': dtn, 'elapsed': 0.0, 'name': task.get_name()}
else: else:
self.stats[self.current]['started'] = time.time() self.stats[self.current]['started'] = dtn
if self._display.verbosity >= 2: if self._display.verbosity >= 2:
self.stats[self.current]['path'] = task.get_path() self.stats[self.current]['path'] = task.get_path()
@ -196,7 +216,7 @@ class CallbackModule(CallbackBase):
# Align summary report header with other callback plugin summary # Align summary report header with other callback plugin summary
self._display.banner("TASKS RECAP") self._display.banner("TASKS RECAP")
self._display.display(tasktime()) self._display.display(tasktime(self))
self._display.display(filled("", fchar="=")) self._display.display(filled("", fchar="="))
timestamp(self) timestamp(self)

View file

@ -361,6 +361,17 @@ EXAMPLES = r'''
src: /tmp/localpath/ src: /tmp/localpath/
dest: /tmp/remotepath dest: /tmp/remotepath
rsync_path: /usr/gnu/bin/rsync rsync_path: /usr/gnu/bin/rsync
# Source files from multiple folders and merge them on the remote
# Files of the same name in /tmp/path_c/ will take precedence over those in /tmp/path_b/, and same for path_b to path_a
- name: Copy files from multiple folders and merge them into dest
ansible.posix.synchronize:
src:
- /tmp/path_a/
- /tmp/path_b/
- /tmp/path_c/
dest: /tmp/dest/
recursive: True
''' '''
@ -396,9 +407,9 @@ def substitute_controller(path):
def is_rsh_needed(source, dest): def is_rsh_needed(source, dest):
if source.startswith('rsync://') or dest.startswith('rsync://'): if all(e.startswith('rsync://') for e in source) or dest.startswith('rsync://'):
return False return False
if ':' in source or ':' in dest: if any(':' in e for e in source) or ':' in dest:
return True return True
return False return False
@ -406,7 +417,7 @@ def is_rsh_needed(source, dest):
def main(): def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec=dict( argument_spec=dict(
src=dict(type='path', required=True), src=dict(type='list', required=True),
dest=dict(type='path', required=True), dest=dict(type='path', required=True),
dest_port=dict(type='int'), dest_port=dict(type='int'),
delete=dict(type='bool', default=False), delete=dict(type='bool', default=False),
@ -540,11 +551,10 @@ def main():
if dirs: if dirs:
cmd.append('--dirs') cmd.append('--dirs')
if source.startswith('rsync://') and dest.startswith('rsync://'): if all(e.startswith('rsync://') for e in source) and dest.startswith('rsync://'):
module.fail_json(msg='either src or dest must be a localhost', rc=1) module.fail_json(msg='either src or dest must be a localhost', rc=1)
if is_rsh_needed(source, dest): if is_rsh_needed(source, dest):
# https://github.com/ansible/ansible/issues/15907 # https://github.com/ansible/ansible/issues/15907
has_rsh = False has_rsh = False
for rsync_opt in rsync_opts: for rsync_opt in rsync_opts:
@ -600,7 +610,7 @@ def main():
changed_marker = '<<CHANGED>>' changed_marker = '<<CHANGED>>'
cmd.append('--out-format=%s' % shlex_quote(changed_marker + '%i %n%L')) cmd.append('--out-format=%s' % shlex_quote(changed_marker + '%i %n%L'))
cmd.append(shlex_quote(source)) [cmd.append(shlex_quote(e)) for e in source]
cmd.append(shlex_quote(dest)) cmd.append(shlex_quote(dest))
cmdstr = ' '.join(cmd) cmdstr = ' '.join(cmd)