update collections/requirements.yml

This commit is contained in:
Chris Hammer
2022-02-22 00:08:05 -05:00
parent bb2e0613dd
commit a4cce424a9
3404 changed files with 407900 additions and 0 deletions

View File

@ -0,0 +1,384 @@
#!/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: yes
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: yes
default:
description:
- If the target is a directory, setting this to C(yes) will make it the default ACL for entities created inside the directory.
- Setting C(default) to C(yes) causes an error if the path is a file.
type: bool
default: no
entity:
description:
- The actual user or group that the ACL applies to when matching entity types user or group are selected.
type: str
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: no
aliases: [ recurse ]
use_nfsv4_acls:
description:
- Use NFSv4 ACLs instead of POSIX ACLs.
type: bool
default: no
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: yes
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

@ -0,0 +1,195 @@
#!/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: no
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: yes
'''
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

@ -0,0 +1,684 @@
#!/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(yes), 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=no) 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: yes
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: no
validate_certs:
description:
- This only applies if using a https url as the source of the keys.
- If set to C(no), the SSL certificates will not be validated.
- This should only set to C(no) 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(yes).
type: bool
default: yes
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: no
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 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(no), 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)
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()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,391 @@
#!/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: no
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: yes
- name: Gather information about specific zones
ansible.posix.firewalld_info:
zones:
- public
- external
- internal
'''
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_zones:
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 distutils.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

