1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2026-02-04 07:51:50 +00:00

Reformat everything.

This commit is contained in:
Felix Fontein 2025-11-01 12:08:41 +01:00
parent 3f2213791a
commit 340ff8586d
1008 changed files with 61301 additions and 58309 deletions

View file

@ -15,9 +15,8 @@ display = Display()
class ActionModule(ActionBase):
# Keep internal params away from user interactions
_VALID_ARGS = frozenset(('path', 'state', 'table', 'noflush', 'counters', 'modprobe', 'ip_version', 'wait'))
_VALID_ARGS = frozenset(("path", "state", "table", "noflush", "counters", "modprobe", "ip_version", "wait"))
DEFAULT_SUDOABLE = True
@staticmethod
@ -27,7 +26,8 @@ class ActionModule(ActionBase):
"is set to 'restored'. To enable its rollback feature (that needs the "
"module to run asynchronously on the remote), please set task attribute "
f"'poll' (={task_poll}) to 0, and 'async' (={task_async}) to a value >2 and not greater than "
f"'ansible_timeout' (={max_timeout}) (recommended).")
f"'ansible_timeout' (={max_timeout}) (recommended)."
)
@staticmethod
def msg_warning__no_async_is_no_rollback(task_poll, task_async, max_timeout):
@ -37,7 +37,8 @@ class ActionModule(ActionBase):
"regain it before fixing firewall rules through a serial console, or any "
f"other way except SSH. Please set task attribute 'poll' (={task_poll}) to 0, and "
f"'async' (={task_async}) to a value >2 and not greater than 'ansible_timeout' (={max_timeout}) "
"(recommended).")
"(recommended)."
)
@staticmethod
def msg_warning__async_greater_than_timeout(task_poll, task_async, max_timeout):
@ -46,44 +47,48 @@ class ActionModule(ActionBase):
"but with settings that will lead this rollback to happen AFTER that the "
"controller will reach its own timeout. Please set task attribute 'poll' "
f"(={task_poll}) to 0, and 'async' (={task_async}) to a value >2 and not greater than "
f"'ansible_timeout' (={max_timeout}) (recommended).")
f"'ansible_timeout' (={max_timeout}) (recommended)."
)
def _async_result(self, async_status_args, task_vars, timeout):
'''
"""
Retrieve results of the asynchronous task, and display them in place of
the async wrapper results (those with the ansible_job_id key).
'''
"""
async_status = self._task.copy()
async_status.args = async_status_args
async_status.action = 'ansible.builtin.async_status'
async_status.action = "ansible.builtin.async_status"
async_status.async_val = 0
async_action = self._shared_loader_obj.action_loader.get(
async_status.action, task=async_status, connection=self._connection,
play_context=self._play_context, loader=self._loader, templar=self._templar,
shared_loader_obj=self._shared_loader_obj)
async_status.action,
task=async_status,
connection=self._connection,
play_context=self._play_context,
loader=self._loader,
templar=self._templar,
shared_loader_obj=self._shared_loader_obj,
)
if async_status.args['mode'] == 'cleanup':
if async_status.args["mode"] == "cleanup":
return async_action.run(task_vars=task_vars)
# At least one iteration is required, even if timeout is 0.
for dummy in range(max(1, timeout)):
async_result = async_action.run(task_vars=task_vars)
if async_result.get('finished', 0) == 1:
if async_result.get("finished", 0) == 1:
break
time.sleep(min(1, timeout))
return async_result
def run(self, tmp=None, task_vars=None):
self._supports_check_mode = True
self._supports_async = True
result = super().run(tmp, task_vars)
del tmp # tmp no longer has any effect
if not result.get('skipped'):
if not result.get("skipped"):
# FUTURE: better to let _execute_module calculate this internally?
wrap_async = self._task.async_val and not self._connection.has_native_async
@ -98,41 +103,38 @@ class ActionModule(ActionBase):
starter_cmd = None
confirm_cmd = None
if module_args.get('state', None) == 'restored':
if module_args.get("state", None) == "restored":
if not wrap_async:
if not check_mode:
display.warning(self.msg_error__async_and_poll_not_zero(
task_poll,
task_async,
max_timeout))
display.warning(self.msg_error__async_and_poll_not_zero(task_poll, task_async, max_timeout))
elif task_poll:
raise AnsibleActionFail(self.msg_warning__no_async_is_no_rollback(
task_poll,
task_async,
max_timeout))
raise AnsibleActionFail(
self.msg_warning__no_async_is_no_rollback(task_poll, task_async, max_timeout)
)
else:
if task_async > max_timeout and not check_mode:
display.warning(self.msg_warning__async_greater_than_timeout(
task_poll,
task_async,
max_timeout))
display.warning(
self.msg_warning__async_greater_than_timeout(task_poll, task_async, max_timeout)
)
# inject the async directory based on the shell option into the
# module args
async_dir = self.get_shell_option('async_dir', default="~/.ansible_async")
async_dir = self.get_shell_option("async_dir", default="~/.ansible_async")
# Bind the loop max duration to consistent values on both
# remote and local sides (if not the same, make the loop
# longer on the controller); and set a backup file path.
module_args['_timeout'] = task_async
module_args['_back'] = f'{async_dir}/iptables.state'
async_status_args = dict(mode='status')
module_args["_timeout"] = task_async
module_args["_back"] = f"{async_dir}/iptables.state"
async_status_args = dict(mode="status")
confirm_cmd = f"rm -f {module_args['_back']}"
starter_cmd = f"touch {module_args['_back']}.starter"
remaining_time = max(task_async, max_timeout)
# do work!
result = merge_hash(result, self._execute_module(module_args=module_args, task_vars=task_vars, wrap_async=wrap_async))
result = merge_hash(
result, self._execute_module(module_args=module_args, task_vars=task_vars, wrap_async=wrap_async)
)
# Then the 3-steps "go ahead or rollback":
# 1. Catch early errors of the module (in asynchronous task) if any.
@ -140,9 +142,9 @@ class ActionModule(ActionBase):
# 2. Reset connection to ensure a persistent one will not be reused.
# 3. Confirm the restored state by removing the backup on the remote.
# Retrieve the results of the asynchronous task to return them.
if '_back' in module_args:
async_status_args['jid'] = result.get('ansible_job_id', None)
if async_status_args['jid'] is None:
if "_back" in module_args:
async_status_args["jid"] = result.get("ansible_job_id", None)
if async_status_args["jid"] is None:
raise AnsibleActionFail("Unable to get 'ansible_job_id'.")
# Catch early errors due to missing mandatory option, bad
@ -156,7 +158,7 @@ class ActionModule(ActionBase):
# As the main command is not yet executed on the target, here
# 'finished' means 'failed before main command be executed'.
if not result['finished']:
if not result["finished"]:
try:
self._connection.reset()
except AttributeError:
@ -178,16 +180,16 @@ class ActionModule(ActionBase):
result = merge_hash(result, self._async_result(async_status_args, task_vars, remaining_time))
# Cleanup async related stuff and internal params
for key in ('ansible_job_id', 'results_file', 'started', 'finished'):
for key in ("ansible_job_id", "results_file", "started", "finished"):
if result.get(key):
del result[key]
if result.get('invocation', {}).get('module_args'):
for key in ('_back', '_timeout', '_async_dir', 'jid'):
if result['invocation']['module_args'].get(key):
del result['invocation']['module_args'][key]
if result.get("invocation", {}).get("module_args"):
for key in ("_back", "_timeout", "_async_dir", "jid"):
if result["invocation"]["module_args"].get(key):
del result["invocation"]["module_args"][key]
async_status_args['mode'] = 'cleanup'
async_status_args["mode"] = "cleanup"
dummy = self._async_result(async_status_args, task_vars, 0)
if not wrap_async:

View file

@ -26,35 +26,31 @@ class TimedOutException(Exception):
class ActionModule(ActionBase):
TRANSFERS_FILES = False
_VALID_ARGS = frozenset((
'msg',
'delay',
'search_paths'
))
_VALID_ARGS = frozenset(("msg", "delay", "search_paths"))
DEFAULT_CONNECT_TIMEOUT = None
DEFAULT_PRE_SHUTDOWN_DELAY = 0
DEFAULT_SHUTDOWN_MESSAGE = 'Shut down initiated by Ansible'
DEFAULT_SHUTDOWN_COMMAND = 'shutdown'
DEFAULT_SHUTDOWN_MESSAGE = "Shut down initiated by Ansible"
DEFAULT_SHUTDOWN_COMMAND = "shutdown"
DEFAULT_SHUTDOWN_COMMAND_ARGS = '-h {delay_min} "{message}"'
DEFAULT_SUDOABLE = True
SHUTDOWN_COMMANDS = {
'alpine': 'poweroff',
'vmkernel': 'halt',
"alpine": "poweroff",
"vmkernel": "halt",
}
SHUTDOWN_COMMAND_ARGS = {
'alpine': '',
'void': '-h +{delay_min} "{message}"',
'freebsd': '-p +{delay_sec}s "{message}"',
'linux': DEFAULT_SHUTDOWN_COMMAND_ARGS,
'macosx': '-h +{delay_min} "{message}"',
'openbsd': '-h +{delay_min} "{message}"',
'solaris': '-y -g {delay_sec} -i 5 "{message}"',
'sunos': '-y -g {delay_sec} -i 5 "{message}"',
'vmkernel': '-d {delay_sec}',
'aix': '-Fh',
"alpine": "",
"void": '-h +{delay_min} "{message}"',
"freebsd": '-p +{delay_sec}s "{message}"',
"linux": DEFAULT_SHUTDOWN_COMMAND_ARGS,
"macosx": '-h +{delay_min} "{message}"',
"openbsd": '-h +{delay_min} "{message}"',
"solaris": '-y -g {delay_sec} -i 5 "{message}"',
"sunos": '-y -g {delay_sec} -i 5 "{message}"',
"vmkernel": "-d {delay_sec}",
"aix": "-Fh",
}
def __init__(self, *args, **kwargs):
@ -62,7 +58,7 @@ class ActionModule(ActionBase):
@property
def delay(self):
return self._check_delay('delay', self.DEFAULT_PRE_SHUTDOWN_DELAY)
return self._check_delay("delay", self.DEFAULT_PRE_SHUTDOWN_DELAY)
def _check_delay(self, key, default):
"""Ensure that the value is positive or zero"""
@ -75,29 +71,28 @@ class ActionModule(ActionBase):
"""Get dist+version specific args first, then distribution, then family, lastly use default"""
attr = getattr(self, variable_name)
value = attr.get(
distribution['name'] + distribution['version'],
attr.get(
distribution['name'],
attr.get(
distribution['family'],
getattr(self, default_value))))
distribution["name"] + distribution["version"],
attr.get(distribution["name"], attr.get(distribution["family"], getattr(self, default_value))),
)
return value
def get_distribution(self, task_vars):
# FIXME: only execute the module if we don't already have the facts we need
distribution = {}
display.debug(f'{self._task.action}: running setup module to get distribution')
display.debug(f"{self._task.action}: running setup module to get distribution")
module_output = self._execute_module(
task_vars=task_vars,
module_name='ansible.legacy.setup',
module_args={'gather_subset': 'min'})
task_vars=task_vars, module_name="ansible.legacy.setup", module_args={"gather_subset": "min"}
)
try:
if module_output.get('failed', False):
raise AnsibleError(f"Failed to determine system distribution. {fmt(module_output, 'module_stdout')}, {fmt(module_output, 'module_stderr')}")
distribution['name'] = module_output['ansible_facts']['ansible_distribution'].lower()
distribution['version'] = to_text(
module_output['ansible_facts']['ansible_distribution_version'].split('.')[0])
distribution['family'] = to_text(module_output['ansible_facts']['ansible_os_family'].lower())
if module_output.get("failed", False):
raise AnsibleError(
f"Failed to determine system distribution. {fmt(module_output, 'module_stdout')}, {fmt(module_output, 'module_stderr')}"
)
distribution["name"] = module_output["ansible_facts"]["ansible_distribution"].lower()
distribution["version"] = to_text(
module_output["ansible_facts"]["ansible_distribution_version"].split(".")[0]
)
distribution["family"] = to_text(module_output["ansible_facts"]["ansible_os_family"].lower())
display.debug(f"{self._task.action}: distribution: {distribution}")
return distribution
except KeyError as ke:
@ -105,22 +100,20 @@ class ActionModule(ActionBase):
def get_shutdown_command(self, task_vars, distribution):
def find_command(command, find_search_paths):
display.debug(f'{self._task.action}: running find module looking in {find_search_paths} to get path for "{command}"')
display.debug(
f'{self._task.action}: running find module looking in {find_search_paths} to get path for "{command}"'
)
find_result = self._execute_module(
task_vars=task_vars,
# prevent collection search by calling with ansible.legacy (still allows library/ override of find)
module_name='ansible.legacy.find',
module_args={
'paths': find_search_paths,
'patterns': [command],
'file_type': 'any'
}
module_name="ansible.legacy.find",
module_args={"paths": find_search_paths, "patterns": [command], "file_type": "any"},
)
return [x['path'] for x in find_result['files']]
return [x["path"] for x in find_result["files"]]
shutdown_bin = self._get_value_from_facts('SHUTDOWN_COMMANDS', distribution, 'DEFAULT_SHUTDOWN_COMMAND')
default_search_paths = ['/sbin', '/usr/sbin', '/usr/local/sbin']
search_paths = self._task.args.get('search_paths', default_search_paths)
shutdown_bin = self._get_value_from_facts("SHUTDOWN_COMMANDS", distribution, "DEFAULT_SHUTDOWN_COMMAND")
default_search_paths = ["/sbin", "/usr/sbin", "/usr/local/sbin"]
search_paths = self._task.args.get("search_paths", default_search_paths)
# FIXME: switch all this to user arg spec validation methods when they are available
# Convert bare strings to a list
@ -138,26 +131,28 @@ class ActionModule(ActionBase):
full_path = find_command(shutdown_bin, search_paths) # find the path to the shutdown command
if not full_path: # if we could not find the shutdown command
# tell the user we will try with systemd
display.vvv(f'Unable to find command "{shutdown_bin}" in search paths: {search_paths}, will attempt a shutdown using systemd directly.')
systemctl_search_paths = ['/bin', '/usr/bin']
full_path = find_command('systemctl', systemctl_search_paths) # find the path to the systemctl command
display.vvv(
f'Unable to find command "{shutdown_bin}" in search paths: {search_paths}, will attempt a shutdown using systemd directly.'
)
systemctl_search_paths = ["/bin", "/usr/bin"]
full_path = find_command("systemctl", systemctl_search_paths) # find the path to the systemctl command
if not full_path: # if we couldn't find systemctl
raise AnsibleError(
f'Could not find command "{shutdown_bin}" in search paths: {search_paths} or systemctl'
f' command in search paths: {systemctl_search_paths}, unable to shutdown.') # we give up here
f" command in search paths: {systemctl_search_paths}, unable to shutdown."
) # we give up here
else:
return f"{full_path[0]} poweroff" # done, since we cannot use args with systemd shutdown
# systemd case taken care of, here we add args to the command
args = self._get_value_from_facts('SHUTDOWN_COMMAND_ARGS', distribution, 'DEFAULT_SHUTDOWN_COMMAND_ARGS')
args = self._get_value_from_facts("SHUTDOWN_COMMAND_ARGS", distribution, "DEFAULT_SHUTDOWN_COMMAND_ARGS")
# Convert seconds to minutes. If less that 60, set it to 0.
delay_sec = self.delay
shutdown_message = self._task.args.get('msg', self.DEFAULT_SHUTDOWN_MESSAGE)
shutdown_message = self._task.args.get("msg", self.DEFAULT_SHUTDOWN_MESSAGE)
af = args.format(delay_sec=delay_sec, delay_min=delay_sec // 60, message=shutdown_message)
return f'{full_path[0]} {af}'
return f"{full_path[0]} {af}"
def perform_shutdown(self, task_vars, distribution):
result = {}
@ -169,23 +164,24 @@ class ActionModule(ActionBase):
display.vvv(f"{self._task.action}: shutting down server...")
display.debug(f"{self._task.action}: shutting down server with command '{shutdown_command_exec}'")
if self._play_context.check_mode:
shutdown_result['rc'] = 0
shutdown_result["rc"] = 0
else:
shutdown_result = self._low_level_execute_command(shutdown_command_exec, sudoable=self.DEFAULT_SUDOABLE)
except AnsibleConnectionFailure as e:
# If the connection is closed too quickly due to the system being shutdown, carry on
display.debug(
f'{self._task.action}: AnsibleConnectionFailure caught and handled: {e}')
shutdown_result['rc'] = 0
display.debug(f"{self._task.action}: AnsibleConnectionFailure caught and handled: {e}")
shutdown_result["rc"] = 0
if shutdown_result['rc'] != 0:
result['failed'] = True
result['shutdown'] = False
result['msg'] = f"Shutdown command failed. Error was {fmt(shutdown_result, 'stdout')}, {fmt(shutdown_result, 'stderr')}"
if shutdown_result["rc"] != 0:
result["failed"] = True
result["shutdown"] = False
result["msg"] = (
f"Shutdown command failed. Error was {fmt(shutdown_result, 'stdout')}, {fmt(shutdown_result, 'stderr')}"
)
return result
result['failed'] = False
result['shutdown_command'] = shutdown_command_exec
result["failed"] = False
result["shutdown_command"] = shutdown_command_exec
return result
def run(self, tmp=None, task_vars=None):
@ -193,16 +189,16 @@ class ActionModule(ActionBase):
self._supports_async = True
# If running with local connection, fail so we don't shutdown ourself
if self._connection.transport == 'local' and (not self._play_context.check_mode):
msg = f'Running {self._task.action} with local connection would shutdown the control node.'
return {'changed': False, 'elapsed': 0, 'shutdown': False, 'failed': True, 'msg': msg}
if self._connection.transport == "local" and (not self._play_context.check_mode):
msg = f"Running {self._task.action} with local connection would shutdown the control node."
return {"changed": False, "elapsed": 0, "shutdown": False, "failed": True, "msg": msg}
if task_vars is None:
task_vars = {}
result = super().run(tmp, task_vars)
if result.get('skipped', False) or result.get('failed', False):
if result.get("skipped", False) or result.get("failed", False):
return result
distribution = self.get_distribution(task_vars)
@ -210,12 +206,12 @@ class ActionModule(ActionBase):
# Initiate shutdown
shutdown_result = self.perform_shutdown(task_vars, distribution)
if shutdown_result['failed']:
if shutdown_result["failed"]:
result = shutdown_result
return result
result['shutdown'] = True
result['changed'] = True
result['shutdown_command'] = shutdown_result['shutdown_command']
result["shutdown"] = True
result["changed"] = True
result["shutdown_command"] = shutdown_result["shutdown_command"]
return result

View file

@ -94,23 +94,22 @@ from ansible.plugins.become import BecomeBase
class BecomeModule(BecomeBase):
name = 'community.general.doas'
name = "community.general.doas"
# messages for detecting prompted password issues
fail = ('Permission denied',)
missing = ('Authorization required',)
fail = ("Permission denied",)
missing = ("Authorization required",)
# See https://github.com/ansible-collections/community.general/issues/9977,
# https://github.com/ansible/ansible/pull/78111
pipelining = False
def check_password_prompt(self, b_output):
''' checks if the expected password prompt exists in b_output '''
"""checks if the expected password prompt exists in b_output"""
# FIXME: more accurate would be: 'doas (%s@' % remote_user
# however become plugins don't have that information currently
b_prompts = [to_bytes(p) for p in self.get_option('prompt_l10n')] or [br'doas \(', br'Password:']
b_prompts = [to_bytes(p) for p in self.get_option("prompt_l10n")] or [rb"doas \(", rb"Password:"]
b_prompt = b"|".join(b_prompts)
return bool(re.match(b_prompt, b_output))
@ -123,16 +122,16 @@ class BecomeModule(BecomeBase):
self.prompt = True
become_exe = self.get_option('become_exe')
become_exe = self.get_option("become_exe")
flags = self.get_option('become_flags')
if not self.get_option('become_pass') and '-n' not in flags:
flags += ' -n'
flags = self.get_option("become_flags")
if not self.get_option("become_pass") and "-n" not in flags:
flags += " -n"
become_user = self.get_option('become_user')
user = f'-u {become_user}' if become_user else ''
become_user = self.get_option("become_user")
user = f"-u {become_user}" if become_user else ""
success_cmd = self._build_success_command(cmd, shell, noexe=True)
executable = getattr(shell, 'executable', shell.SHELL_FAMILY)
executable = getattr(shell, "executable", shell.SHELL_FAMILY)
return f'{become_exe} {flags} {user} {executable} -c {success_cmd}'
return f"{become_exe} {flags} {user} {executable} -c {success_cmd}"

View file

@ -74,11 +74,10 @@ from ansible.plugins.become import BecomeBase
class BecomeModule(BecomeBase):
name = 'community.general.dzdo'
name = "community.general.dzdo"
# messages for detecting prompted password issues
fail = ('Sorry, try again.',)
fail = ("Sorry, try again.",)
def build_become_command(self, cmd, shell):
super().build_become_command(cmd, shell)
@ -86,14 +85,14 @@ class BecomeModule(BecomeBase):
if not cmd:
return cmd
becomecmd = self.get_option('become_exe')
becomecmd = self.get_option("become_exe")
flags = self.get_option('become_flags')
if self.get_option('become_pass'):
self.prompt = f'[dzdo via ansible, key={self._id}] password:'
flags = f"{flags.replace('-n', '')} -p \"{self.prompt}\""
flags = self.get_option("become_flags")
if self.get_option("become_pass"):
self.prompt = f"[dzdo via ansible, key={self._id}] password:"
flags = f'{flags.replace("-n", "")} -p "{self.prompt}"'
become_user = self.get_option('become_user')
user = f'-u {become_user}' if become_user else ''
become_user = self.get_option("become_user")
user = f"-u {become_user}" if become_user else ""
return f"{becomecmd} {flags} {user} {self._build_success_command(cmd, shell)}"

View file

@ -92,23 +92,21 @@ from ansible.plugins.become import BecomeBase
class BecomeModule(BecomeBase):
name = 'community.general.ksu'
name = "community.general.ksu"
# messages for detecting prompted password issues
fail = ('Password incorrect',)
missing = ('No password given',)
fail = ("Password incorrect",)
missing = ("No password given",)
def check_password_prompt(self, b_output):
''' checks if the expected password prompt exists in b_output '''
"""checks if the expected password prompt exists in b_output"""
prompts = self.get_option('prompt_l10n') or ["Kerberos password for .*@.*:"]
prompts = self.get_option("prompt_l10n") or ["Kerberos password for .*@.*:"]
b_prompt = b"|".join(to_bytes(p) for p in prompts)
return bool(re.match(b_prompt, b_output))
def build_become_command(self, cmd, shell):
super().build_become_command(cmd, shell)
# Prompt handling for ``ksu`` is more complicated, this
@ -118,8 +116,8 @@ class BecomeModule(BecomeBase):
if not cmd:
return cmd
exe = self.get_option('become_exe')
exe = self.get_option("become_exe")
flags = self.get_option('become_flags')
user = self.get_option('become_user')
return f'{exe} {user} {flags} -e {self._build_success_command(cmd, shell)} '
flags = self.get_option("become_flags")
user = self.get_option("become_user")
return f"{exe} {user} {flags} -e {self._build_success_command(cmd, shell)} "

View file

@ -96,16 +96,15 @@ from ansible.plugins.become import BecomeBase
from ansible.module_utils.common.text.converters import to_bytes
ansi_color_codes = re_compile(to_bytes(r'\x1B\[[0-9;]+m'))
ansi_color_codes = re_compile(to_bytes(r"\x1B\[[0-9;]+m"))
class BecomeModule(BecomeBase):
name = "community.general.machinectl"
name = 'community.general.machinectl'
prompt = 'Password: '
fail = ('==== AUTHENTICATION FAILED ====',)
success = ('==== AUTHENTICATION COMPLETE ====',)
prompt = "Password: "
fail = ("==== AUTHENTICATION FAILED ====",)
success = ("==== AUTHENTICATION COMPLETE ====",)
require_tty = True # see https://github.com/ansible-collections/community.general/issues/6932
# See https://github.com/ansible/ansible/issues/81254,
@ -122,11 +121,11 @@ class BecomeModule(BecomeBase):
if not cmd:
return cmd
become = self.get_option('become_exe')
become = self.get_option("become_exe")
flags = self.get_option('become_flags')
user = self.get_option('become_user')
return f'{become} -q shell {flags} {user}@ {self._build_success_command(cmd, shell)}'
flags = self.get_option("become_flags")
user = self.get_option("become_user")
return f"{become} -q shell {flags} {user}@ {self._build_success_command(cmd, shell)}"
def check_success(self, b_output):
b_output = self.remove_ansi_codes(b_output)

View file

@ -86,10 +86,9 @@ from ansible.plugins.become import BecomeBase
class BecomeModule(BecomeBase):
name = "community.general.pbrun"
name = 'community.general.pbrun'
prompt = 'Password:'
prompt = "Password:"
def build_become_command(self, cmd, shell):
super().build_become_command(cmd, shell)
@ -97,11 +96,11 @@ class BecomeModule(BecomeBase):
if not cmd:
return cmd
become_exe = self.get_option('become_exe')
become_exe = self.get_option("become_exe")
flags = self.get_option('become_flags')
become_user = self.get_option('become_user')
user = f'-u {become_user}' if become_user else ''
noexe = not self.get_option('wrap_exe')
flags = self.get_option("become_flags")
become_user = self.get_option("become_user")
user = f"-u {become_user}" if become_user else ""
noexe = not self.get_option("wrap_exe")
return f"{become_exe} {flags} {user} {self._build_success_command(cmd, shell, noexe=noexe)}"

View file

@ -91,8 +91,7 @@ from ansible.plugins.become import BecomeBase
class BecomeModule(BecomeBase):
name = 'community.general.pfexec'
name = "community.general.pfexec"
def build_become_command(self, cmd, shell):
super().build_become_command(cmd, shell)
@ -100,8 +99,8 @@ class BecomeModule(BecomeBase):
if not cmd:
return cmd
exe = self.get_option('become_exe')
exe = self.get_option("become_exe")
flags = self.get_option('become_flags')
noexe = not self.get_option('wrap_exe')
return f'{exe} {flags} {self._build_success_command(cmd, shell, noexe=noexe)}'
flags = self.get_option("become_flags")
noexe = not self.get_option("wrap_exe")
return f"{exe} {flags} {self._build_success_command(cmd, shell, noexe=noexe)}"

View file

@ -63,9 +63,8 @@ from ansible.plugins.become import BecomeBase
class BecomeModule(BecomeBase):
name = 'community.general.pmrun'
prompt = 'Enter UPM user password:'
name = "community.general.pmrun"
prompt = "Enter UPM user password:"
def build_become_command(self, cmd, shell):
super().build_become_command(cmd, shell)
@ -73,7 +72,7 @@ class BecomeModule(BecomeBase):
if not cmd:
return cmd
become = self.get_option('become_exe')
become = self.get_option("become_exe")
flags = self.get_option('become_flags')
return f'{become} {flags} {shlex_quote(self._build_success_command(cmd, shell))}'
flags = self.get_option("become_flags")
return f"{become} {flags} {shlex_quote(self._build_success_command(cmd, shell))}"

View file

@ -85,15 +85,12 @@ ansi_color_codes = re_compile(to_bytes(r"\x1B\[[0-9;]+m"))
class BecomeModule(BecomeBase):
name = "community.general.run0"
prompt = "Password: "
fail = ("==== AUTHENTICATION FAILED ====",)
success = ("==== AUTHENTICATION COMPLETE ====",)
require_tty = (
True # see https://github.com/ansible-collections/community.general/issues/6932
)
require_tty = True # see https://github.com/ansible-collections/community.general/issues/6932
@staticmethod
def remove_ansi_codes(line):
@ -109,9 +106,7 @@ class BecomeModule(BecomeBase):
flags = self.get_option("become_flags")
user = self.get_option("become_user")
return (
f"{become} --user={user} {flags} {self._build_success_command(cmd, shell)}"
)
return f"{become} --user={user} {flags} {self._build_success_command(cmd, shell)}"
def check_success(self, b_output):
b_output = self.remove_ansi_codes(b_output)

View file

@ -75,11 +75,10 @@ from ansible.plugins.become import BecomeBase
class BecomeModule(BecomeBase):
name = "community.general.sesu"
name = 'community.general.sesu'
prompt = 'Please enter your password:'
fail = missing = ('Sorry, try again with sesu.',)
prompt = "Please enter your password:"
fail = missing = ("Sorry, try again with sesu.",)
def build_become_command(self, cmd, shell):
super().build_become_command(cmd, shell)
@ -87,8 +86,8 @@ class BecomeModule(BecomeBase):
if not cmd:
return cmd
become = self.get_option('become_exe')
become = self.get_option("become_exe")
flags = self.get_option('become_flags')
user = self.get_option('become_user')
return f'{become} {flags} {user} -c {self._build_success_command(cmd, shell)}'
flags = self.get_option("become_flags")
user = self.get_option("become_user")
return f"{become} {flags} {user} -c {self._build_success_command(cmd, shell)}"

View file

@ -79,12 +79,11 @@ from ansible.plugins.become import BecomeBase
class BecomeModule(BecomeBase):
name = 'community.general.sudosu'
name = "community.general.sudosu"
# messages for detecting prompted password issues
fail = ('Sorry, try again.',)
missing = ('Sorry, a password is required to run sudo', 'sudo: a password is required')
fail = ("Sorry, try again.",)
missing = ("Sorry, a password is required to run sudo", "sudo: a password is required")
def build_become_command(self, cmd, shell):
super().build_become_command(cmd, shell)
@ -92,21 +91,21 @@ class BecomeModule(BecomeBase):
if not cmd:
return cmd
becomecmd = 'sudo'
becomecmd = "sudo"
flags = self.get_option('become_flags') or ''
prompt = ''
if self.get_option('become_pass'):
self.prompt = f'[sudo via ansible, key={self._id}] password:'
flags = self.get_option("become_flags") or ""
prompt = ""
if self.get_option("become_pass"):
self.prompt = f"[sudo via ansible, key={self._id}] password:"
if flags: # this could be simplified, but kept as is for now for backwards string matching
flags = flags.replace('-n', '')
flags = flags.replace("-n", "")
prompt = f'-p "{self.prompt}"'
user = self.get_option('become_user') or ''
user = self.get_option("become_user") or ""
if user:
user = f'{user}'
user = f"{user}"
if self.get_option('alt_method'):
if self.get_option("alt_method"):
return f"{becomecmd} {flags} {prompt} su -l {user} -c {self._build_success_command(cmd, shell, True)}"
else:
return f"{becomecmd} {flags} {prompt} su -l {user} {self._build_success_command(cmd, shell)}"

View file

@ -59,6 +59,7 @@ from ansible.utils.display import Display
try:
import memcache
HAS_MEMCACHE = True
except ImportError:
HAS_MEMCACHE = False
@ -75,7 +76,7 @@ class ProxyClientPool:
"""
def __init__(self, *args, **kwargs):
self.max_connections = kwargs.pop('max_connections', 1024)
self.max_connections = kwargs.pop("max_connections", 1024)
self.connection_args = args
self.connection_kwargs = kwargs
self.reset()
@ -123,6 +124,7 @@ class ProxyClientPool:
def __getattr__(self, name):
def wrapped(*args, **kwargs):
return self._proxy_client(name, *args, **kwargs)
return wrapped
def _proxy_client(self, name, *args, **kwargs):
@ -139,7 +141,8 @@ class CacheModuleKeys(MutableSet):
A set subclass that keeps track of insertion time and persists
the set in memcached.
"""
PREFIX = 'ansible_cache_keys'
PREFIX = "ansible_cache_keys"
def __init__(self, cache, *args, **kwargs):
self._cache = cache
@ -171,15 +174,14 @@ class CacheModuleKeys(MutableSet):
class CacheModule(BaseCacheModule):
def __init__(self, *args, **kwargs):
connection = ['127.0.0.1:11211']
connection = ["127.0.0.1:11211"]
super().__init__(*args, **kwargs)
if self.get_option('_uri'):
connection = self.get_option('_uri')
self._timeout = self.get_option('_timeout')
self._prefix = self.get_option('_prefix')
if self.get_option("_uri"):
connection = self.get_option("_uri")
self._timeout = self.get_option("_timeout")
self._prefix = self.get_option("_prefix")
if not HAS_MEMCACHE:
raise AnsibleError("python-memcached is required for the memcached fact cache")

View file

@ -51,14 +51,15 @@ class CacheModule(BaseFileCacheModule):
"""
A caching module backed by pickle files.
"""
_persistent = False # prevent unnecessary JSON serialization and key munging
def _load(self, filepath):
# Pickle is a binary format
with open(filepath, 'rb') as f:
return pickle.load(f, encoding='bytes')
with open(filepath, "rb") as f:
return pickle.load(f, encoding="bytes")
def _dump(self, value, filepath):
with open(filepath, 'wb') as f:
with open(filepath, "wb") as f:
# Use pickle protocol 2 which is compatible with Python 2.3+.
pickle.dump(value, f, protocol=2)

View file

@ -77,6 +77,7 @@ from ansible.utils.display import Display
try:
from redis import StrictRedis, VERSION
HAS_REDIS = True
except ImportError:
HAS_REDIS = False
@ -93,32 +94,35 @@ class CacheModule(BaseCacheModule):
to expire keys. This mechanism is used or a pattern matched 'scan' for
performance.
"""
_sentinel_service_name = None
re_url_conn = re.compile(r'^([^:]+|\[[^]]+\]):(\d+):(\d+)(?::(.*))?$')
re_sent_conn = re.compile(r'^(.*):(\d+)$')
re_url_conn = re.compile(r"^([^:]+|\[[^]]+\]):(\d+):(\d+)(?::(.*))?$")
re_sent_conn = re.compile(r"^(.*):(\d+)$")
def __init__(self, *args, **kwargs):
uri = ''
uri = ""
super().__init__(*args, **kwargs)
if self.get_option('_uri'):
uri = self.get_option('_uri')
self._timeout = float(self.get_option('_timeout'))
self._prefix = self.get_option('_prefix')
self._keys_set = self.get_option('_keyset_name')
self._sentinel_service_name = self.get_option('_sentinel_service_name')
if self.get_option("_uri"):
uri = self.get_option("_uri")
self._timeout = float(self.get_option("_timeout"))
self._prefix = self.get_option("_prefix")
self._keys_set = self.get_option("_keyset_name")
self._sentinel_service_name = self.get_option("_sentinel_service_name")
if not HAS_REDIS:
raise AnsibleError("The 'redis' python module (version 2.4.5 or newer) is required for the redis fact cache, 'pip install redis'")
raise AnsibleError(
"The 'redis' python module (version 2.4.5 or newer) is required for the redis fact cache, 'pip install redis'"
)
self._cache = {}
kw = {}
# tls connection
tlsprefix = 'tls://'
tlsprefix = "tls://"
if uri.startswith(tlsprefix):
kw['ssl'] = True
uri = uri[len(tlsprefix):]
kw["ssl"] = True
uri = uri[len(tlsprefix) :]
# redis sentinel connection
if self._sentinel_service_name:
@ -128,7 +132,7 @@ class CacheModule(BaseCacheModule):
connection = self._parse_connection(self.re_url_conn, uri)
self._db = StrictRedis(*connection, **kw)
display.vv(f'Redis connection: {self._db}')
display.vv(f"Redis connection: {self._db}")
@staticmethod
def _parse_connection(re_patt, uri):
@ -146,33 +150,32 @@ class CacheModule(BaseCacheModule):
except ImportError:
raise AnsibleError("The 'redis' python module (version 2.9.0 or newer) is required to use redis sentinel.")
if ';' not in uri:
raise AnsibleError('_uri does not have sentinel syntax.')
if ";" not in uri:
raise AnsibleError("_uri does not have sentinel syntax.")
# format: "localhost:26379;localhost2:26379;0:changeme"
connections = uri.split(';')
connections = uri.split(";")
connection_args = connections.pop(-1)
if len(connection_args) > 0: # handle if no db nr is given
connection_args = connection_args.split(':')
kw['db'] = connection_args.pop(0)
connection_args = connection_args.split(":")
kw["db"] = connection_args.pop(0)
try:
kw['password'] = connection_args.pop(0)
kw["password"] = connection_args.pop(0)
except IndexError:
pass # password is optional
sentinels = [self._parse_connection(self.re_sent_conn, shost) for shost in connections]
display.vv(f'\nUsing redis sentinels: {sentinels}')
display.vv(f"\nUsing redis sentinels: {sentinels}")
scon = Sentinel(sentinels, **kw)
try:
return scon.master_for(self._sentinel_service_name, socket_timeout=0.2)
except Exception as exc:
raise AnsibleError(f'Could not connect to redis sentinel: {exc}')
raise AnsibleError(f"Could not connect to redis sentinel: {exc}")
def _make_key(self, key):
return self._prefix + key
def get(self, key):
if key not in self._cache:
value = self._db.get(self._make_key(key))
# guard against the key not being removed from the zset;
@ -186,7 +189,6 @@ class CacheModule(BaseCacheModule):
return self._cache.get(key)
def set(self, key, value):
value2 = json.dumps(value, cls=AnsibleJSONEncoder, sort_keys=True, indent=4)
if self._timeout > 0: # a timeout of 0 is handled as meaning 'never expire'
self._db.setex(self._make_key(key), int(self._timeout), value2)
@ -210,7 +212,7 @@ class CacheModule(BaseCacheModule):
def contains(self, key):
self._expire_keys()
return (self._db.zrank(self._keys_set, key) is not None)
return self._db.zrank(self._keys_set, key) is not None
def delete(self, key):
if key in self._cache:

