This commit is contained in:
2024-02-07 16:57:29 -05:00
parent 9af19aac54
commit 2e7ff257d6
3284 changed files with 4 additions and 457935 deletions

View File

@ -1,72 +0,0 @@
# (c) 2015, Brian Coca <briancoca+dev@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
from ansible.errors import AnsibleError, AnsibleAction, _AnsibleActionDone, AnsibleActionFail
from ansible.module_utils._text import to_native
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.plugins.action import ActionBase
class ActionModule(ActionBase):
TRANSFERS_FILES = True
def run(self, tmp=None, task_vars=None):
if task_vars is None:
task_vars = dict()
result = super(ActionModule, self).run(tmp, task_vars)
del tmp # tmp no longer has any effect
src = self._task.args.get('src', None)
remote_src = boolean(self._task.args.get('remote_src', 'no'), strict=False)
try:
if src is None:
raise AnsibleActionFail("src is required")
elif remote_src:
# everything is remote, so we just execute the module
# without changing any of the module arguments
raise _AnsibleActionDone(result=self._execute_module(task_vars=task_vars))
try:
src = self._find_needle('files', src)
except AnsibleError as e:
raise AnsibleActionFail(to_native(e))
tmp_src = self._connection._shell.join_path(self._connection._shell.tmpdir, os.path.basename(src))
self._transfer_file(src, tmp_src)
self._fixup_perms2((self._connection._shell.tmpdir, tmp_src))
new_module_args = self._task.args.copy()
new_module_args.update(
dict(
src=tmp_src,
)
)
result.update(self._execute_module('ansible.posix.patch', module_args=new_module_args, task_vars=task_vars))
except AnsibleAction as e:
result.update(e.result)
finally:
self._remove_tmp_path(self._connection._shell.tmpdir)
return result

View File

@ -1,434 +0,0 @@
# -*- coding: utf-8 -*-
# (c) 2012-2013, Timothy Appnel <tim@appnel.com>
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os.path
from ansible import constants as C
from ansible.module_utils.six import string_types
from ansible.module_utils.six.moves import shlex_quote
from ansible.module_utils._text import to_text
from ansible.module_utils.common._collections_compat import MutableSequence
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.plugins.action import ActionBase
from ansible.plugins.loader import connection_loader
DOCKER = ['docker', 'community.general.docker', 'community.docker.docker']
PODMAN = ['podman', 'ansible.builtin.podman', 'containers.podman.podman']
BUILDAH = ['buildah', 'containers.podman.buildah']
class ActionModule(ActionBase):
def _get_absolute_path(self, path):
original_path = path
#
# Check if we have a local relative path and do not process
# * remote paths (some.server.domain:/some/remote/path/...)
# * URLs (rsync://...)
# * local absolute paths (/some/local/path/...)
#
if ':' in path or path.startswith('/'):
return path
if self._task._role is not None:
path = self._loader.path_dwim_relative(self._task._role._role_path, 'files', path)
else:
path = self._loader.path_dwim_relative(self._loader.get_basedir(), 'files', path)
if original_path and original_path[-1] == '/' and path[-1] != '/':
# make sure the dwim'd path ends in a trailing "/"
# if the original path did
path += '/'
return path
def _host_is_ipv6_address(self, host):
return ':' in to_text(host, errors='surrogate_or_strict')
def _format_rsync_rsh_target(self, host, path, user):
''' formats rsync rsh target, escaping ipv6 addresses if needed '''
user_prefix = ''
if path.startswith('rsync://'):
return path
# If using docker or buildah, do not add user information
if self._remote_transport not in DOCKER + PODMAN + BUILDAH and user:
user_prefix = '%s@' % (user, )
if self._host_is_ipv6_address(host):
return '[%s%s]:%s' % (user_prefix, host, path)
return '%s%s:%s' % (user_prefix, host, path)
def _process_origin(self, host, path, user):
if host not in C.LOCALHOST:
return self._format_rsync_rsh_target(host, path, user)
path = self._get_absolute_path(path=path)
return path
def _process_remote(self, task_args, host, path, user, port_matches_localhost_port):
"""
:arg host: hostname for the path
:arg path: file path
:arg user: username for the transfer
:arg port_matches_localhost_port: boolean whether the remote port
matches the port used by localhost's sshd. This is used in
conjunction with seeing whether the host is localhost to know
if we need to have the module substitute the pathname or if it
is a different host (for instance, an ssh tunnelled port or an
alternative ssh port to a vagrant host.)
"""
transport = self._connection.transport
# If we're connecting to a remote host or we're delegating to another
# host or we're connecting to a different ssh instance on the
# localhost then we have to format the path as a remote rsync path
if host not in C.LOCALHOST or transport != "local" or \
(host in C.LOCALHOST and not port_matches_localhost_port):
# If we're delegating to non-localhost and but the
# inventory_hostname host is localhost then we need the module to
# fix up the rsync path to use the controller's public DNS/IP
# instead of "localhost"
if port_matches_localhost_port and host in C.LOCALHOST:
task_args['_substitute_controller'] = True
return self._format_rsync_rsh_target(host, path, user)
path = self._get_absolute_path(path=path)
return path
def _override_module_replaced_vars(self, task_vars):
""" Some vars are substituted into the modules. Have to make sure
that those are correct for localhost when synchronize creates its own
connection to localhost."""
# Clear the current definition of these variables as they came from the
# connection to the remote host
if 'ansible_syslog_facility' in task_vars:
del task_vars['ansible_syslog_facility']
for key in list(task_vars.keys()):
if key.startswith("ansible_") and key.endswith("_interpreter"):
del task_vars[key]
# Add the definitions from localhost
for host in C.LOCALHOST:
if host in task_vars['hostvars']:
localhost = task_vars['hostvars'][host]
break
if 'ansible_syslog_facility' in localhost:
task_vars['ansible_syslog_facility'] = localhost['ansible_syslog_facility']
for key in localhost:
if key.startswith("ansible_") and key.endswith("_interpreter"):
task_vars[key] = localhost[key]
def run(self, tmp=None, task_vars=None):
''' generates params and passes them on to the rsync module '''
# When modifying this function be aware of the tricky convolutions
# your thoughts have to go through:
#
# In normal ansible, we connect from controller to inventory_hostname
# (playbook's hosts: field) or controller to delegate_to host and run
# a module on one of those hosts.
#
# So things that are directly related to the core of ansible are in
# terms of that sort of connection that always originate on the
# controller.
#
# In synchronize we use ansible to connect to either the controller or
# to the delegate_to host and then run rsync which makes its own
# connection from controller to inventory_hostname or delegate_to to
# inventory_hostname.
#
# That means synchronize needs to have some knowledge of the
# controller to inventory_host/delegate host that ansible typically
# establishes and use those to construct a command line for rsync to
# connect from the inventory_host to the controller/delegate. The
# challenge for coders is remembering which leg of the trip is
# associated with the conditions that you're checking at any one time.
if task_vars is None:
task_vars = dict()
# We make a copy of the args here because we may fail and be asked to
# retry. If that happens we don't want to pass the munged args through
# to our next invocation. Munged args are single use only.
_tmp_args = self._task.args.copy()
result = super(ActionModule, self).run(tmp, task_vars)
del tmp # tmp no longer has any effect
# Store remote connection type
self._remote_transport = self._connection.transport
use_ssh_args = _tmp_args.pop('use_ssh_args', None)
if use_ssh_args and self._connection.transport == 'ssh':
ssh_args = [
self._connection.get_option('ssh_args'),
self._connection.get_option('ssh_common_args'),
self._connection.get_option('ssh_extra_args'),
]
_tmp_args['ssh_args'] = ' '.join([a for a in ssh_args if a])
# Handle docker connection options
if self._remote_transport in DOCKER:
self._docker_cmd = self._connection.docker_cmd
if self._play_context.docker_extra_args:
self._docker_cmd = "%s %s" % (self._docker_cmd, self._play_context.docker_extra_args)
elif self._remote_transport in PODMAN:
self._docker_cmd = self._connection._options['podman_executable']
if self._connection._options.get('podman_extra_args'):
self._docker_cmd = "%s %s" % (self._docker_cmd, self._connection._options['podman_extra_args'])
# self._connection accounts for delegate_to so
# remote_transport is the transport ansible thought it would need
# between the controller and the delegate_to host or the controller
# and the remote_host if delegate_to isn't set.
remote_transport = False
if self._connection.transport != 'local':
remote_transport = True
try:
delegate_to = self._task.delegate_to
except (AttributeError, KeyError):
delegate_to = None
# ssh paramiko docker buildah and local are fully supported transports. Anything
# else only works with delegate_to
if delegate_to is None and self._connection.transport not in [
'ssh', 'paramiko', 'local'] + DOCKER + PODMAN + BUILDAH:
result['failed'] = True
result['msg'] = (
"synchronize uses rsync to function. rsync needs to connect to the remote "
"host via ssh, docker client or a direct filesystem "
"copy. This remote host is being accessed via %s instead "
"so it cannot work." % self._connection.transport)
return result
# Parameter name needed by the ansible module
_tmp_args['_local_rsync_path'] = task_vars.get('ansible_rsync_path') or 'rsync'
# rsync thinks that one end of the connection is localhost and the
# other is the host we're running the task for (Note: We use
# ansible's delegate_to mechanism to determine which host rsync is
# running on so localhost could be a non-controller machine if
# delegate_to is used)
src_host = '127.0.0.1'
inventory_hostname = task_vars.get('inventory_hostname')
dest_host_inventory_vars = task_vars['hostvars'].get(inventory_hostname)
dest_host = dest_host_inventory_vars.get('ansible_host', inventory_hostname)
dest_host_ids = [hostid for hostid in (dest_host_inventory_vars.get('inventory_hostname'),
dest_host_inventory_vars.get('ansible_host'))
if hostid is not None]
localhost_ports = set()
for host in C.LOCALHOST:
localhost_vars = task_vars['hostvars'].get(host, {})
for port_var in C.MAGIC_VARIABLE_MAPPING['port']:
port = localhost_vars.get(port_var, None)
if port:
break
else:
port = C.DEFAULT_REMOTE_PORT
localhost_ports.add(port)
# dest_is_local tells us if the host rsync runs on is the same as the
# host rsync puts the files on. This is about *rsync's connection*,
# not about the ansible connection to run the module.
dest_is_local = False
if delegate_to is None and remote_transport is False:
dest_is_local = True
elif delegate_to is not None and delegate_to in dest_host_ids:
dest_is_local = True
# CHECK FOR NON-DEFAULT SSH PORT
inv_port = task_vars.get('ansible_port', None) or C.DEFAULT_REMOTE_PORT
if _tmp_args.get('dest_port', None) is None:
if inv_port is not None:
_tmp_args['dest_port'] = inv_port
# Set use_delegate if we are going to run rsync on a delegated host
# instead of localhost
use_delegate = False
if delegate_to is not None and delegate_to in dest_host_ids:
# edge case: explicit delegate and dest_host are the same
# so we run rsync on the remote machine targeting its localhost
# (itself)
dest_host = '127.0.0.1'
use_delegate = True
elif delegate_to is not None and remote_transport:
# If we're delegating to a remote host then we need to use the
# delegate_to settings
use_delegate = True
# Delegate to localhost as the source of the rsync unless we've been
# told (via delegate_to) that a different host is the source of the
# rsync
if not use_delegate and remote_transport:
# Create a connection to localhost to run rsync on
new_stdin = self._connection._new_stdin
# Unlike port, there can be only one shell
localhost_shell = None
for host in C.LOCALHOST:
localhost_vars = task_vars['hostvars'].get(host, {})
for shell_var in C.MAGIC_VARIABLE_MAPPING['shell']:
localhost_shell = localhost_vars.get(shell_var, None)
if localhost_shell:
break
if localhost_shell:
break
else:
localhost_shell = os.path.basename(C.DEFAULT_EXECUTABLE)
self._play_context.shell = localhost_shell
# Unlike port, there can be only one executable
localhost_executable = None
for host in C.LOCALHOST:
localhost_vars = task_vars['hostvars'].get(host, {})
for executable_var in C.MAGIC_VARIABLE_MAPPING['executable']:
localhost_executable = localhost_vars.get(executable_var, None)
if localhost_executable:
break
if localhost_executable:
break
else:
localhost_executable = C.DEFAULT_EXECUTABLE
self._play_context.executable = localhost_executable
new_connection = connection_loader.get('local', self._play_context, new_stdin)
self._connection = new_connection
# Override _remote_is_local as an instance attribute specifically for the synchronize use case
# ensuring we set local tmpdir correctly
self._connection._remote_is_local = True
self._override_module_replaced_vars(task_vars)
# SWITCH SRC AND DEST HOST PER MODE
if _tmp_args.get('mode', 'push') == 'pull':
(dest_host, src_host) = (src_host, dest_host)
# MUNGE SRC AND DEST PER REMOTE_HOST INFO
src = _tmp_args.get('src', None)
dest = _tmp_args.get('dest', None)
if src is None or dest is None:
return dict(failed=True, msg="synchronize requires both src and dest parameters are set")
# Determine if we need a user@ and a password
user = None
password = task_vars.get('ansible_ssh_pass', None) or task_vars.get('ansible_password', None)
if not dest_is_local:
# Src and dest rsync "path" handling
if boolean(_tmp_args.get('set_remote_user', 'yes'), strict=False):
if use_delegate:
user = task_vars.get('ansible_delegated_vars', dict()).get('ansible_user', None)
if not user:
user = task_vars.get('ansible_user') or self._play_context.remote_user
if not user:
user = C.DEFAULT_REMOTE_USER
else:
user = task_vars.get('ansible_user') or self._play_context.remote_user
if self._templar is not None:
user = self._templar.template(user)
# Private key handling
# Use the private_key parameter if passed else use context private_key_file
_tmp_args['private_key'] = _tmp_args.get('private_key', self._play_context.private_key_file)
# use the mode to define src and dest's url
if _tmp_args.get('mode', 'push') == 'pull':
# src is a remote path: <user>@<host>, dest is a local path
src = self._process_remote(_tmp_args, src_host, src, user, inv_port in localhost_ports)
dest = self._process_origin(dest_host, dest, user)
else:
# src is a local path, dest is a remote path: <user>@<host>
src = self._process_origin(src_host, src, user)
dest = self._process_remote(_tmp_args, dest_host, dest, user, inv_port in localhost_ports)
password = dest_host_inventory_vars.get('ansible_ssh_pass', None) or dest_host_inventory_vars.get('ansible_password', None)
if self._templar is not None:
password = self._templar.template(password)
else:
# Still need to munge paths (to account for roles) even if we aren't
# copying files between hosts
src = self._get_absolute_path(path=src)
dest = self._get_absolute_path(path=dest)
_tmp_args['_local_rsync_password'] = password
_tmp_args['src'] = src
_tmp_args['dest'] = dest
# Allow custom rsync path argument
rsync_path = _tmp_args.get('rsync_path', None)
# backup original become as we are probably about to unset it
become = self._play_context.become
if not dest_is_local:
# don't escalate for docker. doing --rsync-path with docker exec fails
# and we can switch directly to the user via docker arguments
if self._play_context.become and not rsync_path and self._remote_transport not in DOCKER + PODMAN:
# If no rsync_path is set, become was originally set, and dest is
# remote then add privilege escalation here.
if self._play_context.become_method == 'sudo':
if self._play_context.become_user:
rsync_path = 'sudo -u %s rsync' % self._play_context.become_user
else:
rsync_path = 'sudo rsync'
# TODO: have to add in the rest of the become methods here
# We cannot use privilege escalation on the machine running the
# module. Instead we run it on the machine rsync is connecting
# to.
self._play_context.become = False
_tmp_args['rsync_path'] = rsync_path
# If launching synchronize against docker container
# use rsync_opts to support container to override rsh options
if self._remote_transport in DOCKER + BUILDAH + PODMAN and not use_delegate:
# Replicate what we do in the module argumentspec handling for lists
if not isinstance(_tmp_args.get('rsync_opts'), MutableSequence):
tmp_rsync_opts = _tmp_args.get('rsync_opts', [])
if isinstance(tmp_rsync_opts, string_types):
tmp_rsync_opts = tmp_rsync_opts.split(',')
elif isinstance(tmp_rsync_opts, (int, float)):
tmp_rsync_opts = [to_text(tmp_rsync_opts)]
_tmp_args['rsync_opts'] = tmp_rsync_opts
if '--blocking-io' not in _tmp_args['rsync_opts']:
_tmp_args['rsync_opts'].append('--blocking-io')
if self._remote_transport in DOCKER + PODMAN:
if become and self._play_context.become_user:
_tmp_args['rsync_opts'].append('--rsh=' + shlex_quote('%s exec -u %s -i' % (self._docker_cmd, self._play_context.become_user)))
elif user is not None:
_tmp_args['rsync_opts'].append('--rsh=' + shlex_quote('%s exec -u %s -i' % (self._docker_cmd, user)))
else:
_tmp_args['rsync_opts'].append('--rsh=' + shlex_quote('%s exec -i' % self._docker_cmd))
elif self._remote_transport in BUILDAH:
_tmp_args['rsync_opts'].append('--rsh=' + shlex_quote('buildah run --'))
# run the module and store the result
result.update(self._execute_module('ansible.posix.synchronize', module_args=_tmp_args, task_vars=task_vars))
return result

View File