@ -0,0 +1,882 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2012, Red Hat, inc
# Written by Seth Vidal
# based on the mount modules from salt and puppet
# 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: mount
short_description: Control active and configured mount points
description:
- This module controls active and configured mount points in C(/etc/fstab).
author:
- Ansible Core Team
- Seth Vidal (@skvidal)
version_added: "1.0.0"
options:
path:
description:
- Path to the mount point (e.g. C(/mnt/files)).
- Before Ansible 2.3 this option was only usable as I(dest), I(destfile) and I(name).
type: path
required: true
aliases: [ name ]
src:
description:
- Device (or NFS volume, or something else) to be mounted on I(path).
- Required when I(state) set to C(present) or C(mounted).
type: path
fstype:
description:
- Filesystem type.
- Required when I(state) is C(present) or C(mounted).
type: str
opts:
description:
- Mount options (see fstab(5), or vfstab(4) on Solaris).
type: str
dump:
description:
- Dump (see fstab(5)).
- Note that if set to C(null) and I(state) set to C(present),
it will cease to work and duplicate entries will be made
with subsequent runs.
- Has no effect on Solaris systems.
type: str
default: 0
passno:
description:
- Passno (see fstab(5)).
- Note that if set to C(null) and I(state) set to C(present),
it will cease to work and duplicate entries will be made
with subsequent runs.
- Deprecated on Solaris systems.
type: str
default: 0
state:
description:
- If C(mounted), the device will be actively mounted and appropriately
configured in I(fstab). If the mount point is not present, the mount
point will be created.
- If C(unmounted), the device will be unmounted without changing I(fstab).
- C(present) only specifies that the device is to be configured in
I(fstab) and does not trigger or require a mount.
- C(absent) specifies that the device mount's entry will be removed from
I(fstab) and will also unmount the device and remove the mount
point.
- C(remounted) specifies that the device will be remounted for when you
want to force a refresh on the mount itself (added in 2.9). This will
always return changed=true. If I(opts) is set, the options will be
applied to the remount, but will not change I(fstab). Additionally,
if I(opts) is set, and the remount command fails, the module will
error to prevent unexpected mount changes. Try using C(mounted)
instead to work around this issue.
type: str
required: true
choices: [ absent, mounted, present, unmounted, remounted ]
fstab:
description:
- File to use instead of C(/etc/fstab).
- You should not use this option unless you really know what you are doing.
- This might be useful if you need to configure mountpoints in a chroot environment.
- OpenBSD does not allow specifying alternate fstab files with mount so do not
use this on OpenBSD with any state that operates on the live filesystem.
- This parameter defaults to /etc/fstab or /etc/vfstab on Solaris.
type: str
boot:
description:
- Determines if the filesystem should be mounted on boot.
- Only applies to Solaris and Linux systems.
- For Solaris systems, C(true) will set C(yes) as the value of mount at boot
in I(/etc/vfstab).
- For Linux, FreeBSD, NetBSD and OpenBSD systems, C(false) will add C(noauto)
to mount options in I(/etc/fstab).
- To avoid mount option conflicts, if C(noauto) specified in C(opts),
mount module will ignore C(boot).
type: bool
default: yes
backup:
description:
- Create a backup file including the timestamp information so you can get
the original file back if you somehow clobbered it incorrectly.
type: bool
default: no
notes:
- As of Ansible 2.3, the I(name) option has been changed to I(path) as
default, but I(name) still works as well.
- Using C(remounted) with I(opts) set may create unexpected results based on
the existing options already defined on mount, so care should be taken to
ensure that conflicting options are not present before hand.
'''
EXAMPLES = r'''
# Before 2.3, option 'name' was used instead of 'path'
- name: Mount DVD read-only
ansible.posix.mount:
path: /mnt/dvd
src: /dev/sr0
fstype: iso9660
opts: ro,noauto
state: present
- name: Mount up device by label
ansible.posix.mount:
path: /srv/disk
src: LABEL=SOME_LABEL
fstype: ext4
state: present
- name: Mount up device by UUID
ansible.posix.mount:
path: /home
src: UUID=b3e48f45-f933-4c8e-a700-22a159ec9077
fstype: xfs
opts: noatime
state: present
- name: Unmount a mounted volume
ansible.posix.mount:
path: /tmp/mnt-pnt
state: unmounted
- name: Remount a mounted volume
ansible.posix.mount:
path: /tmp/mnt-pnt
state: remounted
# The following will not save changes to fstab, and only be temporary until
# a reboot, or until calling "state: unmounted" followed by "state: mounted"
# on the same "path"
- name: Remount a mounted volume and append exec to the existing options
ansible.posix.mount:
path: /tmp
state: remounted
opts: exec
- name: Mount and bind a volume
ansible.posix.mount:
path: /system/new_volume/boot
src: /boot
opts: bind
state: mounted
fstype: none
- name: Mount an NFS volume
ansible.posix.mount:
src: 192.168.1.100:/nfs/ssd/shared_data
path: /mnt/shared_data
opts: rw,sync,hard,intr
state: mounted
fstype: nfs
- name: Mount NFS volumes with noauto according to boot option
ansible.posix.mount:
src: 192.168.1.100:/nfs/ssd/shared_data
path: /mnt/shared_data
opts: rw,sync,hard,intr
boot: no
state: mounted
fstype: nfs
'''
import errno
import os
import platform
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.ansible.posix.plugins.module_utils.mount import ismount
from ansible.module_utils.six import iteritems
from ansible.module_utils._text import to_bytes, to_native
from ansible.module_utils.parsing.convert_bool import boolean
def write_fstab(module, lines, path):
if module.params['backup']:
backup_file = module.backup_local(path)
else:
backup_file = ""
fs_w = open(path, 'w')
for l in lines:
fs_w.write(l)
fs_w.flush()
fs_w.close()
return backup_file
def _escape_fstab(v):
"""Escape invalid characters in fstab fields.
space (040)
ampersand (046)
backslash (134)
"""
if isinstance(v, int):
return v
else:
return(
v.
replace('\\', '\\134').
replace(' ', '\\040').
replace('&', '\\046'))
def set_mount(module, args):
"""Set/change a mount point location in fstab."""
name, backup_lines, changed = _set_mount_save_old(module, args)
return name, changed
def _set_mount_save_old(module, args):
"""Set/change a mount point location in fstab. Save the old fstab contents."""
to_write = []
old_lines = []
exists = False
changed = False
escaped_args = dict([(k, _escape_fstab(v)) for k, v in iteritems(args) if k != 'warnings'])
new_line = '%(src)s %(name)s %(fstype)s %(opts)s %(dump)s %(passno)s\n'
if platform.system() == 'SunOS':
new_line = (
'%(src)s - %(name)s %(fstype)s %(passno)s %(boot)s %(opts)s\n')
for line in open(args['fstab'], 'r').readlines():
old_lines.append(line)
if not line.strip():
to_write.append(line)
continue
if line.strip().startswith('#'):
to_write.append(line)
continue
fields = line.split()
# Check if we got a valid line for splitting
# (on Linux the 5th and the 6th field is optional)
if (
platform.system() == 'SunOS' and len(fields) != 7 or
platform.system() == 'Linux' and len(fields) not in [4, 5, 6] or
platform.system() not in ['SunOS', 'Linux'] and len(fields) != 6):
to_write.append(line)
continue
ld = {}
if platform.system() == 'SunOS':
(
ld['src'],
dash,
ld['name'],
ld['fstype'],
ld['passno'],
ld['boot'],
ld['opts']
) = fields
else:
fields_labels = ['src', 'name', 'fstype', 'opts', 'dump', 'passno']
# The last two fields are optional on Linux so we fill in default values
ld['dump'] = 0
ld['passno'] = 0
# Fill in the rest of the available fields
for i, field in enumerate(fields):
ld[fields_labels[i]] = field
# Check if we found the correct line
if (
ld['name'] != escaped_args['name'] or (
# In the case of swap, check the src instead
'src' in args and
ld['name'] == 'none' and
ld['fstype'] == 'swap' and
ld['src'] != args['src'])):
to_write.append(line)
continue
# If we got here we found a match - let's check if there is any
# difference
exists = True
args_to_check = ('src', 'fstype', 'opts', 'dump', 'passno')
if platform.system() == 'SunOS':
args_to_check = ('src', 'fstype', 'passno', 'boot', 'opts')
for t in args_to_check:
if ld[t] != escaped_args[t]:
ld[t] = escaped_args[t]
changed = True
if changed:
to_write.append(new_line % ld)
else:
to_write.append(line)
if not exists:
to_write.append(new_line % escaped_args)
changed = True
if changed and not module.check_mode:
args['backup_file'] = write_fstab(module, to_write, args['fstab'])
return (args['name'], old_lines, changed)
def unset_mount(module, args):
"""Remove a mount point from fstab."""
to_write = []
changed = False
escaped_name = _escape_fstab(args['name'])
for line in open(args['fstab'], 'r').readlines():
if not line.strip():
to_write.append(line)
continue
if line.strip().startswith('#'):
to_write.append(line)
continue
# Check if we got a valid line for splitting
if (
platform.system() == 'SunOS' and len(line.split()) != 7 or
platform.system() != 'SunOS' and len(line.split()) != 6):
to_write.append(line)
continue
ld = {}
if platform.system() == 'SunOS':
(
ld['src'],
dash,
ld['name'],
ld['fstype'],
ld['passno'],
ld['boot'],
ld['opts']
) = line.split()
else:
(
ld['src'],
ld['name'],
ld['fstype'],
ld['opts'],
ld['dump'],
ld['passno']
) = line.split()
if (
ld['name'] != escaped_name or (
# In the case of swap, check the src instead
'src' in args and
ld['name'] == 'none' and
ld['fstype'] == 'swap' and
ld['src'] != args['src'])):
to_write.append(line)
continue
# If we got here we found a match - continue and mark changed
changed = True
if changed and not module.check_mode:
write_fstab(module, to_write, args['fstab'])
return (args['name'], changed)
def _set_fstab_args(fstab_file):
result = []
if (
fstab_file and
fstab_file != '/etc/fstab' and
platform.system().lower() != 'sunos'):
if platform.system().lower().endswith('bsd'):
result.append('-F')
else:
result.append('-T')
result.append(fstab_file)
return result
def mount(module, args):
"""Mount up a path or remount if needed."""
mount_bin = module.get_bin_path('mount', required=True)
name = args['name']
cmd = [mount_bin]
if platform.system().lower() == 'openbsd':
# Use module.params['fstab'] here as args['fstab'] has been set to the
# default value.
if module.params['fstab'] is not None:
module.fail_json(
msg=(
'OpenBSD does not support alternate fstab files. Do not '
'specify the fstab parameter for OpenBSD hosts'))
else:
cmd += _set_fstab_args(args['fstab'])
cmd += [name]
rc, out, err = module.run_command(cmd)
if rc == 0:
return 0, ''
else:
return rc, out + err
def umount(module, path):
"""Unmount a path."""
umount_bin = module.get_bin_path('umount', required=True)
cmd = [umount_bin, path]
rc, out, err = module.run_command(cmd)
if rc == 0:
return 0, ''
else:
return rc, out + err
def remount(module, args):
"""Try to use 'remount' first and fallback to (u)mount if unsupported."""
mount_bin = module.get_bin_path('mount', required=True)
cmd = [mount_bin]
# Multiplatform remount opts
if platform.system().lower().endswith('bsd'):
if module.params['state'] == 'remounted' and args['opts'] != 'defaults':
cmd += ['-u', '-o', args['opts']]
else:
cmd += ['-u']
else:
if module.params['state'] == 'remounted' and args['opts'] != 'defaults':
cmd += ['-o', 'remount,' + args['opts']]
else:
cmd += ['-o', 'remount']
if platform.system().lower() == 'openbsd':
# Use module.params['fstab'] here as args['fstab'] has been set to the
# default value.
if module.params['fstab'] is not None:
module.fail_json(
msg=(
'OpenBSD does not support alternate fstab files. Do not '
'specify the fstab parameter for OpenBSD hosts'))
else:
cmd += _set_fstab_args(args['fstab'])
cmd += [args['name']]
out = err = ''
try:
if platform.system().lower().endswith('bsd'):
# Note: Forcing BSDs to do umount/mount due to BSD remount not
# working as expected (suspect bug in the BSD mount command)
# Interested contributor could rework this to use mount options on
# the CLI instead of relying on fstab
# https://github.com/ansible/ansible-modules-core/issues/5591
rc = 1
else:
rc, out, err = module.run_command(cmd)
except Exception:
rc = 1
msg = ''
if rc != 0:
msg = out + err
if module.params['state'] == 'remounted' and args['opts'] != 'defaults':
module.fail_json(
msg=(
'Options were specified with remounted, but the remount '
'command failed. Failing in order to prevent an '
'unexpected mount result. Try replacing this command with '
'a "state: unmounted" followed by a "state: mounted" '
'using the full desired mount options instead.'))
rc, msg = umount(module, args['name'])
if rc == 0:
rc, msg = mount(module, args)
return rc, msg
# Note if we wanted to put this into module_utils we'd have to get permission
# from @jupeter -- https://github.com/ansible/ansible-modules-core/pull/2923
# @jtyr -- https://github.com/ansible/ansible-modules-core/issues/4439
# and @abadger to relicense from GPLv3+
def is_bind_mounted(module, linux_mounts, dest, src=None, fstype=None):
"""Return whether the dest is bind mounted
:arg module: The AnsibleModule (used for helper functions)
:arg dest: The directory to be mounted under. This is the primary means
of identifying whether the destination is mounted.
:kwarg src: The source directory. If specified, this is used to help
ensure that we are detecting that the correct source is mounted there.
:kwarg fstype: The filesystem type. If specified this is also used to
help ensure that we are detecting the right mount.
:kwarg linux_mounts: Cached list of mounts for Linux.
:returns: True if the dest is mounted with src otherwise False.
"""
is_mounted = False
if platform.system() == 'Linux' and linux_mounts is not None:
if src is None:
# That's for unmounted/absent
if dest in linux_mounts:
is_mounted = True
else:
if dest in linux_mounts:
is_mounted = linux_mounts[dest]['src'] == src
else:
bin_path = module.get_bin_path('mount', required=True)
cmd = '%s -l' % bin_path
rc, out, err = module.run_command(cmd)
mounts = []
if len(out):
mounts = to_native(out).strip().split('\n')
for mnt in mounts:
arguments = mnt.split()
if (
(arguments[0] == src or src is None) and
arguments[2] == dest and
(arguments[4] == fstype or fstype is None)):
is_mounted = True
if is_mounted:
break
return is_mounted
def get_linux_mounts(module, mntinfo_file="/proc/self/mountinfo"):
"""Gather mount information"""
try:
f = open(mntinfo_file)
except IOError:
return
lines = map(str.strip, f.readlines())
try:
f.close()
except IOError:
module.fail_json(msg="Cannot close file %s" % mntinfo_file)
mntinfo = {}
for line in lines:
fields = line.split()
record = {
'id': int(fields[0]),
'parent_id': int(fields[1]),
'root': fields[3],
'dst': fields[4],
'opts': fields[5],
'fs': fields[-3],
'src': fields[-2]
}
mntinfo[record['id']] = record
mounts = {}
for mnt in mntinfo.values():
if mnt['parent_id'] != 1 and mnt['parent_id'] in mntinfo:
m = mntinfo[mnt['parent_id']]
if (
len(m['root']) > 1 and
mnt['root'].startswith("%s/" % m['root'])):
# Omit the parent's root in the child's root
# == Example:
# 140 136 253:2 /rootfs / rw - ext4 /dev/sdb2 rw
# 141 140 253:2 /rootfs/tmp/aaa /tmp/bbb rw - ext4 /dev/sdb2 rw
# == Expected result:
# src=/tmp/aaa
mnt['root'] = mnt['root'][len(m['root']):]
# Prepend the parent's dst to the child's root
# == Example:
# 42 60 0:35 / /tmp rw - tmpfs tmpfs rw
# 78 42 0:35 /aaa /tmp/bbb rw - tmpfs tmpfs rw
# == Expected result:
# src=/tmp/aaa
if m['dst'] != '/':
mnt['root'] = "%s%s" % (m['dst'], mnt['root'])
src = mnt['root']
else:
src = mnt['src']
record = {
'dst': mnt['dst'],
'src': src,
'opts': mnt['opts'],
'fs': mnt['fs']
}
mounts[mnt['dst']] = record
return mounts
def main():
module = AnsibleModule(
argument_spec=dict(
boot=dict(type='bool', default=True),
dump=dict(type='str'),
fstab=dict(type='str'),
fstype=dict(type='str'),
path=dict(type='path', required=True, aliases=['name']),
opts=dict(type='str'),
passno=dict(type='str', no_log=False),
src=dict(type='path'),
backup=dict(type='bool', default=False),
state=dict(type='str', required=True, choices=['absent', 'mounted', 'present', 'unmounted', 'remounted']),
),
supports_check_mode=True,
required_if=(
['state', 'mounted', ['src', 'fstype']],
['state', 'present', ['src', 'fstype']],
),
)
# solaris args:
# name, src, fstype, opts, boot, passno, state, fstab=/etc/vfstab
# linux args:
# name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab
# Note: Do not modify module.params['fstab'] as we need to know if the user
# explicitly specified it in mount() and remount()
if platform.system().lower() == 'sunos':
args = dict(
name=module.params['path'],
opts='-',
passno='-',
fstab=module.params['fstab'],
boot='yes' if module.params['boot'] else 'no',
warnings=[]
)
if args['fstab'] is None:
args['fstab'] = '/etc/vfstab'
else:
args = dict(
name=module.params['path'],
opts='defaults',
dump='0',
passno='0',
fstab=module.params['fstab'],
boot='yes',
warnings=[]
)
if args['fstab'] is None:
args['fstab'] = '/etc/fstab'
# FreeBSD doesn't have any 'default' so set 'rw' instead
if platform.system() == 'FreeBSD':
args['opts'] = 'rw'
args['backup_file'] = ""
linux_mounts = []
# Cache all mounts here in order we have consistent results if we need to
# call is_bind_mounted() multiple times
if platform.system() == 'Linux':
linux_mounts = get_linux_mounts(module)
if linux_mounts is None:
args['warnings'].append('Cannot open file /proc/self/mountinfo.'
' Bind mounts might be misinterpreted.')
# Override defaults with user specified params
for key in ('src', 'fstype', 'passno', 'opts', 'dump', 'fstab'):
if module.params[key] is not None:
args[key] = module.params[key]
if platform.system().lower() == 'linux' or platform.system().lower().endswith('bsd'):
# Linux, FreeBSD, NetBSD and OpenBSD have 'noauto' as mount option to
# handle mount on boot. To avoid mount option conflicts, if 'noauto'
# specified in 'opts', mount module will ignore 'boot'.
opts = args['opts'].split(',')
if 'noauto' in opts:
args['warnings'].append("Ignore the 'boot' due to 'opts' contains 'noauto'.")
elif not module.params['boot']:
args['boot'] = 'no'
if 'defaults' in opts:
args['warnings'].append("Ignore the 'boot' due to 'opts' contains 'defaults'.")
else:
opts.append('noauto')
args['opts'] = ','.join(opts)
# If fstab file does not exist, we first need to create it. This mainly
# happens when fstab option is passed to the module.
if not os.path.exists(args['fstab']):
if not os.path.exists(os.path.dirname(args['fstab'])):
os.makedirs(os.path.dirname(args['fstab']))
try:
open(args['fstab'], 'a').close()
except PermissionError as e:
module.fail_json(msg="Failed to open %s due to permission issue" % args['fstab'])
except Exception as e:
module.fail_json(msg="Failed to open %s due to %s" % (args['fstab'], to_native(e)))
# absent:
# Remove from fstab and unmounted.
# unmounted:
# Do not change fstab state, but unmount.
# present:
# Add to fstab, do not change mount state.
# mounted:
# Add to fstab if not there and make sure it is mounted. If it has
# changed in fstab then remount it.
state = module.params['state']
name = module.params['path']
changed = False
if state == 'absent':
name, changed = unset_mount(module, args)
if changed and not module.check_mode:
if ismount(name) or is_bind_mounted(module, linux_mounts, name):
res, msg = umount(module, name)
if res:
module.fail_json(
msg="Error unmounting %s: %s" % (name, msg))
if os.path.exists(name):
try:
os.rmdir(name)
except (OSError, IOError) as e:
module.fail_json(msg="Error rmdir %s: %s" % (name, to_native(e)))
elif state == 'unmounted':
if ismount(name) or is_bind_mounted(module, linux_mounts, name):
if not module.check_mode:
res, msg = umount(module, name)
if res:
module.fail_json(
msg="Error unmounting %s: %s" % (name, msg))
changed = True
elif state == 'mounted':
dirs_created = []
if not os.path.exists(name) and not module.check_mode:
try:
# Something like mkdir -p but with the possibility to undo.
# Based on some copy-paste from the "file" module.
curpath = ''
for dirname in name.strip('/').split('/'):
curpath = '/'.join([curpath, dirname])
# Remove leading slash if we're creating a relative path
if not os.path.isabs(name):
curpath = curpath.lstrip('/')
b_curpath = to_bytes(curpath, errors='surrogate_or_strict')
if not os.path.exists(b_curpath):
try:
os.mkdir(b_curpath)
dirs_created.append(b_curpath)
except OSError as ex:
# Possibly something else created the dir since the os.path.exists
# check above. As long as it's a dir, we don't need to error out.
if not (ex.errno == errno.EEXIST and os.path.isdir(b_curpath)):
raise
except (OSError, IOError) as e:
module.fail_json(
msg="Error making dir %s: %s" % (name, to_native(e)))
name, backup_lines, changed = _set_mount_save_old(module, args)
res = 0
if (
ismount(name) or
is_bind_mounted(
module, linux_mounts, name, args['src'], args['fstype'])):
if changed and not module.check_mode:
res, msg = remount(module, args)
changed = True
else:
changed = True
if not module.check_mode:
res, msg = mount(module, args)
if res:
# Not restoring fstab after a failed mount was reported as a bug,
# ansible/ansible#59183
# A non-working fstab entry may break the system at the reboot,
# so undo all the changes if possible.
try:
write_fstab(module, backup_lines, args['fstab'])
except Exception:
pass
try:
for dirname in dirs_created[::-1]:
os.rmdir(dirname)
except Exception:
pass
module.fail_json(msg="Error mounting %s: %s" % (name, msg))
elif state == 'present':
name, changed = set_mount(module, args)
elif state == 'remounted':
if not module.check_mode:
res, msg = remount(module, args)
if res:
module.fail_json(msg="Error remounting %s: %s" % (name, msg))
changed = True
else:
module.fail_json(msg='Unexpected position reached')
# If the managed node is Solaris, convert the boot value type to Boolean
# to match the type of return value with the module argument.
if platform.system().lower() == 'sunos':
args['boot'] = boolean(args['boot'])
module.exit_json(changed=changed, **args)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,223 @@
#!/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 'no', 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(no), it will search for src at originating/controller machine, if C(yes) it will
go to the remote/target machine for the C(src).
type: bool
default: no
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: no
binary:
description:
- Setting to C(yes) will disable patch's heuristic for transforming CRLF
line endings into LF.
- Line endings of src and dest must match.
- If set to C(no), C(patch) will replace CRLF in C(src) files on POSIX.
type: bool
default: no
ignore_whitespace:
description:
- Setting to C(yes) will ignore white space changes between patch and input..
type: bool
default: no
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

@ -0,0 +1,334 @@
#!/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(yes) if the boolean setting should survive a reboot.
type: bool
default: 'no'
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
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: yes
persistent: yes
'''
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'), 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