View file

@ -58,9 +58,9 @@ class CacheModule(BaseFileCacheModule):
"""
def _load(self, filepath):
with open(os.path.abspath(filepath), 'r', encoding='utf-8') as f:
with open(os.path.abspath(filepath), "r", encoding="utf-8") as f:
return AnsibleLoader(f).get_single_data()
def _dump(self, value, filepath):
with open(os.path.abspath(filepath), 'w', encoding='utf-8') as f:
with open(os.path.abspath(filepath), "w", encoding="utf-8") as f:
yaml.dump(value, f, Dumper=AnsibleDumper, default_flow_style=False)

View file

@ -49,6 +49,7 @@ from ansible.plugins.callback import CallbackBase
class MemProf(threading.Thread):
"""Python thread for recording memory usage"""
def __init__(self, path, obj=None):
threading.Thread.__init__(self)
self.obj = obj
@ -66,8 +67,8 @@ class MemProf(threading.Thread):
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'community.general.cgroup_memory_recap'
CALLBACK_TYPE = "aggregate"
CALLBACK_NAME = "community.general.cgroup_memory_recap"
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
@ -80,11 +81,11 @@ class CallbackModule(CallbackBase):
def set_options(self, task_keys=None, var_options=None, direct=None):
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
self.cgroup_max_file = self.get_option('max_mem_file')
self.cgroup_current_file = self.get_option('cur_mem_file')
self.cgroup_max_file = self.get_option("max_mem_file")
self.cgroup_current_file = self.get_option("cur_mem_file")
with open(self.cgroup_max_file, 'w+') as f:
f.write('0')
with open(self.cgroup_max_file, "w+") as f:
f.write("0")
def _profile_memory(self, obj=None):
prev_task = None
@ -112,8 +113,8 @@ class CallbackModule(CallbackBase):
with open(self.cgroup_max_file) as f:
max_results = int(f.read().strip()) / 1024 / 1024
self._display.banner('CGROUP MEMORY RECAP')
self._display.display(f'Execution Maximum: {max_results:0.2f}MB\n\n')
self._display.banner("CGROUP MEMORY RECAP")
self._display.display(f"Execution Maximum: {max_results:0.2f}MB\n\n")
for task, memory in self.task_results:
self._display.display(f'{task.get_name()} ({task._uuid}): {memory:0.2f}MB')
self._display.display(f"{task.get_name()} ({task._uuid}): {memory:0.2f}MB")

View file

@ -25,9 +25,10 @@ class CallbackModule(CallbackBase):
This is a very trivial example of how any callback function can get at play and task objects.
play will be 'None' for runner invocations, and task will be None for 'setup' invocations.
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'community.general.context_demo'
CALLBACK_TYPE = "aggregate"
CALLBACK_NAME = "community.general.context_demo"
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, *args, **kwargs):
@ -40,11 +41,11 @@ class CallbackModule(CallbackBase):
self._display.display(" --- ARGS ")
for i, a in enumerate(args):
self._display.display(f' {i}: {a}')
self._display.display(f" {i}: {a}")
self._display.display(" --- KWARGS ")
for k in kwargs:
self._display.display(f' {k}: {kwargs[k]}')
self._display.display(f" {k}: {kwargs[k]}")
def v2_playbook_on_play_start(self, play):
self.play = play

View file

@ -1,9 +1,9 @@
# Copyright (c) 2018, Ivan Aragones Muniesa <ivan.aragones.muniesa@gmail.com>
# 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
'''
Counter enabled Ansible callback plugin (See DOCUMENTATION for more information)
'''
"""
Counter enabled Ansible callback plugin (See DOCUMENTATION for more information)
"""
from __future__ import annotations
@ -29,15 +29,14 @@ from ansible.playbook.task_include import TaskInclude
class CallbackModule(CallbackBase):
'''
"""
This is the default callback interface, which simply prints messages
to stdout when new callback events are received.
'''
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'community.general.counter_enabled'
CALLBACK_TYPE = "stdout"
CALLBACK_NAME = "community.general.counter_enabled"
_task_counter = 1
_task_total = 0
@ -55,11 +54,7 @@ class CallbackModule(CallbackBase):
def _all_vars(self, host=None, task=None):
# host and task need to be specified in case 'magic variables' (host vars, group vars, etc)
# need to be loaded as well
return self._play.get_variable_manager().get_vars(
play=self._play,
host=host,
task=task
)
return self._play.get_variable_manager().get_vars(play=self._play, host=host, task=task)
def v2_playbook_on_start(self, playbook):
self._playbook = playbook
@ -77,8 +72,8 @@ class CallbackModule(CallbackBase):
self._play = play
self._previous_batch_total = self._current_batch_total
self._current_batch_total = self._previous_batch_total + len(self._all_vars()['vars']['ansible_play_batch'])
self._host_total = len(self._all_vars()['vars']['ansible_play_hosts_all'])
self._current_batch_total = self._previous_batch_total + len(self._all_vars()["vars"]["ansible_play_batch"])
self._host_total = len(self._all_vars()["vars"]["ansible_play_hosts_all"])
self._task_total = len(self._play.get_tasks()[0])
self._task_counter = 1
@ -93,39 +88,39 @@ class CallbackModule(CallbackBase):
f"{hostcolor(host, stat)} : {colorize('ok', stat['ok'], C.COLOR_OK)} {colorize('changed', stat['changed'], C.COLOR_CHANGED)} "
f"{colorize('unreachable', stat['unreachable'], C.COLOR_UNREACHABLE)} {colorize('failed', stat['failures'], C.COLOR_ERROR)} "
f"{colorize('rescued', stat['rescued'], C.COLOR_OK)} {colorize('ignored', stat['ignored'], C.COLOR_WARN)}",
screen_only=True
screen_only=True,
)
self._display.display(
f"{hostcolor(host, stat, False)} : {colorize('ok', stat['ok'], None)} {colorize('changed', stat['changed'], None)} "
f"{colorize('unreachable', stat['unreachable'], None)} {colorize('failed', stat['failures'], None)} "
f"{colorize('rescued', stat['rescued'], None)} {colorize('ignored', stat['ignored'], None)}",
log_only=True
log_only=True,
)
self._display.display("", screen_only=True)
# print custom stats
if self._plugin_options.get('show_custom_stats', C.SHOW_CUSTOM_STATS) and stats.custom:
if self._plugin_options.get("show_custom_stats", C.SHOW_CUSTOM_STATS) and stats.custom:
# fallback on constants for inherited plugins missing docs
self._display.banner("CUSTOM STATS: ")
# per host
# TODO: come up with 'pretty format'
for k in sorted(stats.custom.keys()):
if k == '_run':
if k == "_run":
continue
_custom_stats = self._dump_results(stats.custom[k], indent=1).replace('\n', '')
self._display.display(f'\t{k}: {_custom_stats}')
_custom_stats = self._dump_results(stats.custom[k], indent=1).replace("\n", "")
self._display.display(f"\t{k}: {_custom_stats}")
# print per run custom stats
if '_run' in stats.custom:
if "_run" in stats.custom:
self._display.display("", screen_only=True)
_custom_stats_run = self._dump_results(stats.custom['_run'], indent=1).replace('\n', '')
self._display.display(f'\tRUN: {_custom_stats_run}')
_custom_stats_run = self._dump_results(stats.custom["_run"], indent=1).replace("\n", "")
self._display.display(f"\tRUN: {_custom_stats_run}")
self._display.display("", screen_only=True)
def v2_playbook_on_task_start(self, task, is_conditional):
args = ''
args = ""
# args can be specified as no_log in several places: in the task or in
# the argument spec. We can check whether the task is no_log but the
# argument spec can't be because that is only run on the target
@ -135,8 +130,8 @@ class CallbackModule(CallbackBase):
# that they can secure this if they feel that their stdout is insecure
# (shoulder surfing, logging stdout straight to a file, etc).
if not task.no_log and C.DISPLAY_ARGS_TO_STDOUT:
args = ', '.join(('{k}={v}' for k, v in task.args.items()))
args = f' {args}'
args = ", ".join(("{k}={v}" for k, v in task.args.items()))
args = f" {args}"
self._display.banner(f"TASK {self._task_counter}/{self._task_total} [{task.get_name().strip()}{args}]")
if self._display.verbosity >= 2:
path = task.get_path()
@ -146,17 +141,16 @@ class CallbackModule(CallbackBase):
self._task_counter += 1
def v2_runner_on_ok(self, result):
self._host_counter += 1
delegated_vars = result._result.get('_ansible_delegated_vars', None)
delegated_vars = result._result.get("_ansible_delegated_vars", None)
if self._play.strategy == 'free' and self._last_task_banner != result._task._uuid:
if self._play.strategy == "free" and self._last_task_banner != result._task._uuid:
self._print_task_banner(result._task)
if isinstance(result._task, TaskInclude):
return
elif result._result.get('changed', False):
elif result._result.get("changed", False):
if delegated_vars:
msg = f"changed: {self._host_counter}/{self._host_total} [{result._host.get_name()} -> {delegated_vars['ansible_host']}]"
else:
@ -171,7 +165,7 @@ class CallbackModule(CallbackBase):
self._handle_warnings(result._result)
if result._task.loop and 'results' in result._result:
if result._task.loop and "results" in result._result:
self._process_items(result)
else:
self._clean_results(result._result, result._task.action)
@ -181,19 +175,18 @@ class CallbackModule(CallbackBase):
self._display.display(msg, color=color)
def v2_runner_on_failed(self, result, ignore_errors=False):
self._host_counter += 1
delegated_vars = result._result.get('_ansible_delegated_vars', None)
delegated_vars = result._result.get("_ansible_delegated_vars", None)
self._clean_results(result._result, result._task.action)
if self._play.strategy == 'free' and self._last_task_banner != result._task._uuid:
if self._play.strategy == "free" and self._last_task_banner != result._task._uuid:
self._print_task_banner(result._task)
self._handle_exception(result._result)
self._handle_warnings(result._result)
if result._task.loop and 'results' in result._result:
if result._task.loop and "results" in result._result:
self._process_items(result)
else:
@ -201,12 +194,12 @@ class CallbackModule(CallbackBase):
self._display.display(
f"fatal: {self._host_counter}/{self._host_total} [{result._host.get_name()} -> "
f"{delegated_vars['ansible_host']}]: FAILED! => {self._dump_results(result._result)}",
color=C.COLOR_ERROR
color=C.COLOR_ERROR,
)
else:
self._display.display(
f"fatal: {self._host_counter}/{self._host_total} [{result._host.get_name()}]: FAILED! => {self._dump_results(result._result)}",
color=C.COLOR_ERROR
color=C.COLOR_ERROR,
)
if ignore_errors:
@ -215,14 +208,15 @@ class CallbackModule(CallbackBase):
def v2_runner_on_skipped(self, result):
self._host_counter += 1
if self._plugin_options.get('show_skipped_hosts', C.DISPLAY_SKIPPED_HOSTS): # fallback on constants for inherited plugins missing docs
if self._plugin_options.get(
"show_skipped_hosts", C.DISPLAY_SKIPPED_HOSTS
): # fallback on constants for inherited plugins missing docs
self._clean_results(result._result, result._task.action)
if self._play.strategy == 'free' and self._last_task_banner != result._task._uuid:
if self._play.strategy == "free" and self._last_task_banner != result._task._uuid:
self._print_task_banner(result._task)
if result._task.loop and 'results' in result._result:
if result._task.loop and "results" in result._result:
self._process_items(result)
else:
msg = f"skipping: {self._host_counter}/{self._host_total} [{result._host.get_name()}]"
@ -233,18 +227,18 @@ class CallbackModule(CallbackBase):
def v2_runner_on_unreachable(self, result):
self._host_counter += 1
if self._play.strategy == 'free' and self._last_task_banner != result._task._uuid:
if self._play.strategy == "free" and self._last_task_banner != result._task._uuid:
self._print_task_banner(result._task)
delegated_vars = result._result.get('_ansible_delegated_vars', None)
delegated_vars = result._result.get("_ansible_delegated_vars", None)
if delegated_vars:
self._display.display(
f"fatal: {self._host_counter}/{self._host_total} [{result._host.get_name()} -> "
f"{delegated_vars['ansible_host']}]: UNREACHABLE! => {self._dump_results(result._result)}",
color=C.COLOR_UNREACHABLE
color=C.COLOR_UNREACHABLE,
)
else:
self._display.display(
f"fatal: {self._host_counter}/{self._host_total} [{result._host.get_name()}]: UNREACHABLE! => {self._dump_results(result._result)}",
color=C.COLOR_UNREACHABLE
color=C.COLOR_UNREACHABLE,
)

View file

@ -1,4 +1,3 @@
# Copyright (c) 2024, Felix Fontein <felix@fontein.de>
# 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
@ -36,8 +35,8 @@ from ansible.plugins.callback.default import CallbackModule as Default
class CallbackModule(Default):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'community.general.default_without_diff'
CALLBACK_TYPE = "stdout"
CALLBACK_NAME = "community.general.default_without_diff"
def v2_on_file_diff(self, result):
pass

View file

@ -22,6 +22,7 @@ requirements:
HAS_OD = False
try:
from collections import OrderedDict
HAS_OD = True
except ImportError:
pass
@ -69,66 +70,66 @@ display = Display()
# FIXME: Importing constants as C simply does not work, beats me :-/
# from ansible import constants as C
class C:
COLOR_HIGHLIGHT = 'white'
COLOR_VERBOSE = 'blue'
COLOR_WARN = 'bright purple'
COLOR_ERROR = 'red'
COLOR_DEBUG = 'dark gray'
COLOR_DEPRECATE = 'purple'
COLOR_SKIP = 'cyan'
COLOR_UNREACHABLE = 'bright red'
COLOR_OK = 'green'
COLOR_CHANGED = 'yellow'
COLOR_HIGHLIGHT = "white"
COLOR_VERBOSE = "blue"
COLOR_WARN = "bright purple"
COLOR_ERROR = "red"
COLOR_DEBUG = "dark gray"
COLOR_DEPRECATE = "purple"
COLOR_SKIP = "cyan"
COLOR_UNREACHABLE = "bright red"
COLOR_OK = "green"
COLOR_CHANGED = "yellow"
# Taken from Dstat
class vt100:
black = '\033[0;30m'
darkred = '\033[0;31m'
darkgreen = '\033[0;32m'
darkyellow = '\033[0;33m'
darkblue = '\033[0;34m'
darkmagenta = '\033[0;35m'
darkcyan = '\033[0;36m'
gray = '\033[0;37m'
black = "\033[0;30m"
darkred = "\033[0;31m"
darkgreen = "\033[0;32m"
darkyellow = "\033[0;33m"
darkblue = "\033[0;34m"
darkmagenta = "\033[0;35m"
darkcyan = "\033[0;36m"
gray = "\033[0;37m"
darkgray = '\033[1;30m'
red = '\033[1;31m'
green = '\033[1;32m'
yellow = '\033[1;33m'
blue = '\033[1;34m'
magenta = '\033[1;35m'
cyan = '\033[1;36m'
white = '\033[1;37m'
darkgray = "\033[1;30m"
red = "\033[1;31m"
green = "\033[1;32m"
yellow = "\033[1;33m"
blue = "\033[1;34m"
magenta = "\033[1;35m"
cyan = "\033[1;36m"
white = "\033[1;37m"
blackbg = '\033[40m'
redbg = '\033[41m'
greenbg = '\033[42m'
yellowbg = '\033[43m'
bluebg = '\033[44m'
magentabg = '\033[45m'
cyanbg = '\033[46m'
whitebg = '\033[47m'
blackbg = "\033[40m"
redbg = "\033[41m"
greenbg = "\033[42m"
yellowbg = "\033[43m"
bluebg = "\033[44m"
magentabg = "\033[45m"
cyanbg = "\033[46m"
whitebg = "\033[47m"
reset = '\033[0;0m'
bold = '\033[1m'
reverse = '\033[2m'
underline = '\033[4m'
reset = "\033[0;0m"
bold = "\033[1m"
reverse = "\033[2m"
underline = "\033[4m"
clear = '\033[2J'
# clearline = '\033[K'
clearline = '\033[2K'
save = '\033[s'
restore = '\033[u'
save_all = '\0337'
restore_all = '\0338'
linewrap = '\033[7h'
nolinewrap = '\033[7l'
clear = "\033[2J"
# clearline = '\033[K'
clearline = "\033[2K"
save = "\033[s"
restore = "\033[u"
save_all = "\0337"
restore_all = "\0338"
linewrap = "\033[7h"
nolinewrap = "\033[7l"
up = '\033[1A'
down = '\033[1B'
right = '\033[1C'
left = '\033[1D'
up = "\033[1A"
down = "\033[1B"
right = "\033[1C"
left = "\033[1D"
colors = dict(
@ -140,26 +141,23 @@ colors = dict(
unreachable=vt100.red,
)
states = ('skipped', 'ok', 'changed', 'failed', 'unreachable')
states = ("skipped", "ok", "changed", "failed", "unreachable")
class CallbackModule(CallbackModule_default):
'''
"""
This is the dense callback interface, where screen estate is still valued.
'''
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'dense'
CALLBACK_TYPE = "stdout"
CALLBACK_NAME = "dense"
def __init__(self):
# From CallbackModule
self._display = display
if HAS_OD:
self.disabled = False
self.super_ref = super()
self.super_ref.__init__()
@ -167,14 +165,14 @@ class CallbackModule(CallbackModule_default):
# Attributes to remove from results for more density
self.removed_attributes = (
# 'changed',
'delta',
"delta",
# 'diff',
'end',
'failed',
'failed_when_result',
'invocation',
'start',
'stdout_lines',
"end",
"failed",
"failed_when_result",
"invocation",
"start",
"stdout_lines",
)
# Initiate data structures
@ -182,13 +180,15 @@ class CallbackModule(CallbackModule_default):
self.keep = False
self.shown_title = False
self.count = dict(play=0, handler=0, task=0)
self.type = 'foo'
self.type = "foo"
# Start immediately on the first line
sys.stdout.write(vt100.reset + vt100.save + vt100.clearline)
sys.stdout.flush()
else:
display.warning("The 'dense' callback plugin requires OrderedDict which is not available in this version of python, disabling.")
display.warning(
"The 'dense' callback plugin requires OrderedDict which is not available in this version of python, disabling."
)
self.disabled = True
def __del__(self):
@ -198,27 +198,27 @@ class CallbackModule(CallbackModule_default):
name = result._host.get_name()
# Add a new status in case a failed task is ignored
if status == 'failed' and result._task.ignore_errors:
status = 'ignored'
if status == "failed" and result._task.ignore_errors:
status = "ignored"
# Check if we have to update an existing state (when looping over items)
if name not in self.hosts:
self.hosts[name] = dict(state=status)
elif states.index(self.hosts[name]['state']) < states.index(status):
self.hosts[name]['state'] = status
elif states.index(self.hosts[name]["state"]) < states.index(status):
self.hosts[name]["state"] = status
# Store delegated hostname, if needed
delegated_vars = result._result.get('_ansible_delegated_vars', None)
delegated_vars = result._result.get("_ansible_delegated_vars", None)
if delegated_vars:
self.hosts[name]['delegate'] = delegated_vars['ansible_host']
self.hosts[name]["delegate"] = delegated_vars["ansible_host"]
# Print progress bar
self._display_progress(result)
# # Ensure that tasks with changes/failures stay on-screen, and during diff-mode
# if status in ['changed', 'failed', 'unreachable'] or (result.get('_diff_mode', False) and result._resultget('diff', False)):
# # Ensure that tasks with changes/failures stay on-screen, and during diff-mode
# if status in ['changed', 'failed', 'unreachable'] or (result.get('_diff_mode', False) and result._resultget('diff', False)):
# Ensure that tasks with changes/failures stay on-screen
if status in ['changed', 'failed', 'unreachable']:
if status in ["changed", "failed", "unreachable"]:
self.keep = True
if self._display.verbosity == 1:
@ -239,9 +239,9 @@ class CallbackModule(CallbackModule_default):
del result[attr]
def _handle_exceptions(self, result):
if 'exception' in result:
if "exception" in result:
# Remove the exception from the result so it is not shown every time
del result['exception']
del result["exception"]
if self._display.verbosity == 1:
return "An exception occurred during task execution. To see the full traceback, use -vvv."
@ -249,16 +249,16 @@ class CallbackModule(CallbackModule_default):
def _display_progress(self, result=None):
# Always rewrite the complete line
sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.nolinewrap + vt100.underline)
sys.stdout.write(f'{self.type} {self.count[self.type]}:')
sys.stdout.write(f"{self.type} {self.count[self.type]}:")
sys.stdout.write(vt100.reset)
sys.stdout.flush()
# Print out each host in its own status-color
for name in self.hosts:
sys.stdout.write(' ')
if self.hosts[name].get('delegate', None):
sys.stdout.write(" ")
if self.hosts[name].get("delegate", None):
sys.stdout.write(f"{self.hosts[name]['delegate']}>")
sys.stdout.write(colors[self.hosts[name]['state']] + name + vt100.reset)
sys.stdout.write(colors[self.hosts[name]["state"]] + name + vt100.reset)
sys.stdout.flush()
sys.stdout.write(vt100.linewrap)
@ -267,7 +267,7 @@ class CallbackModule(CallbackModule_default):
if not self.shown_title:
self.shown_title = True
sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.underline)
sys.stdout.write(f'{self.type} {self.count[self.type]}: {self.task.get_name().strip()}')
sys.stdout.write(f"{self.type} {self.count[self.type]}: {self.task.get_name().strip()}")
sys.stdout.write(f"{vt100.restore}{vt100.reset}\n{vt100.save}{vt100.clearline}")
sys.stdout.flush()
else:
@ -284,29 +284,31 @@ class CallbackModule(CallbackModule_default):
self._clean_results(result._result)
dump = ''
if result._task.action == 'include':
dump = ""
if result._task.action == "include":
return
elif status == 'ok':
elif status == "ok":
return
elif status == 'ignored':
elif status == "ignored":
dump = self._handle_exceptions(result._result)
elif status == 'failed':
elif status == "failed":
dump = self._handle_exceptions(result._result)
elif status == 'unreachable':
dump = result._result['msg']
elif status == "unreachable":
dump = result._result["msg"]
if not dump:
dump = self._dump_results(result._result)
if result._task.loop and 'results' in result._result:
if result._task.loop and "results" in result._result:
self._process_items(result)
else:
sys.stdout.write(f"{colors[status] + status}: ")
delegated_vars = result._result.get('_ansible_delegated_vars', None)
delegated_vars = result._result.get("_ansible_delegated_vars", None)
if delegated_vars:
sys.stdout.write(f"{vt100.reset}{result._host.get_name()}>{colors[status]}{delegated_vars['ansible_host']}")
sys.stdout.write(
f"{vt100.reset}{result._host.get_name()}>{colors[status]}{delegated_vars['ansible_host']}"
)
else:
sys.stdout.write(result._host.get_name())
@ -314,7 +316,7 @@ class CallbackModule(CallbackModule_default):
sys.stdout.write(f"{vt100.reset}{vt100.save}{vt100.clearline}")
sys.stdout.flush()
if status == 'changed':
if status == "changed":
self._handle_warnings(result._result)
def v2_playbook_on_play_start(self, play):
@ -327,13 +329,13 @@ class CallbackModule(CallbackModule_default):
# Reset at the start of each play
self.keep = False
self.count.update(dict(handler=0, task=0))
self.count['play'] += 1
self.count["play"] += 1
self.play = play
# Write the next play on screen IN UPPERCASE, and make it permanent
name = play.get_name().strip()
if not name:
name = 'unnamed'
name = "unnamed"
sys.stdout.write(f"PLAY {self.count['play']}: {name.upper()}")
sys.stdout.write(f"{vt100.restore}{vt100.reset}\n{vt100.save}{vt100.clearline}")
sys.stdout.flush()
@ -351,14 +353,14 @@ class CallbackModule(CallbackModule_default):
self.shown_title = False
self.hosts = OrderedDict()
self.task = task
self.type = 'task'
self.type = "task"
# Enumerate task if not setup (task names are too long for dense output)
if task.get_name() != 'setup':
self.count['task'] += 1
if task.get_name() != "setup":
self.count["task"] += 1
# Write the next task on screen (behind the prompt is the previous output)
sys.stdout.write(f'{self.type} {self.count[self.type]}.')
sys.stdout.write(f"{self.type} {self.count[self.type]}.")
sys.stdout.write(vt100.reset)
sys.stdout.flush()
@ -374,36 +376,36 @@ class CallbackModule(CallbackModule_default):
self.shown_title = False
self.hosts = OrderedDict()
self.task = task
self.type = 'handler'
self.type = "handler"
# Enumerate handler if not setup (handler names may be too long for dense output)
if task.get_name() != 'setup':
if task.get_name() != "setup":
self.count[self.type] += 1
# Write the next task on screen (behind the prompt is the previous output)
sys.stdout.write(f'{self.type} {self.count[self.type]}.')
sys.stdout.write(f"{self.type} {self.count[self.type]}.")
sys.stdout.write(vt100.reset)
sys.stdout.flush()
def v2_playbook_on_cleanup_task_start(self, task):
# TBD
sys.stdout.write('cleanup.')
sys.stdout.write("cleanup.")
sys.stdout.flush()
def v2_runner_on_failed(self, result, ignore_errors=False):
self._add_host(result, 'failed')
self._add_host(result, "failed")
def v2_runner_on_ok(self, result):
if result._result.get('changed', False):
self._add_host(result, 'changed')
if result._result.get("changed", False):
self._add_host(result, "changed")
else:
self._add_host(result, 'ok')
self._add_host(result, "ok")
def v2_runner_on_skipped(self, result):
self._add_host(result, 'skipped')
self._add_host(result, "skipped")
def v2_runner_on_unreachable(self, result):
self._add_host(result, 'unreachable')
self._add_host(result, "unreachable")
def v2_runner_on_include(self, included_file):
pass
@ -423,24 +425,24 @@ class CallbackModule(CallbackModule_default):
self.v2_runner_item_on_ok(result)
def v2_runner_item_on_ok(self, result):
if result._result.get('changed', False):
self._add_host(result, 'changed')
if result._result.get("changed", False):
self._add_host(result, "changed")
else:
self._add_host(result, 'ok')
self._add_host(result, "ok")
# Old definition in v2.0
def v2_playbook_item_on_failed(self, result):
self.v2_runner_item_on_failed(result)
def v2_runner_item_on_failed(self, result):
self._add_host(result, 'failed')
self._add_host(result, "failed")
# Old definition in v2.0
def v2_playbook_item_on_skipped(self, result):
self.v2_runner_item_on_skipped(result)
def v2_runner_item_on_skipped(self, result):
self._add_host(result, 'skipped')
self._add_host(result, "skipped")
def v2_playbook_on_no_hosts_remaining(self):
if self._display.verbosity == 0 and self.keep:
@ -467,7 +469,7 @@ class CallbackModule(CallbackModule_default):
return
sys.stdout.write(vt100.bold + vt100.underline)
sys.stdout.write('SUMMARY')
sys.stdout.write("SUMMARY")
sys.stdout.write(f"{vt100.restore}{vt100.reset}\n{vt100.save}{vt100.clearline}")
sys.stdout.flush()
@ -479,7 +481,7 @@ class CallbackModule(CallbackModule_default):
f"{hostcolor(h, t)} : {colorize('ok', t['ok'], C.COLOR_OK)} {colorize('changed', t['changed'], C.COLOR_CHANGED)} "
f"{colorize('unreachable', t['unreachable'], C.COLOR_UNREACHABLE)} {colorize('failed', t['failures'], C.COLOR_ERROR)} "
f"{colorize('rescued', t['rescued'], C.COLOR_OK)} {colorize('ignored', t['ignored'], C.COLOR_WARN)}",
screen_only=True
screen_only=True,
)

View file

@ -1,4 +1,3 @@
# Copyright (c) 2019, Trevor Highfill <trevor.highfill@outlook.com>
# 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
@ -786,6 +785,7 @@ from ansible.module_utils.common.text.converters import to_text
try:
from ansible.template import trust_as_template # noqa: F401, pylint: disable=unused-import
SUPPORTS_DATA_TAGGING = True
except ImportError:
SUPPORTS_DATA_TAGGING = False
@ -806,11 +806,12 @@ class CallbackModule(Default):
"""
Callback plugin that allows you to supply your own custom callback templates to be output.
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'community.general.diy'
DIY_NS = 'ansible_callback_diy'
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = "stdout"
CALLBACK_NAME = "community.general.diy"
DIY_NS = "ansible_callback_diy"
@contextmanager
def _suppress_stdout(self, enabled):
@ -823,50 +824,48 @@ class CallbackModule(Default):
def _get_output_specification(self, loader, variables):
_ret = {}
_calling_method = sys._getframe(1).f_code.co_name
_callback_type = (_calling_method[3:] if _calling_method[:3] == "v2_" else _calling_method)
_callback_options = ['msg', 'msg_color']
_callback_type = _calling_method[3:] if _calling_method[:3] == "v2_" else _calling_method
_callback_options = ["msg", "msg_color"]
for option in _callback_options:
_option_name = f'{_callback_type}_{option}'
_option_template = variables.get(
f"{self.DIY_NS}_{_option_name}",
self.get_option(_option_name)
)
_ret.update({option: self._template(
loader=loader,
template=_option_template,
variables=variables
)})
_option_name = f"{_callback_type}_{option}"
_option_template = variables.get(f"{self.DIY_NS}_{_option_name}", self.get_option(_option_name))
_ret.update({option: self._template(loader=loader, template=_option_template, variables=variables)})
_ret.update({'vars': variables})
_ret.update({"vars": variables})
return _ret
def _using_diy(self, spec):
sentinel = object()
omit = spec['vars'].get('omit', sentinel)
omit = spec["vars"].get("omit", sentinel)
# With Data Tagging, omit is sentinel
return (spec['msg'] is not None) and (spec['msg'] != omit or omit is sentinel)
return (spec["msg"] is not None) and (spec["msg"] != omit or omit is sentinel)
def _parent_has_callback(self):
return hasattr(super(), sys._getframe(1).f_code.co_name)
def _template(self, loader, template, variables):
_templar = Templar(loader=loader, variables=variables)
return _templar.template(
template,
preserve_trailing_newlines=True,
convert_data=False,
escape_backslashes=True
)
return _templar.template(template, preserve_trailing_newlines=True, convert_data=False, escape_backslashes=True)
def _output(self, spec, stderr=False):
_msg = to_text(spec['msg'])
_msg = to_text(spec["msg"])
if len(_msg) > 0:
self._display.display(msg=_msg, color=spec['msg_color'], stderr=stderr)
self._display.display(msg=_msg, color=spec["msg_color"], stderr=stderr)
def _get_vars(self, playbook, play=None, host=None, task=None, included_file=None,
handler=None, result=None, stats=None, remove_attr_ref_loop=True):
def _get_vars(
self,
playbook,
play=None,
host=None,
task=None,
included_file=None,
handler=None,
result=None,
stats=None,
remove_attr_ref_loop=True,
):
def _get_value(obj, attr=None, method=None):
if attr:
return getattr(obj, attr, getattr(obj, f"_{attr}", None))
@ -876,8 +875,8 @@ class CallbackModule(Default):
return _method()
def _remove_attr_ref_loop(obj, attributes):
_loop_var = getattr(obj, 'loop_control', None)
_loop_var = (_loop_var or 'item')
_loop_var = getattr(obj, "loop_control", None)
_loop_var = _loop_var or "item"
for attr in attributes:
if str(_loop_var) in str(_get_value(obj=obj, attr=attr)):
@ -896,56 +895,128 @@ class CallbackModule(Default):
_all = _variable_manager.get_vars()
if play:
_all = play.get_variable_manager().get_vars(
play=play,
host=(host if host else getattr(result, '_host', None)),
task=(handler if handler else task)
play=play, host=(host if host else getattr(result, "_host", None)), task=(handler if handler else task)
)
_ret.update(_all)
_ret.update(_ret.get(self.DIY_NS, {self.DIY_NS: {} if SUPPORTS_DATA_TAGGING else CallbackDIYDict()}))
_ret[self.DIY_NS].update({'playbook': {}})
_playbook_attributes = ['entries', 'file_name', 'basedir']
_ret[self.DIY_NS].update({"playbook": {}})
_playbook_attributes = ["entries", "file_name", "basedir"]
for attr in _playbook_attributes:
_ret[self.DIY_NS]['playbook'].update({attr: _get_value(obj=playbook, attr=attr)})
_ret[self.DIY_NS]["playbook"].update({attr: _get_value(obj=playbook, attr=attr)})
if play:
_ret[self.DIY_NS].update({'play': {}})
_play_attributes = ['any_errors_fatal', 'become', 'become_flags', 'become_method',
'become_user', 'check_mode', 'collections', 'connection',
'debugger', 'diff', 'environment', 'fact_path', 'finalized',
'force_handlers', 'gather_facts', 'gather_subset',
'gather_timeout', 'handlers', 'hosts', 'ignore_errors',
'ignore_unreachable', 'included_conditional', 'included_path',
'max_fail_percentage', 'module_defaults', 'name', 'no_log',
'only_tags', 'order', 'port', 'post_tasks', 'pre_tasks',
'remote_user', 'removed_hosts', 'roles', 'run_once', 'serial',
'skip_tags', 'squashed', 'strategy', 'tags', 'tasks', 'uuid',
'validated', 'vars_files', 'vars_prompt']
_ret[self.DIY_NS].update({"play": {}})
_play_attributes = [
"any_errors_fatal",
"become",
"become_flags",
"become_method",
"become_user",
"check_mode",
"collections",
"connection",
"debugger",
"diff",
"environment",
"fact_path",
"finalized",
"force_handlers",
"gather_facts",
"gather_subset",
"gather_timeout",
"handlers",
"hosts",
"ignore_errors",
"ignore_unreachable",
"included_conditional",
"included_path",
"max_fail_percentage",
"module_defaults",
"name",
"no_log",
"only_tags",
"order",
"port",
"post_tasks",
"pre_tasks",
"remote_user",
"removed_hosts",
"roles",
"run_once",
"serial",
"skip_tags",
"squashed",
"strategy",
"tags",
"tasks",
"uuid",
"validated",
"vars_files",
"vars_prompt",
]
for attr in _play_attributes:
_ret[self.DIY_NS]['play'].update({attr: _get_value(obj=play, attr=attr)})
_ret[self.DIY_NS]["play"].update({attr: _get_value(obj=play, attr=attr)})
if host:
_ret[self.DIY_NS].update({'host': {}})
_host_attributes = ['name', 'uuid', 'address', 'implicit']
_ret[self.DIY_NS].update({"host": {}})
_host_attributes = ["name", "uuid", "address", "implicit"]
for attr in _host_attributes:
_ret[self.DIY_NS]['host'].update({attr: _get_value(obj=host, attr=attr)})
_ret[self.DIY_NS]["host"].update({attr: _get_value(obj=host, attr=attr)})
if task:
_ret[self.DIY_NS].update({'task': {}})
_task_attributes = ['action', 'any_errors_fatal', 'args', 'async', 'async_val',
'become', 'become_flags', 'become_method', 'become_user',
'changed_when', 'check_mode', 'collections', 'connection',
'debugger', 'delay', 'delegate_facts', 'delegate_to', 'diff',
'environment', 'failed_when', 'finalized', 'ignore_errors',
'ignore_unreachable', 'loop', 'loop_control', 'loop_with',
'module_defaults', 'name', 'no_log', 'notify', 'parent', 'poll',
'port', 'register', 'remote_user', 'retries', 'role', 'run_once',
'squashed', 'tags', 'untagged', 'until', 'uuid', 'validated',
'when']
_ret[self.DIY_NS].update({"task": {}})
_task_attributes = [
"action",
"any_errors_fatal",
"args",
"async",
"async_val",
"become",
"become_flags",
"become_method",
"become_user",
"changed_when",
"check_mode",
"collections",
"connection",
"debugger",
"delay",
"delegate_facts",
"delegate_to",
"diff",
"environment",
"failed_when",
"finalized",
"ignore_errors",
"ignore_unreachable",
"loop",
"loop_control",
"loop_with",
"module_defaults",
"name",
"no_log",
"notify",
"parent",
"poll",
"port",
"register",
"remote_user",
"retries",
"role",
"run_once",
"squashed",
"tags",
"untagged",
"until",
"uuid",
"validated",
"when",
]
# remove arguments that reference a loop var because they cause templating issues in
# callbacks that do not have the loop context(e.g. playbook_on_task_start)
@ -953,74 +1024,114 @@ class CallbackModule(Default):
_task_attributes = _remove_attr_ref_loop(obj=task, attributes=_task_attributes)
for attr in _task_attributes:
_ret[self.DIY_NS]['task'].update({attr: _get_value(obj=task, attr=attr)})
_ret[self.DIY_NS]["task"].update({attr: _get_value(obj=task, attr=attr)})
if included_file:
_ret[self.DIY_NS].update({'included_file': {}})
_included_file_attributes = ['args', 'filename', 'hosts', 'is_role', 'task']
_ret[self.DIY_NS].update({"included_file": {}})
_included_file_attributes = ["args", "filename", "hosts", "is_role", "task"]
for attr in _included_file_attributes:
_ret[self.DIY_NS]['included_file'].update({attr: _get_value(
obj=included_file,
attr=attr
)})
_ret[self.DIY_NS]["included_file"].update({attr: _get_value(obj=included_file, attr=attr)})
if handler:
_ret[self.DIY_NS].update({'handler': {}})
_handler_attributes = ['action', 'any_errors_fatal', 'args', 'async', 'async_val',
'become', 'become_flags', 'become_method', 'become_user',
'changed_when', 'check_mode', 'collections', 'connection',
'debugger', 'delay', 'delegate_facts', 'delegate_to', 'diff',
'environment', 'failed_when', 'finalized', 'ignore_errors',
'ignore_unreachable', 'listen', 'loop', 'loop_control',
'loop_with', 'module_defaults', 'name', 'no_log',
'notified_hosts', 'notify', 'parent', 'poll', 'port',
'register', 'remote_user', 'retries', 'role', 'run_once',
'squashed', 'tags', 'untagged', 'until', 'uuid', 'validated',
'when']
_ret[self.DIY_NS].update({"handler": {}})
_handler_attributes = [
"action",
"any_errors_fatal",
"args",
"async",
"async_val",
"become",
"become_flags",
"become_method",
"become_user",
"changed_when",
"check_mode",
"collections",
"connection",
"debugger",
"delay",
"delegate_facts",
"delegate_to",
"diff",
"environment",
"failed_when",
"finalized",
"ignore_errors",
"ignore_unreachable",
"listen",
"loop",
"loop_control",
"loop_with",
"module_defaults",
"name",
"no_log",
"notified_hosts",
"notify",
"parent",
"poll",
"port",
"register",
"remote_user",
"retries",
"role",
"run_once",
"squashed",
"tags",
"untagged",
"until",
"uuid",
"validated",
"when",
]
if handler.loop and remove_attr_ref_loop:
_handler_attributes = _remove_attr_ref_loop(obj=handler,
attributes=_handler_attributes)
_handler_attributes = _remove_attr_ref_loop(obj=handler, attributes=_handler_attributes)
for attr in _handler_attributes:
_ret[self.DIY_NS]['handler'].update({attr: _get_value(obj=handler, attr=attr)})
_ret[self.DIY_NS]["handler"].update({attr: _get_value(obj=handler, attr=attr)})
_ret[self.DIY_NS]['handler'].update({'is_host_notified': handler.is_host_notified(host)})
_ret[self.DIY_NS]["handler"].update({"is_host_notified": handler.is_host_notified(host)})
if result:
_ret[self.DIY_NS].update({'result': {}})
_result_attributes = ['host', 'task', 'task_name']
_ret[self.DIY_NS].update({"result": {}})
_result_attributes = ["host", "task", "task_name"]
for attr in _result_attributes:
_ret[self.DIY_NS]['result'].update({attr: _get_value(obj=result, attr=attr)})
_ret[self.DIY_NS]["result"].update({attr: _get_value(obj=result, attr=attr)})
_result_methods = ['is_changed', 'is_failed', 'is_skipped', 'is_unreachable']
_result_methods = ["is_changed", "is_failed", "is_skipped", "is_unreachable"]
for method in _result_methods:
_ret[self.DIY_NS]['result'].update({method: _get_value(obj=result, method=method)})
_ret[self.DIY_NS]["result"].update({method: _get_value(obj=result, method=method)})
_ret[self.DIY_NS]['result'].update({'output': getattr(result, '_result', None)})
_ret[self.DIY_NS]["result"].update({"output": getattr(result, "_result", None)})
_ret.update(result._result)
if stats:
_ret[self.DIY_NS].update({'stats': {}})
_stats_attributes = ['changed', 'custom', 'dark', 'failures', 'ignored',
'ok', 'processed', 'rescued', 'skipped']
_ret[self.DIY_NS].update({"stats": {}})
_stats_attributes = [
"changed",
"custom",
"dark",
"failures",
"ignored",
"ok",
"processed",
"rescued",
"skipped",
]
for attr in _stats_attributes:
_ret[self.DIY_NS]['stats'].update({attr: _get_value(obj=stats, attr=attr)})
_ret[self.DIY_NS]["stats"].update({attr: _get_value(obj=stats, attr=attr)})
_ret[self.DIY_NS].update({'top_level_var_names': list(_ret.keys())})
_ret[self.DIY_NS].update({"top_level_var_names": list(_ret.keys())})
return _ret
def v2_on_any(self, *args, **kwargs):
self._diy_spec = self._get_output_specification(
loader=self._diy_loader,
variables=self._diy_spec['vars']
)
self._diy_spec = self._get_output_specification(loader=self._diy_loader, variables=self._diy_spec["vars"])
if self._using_diy(spec=self._diy_spec):
self._output(spec=self._diy_spec)
@ -1033,11 +1144,8 @@ class CallbackModule(Default):
self._diy_spec = self._get_output_specification(
loader=self._diy_loader,
variables=self._get_vars(
playbook=self._diy_playbook,
play=self._diy_play,
task=self._diy_task,
result=result
)
playbook=self._diy_playbook, play=self._diy_play, task=self._diy_task, result=result
),
)
if self._using_diy(spec=self._diy_spec):
@ -1051,11 +1159,8 @@ class CallbackModule(Default):
self._diy_spec = self._get_output_specification(
loader=self._diy_loader,
variables=self._get_vars(
playbook=self._diy_playbook,
play=self._diy_play,
task=self._diy_task,
result=result
)
playbook=self._diy_playbook, play=self._diy_play, task=self._diy_task, result=result
),
)
if self._using_diy(spec=self._diy_spec):
@ -1069,11 +1174,8 @@ class CallbackModule(Default):
self._diy_spec = self._get_output_specification(
loader=self._diy_loader,
variables=self._get_vars(
playbook=self._diy_playbook,
play=self._diy_play,
task=self._diy_task,
result=result
)
playbook=self._diy_playbook, play=self._diy_play, task=self._diy_task, result=result
),
)
if self._using_diy(spec=self._diy_spec):
@ -1087,11 +1189,8 @@ class CallbackModule(Default):
self._diy_spec = self._get_output_specification(
loader=self._diy_loader,
variables=self._get_vars(
playbook=self._diy_playbook,
play=self._diy_play,
task=self._diy_task,
result=result
)
playbook=self._diy_playbook, play=self._diy_play, task=self._diy_task, result=result
),
)
if self._using_diy(spec=self._diy_spec):
@ -1121,8 +1220,8 @@ class CallbackModule(Default):
play=self._diy_play,
task=self._diy_task,
result=result,
remove_attr_ref_loop=False
)
remove_attr_ref_loop=False,
),
)
if self._using_diy(spec=self._diy_spec):
@ -1140,8 +1239,8 @@ class CallbackModule(Default):
play=self._diy_play,
task=self._diy_task,
result=result,
remove_attr_ref_loop=False
)
remove_attr_ref_loop=False,
),
)
if self._using_diy(spec=self._diy_spec):
@ -1159,8 +1258,8 @@ class CallbackModule(Default):
play=self._diy_play,
task=self._diy_task,
result=result,
remove_attr_ref_loop=False
)
remove_attr_ref_loop=False,
),
)
if self._using_diy(spec=self._diy_spec):
@ -1174,11 +1273,8 @@ class CallbackModule(Default):
self._diy_spec = self._get_output_specification(
loader=self._diy_loader,
variables=self._get_vars(
playbook=self._diy_playbook,
play=self._diy_play,
task=self._diy_task,
result=result
)
playbook=self._diy_playbook, play=self._diy_play, task=self._diy_task, result=result
),
)
if self._using_diy(spec=self._diy_spec):
@ -1195,11 +1291,8 @@ class CallbackModule(Default):
self._diy_spec = self._get_output_specification(
loader=self._diy_loader,
variables=self._get_vars(
playbook=self._diy_playbook,
play=self._diy_play,
host=self._diy_host,
task=self._diy_task
)
playbook=self._diy_playbook, play=self._diy_play, host=self._diy_host, task=self._diy_task
),
)
if self._using_diy(spec=self._diy_spec):
@ -1214,10 +1307,7 @@ class CallbackModule(Default):
self._diy_loader = self._diy_playbook.get_loader()
self._diy_spec = self._get_output_specification(
loader=self._diy_loader,
variables=self._get_vars(
playbook=self._diy_playbook
)
loader=self._diy_loader, variables=self._get_vars(playbook=self._diy_playbook)
)
if self._using_diy(spec=self._diy_spec):
@ -1234,11 +1324,8 @@ class CallbackModule(Default):
self._diy_spec = self._get_output_specification(
loader=self._diy_loader,
variables=self._get_vars(
playbook=self._diy_playbook,
play=self._diy_play,
host=self._diy_host,
handler=self._diy_handler
)
playbook=self._diy_playbook, play=self._diy_play, host=self._diy_host, handler=self._diy_handler
),
)
if self._using_diy(spec=self._diy_spec):
@ -1249,10 +1336,7 @@ class CallbackModule(Default):
super().v2_playbook_on_notify(handler, host)
def v2_playbook_on_no_hosts_matched(self):
self._diy_spec = self._get_output_specification(
loader=self._diy_loader,
variables=self._diy_spec['vars']
)
self._diy_spec = self._get_output_specification(loader=self._diy_loader, variables=self._diy_spec["vars"])
if self._using_diy(spec=self._diy_spec):
self._output(spec=self._diy_spec)
@ -1262,10 +1346,7 @@ class CallbackModule(Default):
super().v2_playbook_on_no_hosts_matched()
def v2_playbook_on_no_hosts_remaining(self):
self._diy_spec = self._get_output_specification(
loader=self._diy_loader,
variables=self._diy_spec['vars']
)
self._diy_spec = self._get_output_specification(loader=self._diy_loader, variables=self._diy_spec["vars"])
if self._using_diy(spec=self._diy_spec):
self._output(spec=self._diy_spec)
@ -1279,11 +1360,7 @@ class CallbackModule(Default):
self._diy_spec = self._get_output_specification(
loader=self._diy_loader,
variables=self._get_vars(
playbook=self._diy_playbook,
play=self._diy_play,
task=self._diy_task
)
variables=self._get_vars(playbook=self._diy_playbook, play=self._diy_play, task=self._diy_task),
)
if self._using_diy(spec=self._diy_spec):
@ -1302,11 +1379,7 @@ class CallbackModule(Default):
self._diy_spec = self._get_output_specification(
loader=self._diy_loader,
variables=self._get_vars(
playbook=self._diy_playbook,
play=self._diy_play,
task=self._diy_task
)
variables=self._get_vars(playbook=self._diy_playbook, play=self._diy_play, task=self._diy_task),
)
if self._using_diy(spec=self._diy_spec):
@ -1316,13 +1389,19 @@ class CallbackModule(Default):
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
super().v2_playbook_on_handler_task_start(task)
def v2_playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None,
confirm=False, salt_size=None, salt=None, default=None,
unsafe=None):
self._diy_spec = self._get_output_specification(
loader=self._diy_loader,
variables=self._diy_spec['vars']
)
def v2_playbook_on_vars_prompt(
self,
varname,
private=True,
prompt=None,
encrypt=None,
confirm=False,
salt_size=None,
salt=None,
default=None,
unsafe=None,
):
self._diy_spec = self._get_output_specification(loader=self._diy_loader, variables=self._diy_spec["vars"])
if self._using_diy(spec=self._diy_spec):
self._output(spec=self._diy_spec)
@ -1330,9 +1409,7 @@ class CallbackModule(Default):
if self._parent_has_callback():
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
super().v2_playbook_on_vars_prompt(
varname, private, prompt, encrypt,
confirm, salt_size, salt, default,
unsafe
varname, private, prompt, encrypt, confirm, salt_size, salt, default, unsafe
)
# not implemented as the call to this is not implemented yet
@ -1347,11 +1424,7 @@ class CallbackModule(Default):
self._diy_play = play
self._diy_spec = self._get_output_specification(
loader=self._diy_loader,
variables=self._get_vars(
playbook=self._diy_playbook,
play=self._diy_play
)
loader=self._diy_loader, variables=self._get_vars(playbook=self._diy_playbook, play=self._diy_play)
)
if self._using_diy(spec=self._diy_spec):
@ -1366,11 +1439,7 @@ class CallbackModule(Default):
self._diy_spec = self._get_output_specification(
loader=self._diy_loader,
variables=self._get_vars(
playbook=self._diy_playbook,
play=self._diy_play,
stats=self._diy_stats
)
variables=self._get_vars(playbook=self._diy_playbook, play=self._diy_play, stats=self._diy_stats),
)
if self._using_diy(spec=self._diy_spec):
@ -1389,8 +1458,8 @@ class CallbackModule(Default):
playbook=self._diy_playbook,
play=self._diy_play,
task=self._diy_included_file._task,
included_file=self._diy_included_file
)
included_file=self._diy_included_file,
),
)
if self._using_diy(spec=self._diy_spec):
@ -1404,11 +1473,8 @@ class CallbackModule(Default):
self._diy_spec = self._get_output_specification(
loader=self._diy_loader,
variables=self._get_vars(
playbook=self._diy_playbook,
play=self._diy_play,
task=self._diy_task,
result=result
)
playbook=self._diy_playbook, play=self._diy_play, task=self._diy_task, result=result
),
)
if self._using_diy(spec=self._diy_spec):