@ -1,465 +0,0 @@
# -*- coding: utf-8 -*-
# (c) 2018 Matt Martz <matt@sivel.net>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
name: cgroup_perf_recap
type: aggregate
requirements:
- whitelist in configuration
- cgroups
short_description: Profiles system activity of tasks and full execution using cgroups
description:
- This is an ansible callback plugin utilizes cgroups to profile system activity of ansible and
individual tasks, and display a recap at the end of the playbook execution
notes:
- Requires ansible to be run from within a cgroup, such as with
C(cgexec -g cpuacct,memory,pids:ansible_profile ansible-playbook ...)
- This cgroup should only be used by ansible to get accurate results
- To create the cgroup, first use a command such as
C(sudo cgcreate -a ec2-user:ec2-user -t ec2-user:ec2-user -g cpuacct,memory,pids:ansible_profile)
options:
control_group:
required: True
description: Name of cgroups control group
env:
- name: CGROUP_CONTROL_GROUP
ini:
- section: callback_cgroup_perf_recap
key: control_group
cpu_poll_interval:
description: Interval between CPU polling for determining CPU usage. A lower value may produce inaccurate
results, a higher value may not be short enough to collect results for short tasks.
default: 0.25
type: float
env:
- name: CGROUP_CPU_POLL_INTERVAL
ini:
- section: callback_cgroup_perf_recap
key: cpu_poll_interval
memory_poll_interval:
description: Interval between memory polling for determining memory usage. A lower value may produce inaccurate
results, a higher value may not be short enough to collect results for short tasks.
default: 0.25
type: float
env:
- name: CGROUP_MEMORY_POLL_INTERVAL
ini:
- section: callback_cgroup_perf_recap
key: memory_poll_interval
pid_poll_interval:
description: Interval between PID polling for determining PID count. A lower value may produce inaccurate
results, a higher value may not be short enough to collect results for short tasks.
default: 0.25
type: float
env:
- name: CGROUP_PID_POLL_INTERVAL
ini:
- section: callback_cgroup_perf_recap
key: pid_poll_interval
display_recap:
description: Controls whether the recap is printed at the end, useful if you will automatically
process the output files
env:
- name: CGROUP_DISPLAY_RECAP
ini:
- section: callback_cgroup_perf_recap
key: display_recap
type: bool
default: true
file_name_format:
description: Format of filename. Accepts C(%(counter)s), C(%(task_uuid)s),
C(%(feature)s), C(%(ext)s). Defaults to C(%(feature)s.%(ext)s) when C(file_per_task) is C(False)
and C(%(counter)s-%(task_uuid)s-%(feature)s.%(ext)s) when C(True)
env:
- name: CGROUP_FILE_NAME_FORMAT
ini:
- section: callback_cgroup_perf_recap
key: file_name_format
type: str
default: '%(feature)s.%(ext)s'
output_dir:
description: Output directory for files containing recorded performance readings. If the value contains a
single %s, the start time of the playbook run will be inserted in that space. Only the deepest
level directory will be created if it does not exist, parent directories will not be created.
type: path
default: /tmp/ansible-perf-%s
env:
- name: CGROUP_OUTPUT_DIR
ini:
- section: callback_cgroup_perf_recap
key: output_dir
output_format:
description: Output format, either CSV or JSON-seq
env:
- name: CGROUP_OUTPUT_FORMAT
ini:
- section: callback_cgroup_perf_recap
key: output_format
type: str
default: csv
choices:
- csv
- json
file_per_task:
description: When set as C(True) along with C(write_files), this callback will write 1 file per task
instead of 1 file for the entire playbook run
env:
- name: CGROUP_FILE_PER_TASK
ini:
- section: callback_cgroup_perf_recap
key: file_per_task
type: bool
default: False
write_files:
description: Dictates whether files will be written containing performance readings
env:
- name: CGROUP_WRITE_FILES
ini:
- section: callback_cgroup_perf_recap
key: write_files
type: bool
default: false
'''
import csv
import datetime
import os
import time
import threading
from abc import ABCMeta, abstractmethod
from functools import partial
from ansible.module_utils._text import to_bytes, to_text
from ansible.module_utils.six import with_metaclass
from ansible.parsing.ajson import AnsibleJSONEncoder, json
from ansible.plugins.callback import CallbackBase
RS = '\x1e' # RECORD SEPARATOR
LF = '\x0a' # LINE FEED
def dict_fromkeys(keys, default=None):
d = {}
for key in keys:
d[key] = default() if callable(default) else default
return d
class BaseProf(with_metaclass(ABCMeta, threading.Thread)):
def __init__(self, path, obj=None, writer=None):
threading.Thread.__init__(self) # pylint: disable=non-parent-init-called
self.obj = obj
self.path = path
self.max = 0
self.running = True
self.writer = writer
def run(self):
while self.running:
self.poll()
@abstractmethod
def poll(self):
pass
class MemoryProf(BaseProf):
"""Python thread for recording memory usage"""
def __init__(self, path, poll_interval=0.25, obj=None, writer=None):
super(MemoryProf, self).__init__(path, obj=obj, writer=writer)
self._poll_interval = poll_interval
def poll(self):
with open(self.path) as f:
val = int(f.read().strip()) / 1024**2
if val > self.max:
self.max = val
if self.writer:
try:
self.writer(time.time(), self.obj.get_name(), self.obj._uuid, val)
except ValueError:
# We may be profiling after the playbook has ended
self.running = False
time.sleep(self._poll_interval)
class CpuProf(BaseProf):
def __init__(self, path, poll_interval=0.25, obj=None, writer=None):
super(CpuProf, self).__init__(path, obj=obj, writer=writer)
self._poll_interval = poll_interval
def poll(self):
with open(self.path) as f:
start_time = time.time() * 1000**2
start_usage = int(f.read().strip()) / 1000
time.sleep(self._poll_interval)
with open(self.path) as f:
end_time = time.time() * 1000**2
end_usage = int(f.read().strip()) / 1000
val = (end_usage - start_usage) / (end_time - start_time) * 100
if val > self.max:
self.max = val
if self.writer:
try:
self.writer(time.time(), self.obj.get_name(), self.obj._uuid, val)
except ValueError:
# We may be profiling after the playbook has ended
self.running = False
class PidsProf(BaseProf):
def __init__(self, path, poll_interval=0.25, obj=None, writer=None):
super(PidsProf, self).__init__(path, obj=obj, writer=writer)
self._poll_interval = poll_interval
def poll(self):
with open(self.path) as f:
val = int(f.read().strip())
if val > self.max:
self.max = val
if self.writer:
try:
self.writer(time.time(), self.obj.get_name(), self.obj._uuid, val)
except ValueError:
# We may be profiling after the playbook has ended
self.running = False
time.sleep(self._poll_interval)
def csv_writer(writer, timestamp, task_name, task_uuid, value):
writer.writerow([timestamp, task_name, task_uuid, value])
def json_writer(writer, timestamp, task_name, task_uuid, value):
data = {
'timestamp': timestamp,
'task_name': task_name,
'task_uuid': task_uuid,
'value': value,
}
writer.write('%s%s%s' % (RS, json.dumps(data, cls=AnsibleJSONEncoder), LF))
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'ansible.posix.cgroup_perf_recap'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
super(CallbackModule, self).__init__(display)
self._features = ('memory', 'cpu', 'pids')
self._units = {
'memory': 'MB',
'cpu': '%',
'pids': '',
}
self.task_results = dict_fromkeys(self._features, default=list)
self._profilers = dict.fromkeys(self._features)
self._files = dict.fromkeys(self._features)
self._writers = dict.fromkeys(self._features)
self._file_per_task = False
self._counter = 0
self.write_files = False
def _open_files(self, task_uuid=None):
output_format = self._output_format
output_dir = self._output_dir
for feature in self._features:
data = {
b'counter': to_bytes(self._counter),
b'task_uuid': to_bytes(task_uuid),
b'feature': to_bytes(feature),
b'ext': to_bytes(output_format)
}
if self._files.get(feature):
try:
self._files[feature].close()
except Exception:
pass
if self.write_files:
filename = self._file_name_format % data
self._files[feature] = open(os.path.join(output_dir, filename), 'w+')
if output_format == b'csv':
self._writers[feature] = partial(csv_writer, csv.writer(self._files[feature]))
elif output_format == b'json':
self._writers[feature] = partial(json_writer, self._files[feature])
def set_options(self, task_keys=None, var_options=None, direct=None):
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
cpu_poll_interval = self.get_option('cpu_poll_interval')
memory_poll_interval = self.get_option('memory_poll_interval')
pid_poll_interval = self.get_option('pid_poll_interval')
self._display_recap = self.get_option('display_recap')
control_group = to_bytes(self.get_option('control_group'), errors='surrogate_or_strict')
self.mem_max_file = b'/sys/fs/cgroup/memory/%s/memory.max_usage_in_bytes' % control_group
mem_current_file = b'/sys/fs/cgroup/memory/%s/memory.usage_in_bytes' % control_group
cpu_usage_file = b'/sys/fs/cgroup/cpuacct/%s/cpuacct.usage' % control_group
pid_current_file = b'/sys/fs/cgroup/pids/%s/pids.current' % control_group
for path in (self.mem_max_file, mem_current_file, cpu_usage_file, pid_current_file):
try:
with open(path) as f:
pass
except Exception as e:
self._display.warning(
u'Cannot open %s for reading (%s). Disabling %s' % (to_text(path), to_text(e), self.CALLBACK_NAME)
)
self.disabled = True
return
try:
with open(self.mem_max_file, 'w+') as f:
f.write('0')
except Exception as e:
self._display.warning(
u'Unable to reset max memory value in %s: %s' % (to_text(self.mem_max_file), to_text(e))
)
self.disabled = True
return
try:
with open(cpu_usage_file, 'w+') as f:
f.write('0')
except Exception as e:
self._display.warning(
u'Unable to reset CPU usage value in %s: %s' % (to_text(cpu_usage_file), to_text(e))
)
self.disabled = True
return
self._profiler_map = {
'memory': partial(MemoryProf, mem_current_file, poll_interval=memory_poll_interval),
'cpu': partial(CpuProf, cpu_usage_file, poll_interval=cpu_poll_interval),
'pids': partial(PidsProf, pid_current_file, poll_interval=pid_poll_interval),
}
self.write_files = self.get_option('write_files')
file_per_task = self.get_option('file_per_task')
self._output_format = to_bytes(self.get_option('output_format'))
output_dir = to_bytes(self.get_option('output_dir'), errors='surrogate_or_strict')
try:
output_dir %= to_bytes(datetime.datetime.now().isoformat())
except TypeError:
pass
self._output_dir = output_dir
file_name_format = to_bytes(self.get_option('file_name_format'))
if self.write_files:
if file_per_task:
self._file_per_task = True
if file_name_format == b'%(feature)s.%(ext)s':
file_name_format = b'%(counter)s-%(task_uuid)s-%(feature)s.%(ext)s'
else:
file_name_format = to_bytes(self.get_option('file_name_format'))
self._file_name_format = file_name_format
if not os.path.exists(output_dir):
try:
os.mkdir(output_dir)
except Exception as e:
self._display.warning(
u'Could not create the output directory at %s: %s' % (to_text(output_dir), to_text(e))
)
self.disabled = True
return
if not self._file_per_task:
self._open_files()
def _profile(self, obj=None):
prev_task = None
results = dict.fromkeys(self._features)
if not obj or self._file_per_task:
for dummy, f in self._files.items():
if f is None:
continue
try:
f.close()
except Exception:
pass
try:
for name, prof in self._profilers.items():
prof.running = False
for name, prof in self._profilers.items():
results[name] = prof.max
prev_task = prof.obj
except AttributeError:
pass
for name, result in results.items():
if result is not None:
try:
self.task_results[name].append((prev_task, result))
except ValueError:
pass
if obj is not None:
if self._file_per_task or self._counter == 0:
self._open_files(task_uuid=obj._uuid)
for feature in self._features:
self._profilers[feature] = self._profiler_map[feature](obj=obj, writer=self._writers[feature])
self._profilers[feature].start()
self._counter += 1
def v2_playbook_on_task_start(self, task, is_conditional):
self._profile(task)
def v2_playbook_on_stats(self, stats):
self._profile()
if not self._display_recap:
return
with open(self.mem_max_file) as f:
max_results = int(f.read().strip()) / 1024 / 1024
self._display.banner('CGROUP PERF RECAP')
self._display.display('Memory Execution Maximum: %0.2fMB\n' % max_results)
for name, data in self.task_results.items():
if name == 'memory':
continue
try:
self._display.display(
'%s Execution Maximum: %0.2f%s\n' % (name, max((t[1] for t in data)), self._units[name])
)
except Exception as e:
self._display.display('%s profiling error: no results collected: %s\n' % (name, e))
self._display.display('\n')
for name, data in self.task_results.items():
if data:
self._display.display('%s:\n' % name)
for task, value in data:
self._display.display('%s (%s): %0.2f%s' % (task.get_name(), task._uuid, value, self._units[name]))
self._display.display('\n')

View File

@ -1,53 +0,0 @@
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
name: debug
type: stdout
short_description: formatted stdout/stderr display
description:
- Use this callback to sort through extensive debug output
extends_documentation_fragment:
- default_callback
requirements:
- set as stdout in configuration
'''
from ansible.plugins.callback.default import CallbackModule as CallbackModule_default
class CallbackModule(CallbackModule_default): # pylint: disable=too-few-public-methods,no-init
'''
Override for the default callback module.
Render std err/out outside of the rest of the result which it prints with
indentation.
'''
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'ansible.posix.debug'
def _dump_results(self, result, indent=None, sort_keys=True, keep_invocation=False):
'''Return the text to output for a result.'''
# Enable JSON identation
result['_ansible_verbose_always'] = True
save = {}
for key in ['stdout', 'stdout_lines', 'stderr', 'stderr_lines', 'msg', 'module_stdout', 'module_stderr']:
if key in result:
save[key] = result.pop(key)
output = CallbackModule_default._dump_results(self, result)
for key in ['stdout', 'stderr', 'msg', 'module_stdout', 'module_stderr']:
if key in save and save[key]:
output += '\n\n%s:\n\n%s\n' % (key.upper(), save[key])
for key, value in save.items():
result[key] = value
return output

View File

@ -1,197 +0,0 @@
# (c) 2016, Matt Martz <matt@sivel.net>
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
name: json
short_description: Ansible screen output as JSON
description:
- This callback converts all events into JSON output to stdout
type: stdout
requirements:
- Set as stdout in config
options:
show_custom_stats:
name: Show custom stats
description: 'This adds the custom stats set via the set_stats plugin to the play recap'
default: False
env:
- name: ANSIBLE_SHOW_CUSTOM_STATS
ini:
- key: show_custom_stats
section: defaults
type: bool
json_indent:
name: Use indenting for the JSON output
description: 'If specified, use this many spaces for indenting in the JSON output. If <= 0, write to a single line.'
default: 4
env:
- name: ANSIBLE_JSON_INDENT
ini:
- key: json_indent
section: defaults
type: integer
notes:
- When using a strategy such as free, host_pinned, or a custom strategy, host results will
be added to new task results in ``.plays[].tasks[]``. As such, there will exist duplicate
task objects indicated by duplicate task IDs at ``.plays[].tasks[].task.id``, each with an
individual host result for the task.
'''
import datetime
import json
from functools import partial
from ansible.inventory.host import Host
from ansible.module_utils._text import to_text
from ansible.parsing.ajson import AnsibleJSONEncoder
from ansible.plugins.callback import CallbackBase
LOCKSTEP_CALLBACKS = frozenset(('linear', 'debug'))
def current_time():
return '%sZ' % datetime.datetime.utcnow().isoformat()
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'ansible.posix.json'
def __init__(self, display=None):
super(CallbackModule, self).__init__(display)
self.results = []
self._task_map = {}
self._is_lockstep = False
self.set_options()
self._json_indent = self.get_option('json_indent')
if self._json_indent <= 0:
self._json_indent = None
def _new_play(self, play):
self._is_lockstep = play.strategy in LOCKSTEP_CALLBACKS
return {
'play': {
'name': play.get_name(),
'id': to_text(play._uuid),
'path': to_text(play.get_path()),
'duration': {
'start': current_time()
}
},
'tasks': []
}
def _new_task(self, task):
return {
'task': {
'name': task.get_name(),
'id': to_text(task._uuid),
'path': to_text(task.get_path()),
'duration': {
'start': current_time()
}
},
'hosts': {}
}
def _find_result_task(self, host, task):
key = (host.get_name(), task._uuid)
return self._task_map.get(
key,
self.results[-1]['tasks'][-1]
)
def v2_playbook_on_play_start(self, play):
self.results.append(self._new_play(play))
def v2_runner_on_start(self, host, task):
if self._is_lockstep:
return
key = (host.get_name(), task._uuid)
task_result = self._new_task(task)
self._task_map[key] = task_result
self.results[-1]['tasks'].append(task_result)
def v2_playbook_on_task_start(self, task, is_conditional):
if not self._is_lockstep:
return
self.results[-1]['tasks'].append(self._new_task(task))
def v2_playbook_on_handler_task_start(self, task):
if not self._is_lockstep:
return
self.results[-1]['tasks'].append(self._new_task(task))
def _convert_host_to_name(self, key):
if isinstance(key, (Host,)):
return key.get_name()
return key
def v2_playbook_on_stats(self, stats):
"""Display info about playbook statistics"""
hosts = sorted(stats.processed.keys())
summary = {}
for h in hosts:
s = stats.summarize(h)
summary[h] = s
custom_stats = {}
global_custom_stats = {}
if self.get_option('show_custom_stats') and stats.custom:
custom_stats.update(dict((self._convert_host_to_name(k), v) for k, v in stats.custom.items()))
global_custom_stats.update(custom_stats.pop('_run', {}))
output = {
'plays': self.results,
'stats': summary,
'custom_stats': custom_stats,
'global_custom_stats': global_custom_stats,
}
self._display.display(json.dumps(output, cls=AnsibleJSONEncoder, indent=self._json_indent, sort_keys=True))
def _record_task_result(self, on_info, result, **kwargs):
"""This function is used as a partial to add failed/skipped info in a single method"""
host = result._host
task = result._task
result_copy = result._result.copy()
result_copy.update(on_info)
result_copy['action'] = task.action
task_result = self._find_result_task(host, task)
task_result['hosts'][host.name] = result_copy
end_time = current_time()
task_result['task']['duration']['end'] = end_time
self.results[-1]['play']['duration']['end'] = end_time
if not self._is_lockstep:
key = (host.get_name(), task._uuid)
del self._task_map[key]
def __getattribute__(self, name):
"""Return ``_record_task_result`` partial with a dict containing skipped/failed if necessary"""
if name not in ('v2_runner_on_ok', 'v2_runner_on_failed', 'v2_runner_on_unreachable', 'v2_runner_on_skipped'):
return object.__getattribute__(self, name)
on = name.rsplit('_', 1)[1]
on_info = {}
if on in ('failed', 'skipped'):
on_info[on] = True
return partial(self._record_task_result, on_info)

View File

@ -1,214 +0,0 @@
# (c) 2016, Matt Martz <matt@sivel.net>
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
name: jsonl
short_description: Ansible screen output as JSONL (lines in json format)
description:
- This callback converts all events into JSON output to stdout
- This callback in contrast with ansible.posix.json uses less memory, because it doesn't store results.
type: stdout
requirements:
- Set as stdout in config
options:
show_custom_stats:
name: Show custom stats
description: 'This adds the custom stats set via the set_stats plugin to the play recap'
default: False
env:
- name: ANSIBLE_SHOW_CUSTOM_STATS
ini:
- key: show_custom_stats
section: defaults
type: bool
json_indent:
name: Use indenting for the JSON output
description: 'If specified, use this many spaces for indenting in the JSON output. If not specified or <= 0, write to a single line.'
default: 0
env:
- name: ANSIBLE_JSON_INDENT
ini:
- key: json_indent
section: defaults
type: integer
notes:
- When using a strategy such as free, host_pinned, or a custom strategy, host results will
be added to new task results in ``.plays[].tasks[]``. As such, there will exist duplicate
task objects indicated by duplicate task IDs at ``.plays[].tasks[].task.id``, each with an
individual host result for the task.
'''
import datetime
import json
import copy
from functools import partial
from ansible.inventory.host import Host
from ansible.module_utils._text import to_text
from ansible.parsing.ajson import AnsibleJSONEncoder
from ansible.plugins.callback import CallbackBase
LOCKSTEP_CALLBACKS = frozenset(('linear', 'debug'))
def current_time():
return '%sZ' % datetime.datetime.utcnow().isoformat()
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'ansible.posix.jsonl'
def __init__(self, display=None):
super(CallbackModule, self).__init__(display)
self.results = []
self._task_map = {}
self._is_lockstep = False
self.set_options()
self._json_indent = self.get_option('json_indent')
if self._json_indent <= 0:
self._json_indent = None
def _new_play(self, play):
self._is_lockstep = play.strategy in LOCKSTEP_CALLBACKS
return {
'play': {
'name': play.get_name(),
'id': to_text(play._uuid),
'path': to_text(play.get_path()),
'duration': {
'start': current_time()
}
},
'tasks': []
}
def _new_task(self, task):
return {
'task': {
'name': task.get_name(),
'id': to_text(task._uuid),
'path': to_text(task.get_path()),
'duration': {
'start': current_time()
}
},
'hosts': {}
}
def _find_result_task(self, host, task):
key = (host.get_name(), task._uuid)
return self._task_map.get(
key,
self.results[-1]['tasks'][-1]
)
def v2_playbook_on_play_start(self, play):
play_result = self._new_play(play)
self.results.append(play_result)
self._write_event('v2_playbook_on_play_start', play_result)
def v2_runner_on_start(self, host, task):
if self._is_lockstep:
return
key = (host.get_name(), task._uuid)
task_result = self._new_task(task)
self._task_map[key] = task_result
self.results[-1]['tasks'].append(task_result)
self._write_event('v2_runner_on_start', task_result)
def v2_playbook_on_task_start(self, task, is_conditional):
if not self._is_lockstep:
return
task_result = self._new_task(task)
self.results[-1]['tasks'].append(task_result)
self._write_event('v2_playbook_on_task_start', task_result)
def v2_playbook_on_handler_task_start(self, task):
if not self._is_lockstep:
return
task_result = self._new_task(task)
self.results[-1]['tasks'].append(task_result)
self._write_event('v2_playbook_on_handler_task_start', task_result)
def _convert_host_to_name(self, key):
if isinstance(key, (Host,)):
return key.get_name()
return key
def v2_playbook_on_stats(self, stats):
"""Display info about playbook statistics"""
hosts = sorted(stats.processed.keys())
summary = {}
for h in hosts:
s = stats.summarize(h)
summary[h] = s
custom_stats = {}
global_custom_stats = {}
if self.get_option('show_custom_stats') and stats.custom:
custom_stats.update(dict((self._convert_host_to_name(k), v) for k, v in stats.custom.items()))
global_custom_stats.update(custom_stats.pop('_run', {}))
output = {
'stats': summary,
'custom_stats': custom_stats,
'global_custom_stats': global_custom_stats,
}
self._write_event('v2_playbook_on_stats', output)
def _write_event(self, event_name, output):
output['_event'] = event_name
output['_timestamp'] = current_time()
self._display.display(json.dumps(output, cls=AnsibleJSONEncoder, indent=self._json_indent, separators=',:', sort_keys=True))
def _record_task_result(self, event_name, on_info, result, **kwargs):
"""This function is used as a partial to add failed/skipped info in a single method"""
host = result._host
task = result._task
result_copy = result._result.copy()
result_copy.update(on_info)
result_copy['action'] = task.action
task_result = self._find_result_task(host, task)
end_time = current_time()
task_result['task']['duration']['end'] = end_time
self.results[-1]['play']['duration']['end'] = end_time
task_result_copy = copy.deepcopy(task_result)
task_result_copy['hosts'][host.name] = result_copy
if not self._is_lockstep:
key = (host.get_name(), task._uuid)
del self._task_map[key]
self._write_event(event_name, task_result_copy)
def __getattribute__(self, name):
"""Return ``_record_task_result`` partial with a dict containing skipped/failed if necessary"""
if name not in ('v2_runner_on_ok', 'v2_runner_on_failed', 'v2_runner_on_unreachable', 'v2_runner_on_skipped'):
return object.__getattribute__(self, name)
on = name.rsplit('_', 1)[1]
on_info = {}
if on in ('failed', 'skipped'):
on_info[on] = True
return partial(self._record_task_result, name, on_info)

View File