@ -0,0 +1,276 @@
#!/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
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.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()
# 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_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']),
),
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']
runtime_enabled = selinux.is_selinux_enabled()
runtime_policy = selinux.selinux_getpolicytype()[1]
runtime_state = 'disabled'
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)
# 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
module.exit_json(changed=changed, msg=', '.join(msgs), configfile=configfile, policy=policy, state=state, reboot_required=reboot_required)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,635 @@
#!/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: yes
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: no
compress:
description:
- Compress file data during the transfer.
- In most cases, leave this enabled unless it causes problems.
type: bool
default: yes
existing_only:
description:
- Skip creating new files on receiver.
type: bool
default: no
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=yes).
- This option ignores excluded files and behaves like the rsync opt C(--delete-after).
type: bool
default: no
dirs:
description:
- Transfer directories without recursing.
type: bool
default: no
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: no
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(no).
type: bool
default: yes
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: no
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(yes) to allow multiplexing and reduce SSH connection overhead.
- Note that simply setting this option to C(yes) 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: no
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: no
verify_host:
description:
- Verify destination host key.
type: bool
default: no
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: yes
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: 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: no
- name: Synchronization with --archive options enabled except for --recursive
ansible.posix.synchronize:
src: some/relative/path
dest: /some/absolute/path
recursive: no
- 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: yes
times: no
- name: Synchronization without --archive options enabled except use --links
ansible.posix.synchronize:
src: some/relative/path
dest: /some/absolute/path
archive: no
links: yes
- 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: yes
recursive: yes
# 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(source)
cmd.append(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

@ -0,0 +1,420 @@
#!/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: 'no'
reload:
description:
- If C(yes), performs a I(/sbin/sysctl -p) if the C(sysctl_file) is
updated. If C(no), does not reload I(sysctl) even if the
C(sysctl_file) is updated.
type: bool
default: 'yes'
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: 'no'
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: no
# 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: yes
# 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: yes
state: present
reload: yes
'''
# ==============================================================
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()