View file

@ -116,9 +116,9 @@ class TaskData:
def add_host(self, host):
if host.uuid in self.host_data:
if host.status == 'included':
if host.status == "included":
# concatenate task include output from multiple items
host.result = f'{self.host_data[host.uuid].result}\n{host.result}'
host.result = f"{self.host_data[host.uuid].result}\n{host.result}"
else:
return
@ -152,7 +152,7 @@ class ElasticSource:
self._display = display
def start_task(self, tasks_data, hide_task_arguments, play_name, task):
""" record the start of a task for one or more hosts """
"""record the start of a task for one or more hosts"""
uuid = task._uuid
@ -165,29 +165,39 @@ class ElasticSource:
args = None
if not task.no_log and not hide_task_arguments:
args = ', '.join((f'{k}={v}' for k, v in task.args.items()))
args = ", ".join((f"{k}={v}" for k, v in task.args.items()))
tasks_data[uuid] = TaskData(uuid, name, path, play_name, action, args)
def finish_task(self, tasks_data, status, result):
""" record the results of a task for a single host """
"""record the results of a task for a single host"""
task_uuid = result._task._uuid
if hasattr(result, '_host') and result._host is not None:
if hasattr(result, "_host") and result._host is not None:
host_uuid = result._host._uuid
host_name = result._host.name
else:
host_uuid = 'include'
host_name = 'include'
host_uuid = "include"
host_name = "include"
task = tasks_data[task_uuid]
task.add_host(HostData(host_uuid, host_name, status, result))
def generate_distributed_traces(self, tasks_data, status, end_time, traceparent, apm_service_name,
apm_server_url, apm_verify_server_cert, apm_secret_token, apm_api_key):
""" generate distributed traces from the collected TaskData and HostData """
def generate_distributed_traces(
self,
tasks_data,
status,
end_time,
traceparent,
apm_service_name,
apm_server_url,
apm_verify_server_cert,
apm_secret_token,
apm_api_key,
):
"""generate distributed traces from the collected TaskData and HostData"""
tasks = []
parent_start_time = None
@ -196,7 +206,9 @@ class ElasticSource:
parent_start_time = task.start
tasks.append(task)
apm_cli = self.init_apm_client(apm_server_url, apm_service_name, apm_verify_server_cert, apm_secret_token, apm_api_key)
apm_cli = self.init_apm_client(
apm_server_url, apm_service_name, apm_verify_server_cert, apm_secret_token, apm_api_key
)
if apm_cli:
with closing(apm_cli):
instrument() # Only call this once, as early as possible.
@ -218,72 +230,80 @@ class ElasticSource:
apm_cli.end_transaction(name=__name__, result=status, duration=end_time - parent_start_time)
def create_span_data(self, apm_cli, task_data, host_data):
""" create the span with the given TaskData and HostData """
"""create the span with the given TaskData and HostData"""
name = f'[{host_data.name}] {task_data.play}: {task_data.name}'
name = f"[{host_data.name}] {task_data.play}: {task_data.name}"
message = "success"
status = "success"
enriched_error_message = None
if host_data.status == 'included':
if host_data.status == "included":
rc = 0
else:
res = host_data.result._result
rc = res.get('rc', 0)
if host_data.status == 'failed':
rc = res.get("rc", 0)
if host_data.status == "failed":
message = self.get_error_message(res)
enriched_error_message = self.enrich_error_message(res)
status = "failure"
elif host_data.status == 'skipped':
if 'skip_reason' in res:
message = res['skip_reason']
elif host_data.status == "skipped":
if "skip_reason" in res:
message = res["skip_reason"]
else:
message = 'skipped'
message = "skipped"
status = "unknown"
with capture_span(task_data.name,
start=task_data.start,
span_type="ansible.task.run",
duration=host_data.finish - task_data.start,
labels={"ansible.task.args": task_data.args,
"ansible.task.message": message,
"ansible.task.module": task_data.action,
"ansible.task.name": name,
"ansible.task.result": rc,
"ansible.task.host.name": host_data.name,
"ansible.task.host.status": host_data.status}) as span:
with capture_span(
task_data.name,
start=task_data.start,
span_type="ansible.task.run",
duration=host_data.finish - task_data.start,
labels={
"ansible.task.args": task_data.args,
"ansible.task.message": message,
"ansible.task.module": task_data.action,
"ansible.task.name": name,
"ansible.task.result": rc,
"ansible.task.host.name": host_data.name,
"ansible.task.host.status": host_data.status,
},
) as span:
span.outcome = status
if 'failure' in status:
exception = AnsibleRuntimeError(message=f"{task_data.action}: {name} failed with error message {enriched_error_message}")
if "failure" in status:
exception = AnsibleRuntimeError(
message=f"{task_data.action}: {name} failed with error message {enriched_error_message}"
)
apm_cli.capture_exception(exc_info=(type(exception), exception, exception.__traceback__), handled=True)
def init_apm_client(self, apm_server_url, apm_service_name, apm_verify_server_cert, apm_secret_token, apm_api_key):
if apm_server_url:
return Client(service_name=apm_service_name,
server_url=apm_server_url,
verify_server_cert=False,
secret_token=apm_secret_token,
api_key=apm_api_key,
use_elastic_traceparent_header=True,
debug=True)
return Client(
service_name=apm_service_name,
server_url=apm_server_url,
verify_server_cert=False,
secret_token=apm_secret_token,
api_key=apm_api_key,
use_elastic_traceparent_header=True,
debug=True,
)
@staticmethod
def get_error_message(result):
if result.get('exception') is not None:
return ElasticSource._last_line(result['exception'])
return result.get('msg', 'failed')
if result.get("exception") is not None:
return ElasticSource._last_line(result["exception"])
return result.get("msg", "failed")
@staticmethod
def _last_line(text):
lines = text.strip().split('\n')
lines = text.strip().split("\n")
return lines[-1]
@staticmethod
def enrich_error_message(result):
message = result.get('msg', 'failed')
exception = result.get('exception')
stderr = result.get('stderr')
return f"message: \"{message}\"\nexception: \"{exception}\"\nstderr: \"{stderr}\""
message = result.get("msg", "failed")
exception = result.get("exception")
stderr = result.get("stderr")
return f'message: "{message}"\nexception: "{exception}"\nstderr: "{stderr}"'
class CallbackModule(CallbackBase):
@ -292,8 +312,8 @@ class CallbackModule(CallbackBase):
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.elastic'
CALLBACK_TYPE = "notification"
CALLBACK_NAME = "community.general.elastic"
CALLBACK_NEEDS_ENABLED = True
def __init__(self, display=None):
@ -308,7 +328,9 @@ class CallbackModule(CallbackBase):
self.disabled = False
if ELASTIC_LIBRARY_IMPORT_ERROR:
raise AnsibleError('The `elastic-apm` must be installed to use this plugin') from ELASTIC_LIBRARY_IMPORT_ERROR
raise AnsibleError(
"The `elastic-apm` must be installed to use this plugin"
) from ELASTIC_LIBRARY_IMPORT_ERROR
self.tasks_data = OrderedDict()
@ -317,17 +339,17 @@ class CallbackModule(CallbackBase):
def set_options(self, task_keys=None, var_options=None, direct=None):
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
self.hide_task_arguments = self.get_option('hide_task_arguments')
self.hide_task_arguments = self.get_option("hide_task_arguments")
self.apm_service_name = self.get_option('apm_service_name')
self.apm_service_name = self.get_option("apm_service_name")
if not self.apm_service_name:
self.apm_service_name = 'ansible'
self.apm_service_name = "ansible"
self.apm_server_url = self.get_option('apm_server_url')
self.apm_secret_token = self.get_option('apm_secret_token')
self.apm_api_key = self.get_option('apm_api_key')
self.apm_verify_server_cert = self.get_option('apm_verify_server_cert')
self.traceparent = self.get_option('traceparent')
self.apm_server_url = self.get_option("apm_server_url")
self.apm_secret_token = self.get_option("apm_secret_token")
self.apm_api_key = self.get_option("apm_api_key")
self.apm_verify_server_cert = self.get_option("apm_verify_server_cert")
self.traceparent = self.get_option("traceparent")
def v2_playbook_on_start(self, playbook):
self.ansible_playbook = basename(playbook._file_name)
@ -336,65 +358,29 @@ class CallbackModule(CallbackBase):
self.play_name = play.get_name()
def v2_runner_on_no_hosts(self, task):
self.elastic.start_task(
self.tasks_data,
self.hide_task_arguments,
self.play_name,
task
)
self.elastic.start_task(self.tasks_data, self.hide_task_arguments, self.play_name, task)
def v2_playbook_on_task_start(self, task, is_conditional):
self.elastic.start_task(
self.tasks_data,
self.hide_task_arguments,
self.play_name,
task
)
self.elastic.start_task(self.tasks_data, self.hide_task_arguments, self.play_name, task)
def v2_playbook_on_cleanup_task_start(self, task):
self.elastic.start_task(
self.tasks_data,
self.hide_task_arguments,
self.play_name,
task
)
self.elastic.start_task(self.tasks_data, self.hide_task_arguments, self.play_name, task)
def v2_playbook_on_handler_task_start(self, task):
self.elastic.start_task(
self.tasks_data,
self.hide_task_arguments,
self.play_name,
task
)
self.elastic.start_task(self.tasks_data, self.hide_task_arguments, self.play_name, task)
def v2_runner_on_failed(self, result, ignore_errors=False):
self.errors += 1
self.elastic.finish_task(
self.tasks_data,
'failed',
result
)
self.elastic.finish_task(self.tasks_data, "failed", result)
def v2_runner_on_ok(self, result):
self.elastic.finish_task(
self.tasks_data,
'ok',
result
)
self.elastic.finish_task(self.tasks_data, "ok", result)
def v2_runner_on_skipped(self, result):
self.elastic.finish_task(
self.tasks_data,
'skipped',
result
)
self.elastic.finish_task(self.tasks_data, "skipped", result)
def v2_playbook_on_include(self, included_file):
self.elastic.finish_task(
self.tasks_data,
'included',
included_file
)
self.elastic.finish_task(self.tasks_data, "included", included_file)
def v2_playbook_on_stats(self, stats):
if self.errors == 0:
@ -410,7 +396,7 @@ class CallbackModule(CallbackBase):
self.apm_server_url,
self.apm_verify_server_cert,
self.apm_secret_token,
self.apm_api_key
self.apm_api_key,
)
def v2_runner_on_async_failed(self, result, **kwargs):

View file

@ -54,29 +54,31 @@ from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.jabber'
CALLBACK_TYPE = "notification"
CALLBACK_NAME = "community.general.jabber"
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
super().__init__(display=display)
if not HAS_XMPP:
self._display.warning("The required python xmpp library (xmpppy) is not installed. "
"pip install git+https://github.com/ArchipelProject/xmpppy")
self._display.warning(
"The required python xmpp library (xmpppy) is not installed. "
"pip install git+https://github.com/ArchipelProject/xmpppy"
)
self.disabled = True
self.serv = os.getenv('JABBER_SERV')
self.j_user = os.getenv('JABBER_USER')
self.j_pass = os.getenv('JABBER_PASS')
self.j_to = os.getenv('JABBER_TO')
self.serv = os.getenv("JABBER_SERV")
self.j_user = os.getenv("JABBER_USER")
self.j_pass = os.getenv("JABBER_PASS")
self.j_to = os.getenv("JABBER_TO")
if (self.j_user or self.j_pass or self.serv or self.j_to) is None:
self.disabled = True
self._display.warning('Jabber CallBack wants the JABBER_SERV, JABBER_USER, JABBER_PASS and JABBER_TO environment variables')
self._display.warning(
"Jabber CallBack wants the JABBER_SERV, JABBER_USER, JABBER_PASS and JABBER_TO environment variables"
)
def send_msg(self, msg):
"""Send message"""
@ -85,7 +87,7 @@ class CallbackModule(CallbackBase):
client.connect(server=(self.serv, 5222))
client.auth(jid.getNode(), self.j_pass, resource=jid.getResource())
message = xmpp.Message(self.j_to, msg)
message.setAttr('type', 'chat')
message.setAttr("type", "chat")
client.send(message)
client.disconnect()
@ -109,9 +111,9 @@ class CallbackModule(CallbackBase):
unreachable = False
for h in hosts:
s = stats.summarize(h)
if s['failures'] > 0:
if s["failures"] > 0:
failures = True
if s['unreachable'] > 0:
if s["unreachable"] > 0:
unreachable = True
if failures or unreachable:

View file