@ -1,118 +0,0 @@
# (c) 2017, Tennis Smith, https://github.com/gamename
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
name: profile_roles
type: aggregate
short_description: adds timing information to roles
description:
- This callback module provides profiling for ansible roles.
requirements:
- whitelisting in configuration
'''
import collections
import time
from ansible.plugins.callback import CallbackBase
from ansible.module_utils.six.moves import reduce
# define start time
t0 = tn = time.time()
def secondsToStr(t):
# http://bytes.com/topic/python/answers/635958-handy-short-cut-formatting-elapsed-time-floating-point-seconds
def rediv(ll, b):
return list(divmod(ll[0], b)) + ll[1:]
return "%d:%02d:%02d.%03d" % tuple(
reduce(rediv, [[t * 1000, ], 1000, 60, 60]))
def filled(msg, fchar="*"):
if len(msg) == 0:
width = 79
else:
msg = "%s " % msg
width = 79 - len(msg)
if width < 3:
width = 3
filler = fchar * width
return "%s%s " % (msg, filler)
def timestamp(self):
if self.current is not None:
self.stats[self.current] = time.time() - self.stats[self.current]
self.totals[self.current] += self.stats[self.current]
def tasktime():
global tn
time_current = time.strftime('%A %d %B %Y %H:%M:%S %z')
time_elapsed = secondsToStr(time.time() - tn)
time_total_elapsed = secondsToStr(time.time() - t0)
tn = time.time()
return filled('%s (%s)%s%s' %
(time_current, time_elapsed, ' ' * 7, time_total_elapsed))
class CallbackModule(CallbackBase):
"""
This callback module provides profiling for ansible roles.
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'ansible.posix.profile_roles'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self):
self.stats = collections.Counter()
self.totals = collections.Counter()
self.current = None
super(CallbackModule, self).__init__()
def _record_task(self, task):
"""
Logs the start of each task
"""
self._display.display(tasktime())
timestamp(self)
if task._role:
self.current = task._role._role_name
else:
self.current = task.action
self.stats[self.current] = time.time()
def v2_playbook_on_task_start(self, task, is_conditional):
self._record_task(task)
def v2_playbook_on_handler_task_start(self, task):
self._record_task(task)
def playbook_on_setup(self):
self._display.display(tasktime())
def playbook_on_stats(self, stats):
self._display.display(tasktime())
self._display.display(filled("", fchar="="))
timestamp(self)
total_time = sum(self.totals.values())
# Print the timings starting with the largest one
for result in self.totals.most_common():
msg = u"{0:-<70}{1:->9}".format(result[0] + u' ', u' {0:.02f}s'.format(result[1]))
self._display.display(msg)
msg_total = u"{0:-<70}{1:->9}".format(u'total ', u' {0:.02f}s'.format(total_time))
self._display.display(filled("", fchar="~"))
self._display.display(msg_total)

View File

@ -1,201 +0,0 @@
# (C) 2016, Joel, https://github.com/jjshoe
# (C) 2015, Tom Paine, <github@aioue.net>
# (C) 2014, Jharrod LaFon, @JharrodLaFon
# (C) 2012-2013, Michael DeHaan, <michael.dehaan@gmail.com>
# (C) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
name: profile_tasks
type: aggregate
short_description: adds time information to tasks
description:
- Ansible callback plugin for timing individual tasks and overall execution time.
- "Mashup of 2 excellent original works: https://github.com/jlafon/ansible-profile,
https://github.com/junaid18183/ansible_home/blob/master/ansible_plugins/callback_plugins/timestamp.py.old"
- "Format: C(<task start timestamp>) C(<length of previous task>) C(<current elapsed playbook execution time>)"
- It also lists the top/bottom time consuming tasks in the summary (configurable)
- Before 2.4 only the environment variables were available for configuration.
requirements:
- enable in configuration - see examples section below for details.
options:
output_limit:
description: Number of tasks to display in the summary
default: 20
env:
- name: PROFILE_TASKS_TASK_OUTPUT_LIMIT
ini:
- section: callback_profile_tasks
key: task_output_limit
sort_order:
description: Adjust the sorting output of summary tasks
choices: ['descending', 'ascending', 'none']
default: 'descending'
env:
- name: PROFILE_TASKS_SORT_ORDER
ini:
- section: callback_profile_tasks
key: sort_order
'''
EXAMPLES = '''
example: >
To enable, add this to your ansible.cfg file in the defaults block
[defaults]
callbacks_enabled=ansible.posix.profile_tasks
sample output: >
#
# TASK: [ensure messaging security group exists] ********************************
# Thursday 11 June 2017 22:50:53 +0100 (0:00:00.721) 0:00:05.322 *********
# ok: [localhost]
#
# TASK: [ensure db security group exists] ***************************************
# Thursday 11 June 2017 22:50:54 +0100 (0:00:00.558) 0:00:05.880 *********
# changed: [localhost]
#
'''
import collections
import time
from ansible.module_utils.six.moves import reduce
from ansible.plugins.callback import CallbackBase
# define start time
t0 = tn = time.time()
def secondsToStr(t):
# http://bytes.com/topic/python/answers/635958-handy-short-cut-formatting-elapsed-time-floating-point-seconds
def rediv(ll, b):
return list(divmod(ll[0], b)) + ll[1:]
return "%d:%02d:%02d.%03d" % tuple(reduce(rediv, [[t * 1000, ], 1000, 60, 60]))
def filled(msg, fchar="*"):
if len(msg) == 0:
width = 79
else:
msg = "%s " % msg
width = 79 - len(msg)
if width < 3:
width = 3
filler = fchar * width
return "%s%s " % (msg, filler)
def timestamp(self):
if self.current is not None:
elapsed = time.time() - self.stats[self.current]['started']
self.stats[self.current]['elapsed'] += elapsed
def tasktime():
global tn
time_current = time.strftime('%A %d %B %Y %H:%M:%S %z')
time_elapsed = secondsToStr(time.time() - tn)
time_total_elapsed = secondsToStr(time.time() - t0)
tn = time.time()
return filled('%s (%s)%s%s' % (time_current, time_elapsed, ' ' * 7, time_total_elapsed))
class CallbackModule(CallbackBase):
"""
This callback module provides per-task timing, ongoing playbook elapsed time
and ordered list of top 20 longest running tasks at end.
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'ansible.posix.profile_tasks'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self):
self.stats = collections.OrderedDict()
self.current = None
self.sort_order = None
self.task_output_limit = None
super(CallbackModule, self).__init__()
def set_options(self, task_keys=None, var_options=None, direct=None):
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
self.sort_order = self.get_option('sort_order')
if self.sort_order is not None:
if self.sort_order == 'ascending':
self.sort_order = False
elif self.sort_order == 'descending':
self.sort_order = True
elif self.sort_order == 'none':
self.sort_order = None
self.task_output_limit = self.get_option('output_limit')
if self.task_output_limit is not None:
if self.task_output_limit == 'all':
self.task_output_limit = None
else:
self.task_output_limit = int(self.task_output_limit)
def _record_task(self, task):
"""
Logs the start of each task
"""
self._display.display(tasktime())
timestamp(self)
# Record the start time of the current task
# stats[TASK_UUID]:
# started: Current task start time. This value will be updated each time a task
# with the same UUID is executed when `serial` is specified in a playbook.
# elapsed: Elapsed time since the first serialized task was started
self.current = task._uuid
if self.current not in self.stats:
self.stats[self.current] = {'started': time.time(), 'elapsed': 0.0, 'name': task.get_name()}
else:
self.stats[self.current]['started'] = time.time()
if self._display.verbosity >= 2:
self.stats[self.current]['path'] = task.get_path()
def v2_playbook_on_task_start(self, task, is_conditional):
self._record_task(task)
def v2_playbook_on_handler_task_start(self, task):
self._record_task(task)
def playbook_on_setup(self):
self._display.display(tasktime())
def playbook_on_stats(self, stats):
self._display.display(tasktime())
self._display.display(filled("", fchar="="))
timestamp(self)
self.current = None
results = list(self.stats.items())
# Sort the tasks by the specified sort
if self.sort_order is not None:
results = sorted(
self.stats.items(),
key=lambda x: x[1]['elapsed'],
reverse=self.sort_order,
)
# Display the number of tasks specified or the default of 20
results = list(results)[:self.task_output_limit]
# Print the timings
for uuid, result in results:
msg = u"{0:-<{2}}{1:->9}".format(result['name'] + u' ', u' {0:.02f}s'.format(result['elapsed']), self._display.columns - 9)
if 'path' in result:
msg += u"\n{0:-<{1}}".format(result['path'] + u' ', self._display.columns)
self._display.display(msg)

View File

@ -1,43 +0,0 @@
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
name: skippy
type: stdout
requirements:
- set as main display callback
short_description: Ansible screen output that ignores skipped status
deprecated:
why: The 'default' callback plugin now supports this functionality
removed_at_date: '2022-06-01'
alternative: "'default' callback plugin with 'display_skipped_hosts = no' option"
extends_documentation_fragment:
- default_callback
description:
- This callback does the same as the default except it does not output skipped host/task/item status
'''
from ansible.plugins.callback.default import CallbackModule as CallbackModule_default
class CallbackModule(CallbackModule_default):
'''
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 = 'ansible.posix.skippy'
def v2_runner_on_skipped(self, result):
pass
def v2_runner_item_on_skipped(self, result):
pass

View File

@ -1,49 +0,0 @@
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
name: timer
type: aggregate
requirements:
- whitelist in configuration
short_description: Adds time to play stats
description:
- This callback just adds total play duration to the play stats.
'''
from datetime import datetime
from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
"""
This callback module tells you how long your plays ran for.
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'ansible.posix.timer'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self):
super(CallbackModule, self).__init__()
self.start_time = datetime.utcnow()
def days_hours_minutes_seconds(self, runtime):
minutes = (runtime.seconds // 60) % 60
r_seconds = runtime.seconds % 60
return runtime.days, runtime.seconds // 3600, minutes, r_seconds
def playbook_on_stats(self, stats):
self.v2_playbook_on_stats(stats)
def v2_playbook_on_stats(self, stats):
end_time = datetime.utcnow()
runtime = end_time - self.start_time
self._display.display("Playbook run took %s days, %s hours, %s minutes, %s seconds" % (self.days_hours_minutes_seconds(runtime)))

View File

@ -1,344 +0,0 @@
# Vendored copy of distutils/version.py from CPython 3.9.5
#
# Implements multiple version numbering conventions for the
# Python Module Distribution Utilities.
#
# PSF License (see PSF-license.txt or https://opensource.org/licenses/Python-2.0)
#
"""Provides classes to represent module version numbers (one class for
each style of version numbering). There are currently two such classes
implemented: StrictVersion and LooseVersion.
Every version number class implements the following interface:
* the 'parse' method takes a string and parses it to some internal
representation; if the string is an invalid version number,
'parse' raises a ValueError exception
* the class constructor takes an optional string argument which,
if supplied, is passed to 'parse'
* __str__ reconstructs the string that was passed to 'parse' (or
an equivalent string -- ie. one that will generate an equivalent
version number instance)
* __repr__ generates Python code to recreate the version number instance
* _cmp compares the current instance with either another instance
of the same class or a string (which will be parsed to an instance
of the same class, thus must follow the same rules)
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import re
try:
RE_FLAGS = re.VERBOSE | re.ASCII
except AttributeError:
RE_FLAGS = re.VERBOSE
class Version:
"""Abstract base class for version numbering classes. Just provides
constructor (__init__) and reproducer (__repr__), because those
seem to be the same for all version numbering classes; and route
rich comparisons to _cmp.
"""
def __init__(self, vstring=None):
if vstring:
self.parse(vstring)
def __repr__(self):
return "%s ('%s')" % (self.__class__.__name__, str(self))
def __eq__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c == 0
def __lt__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c < 0
def __le__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c <= 0
def __gt__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c > 0
def __ge__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c >= 0
# Interface for version-number classes -- must be implemented
# by the following classes (the concrete ones -- Version should
# be treated as an abstract class).
# __init__ (string) - create and take same action as 'parse'
# (string parameter is optional)
# parse (string) - convert a string representation to whatever
# internal representation is appropriate for
# this style of version numbering
# __str__ (self) - convert back to a string; should be very similar
# (if not identical to) the string supplied to parse
# __repr__ (self) - generate Python code to recreate
# the instance
# _cmp (self, other) - compare two version numbers ('other' may
# be an unparsed version string, or another
# instance of your version class)
class StrictVersion(Version):
"""Version numbering for anal retentives and software idealists.
Implements the standard interface for version number classes as
described above. A version number consists of two or three
dot-separated numeric components, with an optional "pre-release" tag
on the end. The pre-release tag consists of the letter 'a' or 'b'
followed by a number. If the numeric components of two version
numbers are equal, then one with a pre-release tag will always
be deemed earlier (lesser) than one without.
The following are valid version numbers (shown in the order that
would be obtained by sorting according to the supplied cmp function):
0.4 0.4.0 (these two are equivalent)
0.4.1
0.5a1
0.5b3
0.5
0.9.6
1.0
1.0.4a3
1.0.4b1
1.0.4
The following are examples of invalid version numbers:
1
2.7.2.2
1.3.a4
1.3pl1
1.3c4
The rationale for this version numbering system will be explained
in the distutils documentation.
"""
version_re = re.compile(r"^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$", RE_FLAGS)
def parse(self, vstring):
match = self.version_re.match(vstring)
if not match:
raise ValueError("invalid version number '%s'" % vstring)
(major, minor, patch, prerelease, prerelease_num) = match.group(1, 2, 4, 5, 6)
if patch:
self.version = tuple(map(int, [major, minor, patch]))
else:
self.version = tuple(map(int, [major, minor])) + (0,)
if prerelease:
self.prerelease = (prerelease[0], int(prerelease_num))
else:
self.prerelease = None
def __str__(self):
if self.version[2] == 0:
vstring = ".".join(map(str, self.version[0:2]))
else:
vstring = ".".join(map(str, self.version))
if self.prerelease:
vstring = vstring + self.prerelease[0] + str(self.prerelease[1])
return vstring
def _cmp(self, other):
if isinstance(other, str):
other = StrictVersion(other)
elif not isinstance(other, StrictVersion):
return NotImplemented
if self.version != other.version:
# numeric versions don't match
# prerelease stuff doesn't matter
if self.version < other.version:
return -1
else:
return 1
# have to compare prerelease
# case 1: neither has prerelease; they're equal
# case 2: self has prerelease, other doesn't; other is greater
# case 3: self doesn't have prerelease, other does: self is greater
# case 4: both have prerelease: must compare them!
if not self.prerelease and not other.prerelease:
return 0
elif self.prerelease and not other.prerelease:
return -1
elif not self.prerelease and other.prerelease:
return 1
elif self.prerelease and other.prerelease:
if self.prerelease == other.prerelease:
return 0
elif self.prerelease < other.prerelease:
return -1
else:
return 1
else:
raise AssertionError("never get here")
# end class StrictVersion
# The rules according to Greg Stein:
# 1) a version number has 1 or more numbers separated by a period or by
# sequences of letters. If only periods, then these are compared
# left-to-right to determine an ordering.
# 2) sequences of letters are part of the tuple for comparison and are
# compared lexicographically
# 3) recognize the numeric components may have leading zeroes
#
# The LooseVersion class below implements these rules: a version number
# string is split up into a tuple of integer and string components, and
# comparison is a simple tuple comparison. This means that version
# numbers behave in a predictable and obvious way, but a way that might
# not necessarily be how people *want* version numbers to behave. There
# wouldn't be a problem if people could stick to purely numeric version
# numbers: just split on period and compare the numbers as tuples.
# However, people insist on putting letters into their version numbers;
# the most common purpose seems to be:
# - indicating a "pre-release" version
# ('alpha', 'beta', 'a', 'b', 'pre', 'p')
# - indicating a post-release patch ('p', 'pl', 'patch')
# but of course this can't cover all version number schemes, and there's
# no way to know what a programmer means without asking him.
#
# The problem is what to do with letters (and other non-numeric
# characters) in a version number. The current implementation does the
# obvious and predictable thing: keep them as strings and compare
# lexically within a tuple comparison. This has the desired effect if
# an appended letter sequence implies something "post-release":
# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002".
#
# However, if letters in a version number imply a pre-release version,
# the "obvious" thing isn't correct. Eg. you would expect that
# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison
# implemented here, this just isn't so.
#
# Two possible solutions come to mind. The first is to tie the
# comparison algorithm to a particular set of semantic rules, as has
# been done in the StrictVersion class above. This works great as long
# as everyone can go along with bondage and discipline. Hopefully a
# (large) subset of Python module programmers will agree that the
# particular flavour of bondage and discipline provided by StrictVersion
# provides enough benefit to be worth using, and will submit their
# version numbering scheme to its domination. The free-thinking
# anarchists in the lot will never give in, though, and something needs
# to be done to accommodate them.
#
# Perhaps a "moderately strict" version class could be implemented that
# lets almost anything slide (syntactically), and makes some heuristic
# assumptions about non-digits in version number strings. This could
# sink into special-case-hell, though; if I was as talented and
# idiosyncratic as Larry Wall, I'd go ahead and implement a class that
# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is
# just as happy dealing with things like "2g6" and "1.13++". I don't
# think I'm smart enough to do it right though.
#
# In any case, I've coded the test suite for this module (see
# ../test/test_version.py) specifically to fail on things like comparing
# "1.2a2" and "1.2". That's not because the *code* is doing anything
# wrong, it's because the simple, obvious design doesn't match my
# complicated, hairy expectations for real-world version numbers. It
# would be a snap to fix the test suite to say, "Yep, LooseVersion does
# the Right Thing" (ie. the code matches the conception). But I'd rather
# have a conception that matches common notions about version numbers.
class LooseVersion(Version):
"""Version numbering for anarchists and software realists.
Implements the standard interface for version number classes as
described above. A version number consists of a series of numbers,
separated by either periods or strings of letters. When comparing
version numbers, the numeric components will be compared
numerically, and the alphabetic components lexically. The following
are all valid version numbers, in no particular order:
1.5.1
1.5.2b2
161
3.10a
8.02
3.4j
1996.07.12
3.2.pl0
3.1.1.6
2g6
11g
0.960923
2.2beta29
1.13++
5.5.kw
2.0b1pl0
In fact, there is no such thing as an invalid version number under
this scheme; the rules for comparison are simple and predictable,
but may not always give the results you want (for some definition
of "want").
"""
component_re = re.compile(r"(\d+ | [a-z]+ | \.)", re.VERBOSE)
def __init__(self, vstring=None):
if vstring:
self.parse(vstring)
def parse(self, vstring):
# I've given up on thinking I can reconstruct the version string
# from the parsed tuple -- so I just store the string here for
# use by __str__
self.vstring = vstring
components = [x for x in self.component_re.split(vstring) if x and x != "."]
for i, obj in enumerate(components):
try:
components[i] = int(obj)
except ValueError:
pass
self.version = components
def __str__(self):
return self.vstring
def __repr__(self):
return "LooseVersion ('%s')" % str(self)
def _cmp(self, other):
if isinstance(other, str):
other = LooseVersion(other)
elif not isinstance(other, LooseVersion):
return NotImplemented
if self.version == other.version:
return 0
if self.version < other.version:
return -1
if self.version > other.version:
return 1
# end class LooseVersion

View File

@ -1,319 +0,0 @@
# -*- coding: utf-8 -*-
#
# (c) 2013-2018, Adam Miller (maxamillion@fedoraproject.org)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
from ansible_collections.ansible.posix.plugins.module_utils.version import LooseVersion
from ansible.module_utils.basic import missing_required_lib
__metaclass__ = type
FW_VERSION = None
fw = None
fw_offline = False
import_failure = True
try:
import firewall.config
FW_VERSION = firewall.config.VERSION
from firewall.client import FirewallClient
from firewall.client import FirewallClientZoneSettings
from firewall.errors import FirewallError
import_failure = False
try:
fw = FirewallClient()
fw.getDefaultZone()
except (AttributeError, FirewallError):
# Firewalld is not currently running, permanent-only operations
fw_offline = True
# Import other required parts of the firewalld API
#
# NOTE:
# online and offline operations do not share a common firewalld API
try:
from firewall.core.fw_test import Firewall_test
fw = Firewall_test()
except (ModuleNotFoundError):
# In firewalld version 0.7.0 this behavior changed
from firewall.core.fw import Firewall
fw = Firewall(offline=True)
fw.start()
except ImportError:
pass
class FirewallTransaction(object):
"""
FirewallTransaction
This is the base class for all firewalld transactions we might want to have
"""
def __init__(self, module, action_args=(), zone=None, desired_state=None,
permanent=False, immediate=False, enabled_values=None, disabled_values=None):
# type: (firewall.client, tuple, str, bool, bool, bool)
"""
initializer the transaction
:module: AnsibleModule, instance of AnsibleModule
:action_args: tuple, args to pass for the action to take place
:zone: str, firewall zone
:desired_state: str, the desired state (enabled, disabled, etc)
:permanent: bool, action should be permanent
:immediate: bool, action should take place immediately
:enabled_values: str[], acceptable values for enabling something (default: enabled)
:disabled_values: str[], acceptable values for disabling something (default: disabled)
"""
self.module = module
self.fw = fw
self.action_args = action_args
if zone:
self.zone = zone
else:
if fw_offline:
self.zone = fw.get_default_zone()
else:
self.zone = fw.getDefaultZone()
self.desired_state = desired_state
self.permanent = permanent
self.immediate = immediate
self.fw_offline = fw_offline
self.enabled_values = enabled_values or ["enabled"]
self.disabled_values = disabled_values or ["disabled"]
# List of messages that we'll call module.fail_json or module.exit_json
# with.
self.msgs = []
# Allow for custom messages to be added for certain subclass transaction
# types
self.enabled_msg = None
self.disabled_msg = None
#####################
# exception handling
#
def action_handler(self, action_func, action_func_args):
"""
Function to wrap calls to make actions on firewalld in try/except
logic and emit (hopefully) useful error messages
"""
try:
return action_func(*action_func_args)
except Exception as e:
# If there are any commonly known errors that we should provide more
# context for to help the users diagnose what's wrong. Handle that here
if "INVALID_SERVICE" in "%s" % e:
self.msgs.append("Services are defined by port/tcp relationship and named as they are in /etc/services (on most systems)")
if len(self.msgs) > 0:
self.module.fail_json(
msg='ERROR: Exception caught: %s %s' % (e, ', '.join(self.msgs))
)
else:
self.module.fail_json(msg='ERROR: Exception caught: %s' % e)
def get_fw_zone_settings(self):
if self.fw_offline:
fw_zone = self.fw.config.get_zone(self.zone)
fw_settings = FirewallClientZoneSettings(
list(self.fw.config.get_zone_config(fw_zone))
)
else:
fw_zone = self.fw.config().getZoneByName(self.zone)
fw_settings = fw_zone.getSettings()
return (fw_zone, fw_settings)
def update_fw_settings(self, fw_zone, fw_settings):
if self.fw_offline:
self.fw.config.set_zone_config(fw_zone, fw_settings.settings)
else:
fw_zone.update(fw_settings)
def get_enabled_immediate(self):
raise NotImplementedError
def get_enabled_permanent(self):
raise NotImplementedError
def set_enabled_immediate(self):
raise NotImplementedError
def set_enabled_permanent(self):
raise NotImplementedError
def set_disabled_immediate(self):
raise NotImplementedError
def set_disabled_permanent(self):
raise NotImplementedError
def run(self):
"""
run
This function contains the "transaction logic" where as all operations
follow a similar pattern in order to perform their action but simply
call different functions to carry that action out.
"""
self.changed = False
if self.immediate and self.permanent:
is_enabled_permanent = self.action_handler(
self.get_enabled_permanent,
self.action_args
)
is_enabled_immediate = self.action_handler(
self.get_enabled_immediate,
self.action_args
)
self.msgs.append('Permanent and Non-Permanent(immediate) operation')
if self.desired_state in self.enabled_values:
if not is_enabled_permanent or not is_enabled_immediate:
if self.module.check_mode:
self.module.exit_json(changed=True)
if not is_enabled_permanent:
self.action_handler(
self.set_enabled_permanent,
self.action_args
)
self.changed = True
if not is_enabled_immediate:
self.action_handler(
self.set_enabled_immediate,
self.action_args
)
self.changed = True
if self.changed and self.enabled_msg:
self.msgs.append(self.enabled_msg)
elif self.desired_state in self.disabled_values:
if is_enabled_permanent or is_enabled_immediate:
if self.module.check_mode:
self.module.exit_json(changed=True)
if is_enabled_permanent:
self.action_handler(
self.set_disabled_permanent,
self.action_args
)
self.changed = True
if is_enabled_immediate:
self.action_handler(
self.set_disabled_immediate,
self.action_args
)
self.changed = True
if self.changed and self.disabled_msg:
self.msgs.append(self.disabled_msg)
elif self.permanent and not self.immediate:
is_enabled = self.action_handler(
self.get_enabled_permanent,
self.action_args
)
self.msgs.append('Permanent operation')
if self.desired_state in self.enabled_values:
if not is_enabled:
if self.module.check_mode:
self.module.exit_json(changed=True)
self.action_handler(
self.set_enabled_permanent,
self.action_args
)
self.changed = True
if self.changed and self.enabled_msg:
self.msgs.append(self.enabled_msg)
elif self.desired_state in self.disabled_values:
if is_enabled:
if self.module.check_mode:
self.module.exit_json(changed=True)
self.action_handler(
self.set_disabled_permanent,
self.action_args
)
self.changed = True
if self.changed and self.disabled_msg:
self.msgs.append(self.disabled_msg)
elif self.immediate and not self.permanent:
is_enabled = self.action_handler(
self.get_enabled_immediate,
self.action_args
)
self.msgs.append('Non-permanent operation')
if self.desired_state in self.enabled_values:
if not is_enabled:
if self.module.check_mode:
self.module.exit_json(changed=True)
self.action_handler(
self.set_enabled_immediate,
self.action_args
)
self.changed = True
if self.changed and self.enabled_msg:
self.msgs.append(self.enabled_msg)
elif self.desired_state in self.disabled_values:
if is_enabled:
if self.module.check_mode:
self.module.exit_json(changed=True)
self.action_handler(
self.set_disabled_immediate,
self.action_args
)
self.changed = True
if self.changed and self.disabled_msg:
self.msgs.append(self.disabled_msg)
return (self.changed, self.msgs)
@staticmethod
def sanity_check(module):
"""
Perform sanity checking, version checks, etc
:module: AnsibleModule instance
"""
if FW_VERSION and fw_offline:
# Pre-run version checking
if LooseVersion(FW_VERSION) < LooseVersion("0.3.9"):
module.fail_json(msg='unsupported version of firewalld, offline operations require >= 0.3.9 - found: {0}'.format(FW_VERSION))
elif FW_VERSION and not fw_offline:
# Pre-run version checking
if LooseVersion(FW_VERSION) < LooseVersion("0.2.11"):
module.fail_json(msg='unsupported version of firewalld, requires >= 0.2.11 - found: {0}'.format(FW_VERSION))
# Check for firewalld running
try:
if fw.connected is False:
module.fail_json(msg='firewalld service must be running, or try with offline=true')
except AttributeError:
module.fail_json(msg="firewalld connection can't be established,\
installed version (%s) likely too old. Requires firewalld >= 0.2.11" % FW_VERSION)
if import_failure:
module.fail_json(
msg=missing_required_lib('firewall') + '. Version 0.2.11 or newer required (0.3.9 or newer for offline operations)'
)

View File

@ -1,94 +0,0 @@
# This code is part of Ansible, but is an independent component.
# This particular file snippet, and this file snippet only, is based on
# Lib/posixpath.py of cpython
# It is licensed under the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
#
# 1. This LICENSE AGREEMENT is between the Python Software Foundation
# ("PSF"), and the Individual or Organization ("Licensee") accessing and
# otherwise using this software ("Python") in source or binary form and
# its associated documentation.
#
# 2. Subject to the terms and conditions of this License Agreement, PSF hereby
# grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
# analyze, test, perform and/or display publicly, prepare derivative works,
# distribute, and otherwise use Python alone or in any derivative version,
# provided, however, that PSF's License Agreement and PSF's notice of copyright,
# i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
# 2011, 2012, 2013, 2014, 2015 Python Software Foundation; All Rights Reserved"
# are retained in Python alone or in any derivative version prepared by Licensee.
#
# 3. In the event Licensee prepares a derivative work that is based on
# or incorporates Python or any part thereof, and wants to make
# the derivative work available to others as provided herein, then
# Licensee hereby agrees to include in any such work a brief summary of
# the changes made to Python.
#
# 4. PSF is making Python available to Licensee on an "AS IS"
# basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
# IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
# DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
# FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
# INFRINGE ANY THIRD PARTY RIGHTS.
#
# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
# FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
# A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
# OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
#
# 6. This License Agreement will automatically terminate upon a material
# breach of its terms and conditions.
#
# 7. Nothing in this License Agreement shall be deemed to create any
# relationship of agency, partnership, or joint venture between PSF and
# Licensee. This License Agreement does not grant permission to use PSF
# trademarks or trade name in a trademark sense to endorse or promote
# products or services of Licensee, or any third party.
#
# 8. By copying, installing or otherwise using Python, Licensee
# agrees to be bound by the terms and conditions of this License
# Agreement.
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
def ismount(path):
"""Test whether a path is a mount point
This is a copy of the upstream version of ismount(). Originally this was copied here as a workaround
until Python issue 2466 was fixed. Now it is here so this will work on older versions of Python
that may not have the upstream fix.
https://github.com/ansible/ansible-modules-core/issues/2186
http://bugs.python.org/issue2466
"""
try:
s1 = os.lstat(path)
except (OSError, ValueError):
# It doesn't exist -- so not a mount point. :-)
return False
else:
# A symlink can never be a mount point
if os.path.stat.S_ISLNK(s1.st_mode):
return False
if isinstance(path, bytes):
parent = os.path.join(path, b'..')
else:
parent = os.path.join(path, '..')
parent = os.path.realpath(parent)
try:
s2 = os.lstat(parent)
except (OSError, ValueError):
return False
dev1 = s1.st_dev
dev2 = s2.st_dev
if dev1 != dev2:
return True # path/.. on a different device as path
ino1 = s1.st_ino
ino2 = s2.st_ino
if ino1 == ino2:
return True # path/.. is the same i-node as path
return False

View File

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""Provide version object to compare version numbers."""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
# Once we drop support for Ansible 2.9, ansible-base 2.10, and ansible-core 2.11, we can
# remove the _version.py file, and replace the following import by
#
# from ansible.module_utils.compat.version import LooseVersion
from ._version import LooseVersion, StrictVersion
__all__ = ['LooseVersion', 'StrictVersion']

View File

@ -1,385 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: acl
short_description: Set and retrieve file ACL information.
description:
- Set and retrieve file ACL information.
version_added: "1.0.0"
options:
path:
description:
- The full path of the file or object.
type: path
required: true
aliases: [ name ]
state:
description:
- Define whether the ACL should be present or not.
- The C(query) state gets the current ACL without changing it, for use in C(register) operations.
choices: [ absent, present, query ]
default: query
type: str
follow:
description:
- Whether to follow symlinks on the path if a symlink is encountered.
type: bool
default: true
default:
description:
- If the target is a directory, setting this to C(true) will make it the default ACL for entities created inside the directory.
- Setting C(default) to C(true) causes an error if the path is a file.
type: bool
default: false
entity:
description:
- The actual user or group that the ACL applies to when matching entity types user or group are selected.
type: str
default: ""
etype:
description:
- The entity type of the ACL to apply, see C(setfacl) documentation for more info.
choices: [ group, mask, other, user ]
type: str
permissions:
description:
- The permissions to apply/remove can be any combination of C(r), C(w), C(x)
- (read, write and execute respectively), and C(X) (execute permission if the file is a directory or already has execute permission for some user)
type: str
entry:
description:
- DEPRECATED.
- The ACL to set or remove.
- This must always be quoted in the form of C(<etype>:<qualifier>:<perms>).
- The qualifier may be empty for some types, but the type and perms are always required.
- C(-) can be used as placeholder when you do not care about permissions.
- This is now superseded by entity, type and permissions fields.
type: str
recursive:
description:
- Recursively sets the specified ACL.
- Incompatible with C(state=query).
- Alias C(recurse) added in version 1.3.0.
type: bool
default: false
aliases: [ recurse ]
use_nfsv4_acls:
description:
- Use NFSv4 ACLs instead of POSIX ACLs.
type: bool
default: false
recalculate_mask:
description:
- Select if and when to recalculate the effective right masks of the files.
- See C(setfacl) documentation for more info.
- Incompatible with C(state=query).
choices: [ default, mask, no_mask ]
default: default
type: str
author:
- Brian Coca (@bcoca)
- Jérémie Astori (@astorije)
notes:
- The C(acl) module requires that ACLs are enabled on the target filesystem and that the C(setfacl) and C(getfacl) binaries are installed.
- As of Ansible 2.0, this module only supports Linux distributions.
- As of Ansible 2.3, the I(name) option has been changed to I(path) as default, but I(name) still works as well.
'''
EXAMPLES = r'''
- name: Grant user Joe read access to a file
ansible.posix.acl:
path: /etc/foo.conf
entity: joe
etype: user
permissions: r
state: present
- name: Removes the ACL for Joe on a specific file
ansible.posix.acl:
path: /etc/foo.conf
entity: joe
etype: user
state: absent
- name: Sets default ACL for joe on /etc/foo.d/
ansible.posix.acl:
path: /etc/foo.d/
entity: joe
etype: user
permissions: rw
default: true
state: present
- name: Same as previous but using entry shorthand
ansible.posix.acl:
path: /etc/foo.d/
entry: default:user:joe:rw-
state: present
- name: Obtain the ACL for a specific file
ansible.posix.acl:
path: /etc/foo.conf
register: acl_info
'''
RETURN = r'''
acl:
description: Current ACL on provided path (after changes, if any)
returned: success
type: list
sample: [ "user::rwx", "group::rwx", "other::rwx" ]
'''
import os
import platform
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
def split_entry(entry):
''' splits entry and ensures normalized return'''
a = entry.split(':')
d = None
if entry.lower().startswith("d"):
d = True
a.pop(0)
if len(a) == 2:
a.append(None)
t, e, p = a
t = t.lower()
if t.startswith("u"):
t = "user"
elif t.startswith("g"):
t = "group"
elif t.startswith("m"):
t = "mask"
elif t.startswith("o"):
t = "other"
else:
t = None
return [d, t, e, p]
def build_entry(etype, entity, permissions=None, use_nfsv4_acls=False):
'''Builds and returns an entry string. Does not include the permissions bit if they are not provided.'''
if use_nfsv4_acls:
return ':'.join([etype, entity, permissions, 'allow'])
if permissions:
return etype + ':' + entity + ':' + permissions
return etype + ':' + entity
def build_command(module, mode, path, follow, default, recursive, recalculate_mask, entry=''):
'''Builds and returns a getfacl/setfacl command.'''
if mode == 'set':
cmd = [module.get_bin_path('setfacl', True)]
cmd.extend(['-m', entry])
elif mode == 'rm':
cmd = [module.get_bin_path('setfacl', True)]
cmd.extend(['-x', entry])
else: # mode == 'get'
cmd = [module.get_bin_path('getfacl', True)]
# prevents absolute path warnings and removes headers
if platform.system().lower() == 'linux':
cmd.append('--omit-header')
cmd.append('--absolute-names')
if recursive:
cmd.append('--recursive')
if recalculate_mask == 'mask' and mode in ['set', 'rm']:
cmd.append('--mask')
elif recalculate_mask == 'no_mask' and mode in ['set', 'rm']:
cmd.append('--no-mask')
if not follow:
if platform.system().lower() == 'linux':
cmd.append('--physical')
elif platform.system().lower() == 'freebsd':
cmd.append('-h')
if default:
cmd.insert(1, '-d')
cmd.append(path)
return cmd
def acl_changed(module, cmd):
'''Returns true if the provided command affects the existing ACLs, false otherwise.'''
# FreeBSD do not have a --test flag, so by default, it is safer to always say "true"
if platform.system().lower() == 'freebsd':
return True
cmd = cmd[:] # lists are mutables so cmd would be overwritten without this
cmd.insert(1, '--test')
lines = run_acl(module, cmd)
for line in lines:
if not line.endswith('*,*'):
return True
return False
def run_acl(module, cmd, check_rc=True):
try:
(rc, out, err) = module.run_command(cmd, check_rc=check_rc)
except Exception as e:
module.fail_json(msg=to_native(e))
lines = []
for l in out.splitlines():
if not l.startswith('#'):
lines.append(l.strip())
if lines and not lines[-1].split():
# trim last line only when it is empty
return lines[:-1]
return lines
def main():
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path', required=True, aliases=['name']),
entry=dict(type='str'),
entity=dict(type='str', default=''),
etype=dict(
type='str',
choices=['group', 'mask', 'other', 'user'],
),
permissions=dict(type='str'),
state=dict(
type='str',
default='query',
choices=['absent', 'present', 'query'],
),
follow=dict(type='bool', default=True),
default=dict(type='bool', default=False),
recursive=dict(type='bool', default=False, aliases=['recurse']),
recalculate_mask=dict(
type='str',
default='default',
choices=['default', 'mask', 'no_mask'],
),
use_nfsv4_acls=dict(type='bool', default=False)
),
supports_check_mode=True,
)
if platform.system().lower() not in ['linux', 'freebsd']:
module.fail_json(msg="The acl module is not available on this system.")
path = module.params.get('path')
entry = module.params.get('entry')
entity = module.params.get('entity')
etype = module.params.get('etype')
permissions = module.params.get('permissions')
state = module.params.get('state')
follow = module.params.get('follow')
default = module.params.get('default')
recursive = module.params.get('recursive')
recalculate_mask = module.params.get('recalculate_mask')
use_nfsv4_acls = module.params.get('use_nfsv4_acls')
if not os.path.exists(path):
module.fail_json(msg="Path not found or not accessible.")
if state == 'query':
if recursive:
module.fail_json(msg="'recursive' MUST NOT be set when 'state=query'.")
if recalculate_mask in ['mask', 'no_mask']:
module.fail_json(msg="'recalculate_mask' MUST NOT be set to 'mask' or 'no_mask' when 'state=query'.")
if not entry:
if state == 'absent' and permissions:
module.fail_json(msg="'permissions' MUST NOT be set when 'state=absent'.")
if state == 'absent' and not entity:
module.fail_json(msg="'entity' MUST be set when 'state=absent'.")
if state in ['present', 'absent'] and not etype:
module.fail_json(msg="'etype' MUST be set when 'state=%s'." % state)
if entry:
if etype or entity or permissions:
module.fail_json(msg="'entry' MUST NOT be set when 'entity', 'etype' or 'permissions' are set.")
if state == 'present' and not entry.count(":") in [2, 3]:
module.fail_json(msg="'entry' MUST have 3 or 4 sections divided by ':' when 'state=present'.")
if state == 'absent' and not entry.count(":") in [1, 2]:
module.fail_json(msg="'entry' MUST have 2 or 3 sections divided by ':' when 'state=absent'.")
if state == 'query':
module.fail_json(msg="'entry' MUST NOT be set when 'state=query'.")
default_flag, etype, entity, permissions = split_entry(entry)
if default_flag is not None:
default = default_flag
if platform.system().lower() == 'freebsd':
if recursive:
module.fail_json(msg="recursive is not supported on that platform.")
changed = False
msg = ""
if state == 'present':
entry = build_entry(etype, entity, permissions, use_nfsv4_acls)
command = build_command(
module, 'set', path, follow,
default, recursive, recalculate_mask, entry
)
changed = acl_changed(module, command)
if changed and not module.check_mode:
run_acl(module, command)
msg = "%s is present" % entry
elif state == 'absent':
entry = build_entry(etype, entity, use_nfsv4_acls)
command = build_command(
module, 'rm', path, follow,
default, recursive, recalculate_mask, entry
)
changed = acl_changed(module, command)
if changed and not module.check_mode:
run_acl(module, command, False)
msg = "%s is absent" % entry
elif state == 'query':
msg = "current acl"
acl = run_acl(
module,
build_command(module, 'get', path, follow, default, recursive, recalculate_mask)
)
module.exit_json(changed=changed, msg=msg, acl=acl)
if __name__ == '__main__':
main()

View File

@ -1,195 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2014, Richard Isaacson <richard.c.isaacson@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: at
short_description: Schedule the execution of a command or script file via the at command
description:
- Use this module to schedule a command or script file to run once in the future.
- All jobs are executed in the 'a' queue.
version_added: "1.0.0"
options:
command:
description:
- A command to be executed in the future.
type: str
script_file:
description:
- An existing script file to be executed in the future.
type: str
count:
description:
- The count of units in the future to execute the command or script file.
type: int
units:
description:
- The type of units in the future to execute the command or script file.
type: str
choices: [ minutes, hours, days, weeks ]
state:
description:
- The state dictates if the command or script file should be evaluated as present(added) or absent(deleted).
type: str
choices: [ absent, present ]
default: present
unique:
description:
- If a matching job is present a new job will not be added.
type: bool
default: false
requirements:
- at
author:
- Richard Isaacson (@risaacson)
'''
EXAMPLES = r'''
- name: Schedule a command to execute in 20 minutes as root
ansible.posix.at:
command: ls -d / >/dev/null
count: 20
units: minutes
- name: Match a command to an existing job and delete the job
ansible.posix.at:
command: ls -d / >/dev/null
state: absent
- name: Schedule a command to execute in 20 minutes making sure it is unique in the queue
ansible.posix.at:
command: ls -d / >/dev/null
count: 20
units: minutes
unique: true
'''
import os
import platform
import tempfile
from ansible.module_utils.basic import AnsibleModule
def add_job(module, result, at_cmd, count, units, command, script_file):
at_command = "%s -f %s now + %s %s" % (at_cmd, script_file, count, units)
rc, out, err = module.run_command(at_command, check_rc=True)
if command:
os.unlink(script_file)
result['changed'] = True
def delete_job(module, result, at_cmd, command, script_file):
for matching_job in get_matching_jobs(module, at_cmd, script_file):
at_command = "%s -r %s" % (at_cmd, matching_job)
rc, out, err = module.run_command(at_command, check_rc=True)
result['changed'] = True
if command:
os.unlink(script_file)
module.exit_json(**result)
def get_matching_jobs(module, at_cmd, script_file):
matching_jobs = []
atq_cmd = module.get_bin_path('atq', True)
# Get list of job numbers for the user.
atq_command = "%s" % atq_cmd
rc, out, err = module.run_command(atq_command, check_rc=True)
current_jobs = out.splitlines()
if len(current_jobs) == 0:
return matching_jobs
# Read script_file into a string.
with open(script_file) as script_fh:
script_file_string = script_fh.read().strip()
# Loop through the jobs.
# If the script text is contained in a job add job number to list.
for current_job in current_jobs:
split_current_job = current_job.split()
at_opt = '-c' if platform.system() != 'AIX' else '-lv'
at_command = "%s %s %s" % (at_cmd, at_opt, split_current_job[0])
rc, out, err = module.run_command(at_command, check_rc=True)
if script_file_string in out:
matching_jobs.append(split_current_job[0])
# Return the list.
return matching_jobs
def create_tempfile(command):
filed, script_file = tempfile.mkstemp(prefix='at')
fileh = os.fdopen(filed, 'w')
fileh.write(command + os.linesep)
fileh.close()
return script_file
def main():
module = AnsibleModule(
argument_spec=dict(
command=dict(type='str'),
script_file=dict(type='str'),
count=dict(type='int'),
units=dict(type='str', choices=['minutes', 'hours', 'days', 'weeks']),
state=dict(type='str', default='present', choices=['absent', 'present']),
unique=dict(type='bool', default=False),
),
mutually_exclusive=[['command', 'script_file']],
required_one_of=[['command', 'script_file']],
supports_check_mode=False,
)
at_cmd = module.get_bin_path('at', True)
command = module.params['command']
script_file = module.params['script_file']
count = module.params['count']
units = module.params['units']
state = module.params['state']
unique = module.params['unique']
if (state == 'present') and (not count or not units):
module.fail_json(msg="present state requires count and units")
result = dict(
changed=False,
state=state,
)
# If command transform it into a script_file
if command:
script_file = create_tempfile(command)
# if absent remove existing and return
if state == 'absent':
delete_job(module, result, at_cmd, command, script_file)
# if unique if existing return unchanged
if unique:
if len(get_matching_jobs(module, at_cmd, script_file)) != 0:
if command:
os.unlink(script_file)
module.exit_json(**result)
result['script_file'] = script_file
result['count'] = count
result['units'] = units
add_job(module, result, at_cmd, count, units, command, script_file)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@ -1,692 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2012, Brad Olson <brado@movedbylight.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: authorized_key
short_description: Adds or removes an SSH authorized key
description:
- Adds or removes SSH authorized keys for particular user accounts.
version_added: "1.0.0"
options:
user:
description:
- The username on the remote host whose authorized_keys file will be modified.
type: str
required: true
key:
description:
- The SSH public key(s), as a string or (since Ansible 1.9) url (https://github.com/username.keys).
type: str
required: true
path:
description:
- Alternate path to the authorized_keys file.
- When unset, this value defaults to I(~/.ssh/authorized_keys).
type: path
manage_dir:
description:
- Whether this module should manage the directory of the authorized key file.
- If set to C(true), the module will create the directory, as well as set the owner and permissions
of an existing directory.
- Be sure to set C(manage_dir=false) if you are using an alternate directory for authorized_keys,
as set with C(path), since you could lock yourself out of SSH access.
- See the example below.
type: bool
default: true
state:
description:
- Whether the given key (with the given key_options) should or should not be in the file.
type: str
choices: [ absent, present ]
default: present
key_options:
description:
- A string of ssh key options to be prepended to the key in the authorized_keys file.
type: str
exclusive:
description:
- Whether to remove all other non-specified keys from the authorized_keys file.
- Multiple keys can be specified in a single C(key) string value by separating them by newlines.
- This option is not loop aware, so if you use C(with_) , it will be exclusive per iteration of the loop.
- If you want multiple keys in the file you need to pass them all to C(key) in a single batch as mentioned above.
type: bool
default: false
validate_certs:
description:
- This only applies if using a https url as the source of the keys.
- If set to C(false), the SSL certificates will not be validated.
- This should only set to C(false) used on personally controlled sites using self-signed certificates as it avoids verifying the source site.
- Prior to 2.1 the code worked as if this was set to C(true).
type: bool
default: true
comment:
description:
- Change the comment on the public key.
- Rewriting the comment is useful in cases such as fetching it from GitHub or GitLab.
- If no comment is specified, the existing comment will be kept.
type: str
follow:
description:
- Follow path symlink instead of replacing it.
type: bool
default: false
author: Ansible Core Team
'''
EXAMPLES = r'''
- name: Set authorized key taken from file
ansible.posix.authorized_key:
user: charlie
state: present
key: "{{ lookup('file', '/home/charlie/.ssh/id_rsa.pub') }}"
- name: Set authorized keys taken from url
ansible.posix.authorized_key:
user: charlie
state: present
key: https://github.com/charlie.keys
- name: Set authorized keys taken from url using lookup
ansible.posix.authorized_key:
user: charlie
state: present
key: "{{ lookup('url', 'https://github.com/charlie.keys', split_lines=False) }}"
- name: Set authorized key in alternate location
ansible.posix.authorized_key:
user: charlie
state: present
key: "{{ lookup('file', '/home/charlie/.ssh/id_rsa.pub') }}"
path: /etc/ssh/authorized_keys/charlie
manage_dir: false
- name: Set up multiple authorized keys
ansible.posix.authorized_key:
user: deploy
state: present
key: '{{ item }}'
with_file:
- public_keys/doe-jane
- public_keys/doe-john
- name: Set authorized key defining key options
ansible.posix.authorized_key:
user: charlie
state: present
key: "{{ lookup('file', '/home/charlie/.ssh/id_rsa.pub') }}"
key_options: 'no-port-forwarding,from="10.0.1.1"'
- name: Set authorized key without validating the TLS/SSL certificates
ansible.posix.authorized_key:
user: charlie
state: present
key: https://github.com/user.keys
validate_certs: false
- name: Set authorized key, removing all the authorized keys already set
ansible.posix.authorized_key:
user: root
key: "{{ lookup('file', 'public_keys/doe-jane') }}"
state: present
exclusive: true
- name: Set authorized key for user ubuntu copying it from current user
ansible.posix.authorized_key:
user: ubuntu
state: present
key: "{{ lookup('file', lookup('env','HOME') + '/.ssh/id_rsa.pub') }}"
'''
RETURN = r'''
exclusive:
description: If the key has been forced to be exclusive or not.
returned: success
type: bool
sample: false
key:
description: The key that the module was running against.
returned: success
type: str
sample: https://github.com/user.keys
key_option:
description: Key options related to the key.
returned: success
type: str
sample: null
keyfile:
description: Path for authorized key file.
returned: success
type: str
sample: /home/user/.ssh/authorized_keys
manage_dir:
description: Whether this module managed the directory of the authorized key file.
returned: success
type: bool
sample: true
path:
description: Alternate path to the authorized_keys file
returned: success
type: str
sample: null
state:
description: Whether the given key (with the given key_options) should or should not be in the file
returned: success
type: str
sample: present
unique:
description: Whether the key is unique
returned: success
type: bool
sample: false
user:
description: The username on the remote host whose authorized_keys file will be modified
returned: success
type: str
sample: user
validate_certs:
description: This only applies if using a https url as the source of the keys. If set to C(false), the SSL certificates will not be validated.
returned: success
type: bool
sample: true
'''
# Makes sure the public key line is present or absent in the user's .ssh/authorized_keys.
#
# Arguments
# =========
# user = username
# key = line to add to authorized_keys for user
# path = path to the user's authorized_keys file (default: ~/.ssh/authorized_keys)
# manage_dir = whether to create, and control ownership of the directory (default: true)
# state = absent|present (default: present)
#
# see example in examples/playbooks
import os
import pwd
import os.path
import tempfile
import re
import shlex
from operator import itemgetter
from ansible.module_utils._text import to_native
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url
class keydict(dict):
""" a dictionary that maintains the order of keys as they are added
This has become an abuse of the dict interface. Probably should be
rewritten to be an entirely custom object with methods instead of
bracket-notation.
Our requirements are for a data structure that:
* Preserves insertion order
* Can store multiple values for a single key.
The present implementation has the following functions used by the rest of
the code:
* __setitem__(): to add a key=value. The value can never be disassociated
with the key, only new values can be added in addition.
* items(): to retrieve the key, value pairs.
Other dict methods should work but may be surprising. For instance, there
will be multiple keys that are the same in keys() and __getitem__() will
return a list of the values that have been set via __setitem__.
"""
# http://stackoverflow.com/questions/2328235/pythonextend-the-dict-class
def __init__(self, *args, **kw):
super(keydict, self).__init__(*args, **kw)
self.itemlist = list(super(keydict, self).keys())
def __setitem__(self, key, value):
self.itemlist.append(key)
if key in self:
self[key].append(value)
else:
super(keydict, self).__setitem__(key, [value])
def __iter__(self):
return iter(self.itemlist)
def keys(self):
return self.itemlist
def _item_generator(self):
indexes = {}
for key in self.itemlist:
if key in indexes:
indexes[key] += 1
else:
indexes[key] = 0
yield key, self[key][indexes[key]]
def iteritems(self):
raise NotImplementedError("Do not use this as it's not available on py3")
def items(self):
return list(self._item_generator())
def itervalues(self):
raise NotImplementedError("Do not use this as it's not available on py3")
def values(self):
return [item[1] for item in self.items()]
def keyfile(module, user, write=False, path=None, manage_dir=True, follow=False):
"""
Calculate name of authorized keys file, optionally creating the
directories and file, properly setting permissions.
:param str user: name of user in passwd file
:param bool write: if True, write changes to authorized_keys file (creating directories if needed)
:param str path: if not None, use provided path rather than default of '~user/.ssh/authorized_keys'
:param bool manage_dir: if True, create and set ownership of the parent dir of the authorized_keys file
:param bool follow: if True symlinks will be followed and not replaced
:return: full path string to authorized_keys for user
"""
if module.check_mode and path is not None:
keysfile = path
if follow:
return os.path.realpath(keysfile)
return keysfile
try:
user_entry = pwd.getpwnam(user)
except KeyError as e:
if module.check_mode and path is None:
module.fail_json(msg="Either user must exist or you must provide full path to key file in check mode")
module.fail_json(msg="Failed to lookup user %s: %s" % (user, to_native(e)))
if path is None:
homedir = user_entry.pw_dir
sshdir = os.path.join(homedir, ".ssh")
keysfile = os.path.join(sshdir, "authorized_keys")
else:
sshdir = os.path.dirname(path)
keysfile = path
if follow:
keysfile = os.path.realpath(keysfile)
if not write or module.check_mode:
return keysfile
uid = user_entry.pw_uid
gid = user_entry.pw_gid
if manage_dir:
if not os.path.exists(sshdir):
try:
os.mkdir(sshdir, int('0700', 8))
except OSError as e:
module.fail_json(msg="Failed to create directory %s : %s" % (sshdir, to_native(e)))
if module.selinux_enabled():
module.set_default_selinux_context(sshdir, False)
os.chown(sshdir, uid, gid)
os.chmod(sshdir, int('0700', 8))
if not os.path.exists(keysfile):
basedir = os.path.dirname(keysfile)
if not os.path.exists(basedir):
os.makedirs(basedir)
f = None
try:
f = open(keysfile, "w") # touches file so we can set ownership and perms
finally:
f.close()
if module.selinux_enabled():
module.set_default_selinux_context(keysfile, False)
try:
os.chown(keysfile, uid, gid)
os.chmod(keysfile, int('0600', 8))
except OSError:
pass
return keysfile
def parseoptions(module, options):
'''
reads a string containing ssh-key options
and returns a dictionary of those options
'''
options_dict = keydict() # ordered dict
if options:
# the following regex will split on commas while
# ignoring those commas that fall within quotes
regex = re.compile(r'''((?:[^,"']|"[^"]*"|'[^']*')+)''')
parts = regex.split(options)[1:-1]
for part in parts:
if "=" in part:
(key, value) = part.split("=", 1)
options_dict[key] = value
elif part != ",":
options_dict[part] = None
return options_dict
def parsekey(module, raw_key, rank=None):
'''
parses a key, which may or may not contain a list
of ssh-key options at the beginning
rank indicates the keys original ordering, so that
it can be written out in the same order.
'''
VALID_SSH2_KEY_TYPES = [
'sk-ecdsa-sha2-nistp256@openssh.com',
'sk-ecdsa-sha2-nistp256-cert-v01@openssh.com',
'webauthn-sk-ecdsa-sha2-nistp256@openssh.com',
'ecdsa-sha2-nistp256',
'ecdsa-sha2-nistp256-cert-v01@openssh.com',
'ecdsa-sha2-nistp384',
'ecdsa-sha2-nistp384-cert-v01@openssh.com',
'ecdsa-sha2-nistp521',
'ecdsa-sha2-nistp521-cert-v01@openssh.com',
'sk-ssh-ed25519@openssh.com',
'sk-ssh-ed25519-cert-v01@openssh.com',
'ssh-ed25519',
'ssh-ed25519-cert-v01@openssh.com',
'ssh-dss',
'ssh-rsa',
'ssh-xmss@openssh.com',
'ssh-xmss-cert-v01@openssh.com',
'rsa-sha2-256',
'rsa-sha2-512',
'ssh-rsa-cert-v01@openssh.com',
'rsa-sha2-256-cert-v01@openssh.com',
'rsa-sha2-512-cert-v01@openssh.com',
'ssh-dss-cert-v01@openssh.com',
]
options = None # connection options
key = None # encrypted key string
key_type = None # type of ssh key
type_index = None # index of keytype in key string|list
# remove comment yaml escapes
raw_key = raw_key.replace(r'\#', '#')
# split key safely
lex = shlex.shlex(raw_key)
lex.quotes = []
lex.commenters = '' # keep comment hashes
lex.whitespace_split = True
key_parts = list(lex)
if key_parts and key_parts[0] == '#':
# comment line, invalid line, etc.
return (raw_key, 'skipped', None, None, rank)
for i in range(0, len(key_parts)):
if key_parts[i] in VALID_SSH2_KEY_TYPES:
type_index = i
key_type = key_parts[i]
break
# check for options
if type_index is None:
return None
elif type_index > 0:
options = " ".join(key_parts[:type_index])
# parse the options (if any)
options = parseoptions(module, options)
# get key after the type index
key = key_parts[(type_index + 1)]
# set comment to everything after the key
if len(key_parts) > (type_index + 1):
comment = " ".join(key_parts[(type_index + 2):])
return (key, key_type, options, comment, rank)
def readfile(filename):
if not os.path.isfile(filename):
return ''
f = open(filename)
try:
return f.read()
finally:
f.close()
def parsekeys(module, lines):
keys = {}
for rank_index, line in enumerate(lines.splitlines(True)):
key_data = parsekey(module, line, rank=rank_index)
if key_data:
# use key as identifier
keys[key_data[0]] = key_data
else:
# for an invalid line, just set the line
# dict key to the line so it will be re-output later
keys[line] = (line, 'skipped', None, None, rank_index)
return keys
def writefile(module, filename, content):
dummy, tmp_path = tempfile.mkstemp()
try:
with open(tmp_path, "w") as f:
f.write(content)
except IOError as e:
module.add_cleanup_file(tmp_path)
module.fail_json(msg="Failed to write to file %s: %s" % (tmp_path, to_native(e)))
module.atomic_move(tmp_path, filename)
def serialize(keys):
lines = []
new_keys = keys.values()
# order the new_keys by their original ordering, via the rank item in the tuple
ordered_new_keys = sorted(new_keys, key=itemgetter(4))
for key in ordered_new_keys:
try:
(keyhash, key_type, options, comment, rank) = key
option_str = ""
if options:
option_strings = []
for option_key, value in options.items():
if value is None:
option_strings.append("%s" % option_key)
else:
option_strings.append("%s=%s" % (option_key, value))
option_str = ",".join(option_strings)
option_str += " "
# comment line or invalid line, just leave it
if not key_type:
key_line = key
if key_type == 'skipped':
key_line = key[0]
else:
key_line = "%s%s %s %s\n" % (option_str, key_type, keyhash, comment)
except Exception:
key_line = key
lines.append(key_line)
return ''.join(lines)
def enforce_state(module, params):
"""
Add or remove key.
"""
user = params["user"]
key = params["key"]
path = params.get("path", None)
manage_dir = params.get("manage_dir", True)
state = params.get("state", "present")
key_options = params.get("key_options", None)
exclusive = params.get("exclusive", False)
comment = params.get("comment", None)
follow = params.get('follow', False)
error_msg = "Error getting key from: %s"
# if the key is a url, request it and use it as key source
if key.startswith("http"):
try:
resp, info = fetch_url(module, key)
if info['status'] != 200:
module.fail_json(msg=error_msg % key)
else:
key = resp.read()
except Exception:
module.fail_json(msg=error_msg % key)
# resp.read gives bytes on python3, convert to native string type
key = to_native(key, errors='surrogate_or_strict')
# extract individual keys into an array, skipping blank lines and comments
new_keys = [s for s in key.splitlines() if s and not s.startswith('#')]
# check current state -- just get the filename, don't create file
do_write = False
params["keyfile"] = keyfile(module, user, do_write, path, manage_dir)
existing_content = readfile(params["keyfile"])
existing_keys = parsekeys(module, existing_content)
# Add a place holder for keys that should exist in the state=present and
# exclusive=true case
keys_to_exist = []
# we will order any non exclusive new keys higher than all the existing keys,
# resulting in the new keys being written to the key file after existing keys, but
# in the order of new_keys
max_rank_of_existing_keys = len(existing_keys)
# Check our new keys, if any of them exist we'll continue.
for rank_index, new_key in enumerate(new_keys):
parsed_new_key = parsekey(module, new_key, rank=rank_index)
if not parsed_new_key:
module.fail_json(msg="invalid key specified: %s" % new_key)
if key_options is not None:
parsed_options = parseoptions(module, key_options)
# rank here is the rank in the provided new keys, which may be unrelated to rank in existing_keys
parsed_new_key = (parsed_new_key[0], parsed_new_key[1], parsed_options, parsed_new_key[3], parsed_new_key[4])
if comment is not None:
parsed_new_key = (parsed_new_key[0], parsed_new_key[1], parsed_new_key[2], comment, parsed_new_key[4])
matched = False
non_matching_keys = []
if parsed_new_key[0] in existing_keys:
# Then we check if everything (except the rank at index 4) matches, including
# the key type and options. If not, we append this
# existing key to the non-matching list
# We only want it to match everything when the state
# is present
if parsed_new_key[:4] != existing_keys[parsed_new_key[0]][:4] and state == "present":
non_matching_keys.append(existing_keys[parsed_new_key[0]])
else:
matched = True
# handle idempotent state=present
if state == "present":
keys_to_exist.append(parsed_new_key[0])
if len(non_matching_keys) > 0:
for non_matching_key in non_matching_keys:
if non_matching_key[0] in existing_keys:
del existing_keys[non_matching_key[0]]
do_write = True
# new key that didn't exist before. Where should it go in the ordering?
if not matched:
# We want the new key to be after existing keys if not exclusive (rank > max_rank_of_existing_keys)
total_rank = max_rank_of_existing_keys + parsed_new_key[4]
# replace existing key tuple with new parsed key with its total rank
existing_keys[parsed_new_key[0]] = (parsed_new_key[0], parsed_new_key[1], parsed_new_key[2], parsed_new_key[3], total_rank)
do_write = True
elif state == "absent":
if not matched:
continue
del existing_keys[parsed_new_key[0]]
do_write = True
# remove all other keys to honor exclusive
# for 'exclusive', make sure keys are written in the order the new keys were
if state == "present" and exclusive:
to_remove = frozenset(existing_keys).difference(keys_to_exist)
for key in to_remove:
del existing_keys[key]
do_write = True
if do_write:
filename = keyfile(module, user, do_write, path, manage_dir, follow)
new_content = serialize(existing_keys)
diff = None
if module._diff:
diff = {
'before_header': params['keyfile'],
'after_header': filename,
'before': existing_content,
'after': new_content,
}
params['diff'] = diff
if not module.check_mode:
writefile(module, filename, new_content)
params['changed'] = True
return params
def main():
module = AnsibleModule(
argument_spec=dict(
user=dict(type='str', required=True),
key=dict(type='str', required=True, no_log=False),
path=dict(type='path'),
manage_dir=dict(type='bool', default=True),
state=dict(type='str', default='present', choices=['absent', 'present']),
key_options=dict(type='str', no_log=False),
exclusive=dict(type='bool', default=False),
comment=dict(type='str'),
validate_certs=dict(type='bool', default=True),
follow=dict(type='bool', default=False),
),
supports_check_mode=True,
)
results = enforce_state(module, module.params)
module.exit_json(**results)
if __name__ == '__main__':
main()

View File

@ -1,397 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Hideki Saito <saito@fgrep.org>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: firewalld_info
short_description: Gather information about firewalld
description:
- This module gathers information about firewalld rules.
options:
active_zones:
description: Gather information about active zones.
type: bool
default: false
zones:
description:
- Gather information about specific zones.
- If only works if C(active_zones) is set to C(false).
required: false
type: list
elements: str
requirements:
- firewalld >= 0.2.11
- python-firewall
- python-dbus
author:
- Hideki Saito (@saito-hideki)
'''
EXAMPLES = r'''
- name: Gather information about active zones
ansible.posix.firewalld_info:
active_zones: true
register: result
- name: Print default zone for debugging
ansible.builtin.debug:
var: result.firewalld_info.default_zone
- name: Gather information about specific zones
ansible.posix.firewalld_info:
zones:
- public
- external
- internal
register: result
'''
RETURN = r'''
active_zones:
description:
- Gather active zones only if turn it C(true).
returned: success
type: bool
sample: false
collected_zones:
description:
- A list of collected zones.
returned: success
type: list
sample: [external, internal]
undefined_zones:
description:
- A list of undefined zones in C(zones) option.
- C(undefined_zones) will be ignored for gathering process.
returned: success
type: list
sample: [foo, bar]
firewalld_info:
description:
- Returns various information about firewalld configuration.
returned: success
type: complex
contains:
version:
description:
- The version information of firewalld.
returned: success
type: str
sample: 0.8.2
default_zone:
description:
- The zone name of default zone.
returned: success
type: str
sample: public
zones:
description:
- A dict of zones to gather information.
returned: success
type: complex
contains:
zone:
description:
- The zone name registered in firewalld.
returned: success
type: complex
sample: external
contains:
target:
description:
- A list of services in the zone.
returned: success
type: str
sample: ACCEPT
icmp_block_inversion:
description:
- The ICMP block inversion to block
all ICMP requests.
returned: success
type: bool
sample: false
interfaces:
description:
- A list of network interfaces.
returned: success
type: list
sample:
- 'eth0'
- 'eth1'
sources:
description:
- A list of source network address.
returned: success
type: list
sample:
- '172.16.30.0/24'
- '172.16.31.0/24'
services:
description:
- A list of network services.
returned: success
type: list
sample:
- 'dhcp'
- 'dns'
- 'ssh'
ports:
description:
- A list of network port with protocol.
returned: success
type: list
sample:
- - "22"
- "tcp"
- - "80"
- "tcp"
protocols:
description:
- A list of network protocol.
returned: success
type: list
sample:
- "icmp"
- "ipv6-icmp"
forward:
description:
- The network interface forwarding.
- This parameter supports on python-firewall
0.9.0(or later) and is not collected in earlier
versions.
returned: success
type: bool
sample: false
masquerade:
description:
- The network interface masquerading.
returned: success
type: bool
sample: false
forward_ports:
description:
- A list of forwarding port pair with protocol.
returned: success
type: list
sample:
- "icmp"
- "ipv6-icmp"
source_ports:
description:
- A list of network source port with protocol.
returned: success
type: list
sample:
- - "30000"
- "tcp"
- - "30001"
- "tcp"
icmp_blocks:
description:
- A list of blocking icmp protocol.
returned: success
type: list
sample:
- "echo-request"
rich_rules:
description:
- A list of rich language rule.
returned: success
type: list
sample:
- "rule protocol value=\"icmp\" reject"
- "rule priority=\"32767\" reject"
'''
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils._text import to_native
from ansible_collections.ansible.posix.plugins.module_utils.version import StrictVersion
try:
import dbus
HAS_DBUS = True
except ImportError:
HAS_DBUS = False
try:
import firewall.client as fw_client
import firewall.config as fw_config
HAS_FIREWALLD = True
except ImportError:
HAS_FIREWALLD = False
def get_version():
return fw_config.VERSION
def get_active_zones(client):
return client.getActiveZones().keys()
def get_all_zones(client):
return client.getZones()
def get_default_zone(client):
return client.getDefaultZone()
def get_zone_settings(client, zone):
return client.getZoneSettings(zone)
def get_zone_target(zone_settings):
return zone_settings.getTarget()
def get_zone_icmp_block_inversion(zone_settings):
return zone_settings.getIcmpBlockInversion()
def get_zone_interfaces(zone_settings):
return zone_settings.getInterfaces()
def get_zone_sources(zone_settings):
return zone_settings.getSources()
def get_zone_services(zone_settings):
return zone_settings.getServices()
def get_zone_ports(zone_settings):
return zone_settings.getPorts()
def get_zone_protocols(zone_settings):
return zone_settings.getProtocols()
# This function supports python-firewall 0.9.0(or later).
def get_zone_forward(zone_settings):
return zone_settings.getForward()
def get_zone_masquerade(zone_settings):
return zone_settings.getMasquerade()
def get_zone_forward_ports(zone_settings):
return zone_settings.getForwardPorts()
def get_zone_source_ports(zone_settings):
return zone_settings.getSourcePorts()
def get_zone_icmp_blocks(zone_settings):
return zone_settings.getIcmpBlocks()
def get_zone_rich_rules(zone_settings):
return zone_settings.getRichRules()
def main():
module_args = dict(
active_zones=dict(required=False, type='bool', default=False),
zones=dict(required=False, type='list', elements='str'),
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True,
)
firewalld_info = dict()
result = dict(
changed=False,
active_zones=module.params['active_zones'],
collected_zones=list(),
undefined_zones=list(),
warnings=list(),
)
# Exit with failure message if requirements modules are not installed.
if not HAS_DBUS:
module.fail_json(msg=missing_required_lib('python-dbus'))
if not HAS_FIREWALLD:
module.fail_json(msg=missing_required_lib('python-firewall'))
# If you want to show warning messages in the task running process,
# you can append the message to the 'warn' list.
warn = list()
try:
client = fw_client.FirewallClient()
# Gather general information of firewalld.
firewalld_info['version'] = get_version()
firewalld_info['default_zone'] = get_default_zone(client)
# Gather information for zones.
zones_info = dict()
collect_zones = list()
ignore_zones = list()
if module.params['active_zones']:
collect_zones = get_active_zones(client)
elif module.params['zones']:
all_zones = get_all_zones(client)
specified_zones = module.params['zones']
collect_zones = list(set(specified_zones) & set(all_zones))
ignore_zones = list(set(specified_zones) - set(collect_zones))
warn.append(
'Please note: zone:(%s) have been ignored in the gathering process.' % ','.join(ignore_zones))
else:
collect_zones = get_all_zones(client)
for zone in collect_zones:
# Gather settings for each zone based on the output of
# 'firewall-cmd --info-zone=<ZONE>' command.
zone_info = dict()
zone_settings = get_zone_settings(client, zone)
zone_info['target'] = get_zone_target(zone_settings)
zone_info['icmp_block_inversion'] = get_zone_icmp_block_inversion(zone_settings)
zone_info['interfaces'] = get_zone_interfaces(zone_settings)
zone_info['sources'] = get_zone_sources(zone_settings)
zone_info['services'] = get_zone_services(zone_settings)
zone_info['ports'] = get_zone_ports(zone_settings)
zone_info['protocols'] = get_zone_protocols(zone_settings)
zone_info['masquerade'] = get_zone_masquerade(zone_settings)
zone_info['forward_ports'] = get_zone_forward_ports(zone_settings)
zone_info['source_ports'] = get_zone_source_ports(zone_settings)
zone_info['icmp_blocks'] = get_zone_icmp_blocks(zone_settings)
zone_info['rich_rules'] = get_zone_rich_rules(zone_settings)
# The 'forward' parameter supports on python-firewall 0.9.0(or later).
if StrictVersion(firewalld_info['version']) >= StrictVersion('0.9.0'):
zone_info['forward'] = get_zone_forward(zone_settings)
zones_info[zone] = zone_info
firewalld_info['zones'] = zones_info
except AttributeError as e:
module.fail_json(msg=('firewalld probably not be running, Or the following method '
'is not supported with your python-firewall version. (Error: %s)') % to_native(e))
except dbus.exceptions.DBusException as e:
module.fail_json(msg=('Unable to gather firewalld settings.'
' You may need to run as the root user or'
' use become. (Error: %s)' % to_native(e)))
result['collected_zones'] = collect_zones
result['undefined_zones'] = ignore_zones
result['firewalld_info'] = firewalld_info
result['warnings'] = warn
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@ -1,223 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2012, Luis Alberto Perez Lazaro <luisperlazaro@gmail.com>
# Copyright: (c) 2015, Jakub Jirutka <jakub@jirutka.cz>
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: patch
author:
- Jakub Jirutka (@jirutka)
- Luis Alberto Perez Lazaro (@luisperlaz)
description:
- Apply patch files using the GNU patch tool.
short_description: Apply patch files using the GNU patch tool
version_added: "1.0.0"
options:
basedir:
description:
- Path of a base directory in which the patch file will be applied.
- May be omitted when C(dest) option is specified, otherwise required.
type: path
dest:
description:
- Path of the file on the remote machine to be patched.
- The names of the files to be patched are usually taken from the patch
file, but if there's just one file to be patched it can specified with
this option.
type: path
aliases: [ originalfile ]
src:
description:
- Path of the patch file as accepted by the GNU patch tool. If
C(remote_src) is C(false), the patch source file is looked up from the
module's I(files) directory.
type: path
required: true
aliases: [ patchfile ]
state:
description:
- Whether the patch should be applied or reverted.
type: str
choices: [ absent, present ]
default: present
remote_src:
description:
- If C(false), it will search for src at originating/controller machine, if C(true) it will
go to the remote/target machine for the C(src).
type: bool
default: false
strip:
description:
- Number that indicates the smallest prefix containing leading slashes
that will be stripped from each file name found in the patch file.
- For more information see the strip parameter of the GNU patch tool.
type: int
default: 0
backup:
description:
- Passes C(--backup --version-control=numbered) to patch, producing numbered backup copies.
type: bool
default: false
binary:
description:
- Setting to C(true) will disable patch's heuristic for transforming CRLF
line endings into LF.
- Line endings of src and dest must match.
- If set to C(false), C(patch) will replace CRLF in C(src) files on POSIX.
type: bool
default: false
ignore_whitespace:
description:
- Setting to C(true) will ignore white space changes between patch and input.
type: bool
default: false
notes:
- This module requires GNU I(patch) utility to be installed on the remote host.
'''
EXAMPLES = r'''
- name: Apply patch to one file
ansible.posix.patch:
src: /tmp/index.html.patch
dest: /var/www/index.html
- name: Apply patch to multiple files under basedir
ansible.posix.patch:
src: /tmp/customize.patch
basedir: /var/www
strip: 1
- name: Revert patch to one file
ansible.posix.patch:
src: /tmp/index.html.patch
dest: /var/www/index.html
state: absent
'''
import os
import platform
from traceback import format_exc
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
class PatchError(Exception):
pass
def add_dry_run_option(opts):
# Older versions of FreeBSD, OpenBSD and NetBSD support the --check option only.
if platform.system().lower() in ['openbsd', 'netbsd', 'freebsd']:
opts.append('--check')
else:
opts.append('--dry-run')
def is_already_applied(patch_func, patch_file, basedir, dest_file=None, binary=False, ignore_whitespace=False, strip=0, state='present'):
opts = ['--quiet', '--forward',
"--strip=%s" % strip, "--directory='%s'" % basedir,
"--input='%s'" % patch_file]
add_dry_run_option(opts)
if binary:
opts.append('--binary')
if ignore_whitespace:
opts.append('--ignore-whitespace')
if dest_file:
opts.append("'%s'" % dest_file)
if state == 'present':
opts.append('--reverse')
(rc, var1, var2) = patch_func(opts)
return rc == 0
def apply_patch(patch_func, patch_file, basedir, dest_file=None, binary=False, ignore_whitespace=False, strip=0, dry_run=False, backup=False, state='present'):
opts = ['--quiet', '--forward', '--batch', '--reject-file=-',
"--strip=%s" % strip, "--directory='%s'" % basedir,
"--input='%s'" % patch_file]
if dry_run:
add_dry_run_option(opts)
if binary:
opts.append('--binary')
if ignore_whitespace:
opts.append('--ignore-whitespace')
if dest_file:
opts.append("'%s'" % dest_file)
if backup:
opts.append('--backup --version-control=numbered')
if state == 'absent':
opts.append('--reverse')
(rc, out, err) = patch_func(opts)
if rc != 0:
msg = err or out
raise PatchError(msg)
def main():
module = AnsibleModule(
argument_spec=dict(
src=dict(type='path', required=True, aliases=['patchfile']),
dest=dict(type='path', aliases=['originalfile']),
basedir=dict(type='path'),
strip=dict(type='int', default=0),
remote_src=dict(type='bool', default=False),
# NB: for 'backup' parameter, semantics is slightly different from standard
# since patch will create numbered copies, not strftime("%Y-%m-%d@%H:%M:%S~")
backup=dict(type='bool', default=False),
binary=dict(type='bool', default=False),
ignore_whitespace=dict(type='bool', default=False),
state=dict(type='str', default='present', choices=['absent', 'present']),
),
required_one_of=[['dest', 'basedir']],
supports_check_mode=True,
)
# Create type object as namespace for module params
p = type('Params', (), module.params)
if not os.access(p.src, os.R_OK):
module.fail_json(msg="src %s doesn't exist or not readable" % (p.src))
if p.dest and not os.access(p.dest, os.W_OK):
module.fail_json(msg="dest %s doesn't exist or not writable" % (p.dest))
if p.basedir and not os.path.exists(p.basedir):
module.fail_json(msg="basedir %s doesn't exist" % (p.basedir))
if not p.basedir:
p.basedir = os.path.dirname(p.dest)
patch_bin = module.get_bin_path('patch')
if patch_bin is None:
module.fail_json(msg="patch command not found")
def patch_func(opts):
return module.run_command('%s %s' % (patch_bin, ' '.join(opts)))
# patch need an absolute file name
p.src = os.path.abspath(p.src)
changed = False
if not is_already_applied(patch_func, p.src, p.basedir, dest_file=p.dest, binary=p.binary,
ignore_whitespace=p.ignore_whitespace, strip=p.strip, state=p.state):
try:
apply_patch(patch_func, p.src, p.basedir, dest_file=p.dest, binary=p.binary, ignore_whitespace=p.ignore_whitespace, strip=p.strip,
dry_run=module.check_mode, backup=p.backup, state=p.state)
changed = True
except PatchError as e:
module.fail_json(msg=to_native(e), exception=format_exc())
module.exit_json(changed=changed)
if __name__ == '__main__':
main()

View File

@ -1,76 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: Red Hat Inc.
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: rhel_facts
version_added: 1.5.0
short_description: Facts module to set or override RHEL specific facts.
description:
- Compatibility layer for using the "package" module for rpm-ostree based systems via setting the "pkg_mgr" fact correctly.
author:
- Adam Miller (@maxamillion)
requirements:
- rpm-ostree
seealso:
- module: ansible.builtin.package
options: {}
'''
EXAMPLES = '''
- name: Playbook to use the package module on all RHEL footprints
vars:
ansible_facts_modules:
- setup # REQUIRED to be run before all custom fact modules
- ansible.posix.rhel_facts
tasks:
- name: Ensure packages are installed
ansible.builtin.package:
name:
- htop
- ansible
state: present
'''
RETURN = """
ansible_facts:
description: Relevant Ansible Facts
returned: when needed
type: complex
contains:
pkg_mgr:
description: System-level package manager override
returned: when needed
type: str
sample: {'pkg_mgr': 'ansible.posix.rhel_facts'}
"""
import os
from ansible.module_utils.basic import AnsibleModule
def main():
module = AnsibleModule(
argument_spec=dict(),
supports_check_mode=True,
)
ansible_facts = {}
# Verify that the platform is an rpm-ostree based system
if os.path.exists("/run/ostree-booted"):
ansible_facts['pkg_mgr'] = 'ansible.posix.rhel_rpm_ostree'
module.exit_json(ansible_facts=ansible_facts, changed=False)
if __name__ == '__main__':
main()

View File

@ -1,124 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: Red Hat Inc.
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: rhel_rpm_ostree
version_added: 1.5.0
short_description: Ensure packages exist in a RHEL for Edge rpm-ostree based system
description:
- Compatibility layer for using the "package" module for RHEL for Edge systems utilizing the RHEL System Roles.
author:
- Adam Miller (@maxamillion)
requirements:
- rpm-ostree
options:
name:
description:
- A package name or package specifier with version, like C(name-1.0).
- Comparison operators for package version are valid here C(>), C(<), C(>=), C(<=). Example - C(name>=1.0)
- If a previous version is specified, the task also needs to turn C(allow_downgrade) on.
See the C(allow_downgrade) documentation for caveats with downgrading packages.
- When using state=latest, this can be C('*') which means run C(yum -y update).
- You can also pass a url or a local path to a rpm file (using state=present).
To operate on several packages this can accept a comma separated string of packages or (as of 2.0) a list of packages.
aliases: [ pkg ]
type: list
elements: str
default: []
state:
description:
- Whether to install (C(present) or C(installed), C(latest)), or remove (C(absent) or C(removed)) a package.
- C(present) and C(installed) will simply ensure that a desired package is installed.
- C(latest) will update the specified package if it's not of the latest available version.
- C(absent) and C(removed) will remove the specified package.
- Default is C(None), however in effect the default action is C(present) unless the C(autoremove) option is
enabled for this module, then C(absent) is inferred.
type: str
choices: [ absent, installed, latest, present, removed ]
notes:
- This module does not support installing or removing packages to/from an overlay as this is not supported
by RHEL for Edge, packages needed should be defined in the osbuild Blueprint and provided to Image Builder
at build time. This module exists only for C(package) module compatibility.
'''
EXAMPLES = '''
- name: Ensure htop and ansible are installed on rpm-ostree based RHEL
ansible.posix.rhel_rpm_ostree:
name:
- htop
- ansible
state: present
'''
RETURN = """
msg:
description: status of rpm transaction
returned: always
type: str
sample: "No changes made."
"""
import os
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_text
def locally_installed(module, pkgname):
(rc, out, err) = module.run_command('{0} -q {1}'.format(module.get_bin_path("rpm"), pkgname).split())
return (rc == 0)
def rpm_ostree_transaction(module):
pkgs = []
if module.params['state'] in ['present', 'installed', 'latest']:
for pkg in module.params['name']:
if not locally_installed(module, pkg):
pkgs.append(pkg)
elif module.params['state'] in ['absent', 'removed']:
for pkg in module.params['name']:
if locally_installed(module, pkg):
pkgs.append(pkg)
if not pkgs:
module.exit_json(msg="No changes made.")
else:
if module.params['state'] in ['present', 'installed', 'latest']:
module.fail_json(msg="The following packages are absent in the currently booted rpm-ostree commit: %s" ' '.join(pkgs))
else:
module.fail_json(msg="The following packages are present in the currently booted rpm-ostree commit: %s" ' '.join(pkgs))
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(type='list', elements='str', aliases=['pkg'], default=[]),
state=dict(type='str', default=None, choices=['absent', 'installed', 'latest', 'present', 'removed']),
),
)
# Verify that the platform is an rpm-ostree based system
if not os.path.exists("/run/ostree-booted"):
module.fail_json(msg="Module rpm_ostree is only applicable for rpm-ostree based systems.")
try:
rpm_ostree_transaction(module)
except Exception as e:
module.fail_json(msg=to_text(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@ -1,125 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: Red Hat Inc.
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: rpm_ostree_upgrade
short_description: Manage rpm-ostree upgrade transactions
description:
- Manage an rpm-ostree upgrade transactions.
version_added: 1.5.0
author:
- Adam Miller (@maxamillion)
requirements:
- rpm-ostree
options:
os:
description:
- The OSNAME upon which to operate.
type: str
default: ""
required: false
cache_only:
description:
- Perform the transaction using only pre-cached data, do not download.
type: bool
default: false
required: false
allow_downgrade:
description:
- Allow for the upgrade to be a chronologically older tree.
type: bool
default: false
required: false
peer:
description:
- Force peer-to-peer connection instead of using a system message bus.
type: bool
default: false
required: false
'''
EXAMPLES = '''
- name: Upgrade the rpm-ostree image without options, accept all defaults
ansible.posix.rpm_ostree_upgrade:
- name: Upgrade the rpm-ostree image allowing downgrades
ansible.posix.rpm_ostree_upgrade:
allow_downgrade: true
'''
RETURN = '''
msg:
description: The command standard output
returned: always
type: str
sample: 'No upgrade available.'
'''
import os
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native, to_text
def rpm_ostree_transaction(module):
cmd = []
cmd.append(module.get_bin_path("rpm-ostree"))
cmd.append('upgrade')
if module.params['os']:
cmd += ['--os', module.params['os']]
if module.params['cache_only']:
cmd += ['--cache-only']
if module.params['allow_downgrade']:
cmd += ['--allow-downgrade']
if module.params['peer']:
cmd += ['--peer']
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
rc, out, err = module.run_command(cmd)
if rc != 0:
module.fail_json(rc=rc, msg=err)
else:
if to_text("No upgrade available.") in to_text(out):
module.exit_json(msg=out, changed=False)
else:
module.exit_json(msg=out, changed=True)
def main():
module = AnsibleModule(
argument_spec=dict(
os=dict(type='str', default=''),
cache_only=dict(type='bool', default=False),
allow_downgrade=dict(type='bool', default=False),
peer=dict(type='bool', default=False),
),
)
# Verify that the platform is an rpm-ostree based system
if not os.path.exists("/run/ostree-booted"):
module.fail_json(msg="Module rpm_ostree_upgrade is only applicable for rpm-ostree based systems.")
try:
rpm_ostree_transaction(module)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@ -1,335 +0,0 @@
#!/usr/bin/python
# Copyright: (c) 2012, Stephen Fromm <sfromm@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: seboolean
short_description: Toggles SELinux booleans
description:
- Toggles SELinux booleans.
version_added: "1.0.0"
options:
name:
description:
- Name of the boolean to configure.
required: true
type: str
persistent:
description:
- Set to C(true) if the boolean setting should survive a reboot.
type: bool
default: false
state:
description:
- Desired boolean value
type: bool
required: true
ignore_selinux_state:
description:
- Useful for scenarios (chrooted environment) that you can't get the real SELinux state.
type: bool
default: false
notes:
- Not tested on any Debian based system.
requirements:
- libselinux-python
- libsemanage-python
- python3-libsemanage
author:
- Stephen Fromm (@sfromm)
'''
EXAMPLES = r'''
- name: Set httpd_can_network_connect flag on and keep it persistent across reboots
ansible.posix.seboolean:
name: httpd_can_network_connect
state: true
persistent: true
'''
import os
import traceback
SELINUX_IMP_ERR = None
try:
import selinux
HAVE_SELINUX = True
except ImportError:
SELINUX_IMP_ERR = traceback.format_exc()
HAVE_SELINUX = False
SEMANAGE_IMP_ERR = None
try:
import semanage
HAVE_SEMANAGE = True
except ImportError:
SEMANAGE_IMP_ERR = traceback.format_exc()
HAVE_SEMANAGE = False
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.six import binary_type
from ansible.module_utils._text import to_bytes, to_text
def get_runtime_status(ignore_selinux_state=False):
return True if ignore_selinux_state is True else selinux.is_selinux_enabled()
def has_boolean_value(module, name):
bools = []
try:
rc, bools = selinux.security_get_boolean_names()
except OSError:
module.fail_json(msg="Failed to get list of boolean names")
# work around for selinux who changed its API, see
# https://github.com/ansible/ansible/issues/25651
if len(bools) > 0:
if isinstance(bools[0], binary_type):
name = to_bytes(name)
if name in bools:
return True
else:
return False
def get_boolean_value(module, name):
state = 0
try:
state = selinux.security_get_boolean_active(name)
except OSError:
module.fail_json(msg="Failed to determine current state for boolean %s" % name)
if state == 1:
return True
else:
return False
def semanage_get_handle(module):
handle = semanage.semanage_handle_create()
if not handle:
module.fail_json(msg="Failed to create semanage library handle")
managed = semanage.semanage_is_managed(handle)
if managed <= 0:
semanage.semanage_handle_destroy(handle)
if managed < 0:
module.fail_json(msg="Failed to determine whether policy is manage")
if managed == 0:
if os.getuid() == 0:
module.fail_json(msg="Cannot set persistent booleans without managed policy")
else:
module.fail_json(msg="Cannot set persistent booleans; please try as root")
if semanage.semanage_connect(handle) < 0:
semanage.semanage_handle_destroy(handle)
module.fail_json(msg="Failed to connect to semanage")
return handle
def semanage_begin_transaction(module, handle):
if semanage.semanage_begin_transaction(handle) < 0:
semanage.semanage_handle_destroy(handle)
module.fail_json(msg="Failed to begin semanage transaction")
def semanage_set_boolean_value(module, handle, name, value):
rc, t_b = semanage.semanage_bool_create(handle)
if rc < 0:
semanage.semanage_handle_destroy(handle)
module.fail_json(msg="Failed to create seboolean with semanage")
if semanage.semanage_bool_set_name(handle, t_b, name) < 0:
semanage.semanage_handle_destroy(handle)
module.fail_json(msg="Failed to set seboolean name with semanage")
rc, boolkey = semanage.semanage_bool_key_extract(handle, t_b)
if rc < 0:
semanage.semanage_handle_destroy(handle)
module.fail_json(msg="Failed to extract boolean key with semanage")
rc, exists = semanage.semanage_bool_exists(handle, boolkey)
if rc < 0:
semanage.semanage_handle_destroy(handle)
module.fail_json(msg="Failed to check if boolean is defined")
if not exists:
semanage.semanage_handle_destroy(handle)
module.fail_json(msg="SELinux boolean %s is not defined in persistent policy" % name)
rc, sebool = semanage.semanage_bool_query(handle, boolkey)
if rc < 0:
semanage.semanage_handle_destroy(handle)
module.fail_json(msg="Failed to query boolean in persistent policy")
semanage.semanage_bool_set_value(sebool, value)
if semanage.semanage_bool_modify_local(handle, boolkey, sebool) < 0:
semanage.semanage_handle_destroy(handle)
module.fail_json(msg="Failed to modify boolean key with semanage")
if semanage.semanage_bool_set_active(handle, boolkey, sebool) < 0:
semanage.semanage_handle_destroy(handle)
module.fail_json(msg="Failed to set boolean key active with semanage")
semanage.semanage_bool_key_free(boolkey)
semanage.semanage_bool_free(t_b)
semanage.semanage_bool_free(sebool)
def semanage_get_boolean_value(module, handle, name):
rc, t_b = semanage.semanage_bool_create(handle)
if rc < 0:
semanage.semanage_handle_destroy(handle)
module.fail_json(msg="Failed to create seboolean with semanage")
if semanage.semanage_bool_set_name(handle, t_b, name) < 0:
semanage.semanage_handle_destroy(handle)
module.fail_json(msg="Failed to set seboolean name with semanage")
rc, boolkey = semanage.semanage_bool_key_extract(handle, t_b)
if rc < 0:
semanage.semanage_handle_destroy(handle)
module.fail_json(msg="Failed to extract boolean key with semanage")
rc, exists = semanage.semanage_bool_exists(handle, boolkey)
if rc < 0:
semanage.semanage_handle_destroy(handle)
module.fail_json(msg="Failed to check if boolean is defined")
if not exists:
semanage.semanage_handle_destroy(handle)
module.fail_json(msg="SELinux boolean %s is not defined in persistent policy" % name)
rc, sebool = semanage.semanage_bool_query(handle, boolkey)
if rc < 0:
semanage.semanage_handle_destroy(handle)
module.fail_json(msg="Failed to query boolean in persistent policy")
value = semanage.semanage_bool_get_value(sebool)
semanage.semanage_bool_key_free(boolkey)
semanage.semanage_bool_free(t_b)
semanage.semanage_bool_free(sebool)
return value
def semanage_commit(module, handle, load=0):
semanage.semanage_set_reload(handle, load)
if semanage.semanage_commit(handle) < 0:
semanage.semanage_handle_destroy(handle)
module.fail_json(msg="Failed to commit changes to semanage")
def semanage_destroy_handle(module, handle):
rc = semanage.semanage_disconnect(handle)
semanage.semanage_handle_destroy(handle)
if rc < 0:
module.fail_json(msg="Failed to disconnect from semanage")
# The following method implements what setsebool.c does to change
# a boolean and make it persist after reboot..
def semanage_boolean_value(module, name, state):
value = 0
changed = False
if state:
value = 1
try:
handle = semanage_get_handle(module)
semanage_begin_transaction(module, handle)
cur_value = semanage_get_boolean_value(module, handle, name)
if cur_value != value:
changed = True
if not module.check_mode:
semanage_set_boolean_value(module, handle, name, value)
semanage_commit(module, handle)
semanage_destroy_handle(module, handle)
except Exception as e:
module.fail_json(msg=u"Failed to manage policy for boolean %s: %s" % (name, to_text(e)))
return changed
def set_boolean_value(module, name, state):
rc = 0
value = 0
if state:
value = 1
try:
rc = selinux.security_set_boolean(name, value)
except OSError:
module.fail_json(msg="Failed to set boolean %s to %s" % (name, value))
if rc == 0:
return True
else:
return False
def main():
module = AnsibleModule(
argument_spec=dict(
ignore_selinux_state=dict(type='bool', default=False),
name=dict(type='str', required=True),
persistent=dict(type='bool', default=False),
state=dict(type='bool', required=True),
),
supports_check_mode=True,
)
if not HAVE_SELINUX:
module.fail_json(msg=missing_required_lib('libselinux-python'), exception=SELINUX_IMP_ERR)
if not HAVE_SEMANAGE:
module.fail_json(msg=missing_required_lib('libsemanage-python or python3-libsemanage'), exception=SEMANAGE_IMP_ERR)
ignore_selinux_state = module.params['ignore_selinux_state']
if not get_runtime_status(ignore_selinux_state):
module.fail_json(msg="SELinux is disabled on this host.")
name = module.params['name']
persistent = module.params['persistent']
state = module.params['state']
result = dict(
name=name,
persistent=persistent,
state=state
)
changed = False
if hasattr(selinux, 'selinux_boolean_sub'):
# selinux_boolean_sub allows sites to rename a boolean and alias the old name
# Feature only available in selinux library since 2012.
name = selinux.selinux_boolean_sub(name)
if not has_boolean_value(module, name):
module.fail_json(msg="SELinux boolean %s does not exist." % name)
if persistent:
changed = semanage_boolean_value(module, name, state)
else:
cur_value = get_boolean_value(module, name)
if cur_value != state:
changed = True
if not module.check_mode:
changed = set_boolean_value(module, name, state)
if not changed:
module.fail_json(msg="Failed to set boolean %s to %s" % (name, state))
try:
selinux.security_commit_booleans()
except Exception:
module.fail_json(msg="Failed to commit pending boolean %s value" % name)
result['changed'] = changed
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@ -1,347 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2012, Derek Carter<goozbach@friocorte.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: selinux
short_description: Change policy and state of SELinux
description:
- Configures the SELinux mode and policy.
- A reboot may be required after usage.
- Ansible will not issue this reboot but will let you know when it is required.
version_added: "1.0.0"
options:
policy:
description:
- The name of the SELinux policy to use (e.g. C(targeted)) will be required if I(state) is not C(disabled).
type: str
state:
description:
- The SELinux mode.
required: true
choices: [ disabled, enforcing, permissive ]
type: str
update_kernel_param:
description:
- If set to I(true), will update also the kernel boot parameters when disabling/enabling SELinux.
- The C(grubby) tool must be present on the target system for this to work.
default: false
type: bool
version_added: '1.4.0'
configfile:
description:
- The path to the SELinux configuration file, if non-standard.
default: /etc/selinux/config
aliases: [ conf, file ]
type: str
requirements: [ libselinux-python ]
author:
- Derek Carter (@goozbach) <goozbach@friocorte.com>
'''
EXAMPLES = r'''
- name: Enable SELinux
ansible.posix.selinux:
policy: targeted
state: enforcing
- name: Put SELinux in permissive mode, logging actions that would be blocked.
ansible.posix.selinux:
policy: targeted
state: permissive
- name: Disable SELinux
ansible.posix.selinux:
state: disabled
'''
RETURN = r'''
msg:
description: Messages that describe changes that were made.
returned: always
type: str
sample: Config SELinux state changed from 'disabled' to 'permissive'
configfile:
description: Path to SELinux configuration file.
returned: always
type: str
sample: /etc/selinux/config
policy:
description: Name of the SELinux policy.
returned: always
type: str
sample: targeted
state:
description: SELinux mode.
returned: always
type: str
sample: enforcing
reboot_required:
description: Whether or not an reboot is required for the changes to take effect.
returned: always
type: bool
sample: true
'''
import os
import re
import tempfile
import traceback
SELINUX_IMP_ERR = None
try:
import selinux
HAS_SELINUX = True
except ImportError:
SELINUX_IMP_ERR = traceback.format_exc()
HAS_SELINUX = False
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.process import get_bin_path
from ansible.module_utils.facts.utils import get_file_lines
# getter subroutines
def get_config_state(configfile):
lines = get_file_lines(configfile, strip=False)
for line in lines:
stateline = re.match(r'^SELINUX=.*$', line)
if stateline:
return line.split('=')[1].strip()
def get_config_policy(configfile):
lines = get_file_lines(configfile, strip=False)
for line in lines:
stateline = re.match(r'^SELINUXTYPE=.*$', line)
if stateline:
return line.split('=')[1].strip()
def get_kernel_enabled(module, grubby_bin):
if grubby_bin is None:
module.fail_json(msg="'grubby' command not found on host",
details="In order to update the kernel command line"
"enabled/disabled setting, the grubby package"
"needs to be present on the system.")
rc, stdout, stderr = module.run_command([grubby_bin, '--info=ALL'])
if rc != 0:
module.fail_json(msg="unable to run grubby")
all_enabled = True
all_disabled = True
for line in stdout.split('\n'):
match = re.match('^args="(.*)"$', line)
if match is None:
continue
args = match.group(1).split(' ')
if 'selinux=0' in args:
all_enabled = False
else:
all_disabled = False
if all_disabled == all_enabled:
# inconsistent config - return None to force update
return None
return all_enabled
# setter subroutines
def set_config_state(module, state, configfile):
# SELINUX=permissive
# edit config file with state value
stateline = 'SELINUX=%s' % state
lines = get_file_lines(configfile, strip=False)
tmpfd, tmpfile = tempfile.mkstemp()
with open(tmpfile, "w") as write_file:
line_found = False
for line in lines:
if re.match(r'^SELINUX=.*$', line):
line_found = True
write_file.write(re.sub(r'^SELINUX=.*', stateline, line) + '\n')
if not line_found:
write_file.write('SELINUX=%s\n' % state)
module.atomic_move(tmpfile, configfile)
def set_state(module, state):
if state == 'enforcing':
selinux.security_setenforce(1)
elif state == 'permissive':
selinux.security_setenforce(0)
elif state == 'disabled':
pass
else:
msg = 'trying to set invalid runtime state %s' % state
module.fail_json(msg=msg)
def set_kernel_enabled(module, grubby_bin, value):
rc, stdout, stderr = module.run_command([grubby_bin, '--update-kernel=ALL',
'--remove-args' if value else '--args',
'selinux=0'])
if rc != 0:
if value:
module.fail_json(msg='unable to remove selinux=0 from kernel config')
else:
module.fail_json(msg='unable to add selinux=0 to kernel config')
def set_config_policy(module, policy, configfile):
if not os.path.exists('/etc/selinux/%s/policy' % policy):
module.fail_json(msg='Policy %s does not exist in /etc/selinux/' % policy)
# edit config file with state value
# SELINUXTYPE=targeted
policyline = 'SELINUXTYPE=%s' % policy
lines = get_file_lines(configfile, strip=False)
tmpfd, tmpfile = tempfile.mkstemp()
with open(tmpfile, "w") as write_file:
line_found = False
for line in lines:
if re.match(r'^SELINUXTYPE=.*$', line):
line_found = True
write_file.write(re.sub(r'^SELINUXTYPE=.*', policyline, line) + '\n')
if not line_found:
write_file.write('SELINUXTYPE=%s\n' % policy)
module.atomic_move(tmpfile, configfile)
def main():
module = AnsibleModule(
argument_spec=dict(
policy=dict(type='str'),
state=dict(type='str', required=True, choices=['enforcing', 'permissive', 'disabled']),
configfile=dict(type='str', default='/etc/selinux/config', aliases=['conf', 'file']),
update_kernel_param=dict(type='bool', default=False),
),
supports_check_mode=True,
)
if not HAS_SELINUX:
module.fail_json(msg=missing_required_lib('libselinux-python'), exception=SELINUX_IMP_ERR)
# global vars
changed = False
msgs = []
configfile = module.params['configfile']
policy = module.params['policy']
state = module.params['state']
update_kernel_param = module.params['update_kernel_param']
runtime_enabled = selinux.is_selinux_enabled()
runtime_policy = selinux.selinux_getpolicytype()[1]
runtime_state = 'disabled'
kernel_enabled = None
reboot_required = False
if runtime_enabled:
# enabled means 'enforcing' or 'permissive'
if selinux.security_getenforce():
runtime_state = 'enforcing'
else:
runtime_state = 'permissive'
if not os.path.isfile(configfile):
module.fail_json(msg="Unable to find file {0}".format(configfile),
details="Please install SELinux-policy package, "
"if this package is not installed previously.")
config_policy = get_config_policy(configfile)
config_state = get_config_state(configfile)
if update_kernel_param:
try:
grubby_bin = get_bin_path('grubby')
except ValueError:
grubby_bin = None
kernel_enabled = get_kernel_enabled(module, grubby_bin)
# check to see if policy is set if state is not 'disabled'
if state != 'disabled':
if not policy:
module.fail_json(msg="Policy is required if state is not 'disabled'")
else:
if not policy:
policy = config_policy
# check changed values and run changes
if policy != runtime_policy:
if module.check_mode:
module.exit_json(changed=True)
# cannot change runtime policy
msgs.append("Running SELinux policy changed from '%s' to '%s'" % (runtime_policy, policy))
changed = True
if policy != config_policy:
if module.check_mode:
module.exit_json(changed=True)
set_config_policy(module, policy, configfile)
msgs.append("SELinux policy configuration in '%s' changed from '%s' to '%s'" % (configfile, config_policy, policy))
changed = True
if state != runtime_state:
if runtime_enabled:
if state == 'disabled':
if runtime_state != 'permissive':
# Temporarily set state to permissive
if not module.check_mode:
set_state(module, 'permissive')
module.warn("SELinux state temporarily changed from '%s' to 'permissive'. State change will take effect next reboot." % (runtime_state))
changed = True
else:
module.warn('SELinux state change will take effect next reboot')
reboot_required = True
else:
if not module.check_mode:
set_state(module, state)
msgs.append("SELinux state changed from '%s' to '%s'" % (runtime_state, state))
# Only report changes if the file is changed.
# This prevents the task from reporting changes every time the task is run.
changed = True
else:
module.warn("Reboot is required to set SELinux state to '%s'" % state)
reboot_required = True
if state != config_state:
if not module.check_mode:
set_config_state(module, state, configfile)
msgs.append("Config SELinux state changed from '%s' to '%s'" % (config_state, state))
changed = True
requested_kernel_enabled = state in ('enforcing', 'permissive')
# Update kernel enabled/disabled config only when setting is consistent
# across all kernels AND the requested state differs from the current state
if update_kernel_param and kernel_enabled != requested_kernel_enabled:
if not module.check_mode:
set_kernel_enabled(module, grubby_bin, requested_kernel_enabled)
if requested_kernel_enabled:
states = ('disabled', 'enabled')
else:
states = ('enabled', 'disabled')
if kernel_enabled is None:
states = ('<inconsistent>', states[1])
msgs.append("Kernel SELinux state changed from '%s' to '%s'" % states)
changed = True
module.exit_json(changed=changed, msg=', '.join(msgs), configfile=configfile, policy=policy, state=state, reboot_required=reboot_required)
if __name__ == '__main__':
main()

View File

@ -1,635 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2012-2013, Timothy Appnel <tim@appnel.com>
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: synchronize
short_description: A wrapper around rsync to make common tasks in your playbooks quick and easy
description:
- C(synchronize) is a wrapper around rsync to make common tasks in your playbooks quick and easy.
- It is run and originates on the local host where Ansible is being run.
- Of course, you could just use the C(command) action to call rsync yourself, but you also have to add a fair number of
boilerplate options and host facts.
- This module is not intended to provide access to the full power of rsync, but does make the most common
invocations easier to implement. You `still` may need to call rsync directly via C(command) or C(shell) depending on your use case.
version_added: "1.0.0"
options:
src:
description:
- Path on the source host that will be synchronized to the destination.
- The path can be absolute or relative.
type: str
required: true
dest:
description:
- Path on the destination host that will be synchronized from the source.
- The path can be absolute or relative.
type: str
required: true
dest_port:
description:
- Port number for ssh on the destination host.
- Prior to Ansible 2.0, the ansible_ssh_port inventory var took precedence over this value.
- This parameter defaults to the value of C(ansible_port), the C(remote_port) config setting
or the value from ssh client configuration if none of the former have been set.
type: int
mode:
description:
- Specify the direction of the synchronization.
- In push mode the localhost or delegate is the source.
- In pull mode the remote host in context is the source.
type: str
choices: [ pull, push ]
default: push
archive:
description:
- Mirrors the rsync archive flag, enables recursive, links, perms, times, owner, group flags and -D.
type: bool
default: true
checksum:
description:
- Skip based on checksum, rather than mod-time & size; Note that that "archive" option is still enabled by default - the "checksum" option will
not disable it.
type: bool
default: false
compress:
description:
- Compress file data during the transfer.
- In most cases, leave this enabled unless it causes problems.
type: bool
default: true
existing_only:
description:
- Skip creating new files on receiver.
type: bool
default: false
delete:
description:
- Delete files in I(dest) that do not exist (after transfer, not before) in the I(src) path.
- This option requires I(recursive=true).
- This option ignores excluded files and behaves like the rsync opt C(--delete-after).
type: bool
default: false
dirs:
description:
- Transfer directories without recursing.
type: bool
default: false
recursive:
description:
- Recurse into directories.
- This parameter defaults to the value of the archive option.
type: bool
links:
description:
- Copy symlinks as symlinks.
- This parameter defaults to the value of the archive option.
type: bool
copy_links:
description:
- Copy symlinks as the item that they point to (the referent) is copied, rather than the symlink.
type: bool
default: false
perms:
description:
- Preserve permissions.
- This parameter defaults to the value of the archive option.
type: bool
times:
description:
- Preserve modification times.
- This parameter defaults to the value of the archive option.
type: bool
owner:
description:
- Preserve owner (super user only).
- This parameter defaults to the value of the archive option.
type: bool
group:
description:
- Preserve group.
- This parameter defaults to the value of the archive option.
type: bool
rsync_path:
description:
- Specify the rsync command to run on the remote host. See C(--rsync-path) on the rsync man page.
- To specify the rsync command to run on the local host, you need to set this your task var C(ansible_rsync_path).
type: str
rsync_timeout:
description:
- Specify a C(--timeout) for the rsync command in seconds.
type: int
default: 0
set_remote_user:
description:
- Put user@ for the remote paths.
- If you have a custom ssh config to define the remote user for a host
that does not match the inventory user, you should set this parameter to C(false).
type: bool
default: true
use_ssh_args:
description:
- In Ansible 2.10 and lower, it uses the ssh_args specified in C(ansible.cfg).
- In Ansible 2.11 and onwards, when set to C(true), it uses all SSH connection configurations like
C(ansible_ssh_args), C(ansible_ssh_common_args), and C(ansible_ssh_extra_args).
type: bool
default: false
ssh_connection_multiplexing:
description:
- SSH connection multiplexing for rsync is disabled by default to prevent misconfigured ControlSockets from resulting in failed SSH connections.
This is accomplished by setting the SSH C(ControlSocket) to C(none).
- Set this option to C(true) to allow multiplexing and reduce SSH connection overhead.
- Note that simply setting this option to C(true) is not enough;
You must also configure SSH connection multiplexing in your SSH client config by setting values for
C(ControlMaster), C(ControlPersist) and C(ControlPath).
type: bool
default: false
rsync_opts:
description:
- Specify additional rsync options by passing in an array.
- Note that an empty string in C(rsync_opts) will end up transfer the current working directory.
type: list
default:
elements: str
partial:
description:
- Tells rsync to keep the partial file which should make a subsequent transfer of the rest of the file much faster.
type: bool
default: false
verify_host:
description:
- Verify destination host key.
type: bool
default: false
private_key:
description:
- Specify the private key to use for SSH-based rsync connections (e.g. C(~/.ssh/id_rsa)).
type: path
link_dest:
description:
- Add a destination to hard link against during the rsync.
type: list
default:
elements: str
delay_updates:
description:
- This option puts the temporary file from each updated file into a holding directory until the end of the transfer,
at which time all the files are renamed into place in rapid succession.
type: bool
default: true
version_added: '1.3.0'
notes:
- rsync must be installed on both the local and remote host.
- For the C(synchronize) module, the "local host" is the host `the synchronize task originates on`, and the "destination host" is the host
`synchronize is connecting to`.
- The "local host" can be changed to a different host by using `delegate_to`. This enables copying between two remote hosts or entirely on one
remote machine.
- >
The user and permissions for the synchronize `src` are those of the user running the Ansible task on the local host (or the remote_user for a
delegate_to host when delegate_to is used).
- The user and permissions for the synchronize `dest` are those of the `remote_user` on the destination host or the `become_user` if `become=yes` is active.
- In Ansible 2.0 a bug in the synchronize module made become occur on the "local host". This was fixed in Ansible 2.0.1.
- Currently, synchronize is limited to elevating permissions via passwordless sudo. This is because rsync itself is connecting to the remote machine
and rsync doesn't give us a way to pass sudo credentials in.
- Currently there are only a few connection types which support synchronize (ssh, paramiko, local, and docker) because a sync strategy has been
determined for those connection types. Note that the connection for these must not need a password as rsync itself is making the connection and
rsync does not provide us a way to pass a password to the connection.
- Expect that dest=~/x will be ~<remote_user>/x even if using sudo.
- Inspect the verbose output to validate the destination user/host/path are what was expected.
- To exclude files and directories from being synchronized, you may add C(.rsync-filter) files to the source directory.
- rsync daemon must be up and running with correct permission when using rsync protocol in source or destination path.
- The C(synchronize) module enables `--delay-updates` by default to avoid leaving a destination in a broken in-between state if the underlying rsync process
encounters an error. Those synchronizing large numbers of files that are willing to trade safety for performance should disable this option.
- link_destination is subject to the same limitations as the underlying rsync daemon. Hard links are only preserved if the relative subtrees
of the source and destination are the same. Attempts to hardlink into a directory that is a subdirectory of the source will be prevented.
seealso:
- module: ansible.builtin.copy
- module: community.windows.win_robocopy
author:
- Timothy Appnel (@tima)
'''
EXAMPLES = r'''
- name: Synchronization of src on the control machine to dest on the remote hosts
ansible.posix.synchronize:
src: some/relative/path
dest: /some/absolute/path
- name: Synchronization using rsync protocol (push)
ansible.posix.synchronize:
src: some/relative/path/
dest: rsync://somehost.com/path/
- name: Synchronization using rsync protocol (pull)
ansible.posix.synchronize:
mode: pull
src: rsync://somehost.com/path/
dest: /some/absolute/path/
- name: Synchronization using rsync protocol on delegate host (push)
ansible.posix.synchronize:
src: /some/absolute/path/
dest: rsync://somehost.com/path/
delegate_to: delegate.host
- name: Synchronization using rsync protocol on delegate host (pull)
ansible.posix.synchronize:
mode: pull
src: rsync://somehost.com/path/
dest: /some/absolute/path/
delegate_to: delegate.host
- name: Synchronization without any --archive options enabled
ansible.posix.synchronize:
src: some/relative/path
dest: /some/absolute/path
archive: false
- name: Synchronization with --archive options enabled except for --recursive
ansible.posix.synchronize:
src: some/relative/path
dest: /some/absolute/path
recursive: false
- name: Synchronization with --archive options enabled except for --times, with --checksum option enabled
ansible.posix.synchronize:
src: some/relative/path
dest: /some/absolute/path
checksum: true
times: false
- name: Synchronization without --archive options enabled except use --links
ansible.posix.synchronize:
src: some/relative/path
dest: /some/absolute/path
archive: false
links: true
- name: Synchronization of two paths both on the control machine
ansible.posix.synchronize:
src: some/relative/path
dest: /some/absolute/path
delegate_to: localhost
- name: Synchronization of src on the inventory host to the dest on the localhost in pull mode
ansible.posix.synchronize:
mode: pull
src: some/relative/path
dest: /some/absolute/path
- name: Synchronization of src on delegate host to dest on the current inventory host.
ansible.posix.synchronize:
src: /first/absolute/path
dest: /second/absolute/path
delegate_to: delegate.host
- name: Synchronize two directories on one remote host.
ansible.posix.synchronize:
src: /first/absolute/path
dest: /second/absolute/path
delegate_to: "{{ inventory_hostname }}"
- name: Synchronize and delete files in dest on the remote host that are not found in src of localhost.
ansible.posix.synchronize:
src: some/relative/path
dest: /some/absolute/path
delete: true
recursive: true
# This specific command is granted su privileges on the destination
- name: Synchronize using an alternate rsync command
ansible.posix.synchronize:
src: some/relative/path
dest: /some/absolute/path
rsync_path: su -c rsync
# Example .rsync-filter file in the source directory
# - var # exclude any path whose last part is 'var'
# - /var # exclude any path starting with 'var' starting at the source directory
# + /var/conf # include /var/conf even though it was previously excluded
- name: Synchronize passing in extra rsync options
ansible.posix.synchronize:
src: /tmp/helloworld
dest: /var/www/helloworld
rsync_opts:
- "--no-motd"
- "--exclude=.git"
# Hardlink files if they didn't change
- name: Use hardlinks when synchronizing filesystems
ansible.posix.synchronize:
src: /tmp/path_a/foo.txt
dest: /tmp/path_b/foo.txt
link_dest: /tmp/path_a/
# Specify the rsync binary to use on remote host and on local host
- hosts: groupofhosts
vars:
ansible_rsync_path: /usr/gnu/bin/rsync
tasks:
- name: copy /tmp/localpath/ to remote location /tmp/remotepath
ansible.posix.synchronize:
src: /tmp/localpath/
dest: /tmp/remotepath
rsync_path: /usr/gnu/bin/rsync
'''
import os
import errno
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_bytes, to_native
from ansible.module_utils.six.moves import shlex_quote
client_addr = None
def substitute_controller(path):
global client_addr
if not client_addr:
ssh_env_string = os.environ.get('SSH_CLIENT', None)
try:
client_addr, _ = ssh_env_string.split(None, 1)
except AttributeError:
ssh_env_string = os.environ.get('SSH_CONNECTION', None)
try:
client_addr, _ = ssh_env_string.split(None, 1)
except AttributeError:
pass
if not client_addr:
raise ValueError
if path.startswith('localhost:'):
path = path.replace('localhost', client_addr, 1)
return path
def is_rsh_needed(source, dest):
if source.startswith('rsync://') or dest.startswith('rsync://'):
return False
if ':' in source or ':' in dest:
return True
return False
def main():
module = AnsibleModule(
argument_spec=dict(
src=dict(type='str', required=True),
dest=dict(type='str', required=True),
dest_port=dict(type='int'),
delete=dict(type='bool', default=False),
private_key=dict(type='path'),
rsync_path=dict(type='str'),
_local_rsync_path=dict(type='path', default='rsync'),
_local_rsync_password=dict(type='str', no_log=True),
_substitute_controller=dict(type='bool', default=False),
archive=dict(type='bool', default=True),
checksum=dict(type='bool', default=False),
compress=dict(type='bool', default=True),
existing_only=dict(type='bool', default=False),
dirs=dict(type='bool', default=False),
recursive=dict(type='bool'),
links=dict(type='bool'),
copy_links=dict(type='bool', default=False),
perms=dict(type='bool'),
times=dict(type='bool'),
owner=dict(type='bool'),
group=dict(type='bool'),
set_remote_user=dict(type='bool', default=True),
rsync_timeout=dict(type='int', default=0),
rsync_opts=dict(type='list', default=[], elements='str'),
ssh_args=dict(type='str'),
ssh_connection_multiplexing=dict(type='bool', default=False),
partial=dict(type='bool', default=False),
verify_host=dict(type='bool', default=False),
delay_updates=dict(type='bool', default=True),
mode=dict(type='str', default='push', choices=['pull', 'push']),
link_dest=dict(type='list', elements='str'),
),
supports_check_mode=True,
)
if module.params['_substitute_controller']:
try:
source = substitute_controller(module.params['src'])
dest = substitute_controller(module.params['dest'])
except ValueError:
module.fail_json(msg='Could not determine controller hostname for rsync to send to')
else:
source = module.params['src']
dest = module.params['dest']
dest_port = module.params['dest_port']
delete = module.params['delete']
private_key = module.params['private_key']
rsync_path = module.params['rsync_path']
rsync = module.params.get('_local_rsync_path', 'rsync')
rsync_password = module.params.get('_local_rsync_password')
rsync_timeout = module.params.get('rsync_timeout', 'rsync_timeout')
archive = module.params['archive']
checksum = module.params['checksum']
compress = module.params['compress']
existing_only = module.params['existing_only']
dirs = module.params['dirs']
partial = module.params['partial']
# the default of these params depends on the value of archive
recursive = module.params['recursive']
links = module.params['links']
copy_links = module.params['copy_links']
perms = module.params['perms']
times = module.params['times']
owner = module.params['owner']
group = module.params['group']
rsync_opts = module.params['rsync_opts']
ssh_args = module.params['ssh_args']
ssh_connection_multiplexing = module.params['ssh_connection_multiplexing']
verify_host = module.params['verify_host']
link_dest = module.params['link_dest']
delay_updates = module.params['delay_updates']
if '/' not in rsync:
rsync = module.get_bin_path(rsync, required=True)
cmd = [rsync]
_sshpass_pipe = None
if rsync_password:
try:
module.run_command(["sshpass"])
except OSError:
module.fail_json(
msg="to use rsync connection with passwords, you must install the sshpass program"
)
_sshpass_pipe = os.pipe()
cmd = ['sshpass', '-d' + to_native(_sshpass_pipe[0], errors='surrogate_or_strict')] + cmd
if delay_updates:
cmd.append('--delay-updates')
cmd.append('-F')
if compress:
cmd.append('--compress')
if rsync_timeout:
cmd.append('--timeout=%s' % rsync_timeout)
if module.check_mode:
cmd.append('--dry-run')
if delete:
cmd.append('--delete-after')
if existing_only:
cmd.append('--existing')
if checksum:
cmd.append('--checksum')
if copy_links:
cmd.append('--copy-links')
if archive:
cmd.append('--archive')
if recursive is False:
cmd.append('--no-recursive')
if links is False:
cmd.append('--no-links')
if perms is False:
cmd.append('--no-perms')
if times is False:
cmd.append('--no-times')
if owner is False:
cmd.append('--no-owner')
if group is False:
cmd.append('--no-group')
else:
if recursive is True:
cmd.append('--recursive')
if links is True:
cmd.append('--links')
if perms is True:
cmd.append('--perms')
if times is True:
cmd.append('--times')
if owner is True:
cmd.append('--owner')
if group is True:
cmd.append('--group')
if dirs:
cmd.append('--dirs')
if source.startswith('rsync://') and dest.startswith('rsync://'):
module.fail_json(msg='either src or dest must be a localhost', rc=1)
if is_rsh_needed(source, dest):
# https://github.com/ansible/ansible/issues/15907
has_rsh = False
for rsync_opt in rsync_opts:
if '--rsh' in rsync_opt:
has_rsh = True
break
# if the user has not supplied an --rsh option go ahead and add ours
if not has_rsh:
ssh_cmd = [module.get_bin_path('ssh', required=True)]
if not ssh_connection_multiplexing:
ssh_cmd.extend(['-S', 'none'])
if private_key is not None:
ssh_cmd.extend(['-i', private_key])
# If the user specified a port value
# Note: The action plugin takes care of setting this to a port from
# inventory if the user didn't specify an explicit dest_port
if dest_port is not None:
ssh_cmd.extend(['-o', 'Port=%s' % dest_port])
if not verify_host:
ssh_cmd.extend(['-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null'])
ssh_cmd_str = ' '.join(shlex_quote(arg) for arg in ssh_cmd)
if ssh_args:
ssh_cmd_str += ' %s' % ssh_args
cmd.append('--rsh=%s' % shlex_quote(ssh_cmd_str))
if rsync_path:
cmd.append('--rsync-path=%s' % shlex_quote(rsync_path))
if rsync_opts:
if '' in rsync_opts:
module.warn('The empty string is present in rsync_opts which will cause rsync to'
' transfer the current working directory. If this is intended, use "."'
' instead to get rid of this warning. If this is unintended, check for'
' problems in your playbook leading to empty string in rsync_opts.')
cmd.extend(rsync_opts)
if partial:
cmd.append('--partial')
if link_dest:
cmd.append('-H')
# verbose required because rsync does not believe that adding a
# hardlink is actually a change
cmd.append('-vv')
for x in link_dest:
link_path = os.path.abspath(os.path.expanduser(x))
destination_path = os.path.abspath(os.path.dirname(dest))
if destination_path.find(link_path) == 0:
module.fail_json(msg='Hardlinking into a subdirectory of the source would cause recursion. %s and %s' % (destination_path, dest))
cmd.append('--link-dest=%s' % link_path)
changed_marker = '<<CHANGED>>'
cmd.append('--out-format=%s' % shlex_quote(changed_marker + '%i %n%L'))
# expand the paths
if '@' not in source:
source = os.path.expanduser(source)
if '@' not in dest:
dest = os.path.expanduser(dest)
cmd.append(shlex_quote(source))
cmd.append(shlex_quote(dest))
cmdstr = ' '.join(cmd)
# If we are using password authentication, write the password into the pipe
if rsync_password:
def _write_password_to_pipe(proc):
os.close(_sshpass_pipe[0])
try:
os.write(_sshpass_pipe[1], to_bytes(rsync_password) + b'\n')
except OSError as exc:
# Ignore broken pipe errors if the sshpass process has exited.
if exc.errno != errno.EPIPE or proc.poll() is None:
raise
(rc, out, err) = module.run_command(
cmdstr, pass_fds=_sshpass_pipe,
before_communicate_callback=_write_password_to_pipe)
else:
(rc, out, err) = module.run_command(cmdstr)
if rc:
return module.fail_json(msg=err, rc=rc, cmd=cmdstr)
if link_dest:
# a leading period indicates no change
changed = (changed_marker + '.') not in out
else:
changed = changed_marker in out
out_clean = out.replace(changed_marker, '')
out_lines = out_clean.split('\n')
while '' in out_lines:
out_lines.remove('')
if module._diff:
diff = {'prepared': out_clean}
return module.exit_json(changed=changed, msg=out_clean,
rc=rc, cmd=cmdstr, stdout_lines=out_lines,
diff=diff)
return module.exit_json(changed=changed, msg=out_clean,
rc=rc, cmd=cmdstr, stdout_lines=out_lines)
if __name__ == '__main__':
main()

View File

@ -1,420 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2012, David "DaviXX" CHANIAL <david.chanial@gmail.com>
# (c) 2014, James Tanner <tanner.jc@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: sysctl
short_description: Manage entries in sysctl.conf.
description:
- This module manipulates sysctl entries and optionally performs a C(/sbin/sysctl -p) after changing them.
version_added: "1.0.0"
options:
name:
description:
- The dot-separated path (also known as I(key)) specifying the sysctl variable.
required: true
aliases: [ 'key' ]
type: str
value:
description:
- Desired value of the sysctl key.
aliases: [ 'val' ]
type: str
state:
description:
- Whether the entry should be present or absent in the sysctl file.
choices: [ "present", "absent" ]
default: present
type: str
ignoreerrors:
description:
- Use this option to ignore errors about unknown keys.
type: bool
default: false
reload:
description:
- If C(true), performs a I(/sbin/sysctl -p) if the C(sysctl_file) is
updated. If C(false), does not reload I(sysctl) even if the
C(sysctl_file) is updated.
type: bool
default: true
sysctl_file:
description:
- Specifies the absolute path to C(sysctl.conf), if not C(/etc/sysctl.conf).
default: /etc/sysctl.conf
type: path
sysctl_set:
description:
- Verify token value with the sysctl command and set with -w if necessary.
type: bool
default: false
author:
- David CHANIAL (@davixx)
'''
EXAMPLES = r'''
# Set vm.swappiness to 5 in /etc/sysctl.conf
- ansible.posix.sysctl:
name: vm.swappiness
value: '5'
state: present
# Remove kernel.panic entry from /etc/sysctl.conf
- ansible.posix.sysctl:
name: kernel.panic
state: absent
sysctl_file: /etc/sysctl.conf
# Set kernel.panic to 3 in /tmp/test_sysctl.conf
- ansible.posix.sysctl:
name: kernel.panic
value: '3'
sysctl_file: /tmp/test_sysctl.conf
reload: false
# Set ip forwarding on in /proc and verify token value with the sysctl command
- ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: '1'
sysctl_set: true
# Set ip forwarding on in /proc and in the sysctl file and reload if necessary
- ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: '1'
sysctl_set: true
state: present
reload: true
'''
# ==============================================================
import os
import platform
import re
import tempfile
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import string_types
from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE
from ansible.module_utils._text import to_native
class SysctlModule(object):
# We have to use LANG=C because we are capturing STDERR of sysctl to detect
# success or failure.
LANG_ENV = {'LANG': 'C', 'LC_ALL': 'C', 'LC_MESSAGES': 'C'}
def __init__(self, module):
self.module = module
self.args = self.module.params
self.sysctl_cmd = self.module.get_bin_path('sysctl', required=True)
self.sysctl_file = self.args['sysctl_file']
self.proc_value = None # current token value in proc fs
self.file_value = None # current token value in file
self.file_lines = [] # all lines in the file
self.file_values = {} # dict of token values
self.changed = False # will change occur
self.set_proc = False # does sysctl need to set value
self.write_file = False # does the sysctl file need to be reloaded
self.process()
# ==============================================================
# LOGIC
# ==============================================================
def process(self):
self.platform = platform.system().lower()
# Whitespace is bad
self.args['name'] = self.args['name'].strip()
self.args['value'] = self._parse_value(self.args['value'])
thisname = self.args['name']
# get the current proc fs value
self.proc_value = self.get_token_curr_value(thisname)
# get the current sysctl file value
self.read_sysctl_file()
if thisname not in self.file_values:
self.file_values[thisname] = None
# update file contents with desired token/value
self.fix_lines()
# what do we need to do now?
if self.file_values[thisname] is None and self.args['state'] == "present":
self.changed = True
self.write_file = True
elif self.file_values[thisname] is None and self.args['state'] == "absent":
self.changed = False
elif self.file_values[thisname] and self.args['state'] == "absent":
self.changed = True
self.write_file = True
elif self.file_values[thisname] != self.args['value']:
self.changed = True
self.write_file = True
# with reload=yes we should check if the current system values are
# correct, so that we know if we should reload
elif self.args['reload']:
if self.proc_value is None:
self.changed = True
elif not self._values_is_equal(self.proc_value, self.args['value']):
self.changed = True
# use the sysctl command or not?
if self.args['sysctl_set'] and self.args['state'] == "present":
if self.proc_value is None:
self.changed = True
elif not self._values_is_equal(self.proc_value, self.args['value']):
self.changed = True
self.set_proc = True
# Do the work
if not self.module.check_mode:
if self.set_proc:
self.set_token_value(self.args['name'], self.args['value'])
if self.write_file:
self.write_sysctl()
if self.changed and self.args['reload']:
self.reload_sysctl()
def _values_is_equal(self, a, b):
"""Expects two string values. It will split the string by whitespace
and compare each value. It will return True if both lists are the same,
contain the same elements and the same order."""
if a is None or b is None:
return False
a = a.split()
b = b.split()
if len(a) != len(b):
return False
return len([i for i, j in zip(a, b) if i == j]) == len(a)
def _parse_value(self, value):
if value is None:
return ''
elif isinstance(value, bool):
if value:
return '1'
else:
return '0'
elif isinstance(value, string_types):
if value.lower() in BOOLEANS_TRUE:
return '1'
elif value.lower() in BOOLEANS_FALSE:
return '0'
else:
return value.strip()
else:
return value
def _stderr_failed(self, err):
# sysctl can fail to set a value even if it returns an exit status 0
# (https://bugzilla.redhat.com/show_bug.cgi?id=1264080). That's why we
# also have to check stderr for errors. For now we will only fail on
# specific errors defined by the regex below.
errors_regex = r'^sysctl: setting key "[^"]+": (Invalid argument|Read-only file system)$'
return re.search(errors_regex, err, re.MULTILINE) is not None
# ==============================================================
# SYSCTL COMMAND MANAGEMENT
# ==============================================================
# Use the sysctl command to find the current value
def get_token_curr_value(self, token):
if self.platform == 'openbsd':
# openbsd doesn't support -e, just drop it
thiscmd = "%s -n %s" % (self.sysctl_cmd, token)
else:
thiscmd = "%s -e -n %s" % (self.sysctl_cmd, token)
rc, out, err = self.module.run_command(thiscmd, environ_update=self.LANG_ENV)
if rc != 0:
return None
else:
return out
# Use the sysctl command to set the current value
def set_token_value(self, token, value):
if len(value.split()) > 0:
value = '"' + value + '"'
if self.platform == 'openbsd':
# openbsd doesn't accept -w, but since it's not needed, just drop it
thiscmd = "%s %s=%s" % (self.sysctl_cmd, token, value)
elif self.platform == 'freebsd':
ignore_missing = ''
if self.args['ignoreerrors']:
ignore_missing = '-i'
# freebsd doesn't accept -w, but since it's not needed, just drop it
thiscmd = "%s %s %s=%s" % (self.sysctl_cmd, ignore_missing, token, value)
else:
ignore_missing = ''
if self.args['ignoreerrors']:
ignore_missing = '-e'
thiscmd = "%s %s -w %s=%s" % (self.sysctl_cmd, ignore_missing, token, value)
rc, out, err = self.module.run_command(thiscmd, environ_update=self.LANG_ENV)
if rc != 0 or self._stderr_failed(err):
self.module.fail_json(msg='setting %s failed: %s' % (token, out + err))
else:
return rc
# Run sysctl -p
def reload_sysctl(self):
if self.platform == 'freebsd':
# freebsd doesn't support -p, so reload the sysctl service
rc, out, err = self.module.run_command('/etc/rc.d/sysctl reload', environ_update=self.LANG_ENV)
elif self.platform == 'openbsd':
# openbsd doesn't support -p and doesn't have a sysctl service,
# so we have to set every value with its own sysctl call
for k, v in self.file_values.items():
rc = 0
if k != self.args['name']:
rc = self.set_token_value(k, v)
# FIXME this check is probably not needed as set_token_value would fail_json if rc != 0
if rc != 0:
break
if rc == 0 and self.args['state'] == "present":
rc = self.set_token_value(self.args['name'], self.args['value'])
# set_token_value would have called fail_json in case of failure
# so return here and do not continue to the error processing below
# https://github.com/ansible/ansible/issues/58158
return
else:
# system supports reloading via the -p flag to sysctl, so we'll use that
sysctl_args = [self.sysctl_cmd, '-p', self.sysctl_file]
if self.args['ignoreerrors']:
sysctl_args.insert(1, '-e')
rc, out, err = self.module.run_command(sysctl_args, environ_update=self.LANG_ENV)
if rc != 0 or self._stderr_failed(err):
self.module.fail_json(msg="Failed to reload sysctl: %s" % to_native(out) + to_native(err))
# ==============================================================
# SYSCTL FILE MANAGEMENT
# ==============================================================
# Get the token value from the sysctl file
def read_sysctl_file(self):
lines = []
if os.path.isfile(self.sysctl_file):
try:
with open(self.sysctl_file, "r") as read_file:
lines = read_file.readlines()
except IOError as e:
self.module.fail_json(msg="Failed to open %s: %s" % (to_native(self.sysctl_file), to_native(e)))
for line in lines:
line = line.strip()
self.file_lines.append(line)
# don't split empty lines or comments or line without equal sign
if not line or line.startswith(("#", ";")) or "=" not in line:
continue
k, v = line.split('=', 1)
k = k.strip()
v = v.strip()
self.file_values[k] = v.strip()
# Fix the value in the sysctl file content
def fix_lines(self):
checked = []
self.fixed_lines = []
for line in self.file_lines:
if not line.strip() or line.strip().startswith(("#", ";")) or "=" not in line:
self.fixed_lines.append(line)
continue
tmpline = line.strip()
k, v = tmpline.split('=', 1)
k = k.strip()
v = v.strip()
if k not in checked:
checked.append(k)
if k == self.args['name']:
if self.args['state'] == "present":
new_line = "%s=%s\n" % (k, self.args['value'])
self.fixed_lines.append(new_line)
else:
new_line = "%s=%s\n" % (k, v)
self.fixed_lines.append(new_line)
if self.args['name'] not in checked and self.args['state'] == "present":
new_line = "%s=%s\n" % (self.args['name'], self.args['value'])
self.fixed_lines.append(new_line)
# Completely rewrite the sysctl file
def write_sysctl(self):
# open a tmp file
fd, tmp_path = tempfile.mkstemp('.conf', '.ansible_m_sysctl_', os.path.dirname(self.sysctl_file))
f = open(tmp_path, "w")
try:
for l in self.fixed_lines:
f.write(l.strip() + "\n")
except IOError as e:
self.module.fail_json(msg="Failed to write to file %s: %s" % (tmp_path, to_native(e)))
f.flush()
f.close()
# replace the real one
self.module.atomic_move(tmp_path, self.sysctl_file)
# ==============================================================
# main
def main():
# defining module
module = AnsibleModule(
argument_spec=dict(
name=dict(aliases=['key'], required=True),
value=dict(aliases=['val'], required=False, type='str'),
state=dict(default='present', choices=['present', 'absent']),
reload=dict(default=True, type='bool'),
sysctl_set=dict(default=False, type='bool'),
ignoreerrors=dict(default=False, type='bool'),
sysctl_file=dict(default='/etc/sysctl.conf', type='path')
),
supports_check_mode=True,
required_if=[('state', 'present', ['value'])],
)
if module.params['name'] is None:
module.fail_json(msg="name cannot be None")
if module.params['state'] == 'present' and module.params['value'] is None:
module.fail_json(msg="value cannot be None")
# In case of in-line params
if module.params['name'] == '':
module.fail_json(msg="name cannot be blank")
if module.params['state'] == 'present' and module.params['value'] == '':
module.fail_json(msg="value cannot be blank")
result = SysctlModule(module)
module.exit_json(changed=result.changed)
if __name__ == '__main__':
main()

View File

@ -1,47 +0,0 @@
# Copyright (c) 2014, Chris Church <chris@ninemoreminutes.com>
# Copyright (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
name: csh
short_description: C shell (/bin/csh)
description:
- When you have no other option than to use csh
extends_documentation_fragment:
- shell_common
'''
from ansible.module_utils.six import text_type
from ansible.module_utils.six.moves import shlex_quote
from ansible.plugins.shell import ShellBase
class ShellModule(ShellBase):
# Common shell filenames that this plugin handles
COMPATIBLE_SHELLS = frozenset(('csh', 'tcsh'))
# Family of shells this has. Must match the filename without extension
SHELL_FAMILY = 'csh'
# commonly used
ECHO = 'echo'
COMMAND_SEP = ';'
# How to end lines in a python script one-liner
_SHELL_EMBEDDED_PY_EOL = '\\\n'
_SHELL_REDIRECT_ALLNULL = '>& /dev/null'
_SHELL_AND = '&&'
_SHELL_OR = '||'
_SHELL_SUB_LEFT = '"`'
_SHELL_SUB_RIGHT = '`"'
_SHELL_GROUP_LEFT = '('
_SHELL_GROUP_RIGHT = ')'
def env_prefix(self, **kwargs):
ret = []
# All the -u options must be first, so we process them first
ret += ['-u %s' % k for k, v in kwargs.items() if v is None]
ret += ['%s=%s' % (k, shlex_quote(text_type(v))) for k, v in kwargs.items() if v is not None]
return 'env %s' % ' '.join(ret)

View File

@ -1,95 +0,0 @@
# Copyright (c) 2014, Chris Church <chris@ninemoreminutes.com>
# Copyright (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
name: fish
short_description: fish shell (/bin/fish)
description:
- This is here because some people are restricted to fish.
extends_documentation_fragment:
- shell_common
'''
from ansible.module_utils.six import text_type
from ansible.module_utils.six.moves import shlex_quote
from ansible.plugins.shell.sh import ShellModule as ShModule
class ShellModule(ShModule):
# Common shell filenames that this plugin handles
COMPATIBLE_SHELLS = frozenset(('fish',))
# Family of shells this has. Must match the filename without extension
SHELL_FAMILY = 'fish'
_SHELL_EMBEDDED_PY_EOL = '\n'
_SHELL_REDIRECT_ALLNULL = '> /dev/null 2>&1'
_SHELL_AND = '; and'
_SHELL_OR = '; or'
_SHELL_SUB_LEFT = '('
_SHELL_SUB_RIGHT = ')'
_SHELL_GROUP_LEFT = ''
_SHELL_GROUP_RIGHT = ''
def env_prefix(self, **kwargs):
env = self.env.copy()
env.update(kwargs)
ret = []
for k, v in kwargs.items():
if v is None:
ret.append('set -e %s;' % k)
else:
ret.append('set -lx %s %s;' % (k, shlex_quote(text_type(v))))
return ' '.join(ret)
def build_module_command(self, env_string, shebang, cmd, arg_path=None):
# don't quote the cmd if it's an empty string, because this will break pipelining mode
if cmd.strip() != '':
cmd = shlex_quote(cmd)
cmd_parts = [env_string.strip(), shebang.replace("#!", "").strip(), cmd]
if arg_path is not None:
cmd_parts.append(arg_path)
new_cmd = " ".join(cmd_parts)
return new_cmd
def checksum(self, path, python_interp):
# The following test is fish-compliant.
#
# In the following test, each condition is a check and logical
# comparison (or or and) that sets the rc value. Every check is run so
# the last check in the series to fail will be the rc that is
# returned.
#
# If a check fails we error before invoking the hash functions because
# hash functions may successfully take the hash of a directory on BSDs
# (UFS filesystem?) which is not what the rest of the ansible code
# expects
#
# If all of the available hashing methods fail we fail with an rc of
# 0. This logic is added to the end of the cmd at the bottom of this
# function.
# Return codes:
# checksum: success!
# 0: Unknown error
# 1: Remote file does not exist
# 2: No read permissions on the file
# 3: File is a directory
# 4: No python interpreter
# Quoting gets complex here. We're writing a python string that's
# used by a variety of shells on the remote host to invoke a python
# "one-liner".
shell_escaped_path = shlex_quote(path)
test = "set rc flag; [ -r %(p)s ] %(shell_or)s set rc 2; [ -f %(p)s ] %(shell_or)s set rc 1; [ -d %(p)s ] %(shell_and)s set rc 3; %(i)s -V 2>/dev/null %(shell_or)s set rc 4; [ x\"$rc\" != \"xflag\" ] %(shell_and)s echo \"$rc \"%(p)s %(shell_and)s exit 0" % dict(p=shell_escaped_path, i=python_interp, shell_and=self._SHELL_AND, shell_or=self._SHELL_OR) # NOQA
csums = [
u"({0} -c 'import hashlib; BLOCKSIZE = 65536; hasher = hashlib.sha1();{2}afile = open(\"'{1}'\", \"rb\"){2}buf = afile.read(BLOCKSIZE){2}while len(buf) > 0:{2}\thasher.update(buf){2}\tbuf = afile.read(BLOCKSIZE){2}afile.close(){2}print(hasher.hexdigest())' 2>/dev/null)".format(python_interp, shell_escaped_path, self._SHELL_EMBEDDED_PY_EOL), # NOQA Python > 2.4 (including python3)
u"({0} -c 'import sha; BLOCKSIZE = 65536; hasher = sha.sha();{2}afile = open(\"'{1}'\", \"rb\"){2}buf = afile.read(BLOCKSIZE){2}while len(buf) > 0:{2}\thasher.update(buf){2}\tbuf = afile.read(BLOCKSIZE){2}afile.close(){2}print(hasher.hexdigest())' 2>/dev/null)".format(python_interp, shell_escaped_path, self._SHELL_EMBEDDED_PY_EOL), # NOQA Python == 2.4
]
cmd = (" %s " % self._SHELL_OR).join(csums)
cmd = "%s; %s %s (echo \'0 \'%s)" % (test, cmd, self._SHELL_OR, shell_escaped_path)
return cmd