@ -49,9 +49,10 @@ class CallbackModule(CallbackBase):
"""
logs playbook results, per host, in /var/log/ansible/hosts
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.log_plays'
CALLBACK_TYPE = "notification"
CALLBACK_NAME = "community.general.log_plays"
CALLBACK_NEEDS_WHITELIST = True
TIME_FORMAT = "%b %d %Y %H:%M:%S"
@ -61,7 +62,6 @@ class CallbackModule(CallbackBase):
return f"{now} - {playbook} - {task_name} - {task_action} - {category} - {data}\n\n"
def __init__(self):
super().__init__()
def set_options(self, task_keys=None, var_options=None, direct=None):
@ -75,12 +75,12 @@ class CallbackModule(CallbackBase):
def log(self, result, category):
data = result._result
if isinstance(data, MutableMapping):
if '_ansible_verbose_override' in data:
if "_ansible_verbose_override" in data:
# avoid logging extraneous data
data = 'omitted'
data = "omitted"
else:
data = data.copy()
invocation = data.pop('invocation', None)
invocation = data.pop("invocation", None)
data = json.dumps(data, cls=AnsibleJSONEncoder)
if invocation is not None:
data = f"{json.dumps(invocation)} => {data} "
@ -93,25 +93,25 @@ class CallbackModule(CallbackBase):
fd.write(msg)
def v2_runner_on_failed(self, result, ignore_errors=False):
self.log(result, 'FAILED')
self.log(result, "FAILED")
def v2_runner_on_ok(self, result):
self.log(result, 'OK')
self.log(result, "OK")
def v2_runner_on_skipped(self, result):
self.log(result, 'SKIPPED')
self.log(result, "SKIPPED")
def v2_runner_on_unreachable(self, result):
self.log(result, 'UNREACHABLE')
self.log(result, "UNREACHABLE")
def v2_runner_on_async_failed(self, result):
self.log(result, 'ASYNC_FAILED')
self.log(result, "ASYNC_FAILED")
def v2_playbook_on_start(self, playbook):
self.playbook = playbook._file_name
def v2_playbook_on_import_for_host(self, result, imported_file):
self.log(result, 'IMPORTED', imported_file)
self.log(result, "IMPORTED", imported_file)
def v2_playbook_on_not_import_for_host(self, result, missing_file):
self.log(result, 'NOTIMPORTED', missing_file)
self.log(result, "NOTIMPORTED", missing_file)

View file

@ -83,11 +83,10 @@ class AzureLogAnalyticsSource:
def __build_signature(self, date, workspace_id, shared_key, content_length):
# Build authorisation signature for Azure log analytics API call
sigs = f"POST\n{content_length}\napplication/json\nx-ms-date:{date}\n/api/logs"
utf8_sigs = sigs.encode('utf-8')
utf8_sigs = sigs.encode("utf-8")
decoded_shared_key = base64.b64decode(shared_key)
hmac_sha256_sigs = hmac.new(
decoded_shared_key, utf8_sigs, digestmod=hashlib.sha256).digest()
encoded_hash = base64.b64encode(hmac_sha256_sigs).decode('utf-8')
hmac_sha256_sigs = hmac.new(decoded_shared_key, utf8_sigs, digestmod=hashlib.sha256).digest()
encoded_hash = base64.b64encode(hmac_sha256_sigs).decode("utf-8")
signature = f"SharedKey {workspace_id}:{encoded_hash}"
return signature
@ -95,10 +94,10 @@ class AzureLogAnalyticsSource:
return f"https://{workspace_id}.ods.opinsights.azure.com/api/logs?api-version=2016-04-01"
def __rfc1123date(self):
return now().strftime('%a, %d %b %Y %H:%M:%S GMT')
return now().strftime("%a, %d %b %Y %H:%M:%S GMT")
def send_event(self, workspace_id, shared_key, state, result, runtime):
if result._task_fields['args'].get('_ansible_check_mode') is True:
if result._task_fields["args"].get("_ansible_check_mode") is True:
self.ansible_check_mode = True
if result._task._role:
@ -107,31 +106,31 @@ class AzureLogAnalyticsSource:
ansible_role = None
data = {}
data['uuid'] = result._task._uuid
data['session'] = self.session
data['status'] = state
data['timestamp'] = self.__rfc1123date()
data['host'] = self.host
data['user'] = self.user
data['runtime'] = runtime
data['ansible_version'] = ansible_version
data['ansible_check_mode'] = self.ansible_check_mode
data['ansible_host'] = result._host.name
data['ansible_playbook'] = self.ansible_playbook
data['ansible_role'] = ansible_role
data['ansible_task'] = result._task_fields
data["uuid"] = result._task._uuid
data["session"] = self.session
data["status"] = state
data["timestamp"] = self.__rfc1123date()
data["host"] = self.host
data["user"] = self.user
data["runtime"] = runtime
data["ansible_version"] = ansible_version
data["ansible_check_mode"] = self.ansible_check_mode
data["ansible_host"] = result._host.name
data["ansible_playbook"] = self.ansible_playbook
data["ansible_role"] = ansible_role
data["ansible_task"] = result._task_fields
# Removing args since it can contain sensitive data
if 'args' in data['ansible_task']:
data['ansible_task'].pop('args')
data['ansible_result'] = result._result
if 'content' in data['ansible_result']:
data['ansible_result'].pop('content')
if "args" in data["ansible_task"]:
data["ansible_task"].pop("args")
data["ansible_result"] = result._result
if "content" in data["ansible_result"]:
data["ansible_result"].pop("content")
# Adding extra vars info
data['extra_vars'] = self.extra_vars
data["extra_vars"] = self.extra_vars
# Preparing the playbook logs as JSON format and send to Azure log analytics
jsondata = json.dumps({'event': data}, cls=AnsibleJSONEncoder, sort_keys=True)
jsondata = json.dumps({"event": data}, cls=AnsibleJSONEncoder, sort_keys=True)
content_length = len(jsondata)
rfc1123date = self.__rfc1123date()
signature = self.__build_signature(rfc1123date, workspace_id, shared_key, content_length)
@ -141,19 +140,19 @@ class AzureLogAnalyticsSource:
workspace_url,
jsondata,
headers={
'content-type': 'application/json',
'Authorization': signature,
'Log-Type': 'ansible_playbook',
'x-ms-date': rfc1123date
"content-type": "application/json",
"Authorization": signature,
"Log-Type": "ansible_playbook",
"x-ms-date": rfc1123date,
},
method='POST'
method="POST",
)
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'loganalytics'
CALLBACK_TYPE = "notification"
CALLBACK_NAME = "loganalytics"
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
@ -164,15 +163,12 @@ class CallbackModule(CallbackBase):
self.loganalytics = AzureLogAnalyticsSource()
def _seconds_since_start(self, result):
return (
now() -
self.start_datetimes[result._task._uuid]
).total_seconds()
return (now() - self.start_datetimes[result._task._uuid]).total_seconds()
def set_options(self, task_keys=None, var_options=None, direct=None):
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
self.workspace_id = self.get_option('workspace_id')
self.shared_key = self.get_option('shared_key')
self.workspace_id = self.get_option("workspace_id")
self.shared_key = self.get_option("shared_key")
def v2_playbook_on_play_start(self, play):
vm = play.get_variable_manager()
@ -190,45 +186,25 @@ class CallbackModule(CallbackBase):
def v2_runner_on_ok(self, result, **kwargs):
self.loganalytics.send_event(
self.workspace_id,
self.shared_key,
'OK',
result,
self._seconds_since_start(result)
self.workspace_id, self.shared_key, "OK", result, self._seconds_since_start(result)
)
def v2_runner_on_skipped(self, result, **kwargs):
self.loganalytics.send_event(
self.workspace_id,
self.shared_key,
'SKIPPED',
result,
self._seconds_since_start(result)
self.workspace_id, self.shared_key, "SKIPPED", result, self._seconds_since_start(result)
)
def v2_runner_on_failed(self, result, **kwargs):
self.loganalytics.send_event(
self.workspace_id,
self.shared_key,
'FAILED',
result,
self._seconds_since_start(result)
self.workspace_id, self.shared_key, "FAILED", result, self._seconds_since_start(result)
)
def runner_on_async_failed(self, result, **kwargs):
self.loganalytics.send_event(
self.workspace_id,
self.shared_key,
'FAILED',
result,
self._seconds_since_start(result)
self.workspace_id, self.shared_key, "FAILED", result, self._seconds_since_start(result)
)
def v2_runner_on_unreachable(self, result, **kwargs):
self.loganalytics.send_event(
self.workspace_id,
self.shared_key,
'UNREACHABLE',
result,
self._seconds_since_start(result)
self.workspace_id, self.shared_key, "UNREACHABLE", result, self._seconds_since_start(result)
)

View file

@ -64,6 +64,7 @@ from ansible.parsing.ajson import AnsibleJSONEncoder
try:
from logdna import LogDNAHandler
HAS_LOGDNA = True
except ImportError:
HAS_LOGDNA = False
@ -72,12 +73,12 @@ except ImportError:
# Getting MAC Address of system:
def get_mac():
mac = f"{getnode():012x}"
return ":".join(map(lambda index: mac[index:index + 2], range(int(len(mac) / 2))))
return ":".join(map(lambda index: mac[index : index + 2], range(int(len(mac) / 2))))
# Getting hostname of system:
def get_hostname():
return str(socket.gethostname()).split('.local', 1)[0]
return str(socket.gethostname()).split(".local", 1)[0]
# Getting IP of system:
@ -87,10 +88,10 @@ def get_ip():
except Exception:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(('10.255.255.255', 1))
s.connect(("10.255.255.255", 1))
IP = s.getsockname()[0]
except Exception:
IP = '127.0.0.1'
IP = "127.0.0.1"
finally:
s.close()
return IP
@ -107,10 +108,9 @@ def isJSONable(obj):
# LogDNA Callback Module:
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 0.1
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.logdna'
CALLBACK_TYPE = "notification"
CALLBACK_NAME = "community.general.logdna"
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
@ -127,27 +127,27 @@ class CallbackModule(CallbackBase):
def set_options(self, task_keys=None, var_options=None, direct=None):
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
self.conf_key = self.get_option('conf_key')
self.plugin_ignore_errors = self.get_option('plugin_ignore_errors')
self.conf_hostname = self.get_option('conf_hostname')
self.conf_tags = self.get_option('conf_tags')
self.conf_key = self.get_option("conf_key")
self.plugin_ignore_errors = self.get_option("plugin_ignore_errors")
self.conf_hostname = self.get_option("conf_hostname")
self.conf_tags = self.get_option("conf_tags")
self.mac = get_mac()
self.ip = get_ip()
if self.conf_hostname is None:
self.conf_hostname = get_hostname()
self.conf_tags = self.conf_tags.split(',')
self.conf_tags = self.conf_tags.split(",")
if HAS_LOGDNA:
self.log = logging.getLogger('logdna')
self.log = logging.getLogger("logdna")
self.log.setLevel(logging.INFO)
self.options = {'hostname': self.conf_hostname, 'mac': self.mac, 'index_meta': True}
self.options = {"hostname": self.conf_hostname, "mac": self.mac, "index_meta": True}
self.log.addHandler(LogDNAHandler(self.conf_key, self.options))
self.disabled = False
else:
self.disabled = True
self._display.warning('WARNING:\nPlease, install LogDNA Python Package: `pip install logdna`')
self._display.warning("WARNING:\nPlease, install LogDNA Python Package: `pip install logdna`")
def metaIndexing(self, meta):
invalidKeys = []
@ -159,25 +159,25 @@ class CallbackModule(CallbackBase):
if ninvalidKeys > 0:
for key in invalidKeys:
del meta[key]
meta['__errors'] = f"These keys have been sanitized: {', '.join(invalidKeys)}"
meta["__errors"] = f"These keys have been sanitized: {', '.join(invalidKeys)}"
return meta
def sanitizeJSON(self, data):
try:
return json.loads(json.dumps(data, sort_keys=True, cls=AnsibleJSONEncoder))
except Exception:
return {'warnings': ['JSON Formatting Issue', json.dumps(data, sort_keys=True, cls=AnsibleJSONEncoder)]}
return {"warnings": ["JSON Formatting Issue", json.dumps(data, sort_keys=True, cls=AnsibleJSONEncoder)]}
def flush(self, log, options):
if HAS_LOGDNA:
self.log.info(json.dumps(log), options)
def sendLog(self, host, category, logdata):
options = {'app': 'ansible', 'meta': {'playbook': self.playbook_name, 'host': host, 'category': category}}
logdata['info'].pop('invocation', None)
warnings = logdata['info'].pop('warnings', None)
options = {"app": "ansible", "meta": {"playbook": self.playbook_name, "host": host, "category": category}}
logdata["info"].pop("invocation", None)
warnings = logdata["info"].pop("warnings", None)
if warnings is not None:
self.flush({'warn': warnings}, options)
self.flush({"warn": warnings}, options)
self.flush(logdata, options)
def v2_playbook_on_start(self, playbook):
@ -188,21 +188,21 @@ class CallbackModule(CallbackBase):
result = dict()
for host in stats.processed.keys():
result[host] = stats.summarize(host)
self.sendLog(self.conf_hostname, 'STATS', {'info': self.sanitizeJSON(result)})
self.sendLog(self.conf_hostname, "STATS", {"info": self.sanitizeJSON(result)})
def runner_on_failed(self, host, res, ignore_errors=False):
if self.plugin_ignore_errors:
ignore_errors = self.plugin_ignore_errors
self.sendLog(host, 'FAILED', {'info': self.sanitizeJSON(res), 'ignore_errors': ignore_errors})
self.sendLog(host, "FAILED", {"info": self.sanitizeJSON(res), "ignore_errors": ignore_errors})
def runner_on_ok(self, host, res):
self.sendLog(host, 'OK', {'info': self.sanitizeJSON(res)})
self.sendLog(host, "OK", {"info": self.sanitizeJSON(res)})
def runner_on_unreachable(self, host, res):
self.sendLog(host, 'UNREACHABLE', {'info': self.sanitizeJSON(res)})
self.sendLog(host, "UNREACHABLE", {"info": self.sanitizeJSON(res)})
def runner_on_async_failed(self, host, res, jid):
self.sendLog(host, 'ASYNC_FAILED', {'info': self.sanitizeJSON(res), 'job_id': jid})
self.sendLog(host, "ASYNC_FAILED", {"info": self.sanitizeJSON(res), "job_id": jid})
def runner_on_async_ok(self, host, res, jid):
self.sendLog(host, 'ASYNC_OK', {'info': self.sanitizeJSON(res), 'job_id': jid})
self.sendLog(host, "ASYNC_OK", {"info": self.sanitizeJSON(res), "job_id": jid})

View file

@ -103,12 +103,14 @@ import uuid
try:
import certifi
HAS_CERTIFI = True
except ImportError:
HAS_CERTIFI = False
try:
import flatdict
HAS_FLATDICT = True
except ImportError:
HAS_FLATDICT = False
@ -121,8 +123,7 @@ from ansible.plugins.callback import CallbackBase
class PlainTextSocketAppender:
def __init__(self, display, LE_API='data.logentries.com', LE_PORT=80, LE_TLS_PORT=443):
def __init__(self, display, LE_API="data.logentries.com", LE_PORT=80, LE_TLS_PORT=443):
self.LE_API = LE_API
self.LE_PORT = LE_PORT
self.LE_TLS_PORT = LE_TLS_PORT
@ -131,7 +132,7 @@ class PlainTextSocketAppender:
# Error message displayed when an incorrect Token has been detected
self.INVALID_TOKEN = "\n\nIt appears the LOGENTRIES_TOKEN parameter you entered is incorrect!\n\n"
# Unicode Line separator character \u2028
self.LINE_SEP = '\u2028'
self.LINE_SEP = "\u2028"
self._display = display
self._conn = None
@ -170,13 +171,13 @@ class PlainTextSocketAppender:
def put(self, data):
# Replace newlines with Unicode line separator
# for multi-line events
data = to_text(data, errors='surrogate_or_strict')
multiline = data.replace('\n', self.LINE_SEP)
data = to_text(data, errors="surrogate_or_strict")
multiline = data.replace("\n", self.LINE_SEP)
multiline += "\n"
# Send data, reconnect if needed
while True:
try:
self._conn.send(to_bytes(multiline, errors='surrogate_or_strict'))
self._conn.send(to_bytes(multiline, errors="surrogate_or_strict"))
except socket.error:
self.reopen_connection()
continue
@ -187,6 +188,7 @@ class PlainTextSocketAppender:
try:
import ssl
HAS_SSL = True
except ImportError: # for systems without TLS support.
SocketAppender = PlainTextSocketAppender
@ -198,11 +200,13 @@ else:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
context = ssl.create_default_context(
purpose=ssl.Purpose.SERVER_AUTH,
cafile=certifi.where(), )
cafile=certifi.where(),
)
sock = context.wrap_socket(
sock=sock,
do_handshake_on_connect=True,
suppress_ragged_eofs=True, )
suppress_ragged_eofs=True,
)
sock.connect((self.LE_API, self.LE_TLS_PORT))
self._conn = sock
@ -211,12 +215,11 @@ else:
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.logentries'
CALLBACK_TYPE = "notification"
CALLBACK_NAME = "community.general.logentries"
CALLBACK_NEEDS_WHITELIST = True
def __init__(self):
# TODO: allow for alternate posting methods (REST/UDP/agent/etc)
super().__init__()
@ -226,7 +229,9 @@ class CallbackModule(CallbackBase):
if not HAS_CERTIFI:
self.disabled = True
self._display.warning('The `certifi` python module is not installed.\nDisabling the Logentries callback plugin.')
self._display.warning(
"The `certifi` python module is not installed.\nDisabling the Logentries callback plugin."
)
self.le_jobid = str(uuid.uuid4())
@ -234,41 +239,47 @@ class CallbackModule(CallbackBase):
self.timeout = 10
def set_options(self, task_keys=None, var_options=None, direct=None):
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
# get options
try:
self.api_url = self.get_option('api')
self.api_port = self.get_option('port')
self.api_tls_port = self.get_option('tls_port')
self.use_tls = self.get_option('use_tls')
self.flatten = self.get_option('flatten')
self.api_url = self.get_option("api")
self.api_port = self.get_option("port")
self.api_tls_port = self.get_option("tls_port")
self.use_tls = self.get_option("use_tls")
self.flatten = self.get_option("flatten")
except KeyError as e:
self._display.warning(f"Missing option for Logentries callback plugin: {e}")
self.disabled = True
try:
self.token = self.get_option('token')
self.token = self.get_option("token")
except KeyError as e:
self._display.warning('Logentries token was not provided, this is required for this callback to operate, disabling')
self._display.warning(
"Logentries token was not provided, this is required for this callback to operate, disabling"
)
self.disabled = True
if self.flatten and not HAS_FLATDICT:
self.disabled = True
self._display.warning('You have chosen to flatten and the `flatdict` python module is not installed.\nDisabling the Logentries callback plugin.')
self._display.warning(
"You have chosen to flatten and the `flatdict` python module is not installed.\nDisabling the Logentries callback plugin."
)
self._initialize_connections()
def _initialize_connections(self):
if not self.disabled:
if self.use_tls:
self._display.vvvv(f"Connecting to {self.api_url}:{self.api_tls_port} with TLS")
self._appender = TLSSocketAppender(display=self._display, LE_API=self.api_url, LE_TLS_PORT=self.api_tls_port)
self._appender = TLSSocketAppender(
display=self._display, LE_API=self.api_url, LE_TLS_PORT=self.api_tls_port
)
else:
self._display.vvvv(f"Connecting to {self.api_url}:{self.api_port}")
self._appender = PlainTextSocketAppender(display=self._display, LE_API=self.api_url, LE_PORT=self.api_port)
self._appender = PlainTextSocketAppender(
display=self._display, LE_API=self.api_url, LE_PORT=self.api_port
)
self._appender.reopen_connection()
def emit_formatted(self, record):
@ -279,50 +290,50 @@ class CallbackModule(CallbackBase):
self.emit(self._dump_results(record))
def emit(self, record):
msg = record.rstrip('\n')
msg = record.rstrip("\n")
msg = f"{self.token} {msg}"
self._appender.put(msg)
self._display.vvvv("Sent event to logentries")
def _set_info(self, host, res):
return {'le_jobid': self.le_jobid, 'hostname': host, 'results': res}
return {"le_jobid": self.le_jobid, "hostname": host, "results": res}
def runner_on_ok(self, host, res):
results = self._set_info(host, res)
results['status'] = 'OK'
results["status"] = "OK"
self.emit_formatted(results)
def runner_on_failed(self, host, res, ignore_errors=False):
results = self._set_info(host, res)
results['status'] = 'FAILED'
results["status"] = "FAILED"
self.emit_formatted(results)
def runner_on_skipped(self, host, item=None):
results = self._set_info(host, item)
del results['results']
results['status'] = 'SKIPPED'
del results["results"]
results["status"] = "SKIPPED"
self.emit_formatted(results)
def runner_on_unreachable(self, host, res):
results = self._set_info(host, res)
results['status'] = 'UNREACHABLE'
results["status"] = "UNREACHABLE"
self.emit_formatted(results)
def runner_on_async_failed(self, host, res, jid):
results = self._set_info(host, res)
results['jid'] = jid
results['status'] = 'ASYNC_FAILED'
results["jid"] = jid
results["status"] = "ASYNC_FAILED"
self.emit_formatted(results)
def v2_playbook_on_play_start(self, play):
results = {}
results['le_jobid'] = self.le_jobid
results['started_by'] = os.getlogin()
results["le_jobid"] = self.le_jobid
results["started_by"] = os.getlogin()
if play.name:
results['play'] = play.name
results['hosts'] = play.hosts
results["play"] = play.name
results["hosts"] = play.hosts
self.emit_formatted(results)
def playbook_on_stats(self, stats):
""" close connection """
"""close connection"""
self._appender.close_connection()

View file

@ -103,6 +103,7 @@ import logging
try:
import logstash
HAS_LOGSTASH = True
except ImportError:
HAS_LOGSTASH = False
@ -115,10 +116,9 @@ from ansible_collections.community.general.plugins.module_utils.datetime import
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.logstash'
CALLBACK_TYPE = "notification"
CALLBACK_NAME = "community.general.logstash"
CALLBACK_NEEDS_WHITELIST = True
def __init__(self):
@ -132,14 +132,11 @@ class CallbackModule(CallbackBase):
def _init_plugin(self):
if not self.disabled:
self.logger = logging.getLogger('python-logstash-logger')
self.logger = logging.getLogger("python-logstash-logger")
self.logger.setLevel(logging.DEBUG)
self.handler = logstash.TCPLogstashHandler(
self.ls_server,
self.ls_port,
version=1,
message_type=self.ls_type
self.ls_server, self.ls_port, version=1, message_type=self.ls_type
)
self.logger.addHandler(self.handler)
@ -147,42 +144,36 @@ class CallbackModule(CallbackBase):
self.session = str(uuid.uuid4())
self.errors = 0
self.base_data = {
'session': self.session,
'host': self.hostname
}
self.base_data = {"session": self.session, "host": self.hostname}
if self.ls_pre_command is not None:
self.base_data['ansible_pre_command_output'] = os.popen(
self.ls_pre_command).read()
self.base_data["ansible_pre_command_output"] = os.popen(self.ls_pre_command).read()
if context.CLIARGS is not None:
self.base_data['ansible_checkmode'] = context.CLIARGS.get('check')
self.base_data['ansible_tags'] = context.CLIARGS.get('tags')
self.base_data['ansible_skip_tags'] = context.CLIARGS.get('skip_tags')
self.base_data['inventory'] = context.CLIARGS.get('inventory')
self.base_data["ansible_checkmode"] = context.CLIARGS.get("check")
self.base_data["ansible_tags"] = context.CLIARGS.get("tags")
self.base_data["ansible_skip_tags"] = context.CLIARGS.get("skip_tags")
self.base_data["inventory"] = context.CLIARGS.get("inventory")
def set_options(self, task_keys=None, var_options=None, direct=None):
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
self.ls_server = self.get_option('server')
self.ls_port = int(self.get_option('port'))
self.ls_type = self.get_option('type')
self.ls_pre_command = self.get_option('pre_command')
self.ls_format_version = self.get_option('format_version')
self.ls_server = self.get_option("server")
self.ls_port = int(self.get_option("port"))
self.ls_type = self.get_option("type")
self.ls_pre_command = self.get_option("pre_command")
self.ls_format_version = self.get_option("format_version")
self._init_plugin()
def v2_playbook_on_start(self, playbook):
data = self.base_data.copy()
data['ansible_type'] = "start"
data['status'] = "OK"
data['ansible_playbook'] = playbook._file_name
data["ansible_type"] = "start"
data["status"] = "OK"
data["ansible_playbook"] = playbook._file_name
if self.ls_format_version == "v2":
self.logger.info(
"START PLAYBOOK | %s", data['ansible_playbook'], extra=data
)
self.logger.info("START PLAYBOOK | %s", data["ansible_playbook"], extra=data)
else:
self.logger.info("ansible start", extra=data)
@ -199,15 +190,13 @@ class CallbackModule(CallbackBase):
status = "FAILED"
data = self.base_data.copy()
data['ansible_type'] = "finish"
data['status'] = status
data['ansible_playbook_duration'] = runtime.total_seconds()
data['ansible_result'] = json.dumps(summarize_stat) # deprecated field
data["ansible_type"] = "finish"
data["status"] = status
data["ansible_playbook_duration"] = runtime.total_seconds()
data["ansible_result"] = json.dumps(summarize_stat) # deprecated field
if self.ls_format_version == "v2":
self.logger.info(
"FINISH PLAYBOOK | %s", json.dumps(summarize_stat), extra=data
)
self.logger.info("FINISH PLAYBOOK | %s", json.dumps(summarize_stat), extra=data)
else:
self.logger.info("ansible stats", extra=data)
@ -218,10 +207,10 @@ class CallbackModule(CallbackBase):
self.play_name = play.name
data = self.base_data.copy()
data['ansible_type'] = "start"
data['status'] = "OK"
data['ansible_play_id'] = self.play_id
data['ansible_play_name'] = self.play_name
data["ansible_type"] = "start"
data["status"] = "OK"
data["ansible_play_id"] = self.play_id
data["ansible_play_name"] = self.play_name
if self.ls_format_version == "v2":
self.logger.info("START PLAY | %s", self.play_name, extra=data)
@ -231,64 +220,61 @@ class CallbackModule(CallbackBase):
def v2_playbook_on_task_start(self, task, is_conditional):
self.task_id = str(task._uuid)
'''
"""
Tasks and handler tasks are dealt with here
'''
"""
def v2_runner_on_ok(self, result, **kwargs):
task_name = str(result._task).replace('TASK: ', '').replace('HANDLER: ', '')
task_name = str(result._task).replace("TASK: ", "").replace("HANDLER: ", "")
data = self.base_data.copy()
if task_name == 'setup':
data['ansible_type'] = "setup"
data['status'] = "OK"
data['ansible_host'] = result._host.name
data['ansible_play_id'] = self.play_id
data['ansible_play_name'] = self.play_name
data['ansible_task'] = task_name
data['ansible_facts'] = self._dump_results(result._result)
if task_name == "setup":
data["ansible_type"] = "setup"
data["status"] = "OK"
data["ansible_host"] = result._host.name
data["ansible_play_id"] = self.play_id
data["ansible_play_name"] = self.play_name
data["ansible_task"] = task_name
data["ansible_facts"] = self._dump_results(result._result)
if self.ls_format_version == "v2":
self.logger.info(
"SETUP FACTS | %s", self._dump_results(result._result), extra=data
)
self.logger.info("SETUP FACTS | %s", self._dump_results(result._result), extra=data)
else:
self.logger.info("ansible facts", extra=data)
else:
if 'changed' in result._result.keys():
data['ansible_changed'] = result._result['changed']
if "changed" in result._result.keys():
data["ansible_changed"] = result._result["changed"]
else:
data['ansible_changed'] = False
data["ansible_changed"] = False
data['ansible_type'] = "task"
data['status'] = "OK"
data['ansible_host'] = result._host.name
data['ansible_play_id'] = self.play_id
data['ansible_play_name'] = self.play_name
data['ansible_task'] = task_name
data['ansible_task_id'] = self.task_id
data['ansible_result'] = self._dump_results(result._result)
data["ansible_type"] = "task"
data["status"] = "OK"
data["ansible_host"] = result._host.name
data["ansible_play_id"] = self.play_id
data["ansible_play_name"] = self.play_name
data["ansible_task"] = task_name
data["ansible_task_id"] = self.task_id
data["ansible_result"] = self._dump_results(result._result)
if self.ls_format_version == "v2":
self.logger.info(
"TASK OK | %s | RESULT | %s",
task_name, self._dump_results(result._result), extra=data
"TASK OK | %s | RESULT | %s", task_name, self._dump_results(result._result), extra=data
)
else:
self.logger.info("ansible ok", extra=data)
def v2_runner_on_skipped(self, result, **kwargs):
task_name = str(result._task).replace('TASK: ', '').replace('HANDLER: ', '')
task_name = str(result._task).replace("TASK: ", "").replace("HANDLER: ", "")
data = self.base_data.copy()
data['ansible_type'] = "task"
data['status'] = "SKIPPED"
data['ansible_host'] = result._host.name
data['ansible_play_id'] = self.play_id
data['ansible_play_name'] = self.play_name
data['ansible_task'] = task_name
data['ansible_task_id'] = self.task_id
data['ansible_result'] = self._dump_results(result._result)
data["ansible_type"] = "task"
data["status"] = "SKIPPED"
data["ansible_host"] = result._host.name
data["ansible_play_id"] = self.play_id
data["ansible_play_name"] = self.play_name
data["ansible_task"] = task_name
data["ansible_task_id"] = self.task_id
data["ansible_result"] = self._dump_results(result._result)
if self.ls_format_version == "v2":
self.logger.info("TASK SKIPPED | %s", task_name, extra=data)
@ -297,12 +283,12 @@ class CallbackModule(CallbackBase):
def v2_playbook_on_import_for_host(self, result, imported_file):
data = self.base_data.copy()
data['ansible_type'] = "import"
data['status'] = "IMPORTED"
data['ansible_host'] = result._host.name
data['ansible_play_id'] = self.play_id
data['ansible_play_name'] = self.play_name
data['imported_file'] = imported_file
data["ansible_type"] = "import"
data["status"] = "IMPORTED"
data["ansible_host"] = result._host.name
data["ansible_play_id"] = self.play_id
data["ansible_play_name"] = self.play_name
data["imported_file"] = imported_file
if self.ls_format_version == "v2":
self.logger.info("IMPORT | %s", imported_file, extra=data)
@ -311,12 +297,12 @@ class CallbackModule(CallbackBase):
def v2_playbook_on_not_import_for_host(self, result, missing_file):
data = self.base_data.copy()
data['ansible_type'] = "import"
data['status'] = "NOT IMPORTED"
data['ansible_host'] = result._host.name
data['ansible_play_id'] = self.play_id
data['ansible_play_name'] = self.play_name
data['imported_file'] = missing_file
data["ansible_type"] = "import"
data["status"] = "NOT IMPORTED"
data["ansible_host"] = result._host.name
data["ansible_play_id"] = self.play_id
data["ansible_play_name"] = self.play_name
data["imported_file"] = missing_file
if self.ls_format_version == "v2":
self.logger.info("NOT IMPORTED | %s", missing_file, extra=data)
@ -324,75 +310,81 @@ class CallbackModule(CallbackBase):
self.logger.info("ansible import", extra=data)
def v2_runner_on_failed(self, result, **kwargs):
task_name = str(result._task).replace('TASK: ', '').replace('HANDLER: ', '')
task_name = str(result._task).replace("TASK: ", "").replace("HANDLER: ", "")
data = self.base_data.copy()
if 'changed' in result._result.keys():
data['ansible_changed'] = result._result['changed']
if "changed" in result._result.keys():
data["ansible_changed"] = result._result["changed"]
else:
data['ansible_changed'] = False
data["ansible_changed"] = False
data['ansible_type'] = "task"
data['status'] = "FAILED"
data['ansible_host'] = result._host.name
data['ansible_play_id'] = self.play_id
data['ansible_play_name'] = self.play_name
data['ansible_task'] = task_name
data['ansible_task_id'] = self.task_id
data['ansible_result'] = self._dump_results(result._result)
data["ansible_type"] = "task"
data["status"] = "FAILED"
data["ansible_host"] = result._host.name
data["ansible_play_id"] = self.play_id
data["ansible_play_name"] = self.play_name
data["ansible_task"] = task_name
data["ansible_task_id"] = self.task_id
data["ansible_result"] = self._dump_results(result._result)
self.errors += 1
if self.ls_format_version == "v2":
self.logger.error(
"TASK FAILED | %s | HOST | %s | RESULT | %s",
task_name, self.hostname,
self._dump_results(result._result), extra=data
task_name,
self.hostname,
self._dump_results(result._result),
extra=data,
)
else:
self.logger.error("ansible failed", extra=data)
def v2_runner_on_unreachable(self, result, **kwargs):
task_name = str(result._task).replace('TASK: ', '').replace('HANDLER: ', '')
task_name = str(result._task).replace("TASK: ", "").replace("HANDLER: ", "")
data = self.base_data.copy()
data['ansible_type'] = "task"
data['status'] = "UNREACHABLE"
data['ansible_host'] = result._host.name
data['ansible_play_id'] = self.play_id
data['ansible_play_name'] = self.play_name
data['ansible_task'] = task_name
data['ansible_task_id'] = self.task_id
data['ansible_result'] = self._dump_results(result._result)
data["ansible_type"] = "task"
data["status"] = "UNREACHABLE"
data["ansible_host"] = result._host.name
data["ansible_play_id"] = self.play_id
data["ansible_play_name"] = self.play_name
data["ansible_task"] = task_name
data["ansible_task_id"] = self.task_id
data["ansible_result"] = self._dump_results(result._result)
self.errors += 1
if self.ls_format_version == "v2":
self.logger.error(
"UNREACHABLE | %s | HOST | %s | RESULT | %s",
task_name, self.hostname,
self._dump_results(result._result), extra=data
task_name,
self.hostname,
self._dump_results(result._result),
extra=data,
)
else:
self.logger.error("ansible unreachable", extra=data)
def v2_runner_on_async_failed(self, result, **kwargs):
task_name = str(result._task).replace('TASK: ', '').replace('HANDLER: ', '')
task_name = str(result._task).replace("TASK: ", "").replace("HANDLER: ", "")
data = self.base_data.copy()
data['ansible_type'] = "task"
data['status'] = "FAILED"
data['ansible_host'] = result._host.name
data['ansible_play_id'] = self.play_id
data['ansible_play_name'] = self.play_name
data['ansible_task'] = task_name
data['ansible_task_id'] = self.task_id
data['ansible_result'] = self._dump_results(result._result)
data["ansible_type"] = "task"
data["status"] = "FAILED"
data["ansible_host"] = result._host.name
data["ansible_play_id"] = self.play_id
data["ansible_play_name"] = self.play_name
data["ansible_task"] = task_name
data["ansible_task_id"] = self.task_id
data["ansible_result"] = self._dump_results(result._result)
self.errors += 1
if self.ls_format_version == "v2":
self.logger.error(
"ASYNC FAILED | %s | HOST | %s | RESULT | %s",
task_name, self.hostname,
self._dump_results(result._result), extra=data
task_name,
self.hostname,
self._dump_results(result._result),
extra=data,
)
else:
self.logger.error("ansible async", extra=data)

View file

@ -1,4 +1,3 @@
# Copyright (c) 2012, Dag Wieers <dag@wieers.com>
# 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
@ -92,33 +91,33 @@ from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
''' This Ansible callback plugin mails errors to interested parties. '''
"""This Ansible callback plugin mails errors to interested parties."""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.mail'
CALLBACK_TYPE = "notification"
CALLBACK_NAME = "community.general.mail"
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
super().__init__(display=display)
self.sender = None
self.to = 'root'
self.smtphost = os.getenv('SMTPHOST', 'localhost')
self.to = "root"
self.smtphost = os.getenv("SMTPHOST", "localhost")
self.smtpport = 25
self.cc = None
self.bcc = None
def set_options(self, task_keys=None, var_options=None, direct=None):
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
self.sender = self.get_option('sender')
self.to = self.get_option('to')
self.smtphost = self.get_option('mta')
self.smtpport = self.get_option('mtaport')
self.cc = self.get_option('cc')
self.bcc = self.get_option('bcc')
self.sender = self.get_option("sender")
self.to = self.get_option("to")
self.smtphost = self.get_option("mta")
self.smtpport = self.get_option("mtaport")
self.cc = self.get_option("cc")
self.bcc = self.get_option("bcc")
def mail(self, subject='Ansible error mail', body=None):
def mail(self, subject="Ansible error mail", body=None):
if body is None:
body = subject
@ -132,14 +131,14 @@ class CallbackModule(CallbackBase):
if self.bcc:
bcc_addresses = email.utils.getaddresses(self.bcc)
content = f'Date: {email.utils.formatdate()}\n'
content += f'From: {email.utils.formataddr(sender_address)}\n'
content = f"Date: {email.utils.formatdate()}\n"
content += f"From: {email.utils.formataddr(sender_address)}\n"
if self.to:
content += f"To: {', '.join([email.utils.formataddr(pair) for pair in to_addresses])}\n"
if self.cc:
content += f"Cc: {', '.join([email.utils.formataddr(pair) for pair in cc_addresses])}\n"
content += f"Message-ID: {email.utils.make_msgid(domain=self.get_option('message_id_domain'))}\n"
content += f'Subject: {subject.strip()}\n\n'
content += f"Subject: {subject.strip()}\n\n"
content += body
addresses = to_addresses
@ -149,23 +148,23 @@ class CallbackModule(CallbackBase):
addresses += bcc_addresses
if not addresses:
self._display.warning('No receiver has been specified for the mail callback plugin.')
self._display.warning("No receiver has been specified for the mail callback plugin.")
smtp.sendmail(self.sender, [address for name, address in addresses], to_bytes(content))
smtp.quit()
def subject_msg(self, multiline, failtype, linenr):
msg = multiline.strip('\r\n').splitlines()[linenr]
return f'{failtype}: {msg}'
msg = multiline.strip("\r\n").splitlines()[linenr]
return f"{failtype}: {msg}"
def indent(self, multiline, indent=8):
return re.sub('^', ' ' * indent, multiline, flags=re.MULTILINE)
return re.sub("^", " " * indent, multiline, flags=re.MULTILINE)
def body_blob(self, multiline, texttype):
''' Turn some text output in a well-indented block for sending in a mail body '''
intro = f'with the following {texttype}:\n\n'
blob = "\n".join(multiline.strip('\r\n').splitlines())
"""Turn some text output in a well-indented block for sending in a mail body"""
intro = f"with the following {texttype}:\n\n"
blob = "\n".join(multiline.strip("\r\n").splitlines())
return f"{intro}{self.indent(blob)}\n"
def mail_result(self, result, failtype):
@ -176,83 +175,87 @@ class CallbackModule(CallbackBase):
# Add subject
if self.itembody:
subject = self.itemsubject
elif result._result.get('failed_when_result') is True:
elif result._result.get("failed_when_result") is True:
subject = "Failed due to 'failed_when' condition"
elif result._result.get('msg'):
subject = self.subject_msg(result._result['msg'], failtype, 0)
elif result._result.get('stderr'):
subject = self.subject_msg(result._result['stderr'], failtype, -1)
elif result._result.get('stdout'):
subject = self.subject_msg(result._result['stdout'], failtype, -1)
elif result._result.get('exception'): # Unrelated exceptions are added to output :-/
subject = self.subject_msg(result._result['exception'], failtype, -1)
elif result._result.get("msg"):
subject = self.subject_msg(result._result["msg"], failtype, 0)
elif result._result.get("stderr"):
subject = self.subject_msg(result._result["stderr"], failtype, -1)
elif result._result.get("stdout"):
subject = self.subject_msg(result._result["stdout"], failtype, -1)
elif result._result.get("exception"): # Unrelated exceptions are added to output :-/
subject = self.subject_msg(result._result["exception"], failtype, -1)
else:
subject = f'{failtype}: {result._task.name or result._task.action}'
subject = f"{failtype}: {result._task.name or result._task.action}"
# Make playbook name visible (e.g. in Outlook/Gmail condensed view)
body = f'Playbook: {os.path.basename(self.playbook._file_name)}\n'
body = f"Playbook: {os.path.basename(self.playbook._file_name)}\n"
if result._task.name:
body += f'Task: {result._task.name}\n'
body += f'Module: {result._task.action}\n'
body += f'Host: {host}\n'
body += '\n'
body += f"Task: {result._task.name}\n"
body += f"Module: {result._task.action}\n"
body += f"Host: {host}\n"
body += "\n"
# Add task information (as much as possible)
body += 'The following task failed:\n\n'
if 'invocation' in result._result:
body += self.indent(f"{result._task.action}: {json.dumps(result._result['invocation']['module_args'], indent=4)}\n")
body += "The following task failed:\n\n"
if "invocation" in result._result:
body += self.indent(
f"{result._task.action}: {json.dumps(result._result['invocation']['module_args'], indent=4)}\n"
)
elif result._task.name:
body += self.indent(f'{result._task.name} ({result._task.action})\n')
body += self.indent(f"{result._task.name} ({result._task.action})\n")
else:
body += self.indent(f'{result._task.action}\n')
body += '\n'
body += self.indent(f"{result._task.action}\n")
body += "\n"
# Add item / message
if self.itembody:
body += self.itembody
elif result._result.get('failed_when_result') is True:
fail_cond_list = '\n- '.join(result._task.failed_when)
elif result._result.get("failed_when_result") is True:
fail_cond_list = "\n- ".join(result._task.failed_when)
fail_cond = self.indent(f"failed_when:\n- {fail_cond_list}")
body += f"due to the following condition:\n\n{fail_cond}\n\n"
elif result._result.get('msg'):
body += self.body_blob(result._result['msg'], 'message')
elif result._result.get("msg"):
body += self.body_blob(result._result["msg"], "message")
# Add stdout / stderr / exception / warnings / deprecations
if result._result.get('stdout'):
body += self.body_blob(result._result['stdout'], 'standard output')
if result._result.get('stderr'):
body += self.body_blob(result._result['stderr'], 'error output')
if result._result.get('exception'): # Unrelated exceptions are added to output :-/
body += self.body_blob(result._result['exception'], 'exception')
if result._result.get('warnings'):
for i in range(len(result._result.get('warnings'))):
body += self.body_blob(result._result['warnings'][i], f'exception {i + 1}')
if result._result.get('deprecations'):
for i in range(len(result._result.get('deprecations'))):
body += self.body_blob(result._result['deprecations'][i], f'exception {i + 1}')
if result._result.get("stdout"):
body += self.body_blob(result._result["stdout"], "standard output")
if result._result.get("stderr"):
body += self.body_blob(result._result["stderr"], "error output")
if result._result.get("exception"): # Unrelated exceptions are added to output :-/
body += self.body_blob(result._result["exception"], "exception")
if result._result.get("warnings"):
for i in range(len(result._result.get("warnings"))):
body += self.body_blob(result._result["warnings"][i], f"exception {i + 1}")
if result._result.get("deprecations"):
for i in range(len(result._result.get("deprecations"))):
body += self.body_blob(result._result["deprecations"][i], f"exception {i + 1}")
body += 'and a complete dump of the error:\n\n'
body += self.indent(f'{failtype}: {json.dumps(result._result, cls=AnsibleJSONEncoder, indent=4)}')
body += "and a complete dump of the error:\n\n"
body += self.indent(f"{failtype}: {json.dumps(result._result, cls=AnsibleJSONEncoder, indent=4)}")
self.mail(subject=subject, body=body)
def v2_playbook_on_start(self, playbook):
self.playbook = playbook
self.itembody = ''
self.itembody = ""
def v2_runner_on_failed(self, result, ignore_errors=False):
if ignore_errors:
return
self.mail_result(result, 'Failed')
self.mail_result(result, "Failed")
def v2_runner_on_unreachable(self, result):
self.mail_result(result, 'Unreachable')
self.mail_result(result, "Unreachable")
def v2_runner_on_async_failed(self, result):
self.mail_result(result, 'Async failure')
self.mail_result(result, "Async failure")
def v2_runner_item_on_failed(self, result):
# Pass item information to task failure
self.itemsubject = result._result['msg']
self.itembody += self.body_blob(json.dumps(result._result, cls=AnsibleJSONEncoder, indent=4), f"failed item dump '{result._result['item']}'")
self.itemsubject = result._result["msg"]
self.itembody += self.body_blob(
json.dumps(result._result, cls=AnsibleJSONEncoder, indent=4), f"failed item dump '{result._result['item']}'"
)

View file

@ -73,13 +73,13 @@ from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
'''
"""
send ansible-playbook to Nagios server using nrdp protocol
'''
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.nrdp'
CALLBACK_TYPE = "notification"
CALLBACK_NAME = "community.general.nrdp"
CALLBACK_NEEDS_WHITELIST = True
# Nagios states
@ -98,25 +98,26 @@ class CallbackModule(CallbackBase):
def set_options(self, task_keys=None, var_options=None, direct=None):
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
self.url = self.get_option('url')
if not self.url.endswith('/'):
self.url += '/'
self.token = self.get_option('token')
self.hostname = self.get_option('hostname')
self.servicename = self.get_option('servicename')
self.validate_nrdp_certs = self.get_option('validate_certs')
self.url = self.get_option("url")
if not self.url.endswith("/"):
self.url += "/"
self.token = self.get_option("token")
self.hostname = self.get_option("hostname")
self.servicename = self.get_option("servicename")
self.validate_nrdp_certs = self.get_option("validate_certs")
if (self.url or self.token or self.hostname or
self.servicename) is None:
self._display.warning("NRDP callback wants the NRDP_URL,"
" NRDP_TOKEN, NRDP_HOSTNAME,"
" NRDP_SERVICENAME"
" environment variables'."
" The NRDP callback plugin is disabled.")
if (self.url or self.token or self.hostname or self.servicename) is None:
self._display.warning(
"NRDP callback wants the NRDP_URL,"
" NRDP_TOKEN, NRDP_HOSTNAME,"
" NRDP_SERVICENAME"
" environment variables'."
" The NRDP callback plugin is disabled."
)
self.disabled = True
def _send_nrdp(self, state, msg):
'''
"""
nrpd service check send XMLDATA like this:
<?xml version='1.0'?>
<checkresults>
@ -127,7 +128,7 @@ class CallbackModule(CallbackBase):
<output>WARNING: Danger Will Robinson!|perfdata</output>
</checkresult>
</checkresults>
'''
"""
xmldata = "<?xml version='1.0'?>\n"
xmldata += "<checkresults>\n"
xmldata += "<checkresult type='service'>\n"
@ -138,31 +139,24 @@ class CallbackModule(CallbackBase):
xmldata += "</checkresult>\n"
xmldata += "</checkresults>\n"
body = {
'cmd': 'submitcheck',
'token': self.token,
'XMLDATA': to_bytes(xmldata)
}
body = {"cmd": "submitcheck", "token": self.token, "XMLDATA": to_bytes(xmldata)}
try:
response = open_url(self.url,
data=urlencode(body),
method='POST',
validate_certs=self.validate_nrdp_certs)
response = open_url(self.url, data=urlencode(body), method="POST", validate_certs=self.validate_nrdp_certs)
return response.read()
except Exception as ex:
self._display.warning(f"NRDP callback cannot send result {ex}")
def v2_playbook_on_play_start(self, play):
'''
"""
Display Playbook and play start messages
'''
"""
self.play = play
def v2_playbook_on_stats(self, stats):
'''
"""
Display info about playbook statistics
'''
"""
name = self.play
gstats = ""
hosts = sorted(stats.processed.keys())
@ -170,13 +164,14 @@ class CallbackModule(CallbackBase):
for host in hosts:
stat = stats.summarize(host)
gstats += (
f"'{host}_ok'={stat['ok']} '{host}_changed'={stat['changed']} '{host}_unreachable'={stat['unreachable']} '{host}_failed'={stat['failures']} "
f"'{host}_ok'={stat['ok']} '{host}_changed'={stat['changed']}"
f" '{host}_unreachable'={stat['unreachable']} '{host}_failed'={stat['failures']} "
)
# Critical when failed tasks or unreachable host
critical += stat['failures']
critical += stat['unreachable']
critical += stat["failures"]
critical += stat["unreachable"]
# Warning when changed tasks
warning += stat['changed']
warning += stat["changed"]
msg = f"{name} | {gstats}"
if critical:

View file

@ -20,11 +20,10 @@ from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
'''
"""
This callback won't print messages to stdout when new callback events are received.
'''
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'community.general.null'
CALLBACK_TYPE = "stdout"
CALLBACK_NAME = "community.general.null"

View file

@ -155,13 +155,8 @@ try:
from opentelemetry.trace.status import Status, StatusCode
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
BatchSpanProcessor,
SimpleSpanProcessor
)
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter
)
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
except ImportError as imp_exc:
OTEL_LIBRARY_IMPORT_ERROR = imp_exc
else:
@ -186,9 +181,9 @@ class TaskData:
def add_host(self, host):
if host.uuid in self.host_data:
if host.status == 'included':
if host.status == "included":
# concatenate task include output from multiple items
host.result = f'{self.host_data[host.uuid].result}\n{host.result}'
host.result = f"{self.host_data[host.uuid].result}\n{host.result}"
else:
return
@ -223,11 +218,11 @@ class OpenTelemetrySource:
def traceparent_context(self, traceparent):
carrier = dict()
carrier['traceparent'] = traceparent
carrier["traceparent"] = traceparent
return TraceContextTextMapPropagator().extract(carrier=carrier)
def start_task(self, tasks_data, hide_task_arguments, play_name, task):
""" record the start of a task for one or more hosts """
"""record the start of a task for one or more hosts"""
uuid = task._uuid
@ -245,33 +240,35 @@ class OpenTelemetrySource:
tasks_data[uuid] = TaskData(uuid, name, path, play_name, action, args)
def finish_task(self, tasks_data, status, result, dump):
""" record the results of a task for a single host """
"""record the results of a task for a single host"""
task_uuid = result._task._uuid
if hasattr(result, '_host') and result._host is not None:
if hasattr(result, "_host") and result._host is not None:
host_uuid = result._host._uuid
host_name = result._host.name
else:
host_uuid = 'include'
host_name = 'include'
host_uuid = "include"
host_name = "include"
task = tasks_data[task_uuid]
task.dump = dump
task.add_host(HostData(host_uuid, host_name, status, result))
def generate_distributed_traces(self,
otel_service_name,
ansible_playbook,
tasks_data,
status,
traceparent,
disable_logs,
disable_attributes_in_logs,
otel_exporter_otlp_traces_protocol,
store_spans_in_file):
""" generate distributed traces from the collected TaskData and HostData """
def generate_distributed_traces(
self,
otel_service_name,
ansible_playbook,
tasks_data,
status,
traceparent,
disable_logs,
disable_attributes_in_logs,
otel_exporter_otlp_traces_protocol,
store_spans_in_file,
):
"""generate distributed traces from the collected TaskData and HostData"""
tasks = []
parent_start_time = None
@ -280,18 +277,14 @@ class OpenTelemetrySource:
parent_start_time = task.start
tasks.append(task)
trace.set_tracer_provider(
TracerProvider(
resource=Resource.create({SERVICE_NAME: otel_service_name})
)
)
trace.set_tracer_provider(TracerProvider(resource=Resource.create({SERVICE_NAME: otel_service_name})))
otel_exporter = None
if store_spans_in_file:
otel_exporter = InMemorySpanExporter()
processor = SimpleSpanProcessor(otel_exporter)
else:
if otel_exporter_otlp_traces_protocol == 'grpc':
if otel_exporter_otlp_traces_protocol == "grpc":
otel_exporter = GRPCOTLPSpanExporter()
else:
otel_exporter = HTTPOTLPSpanExporter()
@ -301,8 +294,12 @@ class OpenTelemetrySource:
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span(ansible_playbook, context=self.traceparent_context(traceparent),
start_time=parent_start_time, kind=SpanKind.SERVER) as parent:
with tracer.start_as_current_span(
ansible_playbook,
context=self.traceparent_context(traceparent),
start_time=parent_start_time,
kind=SpanKind.SERVER,
) as parent:
parent.set_status(status)
# Populate trace metadata attributes
parent.set_attribute("ansible.version", ansible_version)
@ -319,36 +316,38 @@ class OpenTelemetrySource:
return otel_exporter
def update_span_data(self, task_data, host_data, span, disable_logs, disable_attributes_in_logs):
""" update the span with the given TaskData and HostData """
"""update the span with the given TaskData and HostData"""
name = f'[{host_data.name}] {task_data.play}: {task_data.name}'
name = f"[{host_data.name}] {task_data.play}: {task_data.name}"
message = 'success'
message = "success"
res = {}
rc = 0
status = Status(status_code=StatusCode.OK)
if host_data.status != 'included':
if host_data.status != "included":
# Support loops
enriched_error_message = None
if 'results' in host_data.result._result:
if host_data.status == 'failed':
message = self.get_error_message_from_results(host_data.result._result['results'], task_data.action)
enriched_error_message = self.enrich_error_message_from_results(host_data.result._result['results'], task_data.action)
if "results" in host_data.result._result:
if host_data.status == "failed":
message = self.get_error_message_from_results(host_data.result._result["results"], task_data.action)
enriched_error_message = self.enrich_error_message_from_results(
host_data.result._result["results"], task_data.action
)
else:
res = host_data.result._result
rc = res.get('rc', 0)
if host_data.status == 'failed':
rc = res.get("rc", 0)
if host_data.status == "failed":
message = self.get_error_message(res)
enriched_error_message = self.enrich_error_message(res)
if host_data.status == 'failed':
if host_data.status == "failed":
status = Status(status_code=StatusCode.ERROR, description=message)
# Record an exception with the task message
span.record_exception(BaseException(enriched_error_message))
elif host_data.status == 'skipped':
message = res['skip_reason'] if 'skip_reason' in res else 'skipped'
elif host_data.status == "skipped":
message = res["skip_reason"] if "skip_reason" in res else "skipped"
status = Status(status_code=StatusCode.UNSET)
elif host_data.status == 'ignored':
elif host_data.status == "ignored":
status = Status(status_code=StatusCode.UNSET)
span.set_status(status)
@ -360,7 +359,7 @@ class OpenTelemetrySource:
"ansible.task.name": name,
"ansible.task.result": rc,
"ansible.task.host.name": host_data.name,
"ansible.task.host.status": host_data.status
"ansible.task.host.status": host_data.status,
}
if isinstance(task_data.args, dict) and "gather_facts" not in task_data.action:
names = tuple(self.transform_ansible_unicode_to_str(k) for k in task_data.args.keys())
@ -380,10 +379,10 @@ class OpenTelemetrySource:
span.end(end_time=host_data.finish)
def set_span_attributes(self, span, attributes):
""" update the span attributes with the given attributes if not None """
"""update the span attributes with the given attributes if not None"""
if span is None and self._display is not None:
self._display.warning('span object is None. Please double check if that is expected.')
self._display.warning("span object is None. Please double check if that is expected.")
else:
if attributes is not None:
span.set_attributes(attributes)
@ -411,7 +410,18 @@ class OpenTelemetrySource:
@staticmethod
def url_from_args(args):
# the order matters
url_args = ("url", "api_url", "baseurl", "repo", "server_url", "chart_repo_url", "registry_url", "endpoint", "uri", "updates_url")
url_args = (
"url",
"api_url",
"baseurl",
"repo",
"server_url",
"chart_repo_url",
"registry_url",
"endpoint",
"uri",
"updates_url",
)
for arg in url_args:
if args is not None and args.get(arg):
return args.get(arg)
@ -436,33 +446,33 @@ class OpenTelemetrySource:
@staticmethod
def get_error_message(result):
if result.get('exception') is not None:
return OpenTelemetrySource._last_line(result['exception'])
return result.get('msg', 'failed')
if result.get("exception") is not None:
return OpenTelemetrySource._last_line(result["exception"])
return result.get("msg", "failed")
@staticmethod
def get_error_message_from_results(results, action):
for result in results:
if result.get('failed', False):
if result.get("failed", False):
return f"{action}({result.get('item', 'none')}) - {OpenTelemetrySource.get_error_message(result)}"
@staticmethod
def _last_line(text):
lines = text.strip().split('\n')
lines = text.strip().split("\n")
return lines[-1]
@staticmethod
def enrich_error_message(result):
message = result.get('msg', 'failed')
exception = result.get('exception')
stderr = result.get('stderr')
return f"message: \"{message}\"\nexception: \"{exception}\"\nstderr: \"{stderr}\""
message = result.get("msg", "failed")
exception = result.get("exception")
stderr = result.get("stderr")
return f'message: "{message}"\nexception: "{exception}"\nstderr: "{stderr}"'
@staticmethod
def enrich_error_message_from_results(results, action):
message = ""
for result in results:
if result.get('failed', False):
if result.get("failed", False):
message = f"{action}({result.get('item', 'none')}) - {OpenTelemetrySource.enrich_error_message(result)}\n{message}"
return message
@ -473,8 +483,8 @@ class CallbackModule(CallbackBase):
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.opentelemetry'
CALLBACK_TYPE = "notification"
CALLBACK_NAME = "community.general.opentelemetry"
CALLBACK_NEEDS_ENABLED = True
def __init__(self, display=None):
@ -494,7 +504,7 @@ class CallbackModule(CallbackBase):
if OTEL_LIBRARY_IMPORT_ERROR:
raise AnsibleError(
'The `opentelemetry-api`, `opentelemetry-exporter-otlp` or `opentelemetry-sdk` must be installed to use this plugin'
"The `opentelemetry-api`, `opentelemetry-exporter-otlp` or `opentelemetry-sdk` must be installed to use this plugin"
) from OTEL_LIBRARY_IMPORT_ERROR
self.tasks_data = OrderedDict()
@ -504,33 +514,33 @@ class CallbackModule(CallbackBase):
def set_options(self, task_keys=None, var_options=None, direct=None):
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
environment_variable = self.get_option('enable_from_environment')
if environment_variable is not None and os.environ.get(environment_variable, 'false').lower() != 'true':
environment_variable = self.get_option("enable_from_environment")
if environment_variable is not None and os.environ.get(environment_variable, "false").lower() != "true":
self.disabled = True
self._display.warning(
f"The `enable_from_environment` option has been set and {environment_variable} is not enabled. Disabling the `opentelemetry` callback plugin."
)
self.hide_task_arguments = self.get_option('hide_task_arguments')
self.hide_task_arguments = self.get_option("hide_task_arguments")
self.disable_attributes_in_logs = self.get_option('disable_attributes_in_logs')
self.disable_attributes_in_logs = self.get_option("disable_attributes_in_logs")
self.disable_logs = self.get_option('disable_logs')
self.disable_logs = self.get_option("disable_logs")
self.store_spans_in_file = self.get_option('store_spans_in_file')
self.store_spans_in_file = self.get_option("store_spans_in_file")
self.otel_service_name = self.get_option('otel_service_name')
self.otel_service_name = self.get_option("otel_service_name")
if not self.otel_service_name:
self.otel_service_name = 'ansible'
self.otel_service_name = "ansible"
# See https://github.com/open-telemetry/opentelemetry-specification/issues/740
self.traceparent = self.get_option('traceparent')
self.traceparent = self.get_option("traceparent")
self.otel_exporter_otlp_traces_protocol = self.get_option('otel_exporter_otlp_traces_protocol')
self.otel_exporter_otlp_traces_protocol = self.get_option("otel_exporter_otlp_traces_protocol")
def dump_results(self, task, result):
""" dump the results if disable_logs is not enabled """
"""dump the results if disable_logs is not enabled"""
if self.disable_logs:
return ""
# ansible.builtin.uri contains the response in the json field
@ -550,74 +560,40 @@ class CallbackModule(CallbackBase):
self.play_name = play.get_name()
def v2_runner_on_no_hosts(self, task):
self.opentelemetry.start_task(
self.tasks_data,
self.hide_task_arguments,
self.play_name,
task
)
self.opentelemetry.start_task(self.tasks_data, self.hide_task_arguments, self.play_name, task)
def v2_playbook_on_task_start(self, task, is_conditional):
self.opentelemetry.start_task(
self.tasks_data,
self.hide_task_arguments,
self.play_name,
task
)
self.opentelemetry.start_task(self.tasks_data, self.hide_task_arguments, self.play_name, task)
def v2_playbook_on_cleanup_task_start(self, task):
self.opentelemetry.start_task(
self.tasks_data,
self.hide_task_arguments,
self.play_name,
task
)
self.opentelemetry.start_task(self.tasks_data, self.hide_task_arguments, self.play_name, task)
def v2_playbook_on_handler_task_start(self, task):
self.opentelemetry.start_task(
self.tasks_data,
self.hide_task_arguments,
self.play_name,
task
)
self.opentelemetry.start_task(self.tasks_data, self.hide_task_arguments, self.play_name, task)
def v2_runner_on_failed(self, result, ignore_errors=False):
if ignore_errors:
status = 'ignored'
status = "ignored"
else:
status = 'failed'
status = "failed"
self.errors += 1
self.opentelemetry.finish_task(
self.tasks_data,
status,
result,
self.dump_results(self.tasks_data[result._task._uuid], result)
self.tasks_data, status, result, self.dump_results(self.tasks_data[result._task._uuid], result)
)
def v2_runner_on_ok(self, result):
self.opentelemetry.finish_task(
self.tasks_data,
'ok',
result,
self.dump_results(self.tasks_data[result._task._uuid], result)
self.tasks_data, "ok", result, self.dump_results(self.tasks_data[result._task._uuid], result)
)
def v2_runner_on_skipped(self, result):
self.opentelemetry.finish_task(
self.tasks_data,
'skipped',
result,
self.dump_results(self.tasks_data[result._task._uuid], result)
self.tasks_data, "skipped", result, self.dump_results(self.tasks_data[result._task._uuid], result)
)
def v2_playbook_on_include(self, included_file):
self.opentelemetry.finish_task(
self.tasks_data,
'included',
included_file,
""
)
self.opentelemetry.finish_task(self.tasks_data, "included", included_file, "")
def v2_playbook_on_stats(self, stats):
if self.errors == 0:
@ -633,7 +609,7 @@ class CallbackModule(CallbackBase):
self.disable_logs,
self.disable_attributes_in_logs,
self.otel_exporter_otlp_traces_protocol,
self.store_spans_in_file
self.store_spans_in_file,
)
if self.store_spans_in_file:

View file

@ -37,9 +37,10 @@ class CallbackModule(CallbackBase):
"""
This callback module tells you how long your plays ran for.
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'community.general.print_task'
CALLBACK_TYPE = "aggregate"
CALLBACK_NAME = "community.general.print_task"
CALLBACK_NEEDS_ENABLED = True
@ -48,7 +49,7 @@ class CallbackModule(CallbackBase):
self._printed_message = False
def _print_task(self, task):
if hasattr(task, '_ds'):
if hasattr(task, "_ds"):
task_snippet = load(str([task._ds.copy()]), Loader=SafeLoader)
task_yaml = dump(task_snippet, sort_keys=False, Dumper=SafeDumper)
self._display.display(f"\n{task_yaml}\n")

View file

@ -30,13 +30,13 @@ class CallbackModule(CallbackBase):
"""
makes Ansible much more exciting.
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.say'
CALLBACK_TYPE = "notification"
CALLBACK_NAME = "community.general.say"
CALLBACK_NEEDS_WHITELIST = True
def __init__(self):
super().__init__()
self.FAILED_VOICE = None
@ -45,21 +45,23 @@ class CallbackModule(CallbackBase):
self.LASER_VOICE = None
try:
self.synthesizer = get_bin_path('say')
if platform.system() != 'Darwin':
self.synthesizer = get_bin_path("say")
if platform.system() != "Darwin":
# 'say' binary available, it might be GNUstep tool which doesn't support 'voice' parameter
self._display.warning(f"'say' executable found but system is '{platform.system()}': ignoring voice parameter")
self._display.warning(
f"'say' executable found but system is '{platform.system()}': ignoring voice parameter"
)
else:
self.FAILED_VOICE = 'Zarvox'
self.REGULAR_VOICE = 'Trinoids'
self.HAPPY_VOICE = 'Cellos'
self.LASER_VOICE = 'Princess'
self.FAILED_VOICE = "Zarvox"
self.REGULAR_VOICE = "Trinoids"
self.HAPPY_VOICE = "Cellos"
self.LASER_VOICE = "Princess"
except ValueError:
try:
self.synthesizer = get_bin_path('espeak')
self.FAILED_VOICE = 'klatt'
self.HAPPY_VOICE = 'f5'
self.LASER_VOICE = 'whisper'
self.synthesizer = get_bin_path("espeak")
self.FAILED_VOICE = "klatt"
self.HAPPY_VOICE = "f5"
self.LASER_VOICE = "whisper"
except ValueError:
self.synthesizer = None
@ -67,12 +69,14 @@ class CallbackModule(CallbackBase):
# ansible will not call any callback if disabled is set to True
if not self.synthesizer:
self.disabled = True
self._display.warning(f"Unable to find either 'say' or 'espeak' executable, plugin {os.path.basename(__file__)} disabled")
self._display.warning(
f"Unable to find either 'say' or 'espeak' executable, plugin {os.path.basename(__file__)} disabled"
)
def say(self, msg, voice):
cmd = [self.synthesizer, msg]
if voice:
cmd.extend(('-v', voice))
cmd.extend(("-v", voice))
subprocess.call(cmd)
def runner_on_failed(self, host, res, ignore_errors=False):

View file

@ -45,14 +45,14 @@ from ansible.module_utils.common.text.converters import to_text
DONT_COLORIZE = False
COLORS = {
'normal': '\033[0m',
'ok': f'\x1b[{C.COLOR_CODES[C.COLOR_OK]}m', # type: ignore
'bold': '\033[1m',
'not_so_bold': '\033[1m\033[34m',
'changed': f'\x1b[{C.COLOR_CODES[C.COLOR_CHANGED]}m', # type: ignore
'failed': f'\x1b[{C.COLOR_CODES[C.COLOR_ERROR]}m', # type: ignore
'endc': '\033[0m',
'skipped': f'\x1b[{C.COLOR_CODES[C.COLOR_SKIP]}m', # type: ignore
"normal": "\033[0m",
"ok": f"\x1b[{C.COLOR_CODES[C.COLOR_OK]}m", # type: ignore
"bold": "\033[1m",
"not_so_bold": "\033[1m\033[34m",
"changed": f"\x1b[{C.COLOR_CODES[C.COLOR_CHANGED]}m", # type: ignore
"failed": f"\x1b[{C.COLOR_CODES[C.COLOR_ERROR]}m", # type: ignore
"endc": "\033[0m",
"skipped": f"\x1b[{C.COLOR_CODES[C.COLOR_SKIP]}m", # type: ignore
}
@ -78,8 +78,8 @@ class CallbackModule(CallbackBase):
"""selective.py callback plugin."""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'community.general.selective'
CALLBACK_TYPE = "stdout"
CALLBACK_NAME = "community.general.selective"
def __init__(self, display=None):
"""selective.py callback plugin."""
@ -89,11 +89,10 @@ class CallbackModule(CallbackBase):
self.printed_last_task = False
def set_options(self, task_keys=None, var_options=None, direct=None):
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
global DONT_COLORIZE
DONT_COLORIZE = self.get_option('nocolor')
DONT_COLORIZE = self.get_option("nocolor")
def _print_task(self, task_name=None):
if task_name is None:
@ -105,7 +104,7 @@ class CallbackModule(CallbackBase):
if self.last_skipped:
print()
line = f"# {task_name} "
msg = colorize(f"{line}{'*' * (line_length - len(line))}", 'bold')
msg = colorize(f"{line}{'*' * (line_length - len(line))}", "bold")
print(msg)
def _indent_text(self, text, indent_level):
@ -113,48 +112,51 @@ class CallbackModule(CallbackBase):
result_lines = []
for l in lines:
result_lines.append(f"{' ' * indent_level}{l}")
return '\n'.join(result_lines)
return "\n".join(result_lines)
def _print_diff(self, diff, indent_level):
if isinstance(diff, dict):
try:
diff = '\n'.join(difflib.unified_diff(diff['before'].splitlines(),
diff['after'].splitlines(),
fromfile=diff.get('before_header',
'new_file'),
tofile=diff['after_header']))
diff = "\n".join(
difflib.unified_diff(
diff["before"].splitlines(),
diff["after"].splitlines(),
fromfile=diff.get("before_header", "new_file"),
tofile=diff["after_header"],
)
)
except AttributeError:
diff = dict_diff(diff['before'], diff['after'])
diff = dict_diff(diff["before"], diff["after"])
if diff:
diff = colorize(str(diff), 'changed')
diff = colorize(str(diff), "changed")
print(self._indent_text(diff, indent_level + 4))
def _print_host_or_item(self, host_or_item, changed, msg, diff, is_host, error, stdout, stderr):
if is_host:
indent_level = 0
name = colorize(host_or_item.name, 'not_so_bold')
name = colorize(host_or_item.name, "not_so_bold")
else:
indent_level = 4
if isinstance(host_or_item, dict):
if 'key' in host_or_item.keys():
host_or_item = host_or_item['key']
name = colorize(to_text(host_or_item), 'bold')
if "key" in host_or_item.keys():
host_or_item = host_or_item["key"]
name = colorize(to_text(host_or_item), "bold")
if error:
color = 'failed'
change_string = colorize('FAILED!!!', color)
color = "failed"
change_string = colorize("FAILED!!!", color)
else:
color = 'changed' if changed else 'ok'
color = "changed" if changed else "ok"
change_string = colorize(f"changed={changed}", color)
msg = colorize(msg, color)
line_length = 120
spaces = ' ' * (40 - len(name) - indent_level)
spaces = " " * (40 - len(name) - indent_level)
line = f"{' ' * indent_level} * {name}{spaces}- {change_string}"
if len(msg) < 50:
line += f' -- {msg}'
line += f" -- {msg}"
print(f"{line} {'-' * (line_length - len(line))}---------")
else:
print(f"{line} {'-' * (line_length - len(line))}")
@ -163,10 +165,10 @@ class CallbackModule(CallbackBase):
if diff:
self._print_diff(diff, indent_level)
if stdout:
stdout = colorize(stdout, 'failed')
stdout = colorize(stdout, "failed")
print(self._indent_text(stdout, indent_level + 4))
if stderr:
stderr = colorize(stderr, 'failed')
stderr = colorize(stderr, "failed")
print(self._indent_text(stderr, indent_level + 4))
def v2_playbook_on_play_start(self, play):
@ -181,61 +183,61 @@ class CallbackModule(CallbackBase):
def _print_task_result(self, result, error=False, **kwargs):
"""Run when a task finishes correctly."""
if 'print_action' in result._task.tags or error or self._display.verbosity > 1:
if "print_action" in result._task.tags or error or self._display.verbosity > 1:
self._print_task()
self.last_skipped = False
msg = to_text(result._result.get('msg', '')) or\
to_text(result._result.get('reason', ''))
msg = to_text(result._result.get("msg", "")) or to_text(result._result.get("reason", ""))
stderr = [result._result.get('exception', None),
result._result.get('module_stderr', None)]
stderr = [result._result.get("exception", None), result._result.get("module_stderr", None)]
stderr = "\n".join([e for e in stderr if e]).strip()
self._print_host_or_item(result._host,
result._result.get('changed', False),
msg,
result._result.get('diff', None),
is_host=True,
error=error,
stdout=result._result.get('module_stdout', None),
stderr=stderr.strip(),
)
if 'results' in result._result:
for r in result._result['results']:
failed = 'failed' in r and r['failed']
self._print_host_or_item(
result._host,
result._result.get("changed", False),
msg,
result._result.get("diff", None),
is_host=True,
error=error,
stdout=result._result.get("module_stdout", None),
stderr=stderr.strip(),
)
if "results" in result._result:
for r in result._result["results"]:
failed = "failed" in r and r["failed"]
stderr = [r.get('exception', None), r.get('module_stderr', None)]
stderr = [r.get("exception", None), r.get("module_stderr", None)]
stderr = "\n".join([e for e in stderr if e]).strip()
self._print_host_or_item(r[r['ansible_loop_var']],
r.get('changed', False),
to_text(r.get('msg', '')),
r.get('diff', None),
is_host=False,
error=failed,
stdout=r.get('module_stdout', None),
stderr=stderr.strip(),
)
self._print_host_or_item(
r[r["ansible_loop_var"]],
r.get("changed", False),
to_text(r.get("msg", "")),
r.get("diff", None),
is_host=False,
error=failed,
stdout=r.get("module_stdout", None),
stderr=stderr.strip(),
)
else:
self.last_skipped = True
print('.', end="")
print(".", end="")
def v2_playbook_on_stats(self, stats):
"""Display info about playbook statistics."""
print()
self.printed_last_task = False
self._print_task('STATS')
self._print_task("STATS")
hosts = sorted(stats.processed.keys())
for host in hosts:
s = stats.summarize(host)
if s['failures'] or s['unreachable']:
color = 'failed'
elif s['changed']:
color = 'changed'
if s["failures"] or s["unreachable"]:
color = "failed"
elif s["changed"]:
color = "changed"
else:
color = 'ok'
color = "ok"
msg = (
f"{host} : ok={s['ok']}\tchanged={s['changed']}\tfailed={s['failures']}\tunreachable="
@ -250,14 +252,13 @@ class CallbackModule(CallbackBase):
self.last_skipped = False
line_length = 120
spaces = ' ' * (31 - len(result._host.name) - 4)
spaces = " " * (31 - len(result._host.name) - 4)
line = f" * {colorize(result._host.name, 'not_so_bold')}{spaces}- {colorize('skipped', 'skipped')}"
reason = result._result.get('skipped_reason', '') or \
result._result.get('skip_reason', '')
reason = result._result.get("skipped_reason", "") or result._result.get("skip_reason", "")
if len(reason) < 50:
line += f' -- {reason}'
line += f" -- {reason}"
print(f"{line} {'-' * (line_length - len(line))}---------")
else:
print(f"{line} {'-' * (line_length - len(line))}")

View file

@ -70,6 +70,7 @@ from ansible.plugins.callback import CallbackBase
try:
import prettytable
HAS_PRETTYTABLE = True
except ImportError:
HAS_PRETTYTABLE = False
@ -79,20 +80,20 @@ class CallbackModule(CallbackBase):
"""This is an ansible callback plugin that sends status
updates to a Slack channel during playbook execution.
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.slack'
CALLBACK_TYPE = "notification"
CALLBACK_NAME = "community.general.slack"
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
super().__init__(display=display)
if not HAS_PRETTYTABLE:
self.disabled = True
self._display.warning('The `prettytable` python module is not '
'installed. Disabling the Slack callback '
'plugin.')
self._display.warning(
"The `prettytable` python module is not installed. Disabling the Slack callback plugin."
)
self.playbook_name = None
@ -102,34 +103,34 @@ class CallbackModule(CallbackBase):
self.guid = uuid.uuid4().hex[:6]
def set_options(self, task_keys=None, var_options=None, direct=None):
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
self.webhook_url = self.get_option('webhook_url')
self.channel = self.get_option('channel')
self.username = self.get_option('username')
self.show_invocation = (self._display.verbosity > 1)
self.validate_certs = self.get_option('validate_certs')
self.http_agent = self.get_option('http_agent')
self.webhook_url = self.get_option("webhook_url")
self.channel = self.get_option("channel")
self.username = self.get_option("username")
self.show_invocation = self._display.verbosity > 1
self.validate_certs = self.get_option("validate_certs")
self.http_agent = self.get_option("http_agent")
if self.webhook_url is None:
self.disabled = True
self._display.warning('Slack Webhook URL was not provided. The '
'Slack Webhook URL can be provided using '
'the `SLACK_WEBHOOK_URL` environment '
'variable.')
self._display.warning(
"Slack Webhook URL was not provided. The "
"Slack Webhook URL can be provided using "
"the `SLACK_WEBHOOK_URL` environment "
"variable."
)
def send_msg(self, attachments):
headers = {
'Content-type': 'application/json',
"Content-type": "application/json",
}
payload = {
'channel': self.channel,
'username': self.username,
'attachments': attachments,
'parse': 'none',
'icon_url': ('https://cdn2.hubspot.net/hub/330046/'
'file-449187601-png/ansible_badge.png'),
"channel": self.channel,
"username": self.username,
"attachments": attachments,
"parse": "none",
"icon_url": ("https://cdn2.hubspot.net/hub/330046/file-449187601-png/ansible_badge.png"),
}
data = json.dumps(payload)
@ -145,67 +146,63 @@ class CallbackModule(CallbackBase):
)
return response.read()
except Exception as e:
self._display.warning(f'Could not submit message to Slack: {e}')
self._display.warning(f"Could not submit message to Slack: {e}")
def v2_playbook_on_start(self, playbook):
self.playbook_name = os.path.basename(playbook._file_name)
title = [
f'*Playbook initiated* (_{self.guid}_)'
]
title = [f"*Playbook initiated* (_{self.guid}_)"]
invocation_items = []
if context.CLIARGS and self.show_invocation:
tags = context.CLIARGS['tags']
skip_tags = context.CLIARGS['skip_tags']
extra_vars = context.CLIARGS['extra_vars']
subset = context.CLIARGS['subset']
inventory = [os.path.abspath(i) for i in context.CLIARGS['inventory']]
tags = context.CLIARGS["tags"]
skip_tags = context.CLIARGS["skip_tags"]
extra_vars = context.CLIARGS["extra_vars"]
subset = context.CLIARGS["subset"]
inventory = [os.path.abspath(i) for i in context.CLIARGS["inventory"]]
invocation_items.append(f"Inventory: {', '.join(inventory)}")
if tags and tags != ['all']:
if tags and tags != ["all"]:
invocation_items.append(f"Tags: {', '.join(tags)}")
if skip_tags:
invocation_items.append(f"Skip Tags: {', '.join(skip_tags)}")
if subset:
invocation_items.append(f'Limit: {subset}')
invocation_items.append(f"Limit: {subset}")
if extra_vars:
invocation_items.append(f"Extra Vars: {' '.join(extra_vars)}")
title.append(f"by *{context.CLIARGS['remote_user']}*")
title.append(f'\n\n*{self.playbook_name}*')
msg_items = [' '.join(title)]
title.append(f"\n\n*{self.playbook_name}*")
msg_items = [" ".join(title)]
if invocation_items:
_inv_item = '\n'.join(invocation_items)
msg_items.append(f'```\n{_inv_item}\n```')
_inv_item = "\n".join(invocation_items)
msg_items.append(f"```\n{_inv_item}\n```")
msg = '\n'.join(msg_items)
msg = "\n".join(msg_items)
attachments = [{
'fallback': msg,
'fields': [
{
'value': msg
}
],
'color': 'warning',
'mrkdwn_in': ['text', 'fallback', 'fields'],
}]
attachments = [
{
"fallback": msg,
"fields": [{"value": msg}],
"color": "warning",
"mrkdwn_in": ["text", "fallback", "fields"],
}
]
self.send_msg(attachments=attachments)
def v2_playbook_on_play_start(self, play):
"""Display Play start messages"""
name = play.name or f'Play name not specified ({play._uuid})'
msg = f'*Starting play* (_{self.guid}_)\n\n*{name}*'
name = play.name or f"Play name not specified ({play._uuid})"
msg = f"*Starting play* (_{self.guid}_)\n\n*{name}*"
attachments = [
{
'fallback': msg,
'text': msg,
'color': 'warning',
'mrkdwn_in': ['text', 'fallback', 'fields'],
"fallback": msg,
"text": msg,
"color": "warning",
"mrkdwn_in": ["text", "fallback", "fields"],
}
]
self.send_msg(attachments=attachments)
@ -215,8 +212,7 @@ class CallbackModule(CallbackBase):
hosts = sorted(stats.processed.keys())
t = prettytable.PrettyTable(['Host', 'Ok', 'Changed', 'Unreachable',
'Failures', 'Rescued', 'Ignored'])
t = prettytable.PrettyTable(["Host", "Ok", "Changed", "Unreachable", "Failures", "Rescued", "Ignored"])
failures = False
unreachable = False
@ -224,38 +220,28 @@ class CallbackModule(CallbackBase):
for h in hosts:
s = stats.summarize(h)
if s['failures'] > 0:
if s["failures"] > 0:
failures = True
if s['unreachable'] > 0:
if s["unreachable"] > 0:
unreachable = True
t.add_row([h] + [s[k] for k in ['ok', 'changed', 'unreachable',
'failures', 'rescued', 'ignored']])
t.add_row([h] + [s[k] for k in ["ok", "changed", "unreachable", "failures", "rescued", "ignored"]])
attachments = []
msg_items = [
f'*Playbook Complete* (_{self.guid}_)'
]
msg_items = [f"*Playbook Complete* (_{self.guid}_)"]
if failures or unreachable:
color = 'danger'
msg_items.append('\n*Failed!*')
color = "danger"
msg_items.append("\n*Failed!*")
else:
color = 'good'
msg_items.append('\n*Success!*')
color = "good"
msg_items.append("\n*Success!*")
msg_items.append(f'```\n{t}\n```')
msg_items.append(f"```\n{t}\n```")
msg = '\n'.join(msg_items)
msg = "\n".join(msg_items)
attachments.append({
'fallback': msg,
'fields': [
{
'value': msg
}
],
'color': color,
'mrkdwn_in': ['text', 'fallback', 'fields']
})
attachments.append(
{"fallback": msg, "fields": [{"value": msg}], "color": color, "mrkdwn_in": ["text", "fallback", "fields"]}
)
self.send_msg(attachments=attachments)

View file

@ -110,7 +110,7 @@ class SplunkHTTPCollectorSource:
self.user = getpass.getuser()
def send_event(self, url, authtoken, validate_certs, include_milliseconds, batch, state, result, runtime):
if result._task_fields['args'].get('_ansible_check_mode') is True:
if result._task_fields["args"].get("_ansible_check_mode") is True:
self.ansible_check_mode = True
if result._task._role:
@ -118,33 +118,33 @@ class SplunkHTTPCollectorSource:
else:
ansible_role = None
if 'args' in result._task_fields:
del result._task_fields['args']
if "args" in result._task_fields:
del result._task_fields["args"]
data = {}
data['uuid'] = result._task._uuid
data['session'] = self.session
data["uuid"] = result._task._uuid
data["session"] = self.session
if batch is not None:
data['batch'] = batch
data['status'] = state
data["batch"] = batch
data["status"] = state
if include_milliseconds:
time_format = '%Y-%m-%d %H:%M:%S.%f +0000'
time_format = "%Y-%m-%d %H:%M:%S.%f +0000"
else:
time_format = '%Y-%m-%d %H:%M:%S +0000'
time_format = "%Y-%m-%d %H:%M:%S +0000"
data['timestamp'] = now().strftime(time_format)
data['host'] = self.host
data['ip_address'] = self.ip_address
data['user'] = self.user
data['runtime'] = runtime
data['ansible_version'] = ansible_version
data['ansible_check_mode'] = self.ansible_check_mode
data['ansible_host'] = result._host.name
data['ansible_playbook'] = self.ansible_playbook
data['ansible_role'] = ansible_role
data['ansible_task'] = result._task_fields
data['ansible_result'] = result._result
data["timestamp"] = now().strftime(time_format)
data["host"] = self.host
data["ip_address"] = self.ip_address
data["user"] = self.user
data["runtime"] = runtime
data["ansible_version"] = ansible_version
data["ansible_check_mode"] = self.ansible_check_mode
data["ansible_host"] = result._host.name
data["ansible_playbook"] = self.ansible_playbook
data["ansible_role"] = ansible_role
data["ansible_task"] = result._task_fields
data["ansible_result"] = result._result
# This wraps the json payload in and outer json event needed by Splunk
jsondata = json.dumps({"event": data}, cls=AnsibleJSONEncoder, sort_keys=True)
@ -152,19 +152,16 @@ class SplunkHTTPCollectorSource:
open_url(
url,
jsondata,
headers={
'Content-type': 'application/json',
'Authorization': f"Splunk {authtoken}"
},
method='POST',
validate_certs=validate_certs
headers={"Content-type": "application/json", "Authorization": f"Splunk {authtoken}"},
method="POST",
validate_certs=validate_certs,
)
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.splunk'
CALLBACK_TYPE = "notification"
CALLBACK_NAME = "community.general.splunk"
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
@ -178,39 +175,40 @@ class CallbackModule(CallbackBase):
self.splunk = SplunkHTTPCollectorSource()
def _runtime(self, result):
return (
now() -
self.start_datetimes[result._task._uuid]
).total_seconds()
return (now() - self.start_datetimes[result._task._uuid]).total_seconds()
def set_options(self, task_keys=None, var_options=None, direct=None):
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
self.url = self.get_option('url')
self.url = self.get_option("url")
if self.url is None:
self.disabled = True
self._display.warning('Splunk HTTP collector source URL was '
'not provided. The Splunk HTTP collector '
'source URL can be provided using the '
'`SPLUNK_URL` environment variable or '
'in the ansible.cfg file.')
self._display.warning(
"Splunk HTTP collector source URL was "
"not provided. The Splunk HTTP collector "
"source URL can be provided using the "
"`SPLUNK_URL` environment variable or "
"in the ansible.cfg file."
)
self.authtoken = self.get_option('authtoken')
self.authtoken = self.get_option("authtoken")
if self.authtoken is None:
self.disabled = True
self._display.warning('Splunk HTTP collector requires an authentication'
'token. The Splunk HTTP collector '
'authentication token can be provided using the '
'`SPLUNK_AUTHTOKEN` environment variable or '
'in the ansible.cfg file.')
self._display.warning(
"Splunk HTTP collector requires an authentication"
"token. The Splunk HTTP collector "
"authentication token can be provided using the "
"`SPLUNK_AUTHTOKEN` environment variable or "
"in the ansible.cfg file."
)
self.validate_certs = self.get_option('validate_certs')
self.validate_certs = self.get_option("validate_certs")
self.include_milliseconds = self.get_option('include_milliseconds')
self.include_milliseconds = self.get_option("include_milliseconds")
self.batch = self.get_option('batch')
self.batch = self.get_option("batch")
def v2_playbook_on_start(self, playbook):
self.splunk.ansible_playbook = basename(playbook._file_name)
@ -228,9 +226,9 @@ class CallbackModule(CallbackBase):
self.validate_certs,
self.include_milliseconds,
self.batch,
'OK',
"OK",
result,
self._runtime(result)
self._runtime(result),
)
def v2_runner_on_skipped(self, result, **kwargs):
@ -240,9 +238,9 @@ class CallbackModule(CallbackBase):
self.validate_certs,
self.include_milliseconds,
self.batch,
'SKIPPED',
"SKIPPED",
result,
self._runtime(result)
self._runtime(result),
)
def v2_runner_on_failed(self, result, **kwargs):
@ -252,9 +250,9 @@ class CallbackModule(CallbackBase):
self.validate_certs,
self.include_milliseconds,
self.batch,
'FAILED',
"FAILED",
result,
self._runtime(result)
self._runtime(result),
)
def runner_on_async_failed(self, result, **kwargs):
@ -264,9 +262,9 @@ class CallbackModule(CallbackBase):
self.validate_certs,
self.include_milliseconds,
self.batch,
'FAILED',
"FAILED",
result,
self._runtime(result)
self._runtime(result),
)
def v2_runner_on_unreachable(self, result, **kwargs):
@ -276,7 +274,7 @@ class CallbackModule(CallbackBase):
self.validate_certs,
self.include_milliseconds,
self.batch,
'UNREACHABLE',
"UNREACHABLE",
result,
self._runtime(result)
self._runtime(result),
)

View file

@ -67,7 +67,7 @@ class SumologicHTTPCollectorSource:
self.user = getpass.getuser()
def send_event(self, url, state, result, runtime):
if result._task_fields['args'].get('_ansible_check_mode') is True:
if result._task_fields["args"].get("_ansible_check_mode") is True:
self.ansible_check_mode = True
if result._task._role:
@ -75,41 +75,38 @@ class SumologicHTTPCollectorSource:
else:
ansible_role = None
if 'args' in result._task_fields:
del result._task_fields['args']
if "args" in result._task_fields:
del result._task_fields["args"]
data = {}
data['uuid'] = result._task._uuid
data['session'] = self.session
data['status'] = state
data['timestamp'] = now().strftime('%Y-%m-%d %H:%M:%S +0000')
data['host'] = self.host
data['ip_address'] = self.ip_address
data['user'] = self.user
data['runtime'] = runtime
data['ansible_version'] = ansible_version
data['ansible_check_mode'] = self.ansible_check_mode
data['ansible_host'] = result._host.name
data['ansible_playbook'] = self.ansible_playbook
data['ansible_role'] = ansible_role
data['ansible_task'] = result._task_fields
data['ansible_result'] = result._result
data["uuid"] = result._task._uuid
data["session"] = self.session
data["status"] = state
data["timestamp"] = now().strftime("%Y-%m-%d %H:%M:%S +0000")
data["host"] = self.host
data["ip_address"] = self.ip_address
data["user"] = self.user
data["runtime"] = runtime
data["ansible_version"] = ansible_version
data["ansible_check_mode"] = self.ansible_check_mode
data["ansible_host"] = result._host.name
data["ansible_playbook"] = self.ansible_playbook
data["ansible_role"] = ansible_role
data["ansible_task"] = result._task_fields
data["ansible_result"] = result._result
open_url(
url,
data=json.dumps(data, cls=AnsibleJSONEncoder, sort_keys=True),
headers={
'Content-type': 'application/json',
'X-Sumo-Host': data['ansible_host']
},
method='POST'
headers={"Content-type": "application/json", "X-Sumo-Host": data["ansible_host"]},
method="POST",
)
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.sumologic'
CALLBACK_TYPE = "notification"
CALLBACK_NAME = "community.general.sumologic"
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
@ -119,23 +116,22 @@ class CallbackModule(CallbackBase):
self.sumologic = SumologicHTTPCollectorSource()
def _runtime(self, result):
return (
now() -
self.start_datetimes[result._task._uuid]
).total_seconds()
return (now() - self.start_datetimes[result._task._uuid]).total_seconds()
def set_options(self, task_keys=None, var_options=None, direct=None):
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
self.url = self.get_option('url')
self.url = self.get_option("url")
if self.url is None:
self.disabled = True
self._display.warning('Sumologic HTTP collector source URL was '
'not provided. The Sumologic HTTP collector '
'source URL can be provided using the '
'`SUMOLOGIC_URL` environment variable or '
'in the ansible.cfg file.')
self._display.warning(
"Sumologic HTTP collector source URL was "
"not provided. The Sumologic HTTP collector "
"source URL can be provided using the "
"`SUMOLOGIC_URL` environment variable or "
"in the ansible.cfg file."
)
def v2_playbook_on_start(self, playbook):
self.sumologic.ansible_playbook = basename(playbook._file_name)
@ -147,41 +143,16 @@ class CallbackModule(CallbackBase):
self.start_datetimes[task._uuid] = now()
def v2_runner_on_ok(self, result, **kwargs):
self.sumologic.send_event(
self.url,
'OK',
result,
self._runtime(result)
)
self.sumologic.send_event(self.url, "OK", result, self._runtime(result))
def v2_runner_on_skipped(self, result, **kwargs):
self.sumologic.send_event(
self.url,
'SKIPPED',
result,
self._runtime(result)
)
self.sumologic.send_event(self.url, "SKIPPED", result, self._runtime(result))
def v2_runner_on_failed(self, result, **kwargs):
self.sumologic.send_event(
self.url,
'FAILED',
result,
self._runtime(result)
)
self.sumologic.send_event(self.url, "FAILED", result, self._runtime(result))
def runner_on_async_failed(self, result, **kwargs):
self.sumologic.send_event(
self.url,
'FAILED',
result,
self._runtime(result)
)
self.sumologic.send_event(self.url, "FAILED", result, self._runtime(result))
def v2_runner_on_unreachable(self, result, **kwargs):
self.sumologic.send_event(
self.url,
'UNREACHABLE',
result,
self._runtime(result)
)
self.sumologic.send_event(self.url, "UNREACHABLE", result, self._runtime(result))

View file

@ -68,62 +68,89 @@ class CallbackModule(CallbackBase):
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.syslog_json'
CALLBACK_TYPE = "notification"
CALLBACK_NAME = "community.general.syslog_json"
CALLBACK_NEEDS_WHITELIST = True
def __init__(self):
super().__init__()
def set_options(self, task_keys=None, var_options=None, direct=None):
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
syslog_host = self.get_option("server")
syslog_port = int(self.get_option("port"))
syslog_facility = self.get_option("facility")
self.logger = logging.getLogger('ansible logger')
self.logger = logging.getLogger("ansible logger")
self.logger.setLevel(logging.DEBUG)
self.handler = logging.handlers.SysLogHandler(
address=(syslog_host, syslog_port),
facility=syslog_facility
)
self.handler = logging.handlers.SysLogHandler(address=(syslog_host, syslog_port), facility=syslog_facility)
self.logger.addHandler(self.handler)
self.hostname = socket.gethostname()
def v2_runner_on_failed(self, result, ignore_errors=False):
res = result._result
host = result._host.get_name()
self.logger.error('%s ansible-command: task execution FAILED; host: %s; message: %s', self.hostname, host, self._dump_results(res))
self.logger.error(
"%s ansible-command: task execution FAILED; host: %s; message: %s",
self.hostname,
host,
self._dump_results(res),
)
def v2_runner_on_ok(self, result):
res = result._result
host = result._host.get_name()
if result._task.action != "gather_facts" or self.get_option("setup"):
self.logger.info('%s ansible-command: task execution OK; host: %s; message: %s', self.hostname, host, self._dump_results(res))
self.logger.info(
"%s ansible-command: task execution OK; host: %s; message: %s",
self.hostname,
host,
self._dump_results(res),
)
def v2_runner_on_skipped(self, result):
host = result._host.get_name()
self.logger.info('%s ansible-command: task execution SKIPPED; host: %s; message: %s', self.hostname, host, 'skipped')
self.logger.info(
"%s ansible-command: task execution SKIPPED; host: %s; message: %s", self.hostname, host, "skipped"
)
def v2_runner_on_unreachable(self, result):
res = result._result
host = result._host.get_name()
self.logger.error('%s ansible-command: task execution UNREACHABLE; host: %s; message: %s', self.hostname, host, self._dump_results(res))
self.logger.error(
"%s ansible-command: task execution UNREACHABLE; host: %s; message: %s",
self.hostname,
host,
self._dump_results(res),
)
def v2_runner_on_async_failed(self, result):
res = result._result
host = result._host.get_name()
jid = result._result.get('ansible_job_id')
self.logger.error('%s ansible-command: task execution FAILED; host: %s; message: %s', self.hostname, host, self._dump_results(res))
jid = result._result.get("ansible_job_id")
self.logger.error(
"%s ansible-command: task execution FAILED; host: %s; message: %s",
self.hostname,
host,
self._dump_results(res),
)
def v2_playbook_on_import_for_host(self, result, imported_file):
host = result._host.get_name()
self.logger.info('%s ansible-command: playbook IMPORTED; host: %s; message: imported file %s', self.hostname, host, imported_file)
self.logger.info(
"%s ansible-command: playbook IMPORTED; host: %s; message: imported file %s",
self.hostname,
host,
imported_file,
)
def v2_playbook_on_not_import_for_host(self, result, missing_file):
host = result._host.get_name()
self.logger.info('%s ansible-command: playbook NOT IMPORTED; host: %s; message: missing file %s', self.hostname, host, missing_file)
self.logger.info(
"%s ansible-command: playbook NOT IMPORTED; host: %s; message: missing file %s",
self.hostname,
host,
missing_file,
)

View file

@ -1,4 +1,3 @@
# Copyright (c) 2025, Felix Fontein <felix@fontein.de>
# 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
@ -51,8 +50,8 @@ from ansible.plugins.callback.default import CallbackModule as Default
class CallbackModule(Default):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'community.general.tasks_only'
CALLBACK_TYPE = "stdout"
CALLBACK_NAME = "community.general.tasks_only"
def v2_playbook_on_play_start(self, play):
pass

View file

@ -1,4 +1,3 @@
# Copyright (c) 2024, kurokobo <kurokobo@protonmail.com>
# Copyright (c) 2014, Michael DeHaan <michael.dehaan@gmail.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)

View file

@ -28,8 +28,7 @@ from ansible.plugins.callback.default import CallbackModule as CallbackModule_de
class CallbackModule(CallbackModule_default):
'''
"""
Design goals:
- Print consolidated output that looks like a *NIX startup log
- Defaults should avoid displaying unnecessary information wherever possible
@ -39,14 +38,16 @@ class CallbackModule(CallbackModule_default):
- Add option to display all hostnames on a single line in the appropriate result color (failures may have a separate line)
- Consolidate stats display
- Don't show play name if no hosts found
'''
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'community.general.unixy'
CALLBACK_TYPE = "stdout"
CALLBACK_NAME = "community.general.unixy"
def _run_is_verbose(self, result):
return ((self._display.verbosity > 0 or '_ansible_verbose_always' in result._result) and '_ansible_verbose_override' not in result._result)
return (
self._display.verbosity > 0 or "_ansible_verbose_always" in result._result
) and "_ansible_verbose_override" not in result._result
def _get_task_display_name(self, task):
self.task_display_name = None
@ -59,8 +60,8 @@ class CallbackModule(CallbackModule_default):
self.task_display_name = task_display_name
def _preprocess_result(self, result):
self.delegated_vars = result._result.get('_ansible_delegated_vars', None)
self._handle_exception(result._result, use_stderr=self.get_option('display_failed_stderr'))
self.delegated_vars = result._result.get("_ansible_delegated_vars", None)
self._handle_exception(result._result, use_stderr=self.get_option("display_failed_stderr"))
self._handle_warnings(result._result)
def _process_result_output(self, result, msg):
@ -72,16 +73,16 @@ class CallbackModule(CallbackModule_default):
return task_result
if self.delegated_vars:
task_delegate_host = self.delegated_vars['ansible_host']
task_delegate_host = self.delegated_vars["ansible_host"]
task_result = f"{task_host} -> {task_delegate_host} {msg}"
if result._result.get('msg') and result._result.get('msg') != "All items completed":
if result._result.get("msg") and result._result.get("msg") != "All items completed":
task_result += f" | msg: {to_text(result._result.get('msg'))}"
if result._result.get('stdout'):
if result._result.get("stdout"):
task_result += f" | stdout: {result._result.get('stdout')}"
if result._result.get('stderr'):
if result._result.get("stderr"):
task_result += f" | stderr: {result._result.get('stderr')}"
return task_result
@ -89,7 +90,7 @@ class CallbackModule(CallbackModule_default):
def v2_playbook_on_task_start(self, task, is_conditional):
self._get_task_display_name(task)
if self.task_display_name is not None:
if task.check_mode and self.get_option('check_mode_markers'):
if task.check_mode and self.get_option("check_mode_markers"):
self._display.display(f"{self.task_display_name} (check mode)...")
else:
self._display.display(f"{self.task_display_name}...")
@ -97,14 +98,14 @@ class CallbackModule(CallbackModule_default):
def v2_playbook_on_handler_task_start(self, task):
self._get_task_display_name(task)
if self.task_display_name is not None:
if task.check_mode and self.get_option('check_mode_markers'):
if task.check_mode and self.get_option("check_mode_markers"):
self._display.display(f"{self.task_display_name} (via handler in check mode)... ")
else:
self._display.display(f"{self.task_display_name} (via handler)... ")
def v2_playbook_on_play_start(self, play):
name = play.get_name().strip()
if play.check_mode and self.get_option('check_mode_markers'):
if play.check_mode and self.get_option("check_mode_markers"):
if name and play.hosts:
msg = f"\n- {name} (in check mode) on hosts: {','.join(play.hosts)} -"
else:
@ -118,7 +119,7 @@ class CallbackModule(CallbackModule_default):
self._display.display(msg)
def v2_runner_on_skipped(self, result, ignore_errors=False):
if self.get_option('display_skipped_hosts'):
if self.get_option("display_skipped_hosts"):
self._preprocess_result(result)
display_color = C.COLOR_SKIP
msg = "skipped"
@ -137,12 +138,12 @@ class CallbackModule(CallbackModule_default):
msg += f" | item: {item_value}"
task_result = self._process_result_output(result, msg)
self._display.display(f" {task_result}", display_color, stderr=self.get_option('display_failed_stderr'))
self._display.display(f" {task_result}", display_color, stderr=self.get_option("display_failed_stderr"))
def v2_runner_on_ok(self, result, msg="ok", display_color=C.COLOR_OK):
self._preprocess_result(result)
result_was_changed = ('changed' in result._result and result._result['changed'])
result_was_changed = "changed" in result._result and result._result["changed"]
if result_was_changed:
msg = "done"
item_value = self._get_item_label(result._result)
@ -151,7 +152,7 @@ class CallbackModule(CallbackModule_default):
display_color = C.COLOR_CHANGED
task_result = self._process_result_output(result, msg)
self._display.display(f" {task_result}", display_color)
elif self.get_option('display_ok_hosts'):
elif self.get_option("display_ok_hosts"):
task_result = self._process_result_output(result, msg)
self._display.display(f" {task_result}", display_color)
@ -171,17 +172,17 @@ class CallbackModule(CallbackModule_default):
display_color = C.COLOR_UNREACHABLE
task_result = self._process_result_output(result, msg)
self._display.display(f" {task_result}", display_color, stderr=self.get_option('display_failed_stderr'))
self._display.display(f" {task_result}", display_color, stderr=self.get_option("display_failed_stderr"))
def v2_on_file_diff(self, result):
if result._task.loop and 'results' in result._result:
for res in result._result['results']:
if 'diff' in res and res['diff'] and res.get('changed', False):
diff = self._get_diff(res['diff'])
if result._task.loop and "results" in result._result:
for res in result._result["results"]:
if "diff" in res and res["diff"] and res.get("changed", False):
diff = self._get_diff(res["diff"])
if diff:
self._display.display(diff)
elif 'diff' in result._result and result._result['diff'] and result._result.get('changed', False):
diff = self._get_diff(result._result['diff'])
elif "diff" in result._result and result._result["diff"] and result._result.get("changed", False):
diff = self._get_diff(result._result["diff"])
if diff:
self._display.display(diff)
@ -197,30 +198,30 @@ class CallbackModule(CallbackModule_default):
f" {hostcolor(h, t)} : {colorize('ok', t['ok'], C.COLOR_OK)} {colorize('changed', t['changed'], C.COLOR_CHANGED)} "
f"{colorize('unreachable', t['unreachable'], C.COLOR_UNREACHABLE)} {colorize('failed', t['failures'], C.COLOR_ERROR)} "
f"{colorize('rescued', t['rescued'], C.COLOR_OK)} {colorize('ignored', t['ignored'], C.COLOR_WARN)}",
screen_only=True
screen_only=True,
)
self._display.display(
f" {hostcolor(h, t, False)} : {colorize('ok', t['ok'], None)} {colorize('changed', t['changed'], None)} "
f"{colorize('unreachable', t['unreachable'], None)} {colorize('failed', t['failures'], None)} {colorize('rescued', t['rescued'], None)} "
f"{colorize('ignored', t['ignored'], None)}",
log_only=True
log_only=True,
)
if stats.custom and self.get_option('show_custom_stats'):
if stats.custom and self.get_option("show_custom_stats"):
self._display.banner("CUSTOM STATS: ")
# per host
# TODO: come up with 'pretty format'
for k in sorted(stats.custom.keys()):
if k == '_run':
if k == "_run":
continue
stat_val = self._dump_results(stats.custom[k], indent=1).replace('\n', '')
self._display.display(f'\t{k}: {stat_val}')
stat_val = self._dump_results(stats.custom[k], indent=1).replace("\n", "")
self._display.display(f"\t{k}: {stat_val}")
# print per run custom stats
if '_run' in stats.custom:
if "_run" in stats.custom:
self._display.display("", screen_only=True)
stat_val_run = self._dump_results(stats.custom['_run'], indent=1).replace('\n', '')
self._display.display(f'\tRUN: {stat_val_run}')
stat_val_run = self._dump_results(stats.custom["_run"], indent=1).replace("\n", "")
self._display.display(f"\tRUN: {stat_val_run}")
self._display.display("", screen_only=True)
def v2_playbook_on_no_hosts_matched(self):
@ -230,21 +231,24 @@ class CallbackModule(CallbackModule_default):
self._display.display(" Ran out of hosts!", color=C.COLOR_ERROR)
def v2_playbook_on_start(self, playbook):
if context.CLIARGS['check'] and self.get_option('check_mode_markers'):
if context.CLIARGS["check"] and self.get_option("check_mode_markers"):
self._display.display(f"Executing playbook {basename(playbook._file_name)} in check mode")
else:
self._display.display(f"Executing playbook {basename(playbook._file_name)}")
# show CLI arguments
if self._display.verbosity > 3:
if context.CLIARGS.get('args'):
self._display.display(f"Positional arguments: {' '.join(context.CLIARGS['args'])}",
color=C.COLOR_VERBOSE, screen_only=True)
if context.CLIARGS.get("args"):
self._display.display(
f"Positional arguments: {' '.join(context.CLIARGS['args'])}",
color=C.COLOR_VERBOSE,
screen_only=True,
)
for argument in (a for a in context.CLIARGS if a != 'args'):
for argument in (a for a in context.CLIARGS if a != "args"):
val = context.CLIARGS[argument]
if val:
self._display.vvvv(f'{argument}: {val}')
self._display.vvvv(f"{argument}: {val}")
def v2_runner_retry(self, result):
msg = f" Retrying... ({result._result['attempts']} of {result._result['retries']})"

View file

@ -87,16 +87,16 @@ display = Display()
class Connection(ConnectionBase):
""" Local chroot based connections """
"""Local chroot based connections"""
transport = 'community.general.chroot'
transport = "community.general.chroot"
has_pipelining = True
# su currently has an undiagnosed issue with calculating the file
# checksums (so copy, for instance, doesn't work right)
# Have to look into that before re-enabling this
has_tty = False
default_user = 'root'
default_user = "root"
def __init__(self, play_context, new_stdin, *args, **kwargs):
super().__init__(play_context, new_stdin, *args, **kwargs)
@ -107,7 +107,7 @@ class Connection(ConnectionBase):
if not os.path.isdir(self.chroot):
raise AnsibleError(f"{self.chroot} is not a directory")
chrootsh = os.path.join(self.chroot, 'bin/sh')
chrootsh = os.path.join(self.chroot, "bin/sh")
# Want to check for a usable bourne shell inside the chroot.
# is_executable() == True is sufficient. For symlinks it
# gets really complicated really fast. So we punt on finding that
@ -116,17 +116,18 @@ class Connection(ConnectionBase):
raise AnsibleError(f"{self.chroot} does not look like a chrootable dir (/bin/sh missing)")
def _connect(self):
""" connect to the chroot """
if not self.get_option('disable_root_check') and os.geteuid() != 0:
"""connect to the chroot"""
if not self.get_option("disable_root_check") and os.geteuid() != 0:
raise AnsibleError(
"chroot connection requires running as root. "
"You can override this check with the `disable_root_check` option.")
"You can override this check with the `disable_root_check` option."
)
if os.path.isabs(self.get_option('chroot_exe')):
self.chroot_cmd = self.get_option('chroot_exe')
if os.path.isabs(self.get_option("chroot_exe")):
self.chroot_cmd = self.get_option("chroot_exe")
else:
try:
self.chroot_cmd = get_bin_path(self.get_option('chroot_exe'))
self.chroot_cmd = get_bin_path(self.get_option("chroot_exe"))
except ValueError as e:
raise AnsibleError(str(e))
@ -136,25 +137,24 @@ class Connection(ConnectionBase):
self._connected = True
def _buffered_exec_command(self, cmd, stdin=subprocess.PIPE):
""" run a command on the chroot. This is only needed for implementing
"""run a command on the chroot. This is only needed for implementing
put_file() get_file() so that we don't have to read the whole file
into memory.
compared to exec_command() it looses some niceties like being able to
return the process's exit code immediately.
"""
executable = self.get_option('executable')
local_cmd = [self.chroot_cmd, self.chroot, executable, '-c', cmd]
executable = self.get_option("executable")
local_cmd = [self.chroot_cmd, self.chroot, executable, "-c", cmd]
display.vvv(f"EXEC {local_cmd}", host=self.chroot)
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
p = subprocess.Popen(local_cmd, shell=False, stdin=stdin,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
p = subprocess.Popen(local_cmd, shell=False, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return p
def exec_command(self, cmd, in_data=None, sudoable=False):
""" run a command on the chroot """
"""run a command on the chroot"""
super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
p = self._buffered_exec_command(cmd)
@ -164,33 +164,33 @@ class Connection(ConnectionBase):
@staticmethod
def _prefix_login_path(remote_path):
""" Make sure that we put files into a standard path
"""Make sure that we put files into a standard path
If a path is relative, then we need to choose where to put it.
ssh chooses $HOME but we aren't guaranteed that a home dir will
exist in any given chroot. So for now we're choosing "/" instead.
This also happens to be the former default.
If a path is relative, then we need to choose where to put it.
ssh chooses $HOME but we aren't guaranteed that a home dir will
exist in any given chroot. So for now we're choosing "/" instead.
This also happens to be the former default.
Can revisit using $HOME instead if it is a problem
Can revisit using $HOME instead if it is a problem
"""
if not remote_path.startswith(os.path.sep):
remote_path = os.path.join(os.path.sep, remote_path)
return os.path.normpath(remote_path)
def put_file(self, in_path, out_path):
""" transfer a file from local to chroot """
"""transfer a file from local to chroot"""
super().put_file(in_path, out_path)
display.vvv(f"PUT {in_path} TO {out_path}", host=self.chroot)
out_path = shlex_quote(self._prefix_login_path(out_path))
try:
with open(to_bytes(in_path, errors='surrogate_or_strict'), 'rb') as in_file:
with open(to_bytes(in_path, errors="surrogate_or_strict"), "rb") as in_file:
if not os.fstat(in_file.fileno()).st_size:
count = ' count=0'
count = " count=0"
else:
count = ''
count = ""
try:
p = self._buffered_exec_command(f'dd of={out_path} bs={BUFSIZE}{count}', stdin=in_file)
p = self._buffered_exec_command(f"dd of={out_path} bs={BUFSIZE}{count}", stdin=in_file)
except OSError:
raise AnsibleError("chroot connection requires dd command in the chroot")
try:
@ -204,17 +204,17 @@ class Connection(ConnectionBase):
raise AnsibleError(f"file or module does not exist at: {in_path}")
def fetch_file(self, in_path, out_path):
""" fetch a file from chroot to local """
"""fetch a file from chroot to local"""
super().fetch_file(in_path, out_path)
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.chroot)
in_path = shlex_quote(self._prefix_login_path(in_path))
try:
p = self._buffered_exec_command(f'dd if={in_path} bs={BUFSIZE}')
p = self._buffered_exec_command(f"dd if={in_path} bs={BUFSIZE}")
except OSError:
raise AnsibleError("chroot connection requires dd command in the chroot")
with open(to_bytes(out_path, errors='surrogate_or_strict'), 'wb+') as out_file:
with open(to_bytes(out_path, errors="surrogate_or_strict"), "wb+") as out_file:
try:
chunk = p.stdout.read(BUFSIZE)
while chunk:
@ -228,6 +228,6 @@ class Connection(ConnectionBase):
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}:\n{stdout}\n{stderr}")
def close(self):
""" terminate the connection; nothing to do here """
"""terminate the connection; nothing to do here"""
super().close()
self._connected = False

View file

@ -29,6 +29,7 @@ options:
HAVE_FUNC = False
try:
import func.overlord.client as fc
HAVE_FUNC = True
except ImportError:
pass
@ -45,7 +46,7 @@ display = Display()
class Connection(ConnectionBase):
""" Func-based connections """
"""Func-based connections"""
has_pipelining = False
@ -64,7 +65,7 @@ class Connection(ConnectionBase):
return self
def exec_command(self, cmd, in_data=None, sudoable=True):
""" run a command on the remote minion """
"""run a command on the remote minion"""
if in_data:
raise AnsibleError("Internal Error: this module does not support optimized module pipelining")
@ -82,16 +83,16 @@ class Connection(ConnectionBase):
return os.path.join(prefix, normpath[1:])
def put_file(self, in_path, out_path):
""" transfer a file from local to remote """
"""transfer a file from local to remote"""
out_path = self._normalize_path(out_path, '/')
out_path = self._normalize_path(out_path, "/")
display.vvv(f"PUT {in_path} TO {out_path}", host=self.host)
self.client.local.copyfile.send(in_path, out_path)
def fetch_file(self, in_path, out_path):
""" fetch a file from remote to local """
"""fetch a file from remote to local"""
in_path = self._normalize_path(in_path, '/')
in_path = self._normalize_path(in_path, "/")
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.host)
# need to use a tmp dir due to difference of semantic for getfile
# ( who take a # directory as destination) and fetch_file, who
@ -102,5 +103,5 @@ class Connection(ConnectionBase):
shutil.rmtree(tmpdir)
def close(self):
""" terminate the connection; nothing to do here """
"""terminate the connection; nothing to do here"""
pass

View file

@ -84,7 +84,7 @@ from ansible.plugins.connection import ConnectionBase
class Connection(ConnectionBase):
""" Incus based connections """
"""Incus based connections"""
transport = "incus"
has_pipelining = True
@ -98,12 +98,13 @@ class Connection(ConnectionBase):
raise AnsibleError("incus command not found in PATH")
def _connect(self):
"""connect to Incus (nothing to do here) """
"""connect to Incus (nothing to do here)"""
super()._connect()
if not self._connected:
self._display.vvv(f"ESTABLISH Incus CONNECTION FOR USER: {self.get_option('remote_user')}",
host=self._instance())
self._display.vvv(
f"ESTABLISH Incus CONNECTION FOR USER: {self.get_option('remote_user')}", host=self._instance()
)
self._connected = True
def _build_command(self, cmd) -> list[str]:
@ -111,10 +112,12 @@ class Connection(ConnectionBase):
exec_cmd: list[str] = [
self._incus_cmd,
"--project", self.get_option("project"),
"--project",
self.get_option("project"),
"exec",
f"{self.get_option('remote')}:{self._instance()}",
"--"]
"--",
]
if self.get_option("remote_user") != "root":
self._display.vvv(
@ -122,9 +125,7 @@ class Connection(ConnectionBase):
trying to run 'incus exec' with become method: {self.get_option('incus_become_method')}",
host=self._instance(),
)
exec_cmd.extend(
[self.get_option("incus_become_method"), self.get_option("remote_user"), "-c"]
)
exec_cmd.extend([self.get_option("incus_become_method"), self.get_option("remote_user"), "-c"])
exec_cmd.extend([self.get_option("executable"), "-c", cmd])
@ -133,20 +134,19 @@ class Connection(ConnectionBase):
def _instance(self):
# Return only the leading part of the FQDN as the instance name
# as Incus instance names cannot be a FQDN.
return self.get_option('remote_addr').split(".")[0]
return self.get_option("remote_addr").split(".")[0]
def exec_command(self, cmd, in_data=None, sudoable=True):
""" execute a command on the Incus host """
"""execute a command on the Incus host"""
super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
self._display.vvv(f"EXEC {cmd}",
host=self._instance())
self._display.vvv(f"EXEC {cmd}", host=self._instance())
local_cmd = self._build_command(cmd)
self._display.vvvvv(f"EXEC {local_cmd}", host=self._instance())
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
in_data = to_bytes(in_data, errors='surrogate_or_strict', nonstring='passthru')
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
in_data = to_bytes(in_data, errors="surrogate_or_strict", nonstring="passthru")
process = Popen(local_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
stdout, stderr = process.communicate(in_data)
@ -154,32 +154,22 @@ class Connection(ConnectionBase):
stdout = to_text(stdout)
stderr = to_text(stderr)
if stderr.startswith("Error: ") and stderr.rstrip().endswith(
": Instance is not running"
):
if stderr.startswith("Error: ") and stderr.rstrip().endswith(": Instance is not running"):
raise AnsibleConnectionFailure(
f"instance not running: {self._instance()} (remote={self.get_option('remote')}, project={self.get_option('project')})"
)
if stderr.startswith("Error: ") and stderr.rstrip().endswith(
": Instance not found"
):
if stderr.startswith("Error: ") and stderr.rstrip().endswith(": Instance not found"):
raise AnsibleConnectionFailure(
f"instance not found: {self._instance()} (remote={self.get_option('remote')}, project={self.get_option('project')})"
)
if (
stderr.startswith("Error: ")
and ": User does not have permission " in stderr
):
if stderr.startswith("Error: ") and ": User does not have permission " in stderr:
raise AnsibleConnectionFailure(
f"instance access denied: {self._instance()} (remote={self.get_option('remote')}, project={self.get_option('project')})"
)
if (
stderr.startswith("Error: ")
and ": User does not have entitlement " in stderr
):
if stderr.startswith("Error: ") and ": User does not have entitlement " in stderr:
raise AnsibleConnectionFailure(
f"instance access denied: {self._instance()} (remote={self.get_option('remote')}, project={self.get_option('project')})"
)
@ -191,28 +181,23 @@ class Connection(ConnectionBase):
rc, uid_out, err = self.exec_command("/bin/id -u")
if rc != 0:
raise AnsibleError(
f"Failed to get remote uid for user {self.get_option('remote_user')}: {err}"
)
raise AnsibleError(f"Failed to get remote uid for user {self.get_option('remote_user')}: {err}")
uid = uid_out.strip()
rc, gid_out, err = self.exec_command("/bin/id -g")
if rc != 0:
raise AnsibleError(
f"Failed to get remote gid for user {self.get_option('remote_user')}: {err}"
)
raise AnsibleError(f"Failed to get remote gid for user {self.get_option('remote_user')}: {err}")
gid = gid_out.strip()
return int(uid), int(gid)
def put_file(self, in_path, out_path):
""" put a file from local to Incus """
"""put a file from local to Incus"""
super().put_file(in_path, out_path)
self._display.vvv(f"PUT {in_path} TO {out_path}",
host=self._instance())
self._display.vvv(f"PUT {in_path} TO {out_path}", host=self._instance())
if not os.path.isfile(to_bytes(in_path, errors='surrogate_or_strict')):
if not os.path.isfile(to_bytes(in_path, errors="surrogate_or_strict")):
raise AnsibleFileNotFound(f"input path is not a file: {in_path}")
if self.get_option("remote_user") != "root":
@ -245,30 +230,33 @@ class Connection(ConnectionBase):
self._display.vvvvv(f"PUT {local_cmd}", host=self._instance())
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
call(local_cmd)
def fetch_file(self, in_path, out_path):
""" fetch a file from Incus to local """
"""fetch a file from Incus to local"""
super().fetch_file(in_path, out_path)
self._display.vvv(f"FETCH {in_path} TO {out_path}",
host=self._instance())
self._display.vvv(f"FETCH {in_path} TO {out_path}", host=self._instance())
local_cmd = [
self._incus_cmd,
"--project", self.get_option("project"),
"file", "pull", "--quiet",
"--project",
self.get_option("project"),
"file",
"pull",
"--quiet",
f"{self.get_option('remote')}:{self._instance()}/{in_path}",
out_path]
out_path,
]
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
call(local_cmd)
def close(self):
""" close the connection (nothing to do here) """
"""close the connection (nothing to do here)"""
super().close()
self._connected = False

View file

@ -42,31 +42,33 @@ display = Display()
class Connection(Jail):
""" Local iocage based connections """
"""Local iocage based connections"""
transport = 'community.general.iocage'
transport = "community.general.iocage"
def __init__(self, play_context, new_stdin, *args, **kwargs):
self.ioc_jail = play_context.remote_addr
self.iocage_cmd = Jail._search_executable('iocage')
self.iocage_cmd = Jail._search_executable("iocage")
jail_uuid = self.get_jail_uuid()
kwargs[Jail.modified_jailname_key] = f'ioc-{jail_uuid}'
kwargs[Jail.modified_jailname_key] = f"ioc-{jail_uuid}"
display.vvv(
f"Jail {self.ioc_jail} has been translated to {kwargs[Jail.modified_jailname_key]}",
host=kwargs[Jail.modified_jailname_key]
host=kwargs[Jail.modified_jailname_key],
)
super().__init__(play_context, new_stdin, *args, **kwargs)
def get_jail_uuid(self):
p = subprocess.Popen([self.iocage_cmd, 'get', 'host_hostuuid', self.ioc_jail],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
p = subprocess.Popen(
[self.iocage_cmd, "get", "host_hostuuid", self.ioc_jail],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
stdout, stderr = p.communicate()
@ -82,4 +84,4 @@ class Connection(Jail):
if p.returncode != 0:
raise AnsibleError(f"iocage returned an error: {stdout}")
return stdout.strip('\n')
return stdout.strip("\n")

View file

@ -49,11 +49,11 @@ display = Display()
class Connection(ConnectionBase):
""" Local BSD Jail based connections """
"""Local BSD Jail based connections"""
modified_jailname_key = 'conn_jail_name'
modified_jailname_key = "conn_jail_name"
transport = 'community.general.jail'
transport = "community.general.jail"
# Pipelining may work. Someone needs to test by setting this to True and
# having pipelining=True in their ansible.cfg
has_pipelining = True
@ -69,8 +69,8 @@ class Connection(ConnectionBase):
if os.geteuid() != 0:
raise AnsibleError("jail connection requires running as root")
self.jls_cmd = self._search_executable('jls')
self.jexec_cmd = self._search_executable('jexec')
self.jls_cmd = self._search_executable("jls")
self.jexec_cmd = self._search_executable("jexec")
if self.jail not in self.list_jails():
raise AnsibleError(f"incorrect jail name {self.jail}")
@ -83,23 +83,23 @@ class Connection(ConnectionBase):
raise AnsibleError(f"{executable} command not found in PATH")
def list_jails(self):
p = subprocess.Popen([self.jls_cmd, '-q', 'name'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p = subprocess.Popen(
[self.jls_cmd, "-q", "name"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
stdout, stderr = p.communicate()
return to_text(stdout, errors='surrogate_or_strict').split()
return to_text(stdout, errors="surrogate_or_strict").split()
def _connect(self):
""" connect to the jail; nothing to do here """
"""connect to the jail; nothing to do here"""
super()._connect()
if not self._connected:
display.vvv(f"ESTABLISH JAIL CONNECTION FOR USER: {self._play_context.remote_user}", host=self.jail)
self._connected = True
def _buffered_exec_command(self, cmd, stdin=subprocess.PIPE):
""" run a command on the jail. This is only needed for implementing
"""run a command on the jail. This is only needed for implementing
put_file() get_file() so that we don't have to read the whole file
into memory.
@ -108,24 +108,23 @@ class Connection(ConnectionBase):
"""
local_cmd = [self.jexec_cmd]
set_env = ''
set_env = ""
if self._play_context.remote_user is not None:
local_cmd += ['-U', self._play_context.remote_user]
local_cmd += ["-U", self._play_context.remote_user]
# update HOME since -U does not update the jail environment
set_env = f"HOME=~{self._play_context.remote_user} "
local_cmd += [self.jail, self._play_context.executable, '-c', set_env + cmd]
local_cmd += [self.jail, self._play_context.executable, "-c", set_env + cmd]
display.vvv(f"EXEC {local_cmd}", host=self.jail)
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
p = subprocess.Popen(local_cmd, shell=False, stdin=stdin,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
p = subprocess.Popen(local_cmd, shell=False, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return p
def exec_command(self, cmd, in_data=None, sudoable=False):
""" run a command on the jail """
"""run a command on the jail"""
super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
p = self._buffered_exec_command(cmd)
@ -135,33 +134,33 @@ class Connection(ConnectionBase):
@staticmethod
def _prefix_login_path(remote_path):
""" Make sure that we put files into a standard path
"""Make sure that we put files into a standard path
If a path is relative, then we need to choose where to put it.
ssh chooses $HOME but we aren't guaranteed that a home dir will
exist in any given chroot. So for now we're choosing "/" instead.
This also happens to be the former default.
If a path is relative, then we need to choose where to put it.
ssh chooses $HOME but we aren't guaranteed that a home dir will
exist in any given chroot. So for now we're choosing "/" instead.
This also happens to be the former default.
Can revisit using $HOME instead if it is a problem
Can revisit using $HOME instead if it is a problem
"""
if not remote_path.startswith(os.path.sep):
remote_path = os.path.join(os.path.sep, remote_path)
return os.path.normpath(remote_path)
def put_file(self, in_path, out_path):
""" transfer a file from local to jail """
"""transfer a file from local to jail"""
super().put_file(in_path, out_path)
display.vvv(f"PUT {in_path} TO {out_path}", host=self.jail)
out_path = shlex_quote(self._prefix_login_path(out_path))
try:
with open(to_bytes(in_path, errors='surrogate_or_strict'), 'rb') as in_file:
with open(to_bytes(in_path, errors="surrogate_or_strict"), "rb") as in_file:
if not os.fstat(in_file.fileno()).st_size:
count = ' count=0'
count = " count=0"
else:
count = ''
count = ""
try:
p = self._buffered_exec_command(f'dd of={out_path} bs={BUFSIZE}{count}', stdin=in_file)
p = self._buffered_exec_command(f"dd of={out_path} bs={BUFSIZE}{count}", stdin=in_file)
except OSError:
raise AnsibleError("jail connection requires dd command in the jail")
try:
@ -170,22 +169,24 @@ class Connection(ConnectionBase):
traceback.print_exc()
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}")
if p.returncode != 0:
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}:\n{to_native(stdout)}\n{to_native(stderr)}")
raise AnsibleError(
f"failed to transfer file {in_path} to {out_path}:\n{to_native(stdout)}\n{to_native(stderr)}"
)
except IOError:
raise AnsibleError(f"file or module does not exist at: {in_path}")
def fetch_file(self, in_path, out_path):
""" fetch a file from jail to local """
"""fetch a file from jail to local"""
super().fetch_file(in_path, out_path)
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.jail)
in_path = shlex_quote(self._prefix_login_path(in_path))
try:
p = self._buffered_exec_command(f'dd if={in_path} bs={BUFSIZE}')
p = self._buffered_exec_command(f"dd if={in_path} bs={BUFSIZE}")
except OSError:
raise AnsibleError("jail connection requires dd command in the jail")
with open(to_bytes(out_path, errors='surrogate_or_strict'), 'wb+') as out_file:
with open(to_bytes(out_path, errors="surrogate_or_strict"), "wb+") as out_file:
try:
chunk = p.stdout.read(BUFSIZE)
while chunk:
@ -196,9 +197,11 @@ class Connection(ConnectionBase):
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}")
stdout, stderr = p.communicate()
if p.returncode != 0:
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}:\n{to_native(stdout)}\n{to_native(stderr)}")
raise AnsibleError(
f"failed to transfer file {in_path} to {out_path}:\n{to_native(stdout)}\n{to_native(stderr)}"
)
def close(self):
""" terminate the connection; nothing to do here """
"""terminate the connection; nothing to do here"""
super().close()
self._connected = False

View file

@ -41,6 +41,7 @@ import errno
HAS_LIBLXC = False
try:
import lxc as _lxc
HAS_LIBLXC = True
except ImportError:
pass
@ -51,11 +52,11 @@ from ansible.plugins.connection import ConnectionBase
class Connection(ConnectionBase):
""" Local lxc based connections """
"""Local lxc based connections"""
transport = 'community.general.lxc'
transport = "community.general.lxc"
has_pipelining = True
default_user = 'root'
default_user = "root"
def __init__(self, play_context, new_stdin, *args, **kwargs):
super().__init__(play_context, new_stdin, *args, **kwargs)
@ -64,14 +65,14 @@ class Connection(ConnectionBase):
self.container = None
def _connect(self):
""" connect to the lxc; nothing to do here """
"""connect to the lxc; nothing to do here"""
super()._connect()
if not HAS_LIBLXC:
msg = "lxc python bindings are not installed"
raise errors.AnsibleError(msg)
container_name = self.get_option('remote_addr')
container_name = self.get_option("remote_addr")
if self.container and self.container_name == container_name:
return
@ -98,7 +99,7 @@ class Connection(ConnectionBase):
continue
raise
for fd in ready_writes:
in_data = in_data[os.write(fd, in_data):]
in_data = in_data[os.write(fd, in_data) :]
if len(in_data) == 0:
write_fds.remove(fd)
for fd in ready_reads:
@ -117,12 +118,12 @@ class Connection(ConnectionBase):
return fd
def exec_command(self, cmd, in_data=None, sudoable=False):
""" run a command on the chroot """
"""run a command on the chroot"""
super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
# python2-lxc needs bytes. python3-lxc needs text.
executable = to_native(self.get_option('executable'), errors='surrogate_or_strict')
local_cmd = [executable, '-c', to_native(cmd, errors='surrogate_or_strict')]
executable = to_native(self.get_option("executable"), errors="surrogate_or_strict")
local_cmd = [executable, "-c", to_native(cmd, errors="surrogate_or_strict")]
read_stdout, write_stdout = None, None
read_stderr, write_stderr = None, None
@ -133,14 +134,14 @@ class Connection(ConnectionBase):
read_stderr, write_stderr = os.pipe()
kwargs = {
'stdout': self._set_nonblocking(write_stdout),
'stderr': self._set_nonblocking(write_stderr),
'env_policy': _lxc.LXC_ATTACH_CLEAR_ENV
"stdout": self._set_nonblocking(write_stdout),
"stderr": self._set_nonblocking(write_stderr),
"env_policy": _lxc.LXC_ATTACH_CLEAR_ENV,
}
if in_data:
read_stdin, write_stdin = os.pipe()
kwargs['stdin'] = self._set_nonblocking(read_stdin)
kwargs["stdin"] = self._set_nonblocking(read_stdin)
self._display.vvv(f"EXEC {local_cmd}", host=self.container_name)
pid = self.container.attach(_lxc.attach_run_command, local_cmd, **kwargs)
@ -153,28 +154,19 @@ class Connection(ConnectionBase):
if read_stdin:
read_stdin = os.close(read_stdin)
return self._communicate(pid,
in_data,
write_stdin,
read_stdout,
read_stderr)
return self._communicate(pid, in_data, write_stdin, read_stdout, read_stderr)
finally:
fds = [read_stdout,
write_stdout,
read_stderr,
write_stderr,
read_stdin,
write_stdin]
fds = [read_stdout, write_stdout, read_stderr, write_stderr, read_stdin, write_stdin]
for fd in fds:
if fd:
os.close(fd)
def put_file(self, in_path, out_path):
''' transfer a file from local to lxc '''
"""transfer a file from local to lxc"""
super().put_file(in_path, out_path)
self._display.vvv(f"PUT {in_path} TO {out_path}", host=self.container_name)
in_path = to_bytes(in_path, errors='surrogate_or_strict')
out_path = to_bytes(out_path, errors='surrogate_or_strict')
in_path = to_bytes(in_path, errors="surrogate_or_strict")
out_path = to_bytes(out_path, errors="surrogate_or_strict")
if not os.path.exists(in_path):
msg = f"file or module does not exist: {in_path}"
@ -185,9 +177,11 @@ class Connection(ConnectionBase):
traceback.print_exc()
raise errors.AnsibleError(f"failed to open input file to {in_path}")
try:
def write_file(args):
with open(out_path, 'wb+') as dst_file:
with open(out_path, "wb+") as dst_file:
shutil.copyfileobj(src_file, dst_file)
try:
self.container.attach_wait(write_file, None)
except IOError:
@ -198,11 +192,11 @@ class Connection(ConnectionBase):
src_file.close()
def fetch_file(self, in_path, out_path):
''' fetch a file from lxc to local '''
"""fetch a file from lxc to local"""
super().fetch_file(in_path, out_path)
self._display.vvv(f"FETCH {in_path} TO {out_path}", host=self.container_name)
in_path = to_bytes(in_path, errors='surrogate_or_strict')
out_path = to_bytes(out_path, errors='surrogate_or_strict')
in_path = to_bytes(in_path, errors="surrogate_or_strict")
out_path = to_bytes(out_path, errors="surrogate_or_strict")
try:
dst_file = open(out_path, "wb")
@ -211,14 +205,16 @@ class Connection(ConnectionBase):
msg = f"failed to open output file {out_path}"
raise errors.AnsibleError(msg)
try:
def write_file(args):
try:
with open(in_path, 'rb') as src_file:
with open(in_path, "rb") as src_file:
shutil.copyfileobj(src_file, dst_file)
finally:
# this is needed in the lxc child process
# to flush internal python buffers
dst_file.close()
try:
self.container.attach_wait(write_file, None)
except IOError:
@ -229,6 +225,6 @@ class Connection(ConnectionBase):
dst_file.close()
def close(self):
''' terminate the connection; nothing to do here '''
"""terminate the connection; nothing to do here"""
super().close()
self._connected = False

View file

@ -83,9 +83,9 @@ from ansible.plugins.connection import ConnectionBase
class Connection(ConnectionBase):
""" lxd based connections """
"""lxd based connections"""
transport = 'community.general.lxd'
transport = "community.general.lxd"
has_pipelining = True
def __init__(self, play_context, new_stdin, *args, **kwargs):
@ -97,11 +97,11 @@ class Connection(ConnectionBase):
raise AnsibleError("lxc command not found in PATH")
def _host(self):
""" translate remote_addr to lxd (short) hostname """
"""translate remote_addr to lxd (short) hostname"""
return self.get_option("remote_addr").split(".", 1)[0]
def _connect(self):
"""connect to lxd (nothing to do here) """
"""connect to lxd (nothing to do here)"""
super()._connect()
if not self._connected:
@ -124,16 +124,14 @@ class Connection(ConnectionBase):
trying to run 'lxc exec' with become method: {self.get_option('lxd_become_method')}",
host=self._host(),
)
exec_cmd.extend(
[self.get_option("lxd_become_method"), self.get_option("remote_user"), "-c"]
)
exec_cmd.extend([self.get_option("lxd_become_method"), self.get_option("remote_user"), "-c"])
exec_cmd.extend([self.get_option("executable"), "-c", cmd])
return exec_cmd
def exec_command(self, cmd, in_data=None, sudoable=True):
""" execute a command on the lxd host """
"""execute a command on the lxd host"""
super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
self._display.vvv(f"EXEC {cmd}", host=self._host())
@ -141,8 +139,8 @@ class Connection(ConnectionBase):
local_cmd = self._build_command(cmd)
self._display.vvvvv(f"EXEC {local_cmd}", host=self._host())
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
in_data = to_bytes(in_data, errors='surrogate_or_strict', nonstring='passthru')
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
in_data = to_bytes(in_data, errors="surrogate_or_strict", nonstring="passthru")
process = Popen(local_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
stdout, stderr = process.communicate(in_data)
@ -165,27 +163,23 @@ class Connection(ConnectionBase):
rc, uid_out, err = self.exec_command("/bin/id -u")
if rc != 0:
raise AnsibleError(
f"Failed to get remote uid for user {self.get_option('remote_user')}: {err}"
)
raise AnsibleError(f"Failed to get remote uid for user {self.get_option('remote_user')}: {err}")
uid = uid_out.strip()
rc, gid_out, err = self.exec_command("/bin/id -g")
if rc != 0:
raise AnsibleError(
f"Failed to get remote gid for user {self.get_option('remote_user')}: {err}"
)
raise AnsibleError(f"Failed to get remote gid for user {self.get_option('remote_user')}: {err}")
gid = gid_out.strip()
return int(uid), int(gid)
def put_file(self, in_path, out_path):
""" put a file from local to lxd """
"""put a file from local to lxd"""
super().put_file(in_path, out_path)
self._display.vvv(f"PUT {in_path} TO {out_path}", host=self._host())
if not os.path.isfile(to_bytes(in_path, errors='surrogate_or_strict')):
if not os.path.isfile(to_bytes(in_path, errors="surrogate_or_strict")):
raise AnsibleFileNotFound(f"input path is not a file: {in_path}")
local_cmd = [self._lxc_cmd]
@ -218,13 +212,13 @@ class Connection(ConnectionBase):
self._display.vvvvv(f"PUT {local_cmd}", host=self._host())
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
process = Popen(local_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
process.communicate()
def fetch_file(self, in_path, out_path):
""" fetch a file from lxd to local """
"""fetch a file from lxd to local"""
super().fetch_file(in_path, out_path)
self._display.vvv(f"FETCH {in_path} TO {out_path}", host=self._host())
@ -232,19 +226,15 @@ class Connection(ConnectionBase):
local_cmd = [self._lxc_cmd]
if self.get_option("project"):
local_cmd.extend(["--project", self.get_option("project")])
local_cmd.extend([
"file", "pull",
f"{self.get_option('remote')}:{self._host()}/{in_path}",
out_path
])
local_cmd.extend(["file", "pull", f"{self.get_option('remote')}:{self._host()}/{in_path}", out_path])
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
process = Popen(local_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
process.communicate()
def close(self):
""" close the connection (nothing to do here) """
"""close the connection (nothing to do here)"""
super().close()
self._connected = False

View file

@ -53,7 +53,7 @@ class Connection(ConnectionBase):
"""This is a connection plugin for qubes: it uses qubes-run-vm binary to interact with the containers."""
# String used to identify this Connection class from other classes
transport = 'community.general.qubes'
transport = "community.general.qubes"
has_pipelining = True
def __init__(self, play_context, new_stdin, *args, **kwargs):
@ -88,16 +88,17 @@ class Connection(ConnectionBase):
local_cmd.append(shell)
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
display.vvvv("Local cmd: ", local_cmd)
display.vvv(f"RUN {local_cmd}", host=self._remote_vmname)
p = subprocess.Popen(local_cmd, shell=False, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p = subprocess.Popen(
local_cmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
# Here we are writing the actual command to the remote bash
p.stdin.write(to_bytes(cmd, errors='surrogate_or_strict'))
p.stdin.write(to_bytes(cmd, errors="surrogate_or_strict"))
stdout, stderr = p.communicate(input=in_data)
return p.returncode, stdout, stderr
@ -108,7 +109,7 @@ class Connection(ConnectionBase):
@ensure_connect # type: ignore # TODO: for some reason, the type infos for ensure_connect suck...
def exec_command(self, cmd, in_data=None, sudoable=False):
"""Run specified command in a running QubesVM """
"""Run specified command in a running QubesVM"""
super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
display.vvvv(f"CMD IS: {cmd}")
@ -119,24 +120,24 @@ class Connection(ConnectionBase):
return rc, stdout, stderr
def put_file(self, in_path, out_path):
""" Place a local file located in 'in_path' inside VM at 'out_path' """
"""Place a local file located in 'in_path' inside VM at 'out_path'"""
super().put_file(in_path, out_path)
display.vvv(f"PUT {in_path} TO {out_path}", host=self._remote_vmname)
with open(in_path, "rb") as fobj:
source_data = fobj.read()
retcode, dummy, dummy = self._qubes(f'cat > "{out_path}\"\n', source_data, "qubes.VMRootShell")
retcode, dummy, dummy = self._qubes(f'cat > "{out_path}"\n', source_data, "qubes.VMRootShell")
# if qubes.VMRootShell service not supported, fallback to qubes.VMShell and
# hope it will have appropriate permissions
if retcode == 127:
retcode, dummy, dummy = self._qubes(f'cat > "{out_path}\"\n', source_data)
retcode, dummy, dummy = self._qubes(f'cat > "{out_path}"\n', source_data)
if retcode != 0:
raise AnsibleConnectionFailure(f'Failed to put_file to {out_path}')
raise AnsibleConnectionFailure(f"Failed to put_file to {out_path}")
def fetch_file(self, in_path, out_path):
"""Obtain file specified via 'in_path' from the container and place it at 'out_path' """
"""Obtain file specified via 'in_path' from the container and place it at 'out_path'"""
super().fetch_file(in_path, out_path)
display.vvv(f"FETCH {in_path} TO {out_path}", host=self._remote_vmname)
@ -146,9 +147,9 @@ class Connection(ConnectionBase):
p = subprocess.Popen(cmd_args_list, shell=False, stdout=fobj)
p.communicate()
if p.returncode != 0:
raise AnsibleConnectionFailure(f'Failed to fetch file to {out_path}')
raise AnsibleConnectionFailure(f"Failed to fetch file to {out_path}")
def close(self):
""" Closing the connection """
"""Closing the connection"""
super().close()
self._connected = False

View file

@ -25,18 +25,19 @@ from ansible.plugins.connection import ConnectionBase
HAVE_SALTSTACK = False
try:
import salt.client as sc
HAVE_SALTSTACK = True
except ImportError:
pass
class Connection(ConnectionBase):
""" Salt-based connections """
"""Salt-based connections"""
has_pipelining = False
# while the name of the product is salt, naming that module salt cause
# trouble with module import
transport = 'community.general.saltstack'
transport = "community.general.saltstack"
def __init__(self, play_context, new_stdin, *args, **kwargs):
super().__init__(play_context, new_stdin, *args, **kwargs)
@ -51,7 +52,7 @@ class Connection(ConnectionBase):
return self
def exec_command(self, cmd, in_data=None, sudoable=False):
""" run a command on the remote minion """
"""run a command on the remote minion"""
super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
if in_data:
@ -59,12 +60,14 @@ class Connection(ConnectionBase):
self._display.vvv(f"EXEC {cmd}", host=self.host)
# need to add 'true;' to work around https://github.com/saltstack/salt/issues/28077
res = self.client.cmd(self.host, 'cmd.exec_code_all', ['bash', f"true;{cmd}"])
res = self.client.cmd(self.host, "cmd.exec_code_all", ["bash", f"true;{cmd}"])
if self.host not in res:
raise errors.AnsibleError(f"Minion {self.host} didn't answer, check if salt-minion is running and the name is correct")
raise errors.AnsibleError(
f"Minion {self.host} didn't answer, check if salt-minion is running and the name is correct"
)
p = res[self.host]
return p['retcode'], p['stdout'], p['stderr']
return p["retcode"], p["stdout"], p["stderr"]
@staticmethod
def _normalize_path(path, prefix):
@ -74,27 +77,27 @@ class Connection(ConnectionBase):
return os.path.join(prefix, normpath[1:])
def put_file(self, in_path, out_path):
""" transfer a file from local to remote """
"""transfer a file from local to remote"""
super().put_file(in_path, out_path)
out_path = self._normalize_path(out_path, '/')
out_path = self._normalize_path(out_path, "/")
self._display.vvv(f"PUT {in_path} TO {out_path}", host=self.host)
with open(in_path, 'rb') as in_fh:
with open(in_path, "rb") as in_fh:
content = in_fh.read()
self.client.cmd(self.host, 'hashutil.base64_decodefile', [base64.b64encode(content), out_path])
self.client.cmd(self.host, "hashutil.base64_decodefile", [base64.b64encode(content), out_path])
# TODO test it
def fetch_file(self, in_path, out_path):
""" fetch a file from remote to local """
"""fetch a file from remote to local"""
super().fetch_file(in_path, out_path)
in_path = self._normalize_path(in_path, '/')
in_path = self._normalize_path(in_path, "/")
self._display.vvv(f"FETCH {in_path} TO {out_path}", host=self.host)
content = self.client.cmd(self.host, 'cp.get_file_str', [in_path])[self.host]
open(out_path, 'wb').write(content)
content = self.client.cmd(self.host, "cp.get_file_str", [in_path])[self.host]
open(out_path, "wb").write(content)
def close(self):
""" terminate the connection; nothing to do here """
"""terminate the connection; nothing to do here"""
pass

View file

@ -336,6 +336,7 @@ PARAMIKO_IMPORT_ERR: str | None
try:
import paramiko
from paramiko import MissingHostKeyPolicy
PARAMIKO_IMPORT_ERR = None
except ImportError:
PARAMIKO_IMPORT_ERR = traceback.format_exc()
@ -369,24 +370,22 @@ class MyAddPolicy(MissingHostKeyPolicy):
self._options = connection._options
def missing_host_key(self, client: paramiko.SSHClient, hostname: str, key: paramiko.PKey) -> None:
if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
if all((self.connection.get_option("host_key_checking"), not self.connection.get_option("host_key_auto_add"))):
fingerprint = hexlify(key.get_fingerprint())
ktype = key.get_name()
if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
if self.connection.get_option("use_persistent_connections") or self.connection.force_persistence:
# don't print the prompt string since the user cannot respond
# to the question anyway
raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
inp = to_text(
display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
errors='surrogate_or_strict'
errors="surrogate_or_strict",
)
if inp.lower() not in ['yes', 'y', '']:
raise AnsibleError('host connection rejected by user')
if inp.lower() not in ["yes", "y", ""]:
raise AnsibleError("host connection rejected by user")
key._added_by_ansible_this_time = True # type: ignore
@ -398,88 +397,96 @@ class MyAddPolicy(MissingHostKeyPolicy):
class Connection(ConnectionBase):
""" SSH based connections (paramiko) to WSL """
"""SSH based connections (paramiko) to WSL"""
transport = 'community.general.wsl'
transport = "community.general.wsl"
_log_channel: str | None = None
def __init__(self, play_context: PlayContext, new_stdin: io.TextIOWrapper | None = None, *args: t.Any, **kwargs: t.Any):
def __init__(
self, play_context: PlayContext, new_stdin: io.TextIOWrapper | None = None, *args: t.Any, **kwargs: t.Any
):
super().__init__(play_context, new_stdin, *args, **kwargs)
def _set_log_channel(self, name: str) -> None:
""" Mimic paramiko.SSHClient.set_log_channel """
"""Mimic paramiko.SSHClient.set_log_channel"""
self._log_channel = name
def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
proxy_command = self.get_option('proxy_command') or None
proxy_command = self.get_option("proxy_command") or None
sock_kwarg = {}
if proxy_command:
replacers: t.Dict[str, str] = {
'%h': self.get_option('remote_addr'),
'%p': str(port),
'%r': self.get_option('remote_user')
"%h": self.get_option("remote_addr"),
"%p": str(port),
"%r": self.get_option("remote_user"),
}
for find, replace in replacers.items():
proxy_command = proxy_command.replace(find, replace)
try:
sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
sock_kwarg = {"sock": paramiko.ProxyCommand(proxy_command)}
display.vvv(
f"CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}", host=self.get_option("remote_addr")
)
except AttributeError:
display.warning('Paramiko ProxyCommand support unavailable. '
'Please upgrade to Paramiko 1.9.0 or newer. '
'Not using configured ProxyCommand')
display.warning(
"Paramiko ProxyCommand support unavailable. "
"Please upgrade to Paramiko 1.9.0 or newer. "
"Not using configured ProxyCommand"
)
return sock_kwarg
def _connect(self) -> Connection:
""" activates the connection object """
"""activates the connection object"""
if PARAMIKO_IMPORT_ERR is not None:
raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
raise AnsibleError(f"paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}")
port = self.get_option('port')
display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
host=self.get_option('remote_addr'))
port = self.get_option("port")
display.vvv(
f"ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option('remote_user')} on PORT {to_text(port)} TO {self.get_option('remote_addr')}",
host=self.get_option("remote_addr"),
)
ssh = paramiko.SSHClient()
# Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
# is keeping or omitting rsa-sha2 algorithms
# default_keys: t.Tuple[str] = ()
paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
paramiko_preferred_pubkeys = getattr(paramiko.Transport, "_preferred_pubkeys", ())
paramiko_preferred_hostkeys = getattr(paramiko.Transport, "_preferred_keys", ())
use_rsa_sha2_algorithms = self.get_option("use_rsa_sha2_algorithms")
disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
if not use_rsa_sha2_algorithms:
if paramiko_preferred_pubkeys:
disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
disabled_algorithms["pubkeys"] = tuple(a for a in paramiko_preferred_pubkeys if "rsa-sha2" in a)
if paramiko_preferred_hostkeys:
disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
disabled_algorithms["keys"] = tuple(a for a in paramiko_preferred_hostkeys if "rsa-sha2" in a)
# override paramiko's default logger name
if self._log_channel is not None:
ssh.set_log_channel(self._log_channel)
self.keyfile = os.path.expanduser(self.get_option('user_known_hosts_file'))
self.keyfile = os.path.expanduser(self.get_option("user_known_hosts_file"))
if self.get_option('host_key_checking'):
for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts', self.keyfile):
if self.get_option("host_key_checking"):
for ssh_known_hosts in ("/etc/ssh/ssh_known_hosts", "/etc/openssh/ssh_known_hosts", self.keyfile):
try:
ssh.load_system_host_keys(ssh_known_hosts)
break
except IOError:
pass # file was not found, but not required to function
except paramiko.hostkeys.InvalidHostKey as e:
raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
raise AnsibleConnectionFailure(f"Invalid host key: {to_text(e.line)}")
try:
ssh.load_system_host_keys()
except paramiko.hostkeys.InvalidHostKey as e:
raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
raise AnsibleConnectionFailure(f"Invalid host key: {to_text(e.line)}")
ssh_connect_kwargs = self._parse_proxy_command(port)
ssh.set_missing_host_key_policy(MyAddPolicy(self))
conn_password = self.get_option('password')
conn_password = self.get_option("password")
allow_agent = True
if conn_password is not None:
@ -487,42 +494,42 @@ class Connection(ConnectionBase):
try:
key_filename = None
if self.get_option('private_key_file'):
key_filename = os.path.expanduser(self.get_option('private_key_file'))
if self.get_option("private_key_file"):
key_filename = os.path.expanduser(self.get_option("private_key_file"))
# paramiko 2.2 introduced auth_timeout parameter
if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
if LooseVersion(paramiko.__version__) >= LooseVersion("2.2.0"):
ssh_connect_kwargs["auth_timeout"] = self.get_option("timeout")
# paramiko 1.15 introduced banner timeout parameter
if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
if LooseVersion(paramiko.__version__) >= LooseVersion("1.15.0"):
ssh_connect_kwargs["banner_timeout"] = self.get_option("banner_timeout")
ssh.connect(
self.get_option('remote_addr').lower(),
username=self.get_option('remote_user'),
self.get_option("remote_addr").lower(),
username=self.get_option("remote_user"),
allow_agent=allow_agent,
look_for_keys=self.get_option('look_for_keys'),
look_for_keys=self.get_option("look_for_keys"),
key_filename=key_filename,
password=conn_password,
timeout=self.get_option('timeout'),
timeout=self.get_option("timeout"),
port=port,
disabled_algorithms=disabled_algorithms,
**ssh_connect_kwargs,
)
except paramiko.ssh_exception.BadHostKeyException as e:
raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
raise AnsibleConnectionFailure(f"host key mismatch for {to_text(e.hostname)}")
except paramiko.ssh_exception.AuthenticationException as e:
msg = f'Failed to authenticate: {e}'
msg = f"Failed to authenticate: {e}"
raise AnsibleAuthenticationFailure(msg)
except Exception as e:
msg = to_text(e)
if 'PID check failed' in msg:
raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
elif 'Private key file is encrypted' in msg:
if "PID check failed" in msg:
raise AnsibleError("paramiko version issue, please upgrade paramiko on the machine running ansible")
elif "Private key file is encrypted" in msg:
msg = (
f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : '
f'{msg}\nTo connect as a different user, use -u <username>.'
f"ssh {self.get_option('remote_user')}@{self.get_options('remote_addr')}:{port} : "
f"{msg}\nTo connect as a different user, use -u <username>."
)
raise AnsibleConnectionFailure(msg)
else:
@ -534,7 +541,7 @@ class Connection(ConnectionBase):
def _any_keys_added(self) -> bool:
for hostname, keys in self.ssh._host_keys.items(): # type: ignore[attr-defined] # TODO: figure out what _host_keys is!
for keytype, key in keys.items():
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
added_this_time = getattr(key, "_added_by_ansible_this_time", False)
if added_this_time:
return True
return False
@ -548,42 +555,42 @@ class Connection(ConnectionBase):
if not self._any_keys_added():
return
path = os.path.expanduser('~/.ssh')
path = os.path.expanduser("~/.ssh")
makedirs_safe(path)
with open(filename, 'w') as f:
with open(filename, "w") as f:
for hostname, keys in self.ssh._host_keys.items(): # type: ignore[attr-defined] # TODO: figure out what _host_keys is!
for keytype, key in keys.items():
# was f.write
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
added_this_time = getattr(key, "_added_by_ansible_this_time", False)
if not added_this_time:
f.write(f'{hostname} {keytype} {key.get_base64()}\n')
f.write(f"{hostname} {keytype} {key.get_base64()}\n")
for hostname, keys in self.ssh._host_keys.items(): # type: ignore[attr-defined] # TODO: figure out what _host_keys is!
for keytype, key in keys.items():
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
added_this_time = getattr(key, "_added_by_ansible_this_time", False)
if added_this_time:
f.write(f'{hostname} {keytype} {key.get_base64()}\n')
f.write(f"{hostname} {keytype} {key.get_base64()}\n")
def _build_wsl_command(self, cmd: str) -> str:
wsl_distribution = self.get_option('wsl_distribution')
become = self.get_option('become')
become_user = self.get_option('become_user')
wsl_distribution = self.get_option("wsl_distribution")
become = self.get_option("become")
become_user = self.get_option("become_user")
if become and become_user:
wsl_user = become_user
else:
wsl_user = self.get_option('wsl_user')
args = ['wsl.exe', '--distribution', wsl_distribution]
wsl_user = self.get_option("wsl_user")
args = ["wsl.exe", "--distribution", wsl_distribution]
if wsl_user:
args.extend(['--user', wsl_user])
args.extend(['--'])
args.extend(["--user", wsl_user])
args.extend(["--"])
args.extend(shlex.split(cmd))
if os.getenv('_ANSIBLE_TEST_WSL_CONNECTION_PLUGIN_Waeri5tepheeSha2fae8'):
if os.getenv("_ANSIBLE_TEST_WSL_CONNECTION_PLUGIN_Waeri5tepheeSha2fae8"):
return shlex.join(args)
return list2cmdline(args) # see https://github.com/python/cpython/blob/3.11/Lib/subprocess.py#L576
return list2cmdline(args) # see https://github.com/python/cpython/blob/3.11/Lib/subprocess.py#L576
def exec_command(self, cmd: str, in_data: bytes | None = None, sudoable: bool = True) -> tuple[int, bytes, bytes]:
""" run a command on inside a WSL distribution """
"""run a command on inside a WSL distribution"""
cmd = self._build_wsl_command(cmd)
@ -599,18 +606,18 @@ class Connection(ConnectionBase):
chan = transport.open_session()
except Exception as e:
text_e = to_text(e)
msg = 'Failed to open session'
msg = "Failed to open session"
if text_e:
msg += f': {text_e}'
msg += f": {text_e}"
raise AnsibleConnectionFailure(to_native(msg))
display.vvv(f'EXEC {cmd}', host=self.get_option('remote_addr'))
display.vvv(f"EXEC {cmd}", host=self.get_option("remote_addr"))
cmd = to_bytes(cmd, errors='surrogate_or_strict')
cmd = to_bytes(cmd, errors="surrogate_or_strict")
no_prompt_out = b''
no_prompt_err = b''
become_output = b''
no_prompt_out = b""
no_prompt_err = b""
become_output = b""
try:
chan.exec_command(cmd)
@ -618,14 +625,14 @@ class Connection(ConnectionBase):
password_prompt = False
become_success = False
while not (become_success or password_prompt):
display.debug('Waiting for Privilege Escalation input')
display.debug("Waiting for Privilege Escalation input")
chunk = chan.recv(bufsize)
display.debug(f'chunk is: {to_text(chunk)}')
display.debug(f"chunk is: {to_text(chunk)}")
if not chunk:
if b'unknown user' in become_output:
n_become_user = to_native(self.become.get_option('become_user'))
raise AnsibleError(f'user {n_become_user} does not exist')
if b"unknown user" in become_output:
n_become_user = to_native(self.become.get_option("become_user"))
raise AnsibleError(f"user {n_become_user} does not exist")
else:
break
# raise AnsibleError('ssh connection closed waiting for password prompt')
@ -643,80 +650,78 @@ class Connection(ConnectionBase):
if password_prompt:
if self.become:
become_pass = self.become.get_option('become_pass')
chan.sendall(to_bytes(f"{become_pass}\n", errors='surrogate_or_strict'))
become_pass = self.become.get_option("become_pass")
chan.sendall(to_bytes(f"{become_pass}\n", errors="surrogate_or_strict"))
else:
raise AnsibleError('A password is required but none was supplied')
raise AnsibleError("A password is required but none was supplied")
else:
no_prompt_out += become_output
no_prompt_err += become_output
if in_data:
for i in range(0, len(in_data), bufsize):
chan.send(in_data[i:i + bufsize])
chan.send(in_data[i : i + bufsize])
chan.shutdown_write()
elif in_data == b'':
elif in_data == b"":
chan.shutdown_write()
except socket.timeout:
raise AnsibleError(f'ssh timed out waiting for privilege escalation.\n{to_text(become_output)}')
raise AnsibleError(f"ssh timed out waiting for privilege escalation.\n{to_text(become_output)}")
stdout = b''.join(chan.makefile('rb', bufsize))
stderr = b''.join(chan.makefile_stderr('rb', bufsize))
stdout = b"".join(chan.makefile("rb", bufsize))
stderr = b"".join(chan.makefile_stderr("rb", bufsize))
returncode = chan.recv_exit_status()
# NB the full english error message is:
# 'wsl.exe' is not recognized as an internal or external command,
# operable program or batch file.
if "'wsl.exe' is not recognized" in stderr.decode('utf-8'):
raise AnsibleError(
f'wsl.exe not found in path of host: {to_text(self.get_option("remote_addr"))}')
if "'wsl.exe' is not recognized" in stderr.decode("utf-8"):
raise AnsibleError(f"wsl.exe not found in path of host: {to_text(self.get_option('remote_addr'))}")
return (returncode, no_prompt_out + stdout, no_prompt_out + stderr)
def put_file(self, in_path: str, out_path: str) -> None:
""" transfer a file from local to remote """
"""transfer a file from local to remote"""
display.vvv(f'PUT {in_path} TO {out_path}', host=self.get_option('remote_addr'))
display.vvv(f"PUT {in_path} TO {out_path}", host=self.get_option("remote_addr"))
try:
with open(in_path, 'rb') as f:
with open(in_path, "rb") as f:
data = f.read()
returncode, stdout, stderr = self.exec_command(
f"{self._shell.executable} -c {self._shell.quote(f'cat > {out_path}')}",
in_data=data,
sudoable=False)
sudoable=False,
)
if returncode != 0:
if 'cat: not found' in stderr.decode('utf-8'):
if "cat: not found" in stderr.decode("utf-8"):
raise AnsibleError(
f'cat not found in path of WSL distribution: {to_text(self.get_option("wsl_distribution"))}')
raise AnsibleError(
f'{to_text(stdout)}\n{to_text(stderr)}')
f"cat not found in path of WSL distribution: {to_text(self.get_option('wsl_distribution'))}"
)
raise AnsibleError(f"{to_text(stdout)}\n{to_text(stderr)}")
except Exception as e:
raise AnsibleError(
f'error occurred while putting file from {in_path} to {out_path}!\n{to_text(e)}')
raise AnsibleError(f"error occurred while putting file from {in_path} to {out_path}!\n{to_text(e)}")
def fetch_file(self, in_path: str, out_path: str) -> None:
""" save a remote file to the specified path """
"""save a remote file to the specified path"""
display.vvv(f'FETCH {in_path} TO {out_path}', host=self.get_option('remote_addr'))
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.get_option("remote_addr"))
try:
returncode, stdout, stderr = self.exec_command(
f"{self._shell.executable} -c {self._shell.quote(f'cat {in_path}')}",
sudoable=False)
f"{self._shell.executable} -c {self._shell.quote(f'cat {in_path}')}", sudoable=False
)
if returncode != 0:
if 'cat: not found' in stderr.decode('utf-8'):
if "cat: not found" in stderr.decode("utf-8"):
raise AnsibleError(
f'cat not found in path of WSL distribution: {to_text(self.get_option("wsl_distribution"))}')
raise AnsibleError(
f'{to_text(stdout)}\n{to_text(stderr)}')
with open(out_path, 'wb') as f:
f"cat not found in path of WSL distribution: {to_text(self.get_option('wsl_distribution'))}"
)
raise AnsibleError(f"{to_text(stdout)}\n{to_text(stderr)}")
with open(out_path, "wb") as f:
f.write(stdout)
except Exception as e:
raise AnsibleError(
f'error occurred while fetching file from {in_path} to {out_path}!\n{to_text(e)}')
raise AnsibleError(f"error occurred while fetching file from {in_path} to {out_path}!\n{to_text(e)}")
def reset(self) -> None:
""" reset the connection """
"""reset the connection"""
if not self._connected:
return
@ -724,9 +729,9 @@ class Connection(ConnectionBase):
self._connect()
def close(self) -> None:
""" terminate the connection """
"""terminate the connection"""
if self.get_option('host_key_checking') and self.get_option('record_host_keys') and self._any_keys_added():
if self.get_option("host_key_checking") and self.get_option("record_host_keys") and self._any_keys_added():
# add any new SSH host keys -- warning -- this could be slow
# (This doesn't acquire the connection lock because it needs
# to exclude only other known_hosts writers, not connections
@ -736,7 +741,7 @@ class Connection(ConnectionBase):
makedirs_safe(dirname)
tmp_keyfile_name = None
try:
with FileLock().lock_file(lockfile, dirname, self.get_option('lock_file_timeout')):
with FileLock().lock_file(lockfile, dirname, self.get_option("lock_file_timeout")):
# just in case any were added recently
self.ssh.load_system_host_keys()
@ -769,14 +774,14 @@ class Connection(ConnectionBase):
os.rename(tmp_keyfile_name, self.keyfile)
except LockTimeout:
raise AnsibleError(
f'writing lock file for {self.keyfile} ran in to the timeout of {self.get_option("lock_file_timeout")}s')
f"writing lock file for {self.keyfile} ran in to the timeout of {self.get_option('lock_file_timeout')}s"
)
except paramiko.hostkeys.InvalidHostKey as e:
raise AnsibleConnectionFailure(f'Invalid host key: {e.line}')
raise AnsibleConnectionFailure(f"Invalid host key: {e.line}")
except Exception as e:
# unable to save keys, including scenario when key was invalid
# and caught earlier
raise AnsibleError(
f'error occurred while writing SSH host keys!\n{to_text(e)}')
raise AnsibleError(f"error occurred while writing SSH host keys!\n{to_text(e)}")
finally:
if tmp_keyfile_name is not None:
pathlib.Path(tmp_keyfile_name).unlink(missing_ok=True)

View file

@ -42,9 +42,9 @@ display = Display()
class Connection(ConnectionBase):
""" Local zone based connections """
"""Local zone based connections"""
transport = 'community.general.zone'
transport = "community.general.zone"
has_pipelining = True
has_tty = False
@ -56,8 +56,8 @@ class Connection(ConnectionBase):
if os.geteuid() != 0:
raise AnsibleError("zone connection requires running as root")
self.zoneadm_cmd = to_bytes(self._search_executable('zoneadm'))
self.zlogin_cmd = to_bytes(self._search_executable('zlogin'))
self.zoneadm_cmd = to_bytes(self._search_executable("zoneadm"))
self.zlogin_cmd = to_bytes(self._search_executable("zlogin"))
if self.zone not in self.list_zones():
raise AnsibleError(f"incorrect zone name {self.zone}")
@ -70,15 +70,15 @@ class Connection(ConnectionBase):
raise AnsibleError(f"{executable} command not found in PATH")
def list_zones(self):
process = subprocess.Popen([self.zoneadm_cmd, 'list', '-ip'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process = subprocess.Popen(
[self.zoneadm_cmd, "list", "-ip"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
zones = []
for line in process.stdout.readlines():
# 1:work:running:/zones/work:3126dc59-9a07-4829-cde9-a816e4c5040e:native:shared
s = line.split(':')
if s[1] != 'global':
s = line.split(":")
if s[1] != "global":
zones.append(s[1])
return zones
@ -86,23 +86,26 @@ class Connection(ConnectionBase):
def get_zone_path(self):
# solaris10vm# zoneadm -z cswbuild list -p
# -:cswbuild:installed:/zones/cswbuild:479f3c4b-d0c6-e97b-cd04-fd58f2c0238e:native:shared
process = subprocess.Popen([self.zoneadm_cmd, '-z', to_bytes(self.zone), 'list', '-p'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process = subprocess.Popen(
[self.zoneadm_cmd, "-z", to_bytes(self.zone), "list", "-p"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
# stdout, stderr = p.communicate()
path = process.stdout.readlines()[0].split(':')[3]
path = process.stdout.readlines()[0].split(":")[3]
return f"{path}/root"
def _connect(self):
""" connect to the zone; nothing to do here """
"""connect to the zone; nothing to do here"""
super()._connect()
if not self._connected:
display.vvv("THIS IS A LOCAL ZONE DIR", host=self.zone)
self._connected = True
def _buffered_exec_command(self, cmd, stdin=subprocess.PIPE):
""" run a command on the zone. This is only needed for implementing
"""run a command on the zone. This is only needed for implementing
put_file() get_file() so that we don't have to read the whole file
into memory.
@ -116,13 +119,12 @@ class Connection(ConnectionBase):
local_cmd = map(to_bytes, local_cmd)
display.vvv(f"EXEC {local_cmd}", host=self.zone)
p = subprocess.Popen(local_cmd, shell=False, stdin=stdin,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p = subprocess.Popen(local_cmd, shell=False, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return p
def exec_command(self, cmd, in_data=None, sudoable=False):
""" run a command on the zone """
"""run a command on the zone"""
super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
p = self._buffered_exec_command(cmd)
@ -131,33 +133,33 @@ class Connection(ConnectionBase):
return p.returncode, stdout, stderr
def _prefix_login_path(self, remote_path):
""" Make sure that we put files into a standard path
"""Make sure that we put files into a standard path
If a path is relative, then we need to choose where to put it.
ssh chooses $HOME but we aren't guaranteed that a home dir will
exist in any given chroot. So for now we're choosing "/" instead.
This also happens to be the former default.
If a path is relative, then we need to choose where to put it.
ssh chooses $HOME but we aren't guaranteed that a home dir will
exist in any given chroot. So for now we're choosing "/" instead.
This also happens to be the former default.
Can revisit using $HOME instead if it is a problem
Can revisit using $HOME instead if it is a problem
"""
if not remote_path.startswith(os.path.sep):
remote_path = os.path.join(os.path.sep, remote_path)
return os.path.normpath(remote_path)
def put_file(self, in_path, out_path):
""" transfer a file from local to zone """
"""transfer a file from local to zone"""
super().put_file(in_path, out_path)
display.vvv(f"PUT {in_path} TO {out_path}", host=self.zone)
out_path = shlex_quote(self._prefix_login_path(out_path))
try:
with open(in_path, 'rb') as in_file:
with open(in_path, "rb") as in_file:
if not os.fstat(in_file.fileno()).st_size:
count = ' count=0'
count = " count=0"
else:
count = ''
count = ""
try:
p = self._buffered_exec_command(f'dd of={out_path} bs={BUFSIZE}{count}', stdin=in_file)
p = self._buffered_exec_command(f"dd of={out_path} bs={BUFSIZE}{count}", stdin=in_file)
except OSError:
raise AnsibleError("jail connection requires dd command in the jail")
try:
@ -171,17 +173,17 @@ class Connection(ConnectionBase):
raise AnsibleError(f"file or module does not exist at: {in_path}")
def fetch_file(self, in_path, out_path):
""" fetch a file from zone to local """
"""fetch a file from zone to local"""
super().fetch_file(in_path, out_path)
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.zone)
in_path = shlex_quote(self._prefix_login_path(in_path))
try:
p = self._buffered_exec_command(f'dd if={in_path} bs={BUFSIZE}')
p = self._buffered_exec_command(f"dd if={in_path} bs={BUFSIZE}")
except OSError:
raise AnsibleError("zone connection requires dd command in the zone")
with open(out_path, 'wb+') as out_file:
with open(out_path, "wb+") as out_file:
try:
chunk = p.stdout.read(BUFSIZE)
while chunk:
@ -195,6 +197,6 @@ class Connection(ConnectionBase):
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}:\n{stdout}\n{stderr}")
def close(self):
""" terminate the connection; nothing to do here """
"""terminate the connection; nothing to do here"""
super().close()
self._connected = False

View file

@ -1,4 +1,3 @@
# Copyright (c) 2017-present Alibaba Group Holding Limited. He Guimin <heguimin36@163.com>
# 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
@ -7,7 +6,6 @@ from __future__ import annotations
class ModuleDocFragment:
# Alicloud only documentation fragment
DOCUMENTATION = r"""
options:

View file

@ -1,4 +1,3 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
@ -7,7 +6,6 @@ from __future__ import annotations
class ModuleDocFragment:
# Standard documentation fragment
DOCUMENTATION = r"""
options: {}
@ -27,7 +25,7 @@ attributes:
"""
# Should be used together with the standard fragment
INFO_MODULE = r'''
INFO_MODULE = r"""
options: {}
attributes:
check_mode:
@ -38,7 +36,7 @@ attributes:
support: N/A
details:
- This action does not modify state.
'''
"""
CONN = r"""
options: {}
@ -59,7 +57,7 @@ attributes:
"""
# Should be used together with the standard fragment and the FACTS fragment
FACTS_MODULE = r'''
FACTS_MODULE = r"""
options: {}
attributes:
check_mode:
@ -72,7 +70,7 @@ attributes:
- This action does not modify state.
facts:
support: full
'''
"""
FILES = r"""
options: {}

View file

@ -6,7 +6,6 @@ from __future__ import annotations
class ModuleDocFragment:
# Standard files documentation fragment
DOCUMENTATION = r"""
options:

View file

@ -1,4 +1,3 @@
# Copyright (c) 2019, Evgeniy Krysanov <evgeniy.krysanov@gmail.com>
# 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
@ -7,7 +6,6 @@ from __future__ import annotations
class ModuleDocFragment:
# Standard documentation fragment
DOCUMENTATION = r"""
options:

View file

@ -16,7 +16,6 @@ from __future__ import annotations
class ModuleDocFragment:
# Dimension Data doc fragment
DOCUMENTATION = r"""
options:

View file

@ -16,7 +16,6 @@ from __future__ import annotations
class ModuleDocFragment:
# Dimension Data ("wait-for-completion" parameters) doc fragment
DOCUMENTATION = r"""
options:

View file

@ -1,4 +1,3 @@
# Copyright (c) 2018, Luca Lorenzetto (@remix_tj) <lorenzetto.luca@gmail.com>
# 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
@ -7,9 +6,8 @@ from __future__ import annotations
class ModuleDocFragment:
# Documentation fragment for VNX (emc_vnx)
EMC_VNX = r'''
EMC_VNX = r"""
options:
sp_address:
description:
@ -31,4 +29,4 @@ requirements:
- storops (0.5.10 or greater). Install using C(pip install storops).
notes:
- The modules prefixed with C(emc_vnx) are built to support the EMC VNX storage platform.
'''
"""

View file

@ -6,7 +6,6 @@ from __future__ import annotations
class ModuleDocFragment:
# Standard files documentation fragment
DOCUMENTATION = r"""
requirements:

View file

@ -6,7 +6,6 @@ from __future__ import annotations
class ModuleDocFragment:
# HPE 3PAR doc fragment
DOCUMENTATION = r"""
options:

View file

@ -6,7 +6,6 @@ from __future__ import annotations
class ModuleDocFragment:
# HWC doc fragment.
DOCUMENTATION = r"""
options:

View file

@ -1,4 +1,3 @@
# Copyright (c) 2018, IBM CORPORATION
# Author(s): Tzur Eliyahu <tzure@il.ibm.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
@ -8,7 +7,6 @@ from __future__ import annotations
class ModuleDocFragment:
# ibm_storage documentation fragment
DOCUMENTATION = r"""
options:

View file

@ -1,4 +1,3 @@
# Copyright (c) 2017, Ansible Project
# Copyright (c) 2017, Abhijeet Kasurde (akasurde@redhat.com)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)

View file

@ -1,4 +1,3 @@
# Copyright (c) 2017-18, Ansible Project
# Copyright (c) 2017-18, Abhijeet Kasurde (akasurde@redhat.com)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)

View file

@ -1,4 +1,3 @@
# Copyright (c) 2017, Eike Frost <ei@kefro.st>
# 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
@ -7,7 +6,6 @@ from __future__ import annotations
class ModuleDocFragment:
# Standard documentation fragment
DOCUMENTATION = r"""
options:

View file

@ -1,4 +1,3 @@
# Copyright (c) 2016, Peter Sagerson <psagers@ignorare.net>
# Copyright (c) 2016, Jiri Tyr <jiri.tyr@gmail.com>
# Copyright (c) 2017-2018 Keller Fuchs (@KellerFuchs) <kellerfuchs@hashbang.sh>

View file

@ -1,4 +1,3 @@
# Copyright (C) 2017 Lenovo, Inc.
# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause)
# SPDX-License-Identifier: BSD-2-Clause

View file

@ -1,4 +1,3 @@
# Copyright (c) 2017, Daniel Korn <korndaniel1@gmail.com>
# 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
@ -7,7 +6,6 @@ from __future__ import annotations
class ModuleDocFragment:
# Standard ManageIQ documentation fragment
DOCUMENTATION = r"""
options:

View file

@ -1,4 +1,3 @@
# Copyright (c) 2020 FERREIRA Christophe <christophe.ferreira@cnaf.fr>
# 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
@ -7,7 +6,6 @@ from __future__ import annotations
class ModuleDocFragment:
# Standard files documentation fragment
DOCUMENTATION = r"""
options:

View file

@ -1,4 +1,3 @@
# Copyright (c) 2023, Ansible Project
# 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

View file

@ -7,7 +7,6 @@ from __future__ import annotations
class ModuleDocFragment:
# OneView doc fragment
DOCUMENTATION = r"""
options:

View file

@ -6,7 +6,6 @@ from __future__ import annotations
class ModuleDocFragment:
# Standard documentation fragment
DOCUMENTATION = r"""
options:

View file

@ -1,4 +1,3 @@
# Copyright (c) 2018, www.privaz.io Valletech AB
# 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

View file

@ -1,4 +1,3 @@
# Copyright (c) 2015, Peter Sprygada <psprygada@ansible.com>
# 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
@ -7,7 +6,6 @@ from __future__ import annotations
class ModuleDocFragment:
# Standard files documentation fragment
DOCUMENTATION = r"""
options:

View file

@ -1,4 +1,3 @@
# Copyright (c) 2024, Alexei Znamensky <russoz@gmail.com>
# 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

View file

@ -1,4 +1,3 @@
# Copyright (c) 2021, Florian Dambrine <android.florian@gmail.com>
# 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
@ -7,7 +6,6 @@ from __future__ import annotations
class ModuleDocFragment:
DOCUMENTATION = r"""
options:
pritunl_url:

View file

@ -1,4 +1,3 @@
# Copyright (c) 2025 Ansible community
# 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
@ -7,7 +6,6 @@ from __future__ import annotations
class ModuleDocFragment:
# Use together with the community.general.redfish module utils' REDFISH_COMMON_ARGUMENT_SPEC
DOCUMENTATION = r"""
options:

View file

@ -1,4 +1,3 @@
# Copyright (c) 2021, Andreas Botzner <andreas at botzner dot com>
# 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

View file

@ -1,4 +1,3 @@
# Copyright (c) 2021, Phillipe Smith <phsmithcc@gmail.com>
# 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
@ -7,7 +6,6 @@ from __future__ import annotations
class ModuleDocFragment:
# Standard files documentation fragment
DOCUMENTATION = r"""
options:

View file

@ -1,4 +1,3 @@
# Copyright (c) 2018, Yanis Guenane <yanis+ansible@guenane.org>
# 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
@ -7,7 +6,6 @@ from __future__ import annotations
class ModuleDocFragment:
# Standard documentation fragment
DOCUMENTATION = r"""
options:

View file

@ -1,4 +1,3 @@
# Copyright (c) 2022, Guillaume MARTINEZ <lunik@tiwabbit.fr>
# 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
@ -7,7 +6,6 @@ from __future__ import annotations
class ModuleDocFragment:
# Standard documentation fragment
DOCUMENTATION = r"""
options:

View file

@ -1,4 +1,3 @@
# Copyright (c) 2018, Johannes Brunswicker <johannes.brunswicker@gmail.com>
# 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

View file

@ -7,9 +7,8 @@ from __future__ import annotations
class ModuleDocFragment:
# Documentation fragment for Vexata VX100 series
VX100 = r'''
VX100 = r"""
options:
array:
description:
@ -41,4 +40,4 @@ requirements:
- vexatapi >= 0.0.1
- E(VEXATA_USER) and E(VEXATA_PASSWORD) environment variables must be set if
user and password arguments are not passed to the module directly.
'''
"""

View file

@ -1,4 +1,3 @@
# Copyright (c) 2018, Bojan Vitnik <bvitnik@mainstream.rs>
# 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

View file

@ -49,14 +49,13 @@ from ansible.errors import AnsibleFilterError
def list_accumulate(sequence):
if not isinstance(sequence, Sequence):
raise AnsibleFilterError(f'Invalid value type ({type(sequence)}) for accumulate ({sequence!r})')
raise AnsibleFilterError(f"Invalid value type ({type(sequence)}) for accumulate ({sequence!r})")
return accumulate(sequence)
class FilterModule:
def filters(self):
return {
'accumulate': list_accumulate,
"accumulate": list_accumulate,
}

View file

@ -40,9 +40,11 @@ from collections import Counter
def counter(sequence):
''' Count elements in a sequence. Returns dict with count result. '''
"""Count elements in a sequence. Returns dict with count result."""
if not isinstance(sequence, Sequence):
raise AnsibleFilterError(f'Argument for community.general.counter must be a sequence (string or list). {sequence} is {type(sequence)}')
raise AnsibleFilterError(
f"Argument for community.general.counter must be a sequence (string or list). {sequence} is {type(sequence)}"
)
try:
result = dict(Counter(sequence))
@ -54,11 +56,11 @@ def counter(sequence):
class FilterModule:
''' Ansible counter jinja2 filters '''
"""Ansible counter jinja2 filters"""
def filters(self):
filters = {
'counter': counter,
"counter": counter,
}
return filters

View file

@ -9,6 +9,7 @@ from ansible.module_utils.common.collections import is_string
try:
from zlib import crc32
HAS_ZLIB = True
except ImportError:
HAS_ZLIB = False
@ -45,17 +46,17 @@ _value:
def crc32s(value):
if not is_string(value):
raise AnsibleFilterError(f'Invalid value type ({type(value)}) for crc32 ({value!r})')
raise AnsibleFilterError(f"Invalid value type ({type(value)}) for crc32 ({value!r})")
if not HAS_ZLIB:
raise AnsibleFilterError('Failed to import zlib module')
raise AnsibleFilterError("Failed to import zlib module")
data = to_bytes(value, errors='surrogate_or_strict')
return f"{crc32(data) & 0xffffffff:x}"
data = to_bytes(value, errors="surrogate_or_strict")
return f"{crc32(data) & 0xFFFFFFFF:x}"
class FilterModule:
def filters(self):
return {
'crc32': crc32s,
"crc32": crc32s,
}

View file

@ -1,4 +1,3 @@
# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
# 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
@ -61,17 +60,17 @@ _value:
def dict_filter(sequence):
'''Convert a list of tuples to a dictionary.
"""Convert a list of tuples to a dictionary.
Example: ``[[1, 2], ['a', 'b']] | community.general.dict`` results in ``{1: 2, 'a': 'b'}``
'''
"""
return dict(sequence)
class FilterModule:
'''Ansible jinja2 filters'''
"""Ansible jinja2 filters"""
def filters(self):
return {
'dict': dict_filter,
"dict": dict_filter,
}

View file

@ -38,7 +38,7 @@ _value:
def dict_kv(value, key):
'''Return a dictionary with a single key-value pair
"""Return a dictionary with a single key-value pair
Example:
@ -89,14 +89,12 @@ def dict_kv(value, key):
}
]
}
'''
"""
return {key: value}
class FilterModule:
''' Query filter '''
"""Query filter"""
def filters(self):
return {
'dict_kv': dict_kv
}
return {"dict_kv": dict_kv}

View file

@ -1,4 +1,3 @@
# Copyright (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
# Copyright (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
@ -80,13 +79,16 @@ _value:
from ansible.errors import AnsibleFilterError
from ansible_collections.community.general.plugins.module_utils.csv import (initialize_dialect, read_csv, CSVError,
DialectNotAvailableError,
CustomDialectFailureError)
from ansible_collections.community.general.plugins.module_utils.csv import (
initialize_dialect,
read_csv,
CSVError,
DialectNotAvailableError,
CustomDialectFailureError,
)
def from_csv(data, dialect='excel', fieldnames=None, delimiter=None, skipinitialspace=None, strict=None):
def from_csv(data, dialect="excel", fieldnames=None, delimiter=None, skipinitialspace=None, strict=None):
dialect_params = {
"delimiter": delimiter,
"skipinitialspace": skipinitialspace,
@ -112,8 +114,5 @@ def from_csv(data, dialect='excel', fieldnames=None, delimiter=None, skipinitial
class FilterModule:
def filters(self):
return {
'from_csv': from_csv
}
return {"from_csv": from_csv}

View file

@ -1,4 +1,3 @@
# Copyright (c) 2023, Steffen Scheib <steffen@scheib.me>
# 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
@ -51,7 +50,7 @@ from ansible.errors import AnsibleFilterError
class IniParser(ConfigParser):
''' Implements a configparser which is able to return a dict '''
"""Implements a configparser which is able to return a dict"""
def __init__(self):
super().__init__(interpolation=None)
@ -61,35 +60,32 @@ class IniParser(ConfigParser):
d = dict(self._sections)
for k in d:
d[k] = dict(self._defaults, **d[k])
d[k].pop('__name__', None)
d[k].pop("__name__", None)
if self._defaults:
d['DEFAULT'] = dict(self._defaults)
d["DEFAULT"] = dict(self._defaults)
return d
def from_ini(obj):
''' Read the given string as INI file and return a dict '''
"""Read the given string as INI file and return a dict"""
if not isinstance(obj, str):
raise AnsibleFilterError(f'from_ini requires a str, got {type(obj)}')
raise AnsibleFilterError(f"from_ini requires a str, got {type(obj)}")
parser = IniParser()
try:
parser.read_file(StringIO(obj))
except Exception as ex:
raise AnsibleFilterError(f'from_ini failed to parse given string: {ex}', orig_exc=ex)
raise AnsibleFilterError(f"from_ini failed to parse given string: {ex}", orig_exc=ex)
return parser.as_dict()
class FilterModule:
''' Query filter '''
"""Query filter"""
def filters(self):
return {
'from_ini': from_ini
}
return {"from_ini": from_ini}

View file

@ -57,33 +57,33 @@ from collections.abc import Mapping, Sequence
def groupby_as_dict(sequence, attribute):
'''
"""
Given a sequence of dictionaries and an attribute name, returns a dictionary mapping
the value of this attribute to the dictionary.
If multiple dictionaries in the sequence have the same value for this attribute,
the filter will fail.
'''
"""
if not isinstance(sequence, Sequence):
raise AnsibleFilterError('Input is not a sequence')
raise AnsibleFilterError("Input is not a sequence")
result = dict()
for list_index, element in enumerate(sequence):
if not isinstance(element, Mapping):
raise AnsibleFilterError(f'Sequence element #{list_index} is not a mapping')
raise AnsibleFilterError(f"Sequence element #{list_index} is not a mapping")
if attribute not in element:
raise AnsibleFilterError(f'Attribute not contained in element #{list_index} of sequence')
raise AnsibleFilterError(f"Attribute not contained in element #{list_index} of sequence")
result_index = element[attribute]
if result_index in result:
raise AnsibleFilterError(f'Multiple sequence entries have attribute value {result_index!r}')
raise AnsibleFilterError(f"Multiple sequence entries have attribute value {result_index!r}")
result[result_index] = element
return result
class FilterModule:
''' Ansible list filters '''
"""Ansible list filters"""
def filters(self):
return {
'groupby_as_dict': groupby_as_dict,
"groupby_as_dict": groupby_as_dict,
}

View file

@ -1,4 +1,3 @@
# Copyright (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
# 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
@ -20,6 +19,7 @@ except ImportError:
try:
from hashids import Hashids
HAS_HASHIDS = True
except ImportError:
HAS_HASHIDS = False
@ -35,27 +35,21 @@ def initialize_hashids(**kwargs):
return Hashids(**params)
except TypeError as e:
raise AnsibleFilterError(
"The provided parameters %s are invalid: %s" % (
', '.join(["%s=%s" % (k, v) for k, v in params.items()]),
to_native(e)
)
"The provided parameters %s are invalid: %s"
% (", ".join(["%s=%s" % (k, v) for k, v in params.items()]), to_native(e))
)
def hashids_encode(nums, salt=None, alphabet=None, min_length=None):
"""Generates a YouTube-like hash from a sequence of ints
:nums: Sequence of one or more ints to hash
:salt: String to use as salt when hashing
:alphabet: String of 16 or more unique characters to produce a hash
:min_length: Minimum length of hash produced
:nums: Sequence of one or more ints to hash
:salt: String to use as salt when hashing
:alphabet: String of 16 or more unique characters to produce a hash
:min_length: Minimum length of hash produced
"""
hashids = initialize_hashids(
salt=salt,
alphabet=alphabet,
min_length=min_length
)
hashids = initialize_hashids(salt=salt, alphabet=alphabet, min_length=min_length)
# Handles the case where a single int is not encapsulated in a list or tuple.
# User convenience seems preferable to strict typing in this case
@ -74,25 +68,20 @@ def hashids_encode(nums, salt=None, alphabet=None, min_length=None):
def hashids_decode(hashid, salt=None, alphabet=None, min_length=None):
"""Decodes a YouTube-like hash to a sequence of ints
:hashid: Hash string to decode
:salt: String to use as salt when hashing
:alphabet: String of 16 or more unique characters to produce a hash
:min_length: Minimum length of hash produced
:hashid: Hash string to decode
:salt: String to use as salt when hashing
:alphabet: String of 16 or more unique characters to produce a hash
:min_length: Minimum length of hash produced
"""
hashids = initialize_hashids(
salt=salt,
alphabet=alphabet,
min_length=min_length
)
hashids = initialize_hashids(salt=salt, alphabet=alphabet, min_length=min_length)
nums = hashids.decode(hashid)
return list(nums)
class FilterModule:
def filters(self):
return {
'hashids_encode': hashids_encode,
'hashids_decode': hashids_decode,
"hashids_encode": hashids_encode,
"hashids_decode": hashids_decode,
}

View file

@ -79,6 +79,7 @@ import importlib
try:
import jc
HAS_LIB = True
except ImportError:
HAS_LIB = False
@ -133,26 +134,28 @@ def jc_filter(data, parser, quiet=True, raw=False):
"""
if not HAS_LIB:
raise AnsibleError('You need to install "jc" as a Python library on the Ansible controller prior to running jc filter')
raise AnsibleError(
'You need to install "jc" as a Python library on the Ansible controller prior to running jc filter'
)
try:
# new API (jc v1.18.0 and higher) allows use of plugin parsers
if hasattr(jc, 'parse'):
if hasattr(jc, "parse"):
return jc.parse(parser, data, quiet=quiet, raw=raw)
# old API (jc v1.17.7 and lower)
else:
jc_parser = importlib.import_module(f'jc.parsers.{parser}')
jc_parser = importlib.import_module(f"jc.parsers.{parser}")
return jc_parser.parse(data, quiet=quiet, raw=raw)
except Exception as e:
raise AnsibleFilterError(f'Error in jc filter plugin: {e}')
raise AnsibleFilterError(f"Error in jc filter plugin: {e}")
class FilterModule:
''' Query filter '''
"""Query filter"""
def filters(self):
return {
'jc': jc_filter,
"jc": jc_filter,
}

View file

@ -35,39 +35,28 @@ class FilterModule:
try:
return loads(inp)
except Exception as e:
raise AnsibleFilterError(
f"{filter_name}: could not decode JSON from {object_name}: {e}"
) from e
raise AnsibleFilterError(f"{filter_name}: could not decode JSON from {object_name}: {e}") from e
if not isinstance(inp, (list, dict)):
raise AnsibleFilterError(
f"{filter_name}: {object_name} is not dictionary, list or string"
)
raise AnsibleFilterError(f"{filter_name}: {object_name} is not dictionary, list or string")
return inp
def check_patch_arguments(self, filter_name: str, args: dict):
if "op" not in args or not isinstance(args["op"], str):
raise AnsibleFilterError(f"{filter_name}: 'op' argument is not a string")
if args["op"] not in OPERATIONS_AVAILABLE:
raise AnsibleFilterError(
f"{filter_name}: unsupported 'op' argument: {args['op']}"
)
raise AnsibleFilterError(f"{filter_name}: unsupported 'op' argument: {args['op']}")
if "path" not in args or not isinstance(args["path"], str):
raise AnsibleFilterError(f"{filter_name}: 'path' argument is not a string")
if args["op"] in OPERATIONS_NEEDING_FROM:
if "from" not in args:
raise AnsibleFilterError(
f"{filter_name}: 'from' argument missing for '{args['op']}' operation"
)
raise AnsibleFilterError(f"{filter_name}: 'from' argument missing for '{args['op']}' operation")
if not isinstance(args["from"], str):
raise AnsibleFilterError(
f"{filter_name}: 'from' argument is not a string"
)
raise AnsibleFilterError(f"{filter_name}: 'from' argument is not a string")
def json_patch(
self,
@ -77,7 +66,6 @@ class FilterModule:
value: Any = None,
**kwargs: dict,
) -> Any:
if not HAS_LIB:
raise AnsibleFilterError(
"You need to install 'jsonpatch' package prior to running 'json_patch' filter"
@ -88,9 +76,7 @@ class FilterModule:
fail_test = kwargs.pop("fail_test", False)
if kwargs:
raise AnsibleFilterError(
f"json_patch: unexpected keywords arguments: {', '.join(sorted(kwargs))}"
)
raise AnsibleFilterError(f"json_patch: unexpected keywords arguments: {', '.join(sorted(kwargs))}")
if not isinstance(fail_test, bool):
raise AnsibleFilterError("json_patch: 'fail_test' argument is not a bool")
@ -109,9 +95,7 @@ class FilterModule:
result = jsonpatch.apply_patch(inp, [args])
except jsonpatch.JsonPatchTestFailed as e:
if fail_test:
raise AnsibleFilterError(
f"json_patch: test operation failed: {e}"
) from e
raise AnsibleFilterError(f"json_patch: test operation failed: {e}") from e
else:
pass
except Exception as e:
@ -126,16 +110,13 @@ class FilterModule:
/,
fail_test: bool = False,
) -> Any:
if not HAS_LIB:
raise AnsibleFilterError(
"You need to install 'jsonpatch' package prior to running 'json_patch_recipe' filter"
) from JSONPATCH_IMPORT_ERROR
if not isinstance(operations, list):
raise AnsibleFilterError(
"json_patch_recipe: 'operations' needs to be a list"
)
raise AnsibleFilterError("json_patch_recipe: 'operations' needs to be a list")
if not isinstance(fail_test, bool):
raise AnsibleFilterError("json_patch: 'fail_test' argument is not a bool")
@ -150,9 +131,7 @@ class FilterModule:
result = jsonpatch.apply_patch(inp, operations)
except jsonpatch.JsonPatchTestFailed as e:
if fail_test:
raise AnsibleFilterError(
f"json_patch_recipe: test operation failed: {e}"
) from e
raise AnsibleFilterError(f"json_patch_recipe: test operation failed: {e}") from e
else:
pass
except Exception as e:
@ -165,7 +144,6 @@ class FilterModule:
inp: Union[str, list, dict, bytes, bytearray],
target: Union[str, list, dict, bytes, bytearray],
) -> list:
if not HAS_LIB:
raise AnsibleFilterError(
"You need to install 'jsonpatch' package prior to running 'json_diff' filter"

View file

@ -109,44 +109,46 @@ from ansible.errors import AnsibleError, AnsibleFilterError
try:
import jmespath
HAS_LIB = True
except ImportError:
HAS_LIB = False
def json_query(data, expr):
'''Query data using jmespath query language ( http://jmespath.org ). Example:
"""Query data using jmespath query language ( http://jmespath.org ). Example:
- ansible.builtin.debug: msg="{{ instance | json_query(tagged_instances[*].block_device_mapping.*.volume_id') }}"
'''
"""
if not HAS_LIB:
raise AnsibleError('You need to install "jmespath" prior to running '
'json_query filter')
raise AnsibleError('You need to install "jmespath" prior to running json_query filter')
# Hack to handle Ansible Unsafe text, AnsibleMapping and AnsibleSequence
# See issues https://github.com/ansible-collections/community.general/issues/320
# and https://github.com/ansible/ansible/issues/85600.
jmespath.functions.REVERSE_TYPES_MAP['string'] = jmespath.functions.REVERSE_TYPES_MAP['string'] + (
'AnsibleUnicode', 'AnsibleUnsafeText', '_AnsibleTaggedStr',
jmespath.functions.REVERSE_TYPES_MAP["string"] = jmespath.functions.REVERSE_TYPES_MAP["string"] + (
"AnsibleUnicode",
"AnsibleUnsafeText",
"_AnsibleTaggedStr",
)
jmespath.functions.REVERSE_TYPES_MAP['array'] = jmespath.functions.REVERSE_TYPES_MAP['array'] + (
'AnsibleSequence', '_AnsibleLazyTemplateList',
jmespath.functions.REVERSE_TYPES_MAP["array"] = jmespath.functions.REVERSE_TYPES_MAP["array"] + (
"AnsibleSequence",
"_AnsibleLazyTemplateList",
)
jmespath.functions.REVERSE_TYPES_MAP['object'] = jmespath.functions.REVERSE_TYPES_MAP['object'] + (
'AnsibleMapping', '_AnsibleLazyTemplateDict',
jmespath.functions.REVERSE_TYPES_MAP["object"] = jmespath.functions.REVERSE_TYPES_MAP["object"] + (
"AnsibleMapping",
"_AnsibleLazyTemplateDict",
)
try:
return jmespath.search(expr, data)
except jmespath.exceptions.JMESPathError as e:
raise AnsibleFilterError(f'JMESPathError in json_query filter plugin:\n{e}')
raise AnsibleFilterError(f"JMESPathError in json_query filter plugin:\n{e}")
except Exception as e:
# For older jmespath, we can get ValueError and TypeError without much info.
raise AnsibleFilterError(f'Error in jmespath.search in json_query filter plugin:\n{e}')
raise AnsibleFilterError(f"Error in jmespath.search in json_query filter plugin:\n{e}")
class FilterModule:
''' Query filter '''
"""Query filter"""
def filters(self):
return {
'json_query': json_query
}
return {"json_query": json_query}

View file

@ -101,10 +101,11 @@ _value:
from ansible_collections.community.general.plugins.plugin_utils.keys_filter import (
_keys_filter_params,
_keys_filter_target_str)
_keys_filter_target_str,
)
def keep_keys(data, target=None, matching_parameter='equal'):
def keep_keys(data, target=None, matching_parameter="equal"):
"""keep specific keys from dictionaries in a list"""
# test parameters
@ -112,16 +113,20 @@ def keep_keys(data, target=None, matching_parameter='equal'):
# test and transform target
tt = _keys_filter_target_str(target, matching_parameter)
if matching_parameter == 'equal':
if matching_parameter == "equal":
def keep_key(key):
return key in tt
elif matching_parameter == 'starts_with':
elif matching_parameter == "starts_with":
def keep_key(key):
return key.startswith(tt)
elif matching_parameter == 'ends_with':
elif matching_parameter == "ends_with":
def keep_key(key):
return key.endswith(tt)
elif matching_parameter == 'regex':
elif matching_parameter == "regex":
def keep_key(key):
return tt.match(key) is not None
@ -129,8 +134,7 @@ def keep_keys(data, target=None, matching_parameter='equal'):
class FilterModule:
def filters(self):
return {
'keep_keys': keep_keys,
"keep_keys": keep_keys,
}

Some files were not shown because too many files have changed in this diff Show more