Vendor Galaxy Roles and Collections
This commit is contained in:
parent
c1e1897cda
commit
2aed20393f
3553 changed files with 387444 additions and 2 deletions
|
|
@ -0,0 +1,116 @@
|
|||
# cran.py: install or remove R packages
|
||||
# Homepage: https://github.com/yutannihilation/ansible-module-cran
|
||||
|
||||
# Copyright (C) 2016 Hiroaki Yutani <yutani.ini@gmail.com>
|
||||
# Copyright (C) 2017 Maciej Delmanowski <drybjed@gmail.com>
|
||||
# Copyright (C) 2017 DebOps <https://debops.org/>
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cran
|
||||
short_description: Install R packages.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- The name of an R package.
|
||||
required: true
|
||||
default: null
|
||||
state:
|
||||
description:
|
||||
- The state of module
|
||||
required: false
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
repo:
|
||||
description:
|
||||
- The repository
|
||||
required: false
|
||||
default: "https://cran.rstudio.com/"
|
||||
'''
|
||||
|
||||
RSCRIPT = '/usr/bin/Rscript'
|
||||
|
||||
|
||||
def get_installed_version(module):
|
||||
cmnd = [RSCRIPT, '--slave', '--no-save', '--no-restore-history', '-e',
|
||||
'p <- installed.packages(); cat(p[p[,1] == "{name:}",'
|
||||
'3])'.format(name=module.params['name'])]
|
||||
(rc, stdout, stderr) = module.run_command(cmnd, check_rc=False)
|
||||
return stdout.strip() if rc == 0 else None
|
||||
|
||||
|
||||
def install(module):
|
||||
cmnd = [RSCRIPT, '--slave', '--no-save', '--no-restore-history', '-e',
|
||||
'install.packages(pkgs="{name:}",repos="{repos:}")'
|
||||
''.format(name=module.params['name'],
|
||||
repos=module.params['repo'])]
|
||||
(rc, stdout, stderr) = module.run_command(cmnd, check_rc=True)
|
||||
return stderr
|
||||
|
||||
|
||||
def uninstall(module):
|
||||
cmnd = [RSCRIPT, '--slave', '--no-save', '--no-restore-history', '-e',
|
||||
'remove.packages(pkgs="{name:}")'
|
||||
''.format(name=module.params['name'])]
|
||||
(rc, stdout, stderr) = module.run_command(cmnd, check_rc=True)
|
||||
return stderr
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
name=dict(required=True),
|
||||
repo=dict(default='https://cran.rstudio.com/')
|
||||
)
|
||||
)
|
||||
state = module.params['state']
|
||||
name = module.params['name']
|
||||
changed = False
|
||||
version = get_installed_version(module)
|
||||
|
||||
if state == 'present' and not version:
|
||||
stderr = install(module)
|
||||
version = get_installed_version(module)
|
||||
if not version:
|
||||
module.fail_json(
|
||||
msg='Failed to install {name:}: {err:}'.format(
|
||||
name=name, err=stderr, version=version))
|
||||
changed = True
|
||||
|
||||
elif state == 'absent' and version:
|
||||
stderr = uninstall(module)
|
||||
version = get_installed_version(module)
|
||||
if version:
|
||||
module.fail_json(
|
||||
msg='Failed to install {name:}: {err:}'.format(
|
||||
name=name, err=stderr))
|
||||
changed = True
|
||||
|
||||
module.exit_json(changed=changed, name=name, version=version)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,409 @@
|
|||
# Copyright (c) 2017-2018, Yann Amar <quidame@poivron.org>
|
||||
# Copyright (c) 2019, Maciej Delmanowski <drybjed@gmail.com>
|
||||
# Copyright (c) 2019, DebOps https://debops.org/
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# GNU General Public License v3.0+
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# Source: https://github.com/quidame/ansible-module-dpkg_divert
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
import os.path
|
||||
import errno
|
||||
import os
|
||||
import re
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
ANSIBLE_METADATA = {
|
||||
'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'
|
||||
}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: dpkg_divert
|
||||
short_description: Override a package's version of a file
|
||||
description:
|
||||
- A diversion is for C(dpkg) the knowledge that only a given I(package)
|
||||
is allowed to install a file at a given I(path). Other packages shipping
|
||||
their own version of this file will be forced to I(divert) it, i.e. to
|
||||
install it at another location. It allows one to keep changes in a file
|
||||
provided by a debian package by preventing its overwrite at package
|
||||
upgrade.
|
||||
- This module manages diversions of debian packages files using the
|
||||
C(dpkg-divert)(1) commandline tool. It can either create or remove a
|
||||
diversion for a given file, but also update an existing diversion to
|
||||
modify its holder and/or its divert path.
|
||||
- It's a feature of this module to mimic C(dpkg-divert)'s behaviour
|
||||
regarding the renaming of files when removing as well as adding a
|
||||
diversion; existing files are never overwritten.
|
||||
version_added: "2.4"
|
||||
author: "quidame@poivron.org"
|
||||
options:
|
||||
path:
|
||||
description:
|
||||
- The original and absolute path of the file to be diverted or
|
||||
undiverted. This path is unique, i.e. it is not possible to get
|
||||
two diversions for the same I(path).
|
||||
required: true
|
||||
type: 'path'
|
||||
aliases: [ 'name' ]
|
||||
state:
|
||||
description:
|
||||
- When I(state=absent), remove the diversion of the specified
|
||||
I(path); when I(state=present), create the diversion if it does
|
||||
not exist, or update its I(package) holder or I(divert) path,
|
||||
if any, and if I(force) is C(True).
|
||||
- Unless I(force) is C(True), the removal of I(path)'s diversion
|
||||
only happens if the diversion matches the I(divert) and
|
||||
I(package) values, if any.
|
||||
type: 'string'
|
||||
default: 'present'
|
||||
choices: [ 'absent', 'present' ]
|
||||
package:
|
||||
description:
|
||||
- The name of the package whose copy of file is not diverted, also
|
||||
known as the diversion holder or the package the diversion
|
||||
belongs to.
|
||||
- The actual package does not have to be installed or even to exist
|
||||
for its name to be valid. If not specified, the diversion is held
|
||||
by 'LOCAL', which is reserved for local diversions.
|
||||
- Removing or updating a diversion fails if the diversion exists
|
||||
and belongs to another package, unless I(force) is C(True).
|
||||
divert:
|
||||
description:
|
||||
- The location where the versions of file will be diverted.
|
||||
- The default suffix is C(.distrib) for diversions defined by a
|
||||
package and C(.dpkg-divert) for 'LOCAL' diversions.
|
||||
type: 'path'
|
||||
rename:
|
||||
description:
|
||||
- Actually move the file aside (or back).
|
||||
- Renaming is skipped (but module doesn't fail) in case the
|
||||
destination file already exists. This is a C(dpkg-divert)
|
||||
feature, and its purpose is to never overwrite a file. It also
|
||||
makes the command itself idempotent, and the module's I(force)
|
||||
parameter has no effect on this behaviour.
|
||||
- Also, I(rename) is ignored if the diversion entry is unchanged
|
||||
in the diversion database (adding an already existing diversion
|
||||
or removing a non-existing one).
|
||||
type: 'bool'
|
||||
default: true
|
||||
delete:
|
||||
description:
|
||||
- When I(yes), delete the file in place of the original before
|
||||
reverting. This only applies with I(state=absent) to avoid
|
||||
C(dpkg-divert) command complaining about existing file in place
|
||||
of the diverted one.
|
||||
type: 'bool'
|
||||
default: false
|
||||
force:
|
||||
description:
|
||||
- Force to divert file when diversion already exists and is hold
|
||||
by another I(package) or points to another I(divert). There is
|
||||
no need to use it for I(remove) action if I(divert) or I(package)
|
||||
are not used.
|
||||
- This doesn't override the rename's lock feature, i.e. it doesn't
|
||||
help to force I(rename), but only to force the diversion for
|
||||
dpkg.
|
||||
type: 'bool'
|
||||
default: false
|
||||
requirements: [ dpkg-divert, env ]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Divert /etc/screenrc to /etc/screenrc.dpkg-divert and rename the file
|
||||
- name: Create local diversion
|
||||
dpkg_divert: path=/etc/screenrc
|
||||
|
||||
# Divert /etc/screenrc to /etc/screenrc.distrib for package 'branding' and
|
||||
# rename the file
|
||||
- name: Create diversion for APT package
|
||||
dpkg_divert:
|
||||
name: /etc/screenrc
|
||||
package: branding
|
||||
|
||||
- name: Delete the file in place of the original and remove the diversion
|
||||
dpkg_divert:
|
||||
name: /etc/screenrc
|
||||
state: absent
|
||||
delete: yes
|
||||
|
||||
- name: remove the screenrc diversion only if belonging to 'branding'
|
||||
dpkg_divert:
|
||||
name: /etc/screenrc
|
||||
package: branding
|
||||
state: absent
|
||||
|
||||
# Divert screenrc to screenrc.dpkg-divert, but don't rename the file
|
||||
- name: Divert with custom rename
|
||||
dpkg_divert:
|
||||
path: /etc/screenrc
|
||||
divert: /etc/screenrc.dpkg-divert
|
||||
rename: no
|
||||
|
||||
# Divert and rename screenrc to screenrc.dpkg-divert, even if diversion is
|
||||
# already set
|
||||
- name: Divert with custom rename
|
||||
dpkg_divert:
|
||||
path: /etc/screenrc
|
||||
divert: /etc/screenrc.dpkg-divert
|
||||
rename: yes
|
||||
force: yes
|
||||
|
||||
# Remove the screenrc diversion and maybe move the diverted file to its
|
||||
# original place
|
||||
- name: Remove diversion and rename file
|
||||
dpkg_divert:
|
||||
path: /etc/screenrc
|
||||
state: absent
|
||||
rename: yes
|
||||
'''
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
# Mimic the behaviour of the dpkg-divert(1) command: '--add' is implicit
|
||||
# when not using '--remove'; '--rename' takes care to never overwrite
|
||||
# existing files; and options are intended to not conflict between them.
|
||||
|
||||
# 'force' is an option of the module, not of the command, and implies to
|
||||
# run the command twice. Its purpose is to allow one to re-divert a file
|
||||
# with another target path or to 'give' it to another package, in one task.
|
||||
# This is very easy because one of the values is unique in the diversion
|
||||
# database, and dpkg-divert itself is idempotent (does nothing when nothing
|
||||
# needs doing).
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
path=dict(required=True, type='path', aliases=['name']),
|
||||
state=dict(required=False, type='str', default='present',
|
||||
choices=['absent', 'present']),
|
||||
package=dict(required=False, type='str', default='LOCAL'),
|
||||
divert=dict(required=False, type='path'),
|
||||
rename=dict(required=False, type='bool', default=True),
|
||||
delete=dict(required=False, type='bool', default=False),
|
||||
force=dict(required=False, type='bool', default=False),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
path = module.params['path']
|
||||
state = module.params['state']
|
||||
package = module.params['package']
|
||||
divert = module.params['divert']
|
||||
rename = module.params['rename']
|
||||
delete = module.params['delete']
|
||||
force = module.params['force']
|
||||
|
||||
DPKG_DIVERT = module.get_bin_path('dpkg-divert', required=True)
|
||||
# We need to parse the command's output, which is localized.
|
||||
# So we have to reset environment variable (LC_ALL).
|
||||
ENVIRONMENT = module.get_bin_path('env', required=True)
|
||||
|
||||
# Start to build the commandline we'll have to run
|
||||
COMMANDLINE = [ENVIRONMENT, 'LC_ALL=C', DPKG_DIVERT, path]
|
||||
|
||||
# Then insert options as requested in the task parameters:
|
||||
if state == 'absent':
|
||||
COMMANDLINE.insert(3, '--remove')
|
||||
elif state == 'present':
|
||||
COMMANDLINE.insert(3, '--add')
|
||||
|
||||
if rename:
|
||||
COMMANDLINE.insert(3, '--rename')
|
||||
|
||||
if divert:
|
||||
COMMANDLINE.insert(3, '--divert')
|
||||
COMMANDLINE.insert(4, divert)
|
||||
else:
|
||||
if package == 'LOCAL':
|
||||
COMMANDLINE.insert(3, '--divert')
|
||||
COMMANDLINE.insert(4, '.'.join([path, 'dpkg-divert']))
|
||||
elif package:
|
||||
COMMANDLINE.insert(3, '--divert')
|
||||
COMMANDLINE.insert(4, '.'.join([path, 'distrib']))
|
||||
|
||||
if package == 'LOCAL':
|
||||
COMMANDLINE.insert(3, '--local')
|
||||
elif package:
|
||||
COMMANDLINE.insert(3, '--package')
|
||||
COMMANDLINE.insert(4, package)
|
||||
|
||||
# dpkg-divert has a useful --test option that we will use in check mode or
|
||||
# when needing to parse output before actually doing anything.
|
||||
TESTCOMMAND = list(COMMANDLINE)
|
||||
TESTCOMMAND.insert(3, '--test')
|
||||
if module.check_mode:
|
||||
COMMANDLINE = list(TESTCOMMAND)
|
||||
|
||||
cmd = ' '.join(COMMANDLINE)
|
||||
|
||||
# `dpkg-divert --listpackage FILE` always returns 0, but not diverted files
|
||||
# provide no output.
|
||||
rc, listpackage, _ = module.run_command(
|
||||
[DPKG_DIVERT, '--listpackage', path])
|
||||
rc, placeholder, _ = module.run_command(TESTCOMMAND)
|
||||
|
||||
# There is probably no need to do more than that. Please read the first
|
||||
# sentence of the next comment for a better understanding of the following
|
||||
# `if` statement:
|
||||
if rc == 0 or not force or not listpackage:
|
||||
|
||||
# If requested, delete the file to make way for the reverted one, but
|
||||
# only of the diversion currently exists.
|
||||
if not module.check_mode:
|
||||
if state == 'absent' and listpackage and delete:
|
||||
try:
|
||||
os.unlink(path)
|
||||
except OSError as e:
|
||||
# It may already have been removed
|
||||
if e.errno != errno.ENOENT:
|
||||
raise AnsibleModuleError(
|
||||
results={'msg': "unlinking failed: %s "
|
||||
% to_native(e), 'path': path})
|
||||
|
||||
# In the check mode, the 'dpkg-divert' command still tests the
|
||||
# diversion removal for real and returns with an error when a changed
|
||||
# file is in place. In that specific case, we instead simulate a file
|
||||
# deletion and diversion removal ourselves to have the check mode
|
||||
# succeed.
|
||||
if (module.check_mode and state == 'absent' and delete and
|
||||
listpackage and os.path.exists(path)):
|
||||
fake_stdout = ['Deleting', path, 'and', 'removing']
|
||||
if package == 'LOCAL':
|
||||
fake_stdout.append('local')
|
||||
fake_stdout.extend(['diversion', 'of', path, 'to'])
|
||||
if divert:
|
||||
fake_stdout.append(divert)
|
||||
else:
|
||||
if package == 'LOCAL':
|
||||
fake_stdout.append('.'.join([path, 'dpkg-divert']))
|
||||
elif package:
|
||||
fake_stdout.append('.'.join([path, 'distrib']))
|
||||
|
||||
rc, stdout, stderr = [0, ' '.join(fake_stdout), '']
|
||||
else:
|
||||
rc, stdout, stderr = module.run_command(COMMANDLINE, check_rc=True)
|
||||
|
||||
if re.match('^(Leaving|No diversion)', stdout):
|
||||
module.exit_json(changed=False, stdout=stdout,
|
||||
stderr=stderr, cmd=cmd)
|
||||
else:
|
||||
module.exit_json(changed=True, stdout=stdout,
|
||||
stderr=stderr, cmd=cmd)
|
||||
|
||||
# So, here we are: the test failed AND force is true AND a diversion exists
|
||||
# for the file. Anyway, we have to remove it first (then stop here, or add
|
||||
# a new diversion for the same file), and without failure. Cases of failure
|
||||
# with dpkg-divert are:
|
||||
# - The diversion does not belong to the same package (or LOCAL)
|
||||
# - The divert filename is not the same (e.g. path.distrib != path.divert)
|
||||
# So: force removal by stripping '--package' and '--divert' options... and
|
||||
# their arguments. Fortunately, this module accepts only a few parameters,
|
||||
# so we can rebuild a whole command line from scratch at no cost:
|
||||
FORCEREMOVE = [ENVIRONMENT, 'LC_ALL=C', DPKG_DIVERT, '--remove', path]
|
||||
module.check_mode and FORCEREMOVE.insert(3, '--test')
|
||||
rename and FORCEREMOVE.insert(3, '--rename')
|
||||
forcerm = ' '.join(FORCEREMOVE)
|
||||
|
||||
if state == 'absent':
|
||||
rc, stdout, stderr = module.run_command(FORCEREMOVE, check_rc=True)
|
||||
module.exit_json(changed=True, stdout=stdout,
|
||||
stderr=stderr, cmd=forcerm)
|
||||
|
||||
# The situation is that we want to modify the settings (package or divert)
|
||||
# of an existing diversion. dpkg-divert does not handle this, and we have
|
||||
# to remove the diversion and set a new one. First, get state info:
|
||||
rc, truename, _ = module.run_command([DPKG_DIVERT, '--truename', path])
|
||||
rc, rmout, rmerr = module.run_command(FORCEREMOVE, check_rc=True)
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True, cmd=[forcerm, cmd],
|
||||
msg=[rmout,
|
||||
"*** RUNNING IN CHECK MODE ***",
|
||||
"The next step can't be actually performed - "
|
||||
"even dry-run - without error (since the "
|
||||
"previous removal didn't happen) but is "
|
||||
"supposed to achieve the task."])
|
||||
|
||||
old = truename.rstrip()
|
||||
if divert:
|
||||
new = divert
|
||||
else:
|
||||
if package == 'LOCAL':
|
||||
new = '.'.join([path, 'dpkg-divert'])
|
||||
elif package:
|
||||
new = '.'.join([path, 'distrib'])
|
||||
|
||||
# Store state of files as they may change
|
||||
old_exists = os.path.isfile(old)
|
||||
new_exists = os.path.isfile(new)
|
||||
|
||||
# RENAMING NOT REMAINING
|
||||
# The behaviour of this module is to NEVER overwrite a file, i.e. never
|
||||
# change file contents but only file paths and only if not conflicting,
|
||||
# as does dpkg-divert. It means that if there is already a diversion for
|
||||
# a given file and the divert file exists too, the divert file must be
|
||||
# moved from old to new divert paths between the two dpkg-divert commands,
|
||||
# because:
|
||||
#
|
||||
# src = /etc/screenrc (tweaked ; exists)
|
||||
# old = /etc/screentc.distrib (default ; exists)
|
||||
# new = /etc/screenrc.ansible (not existing yet)
|
||||
#
|
||||
# Without extra move:
|
||||
# 1. dpkg-divert --rename --remove src
|
||||
# => dont move old to src because src exists
|
||||
# 2. dpkg-divert --rename --divert new --add src
|
||||
# => move src to new because new doesn't exist
|
||||
# Results:
|
||||
# - old still exists with default contents
|
||||
# - new holds the tweaked contents
|
||||
# - src is missing
|
||||
# => confusing, kind of breakage
|
||||
#
|
||||
# With extra move:
|
||||
# 1. dpkg-divert --rename --remove src
|
||||
# => dont move old to src because src exists
|
||||
# 2. os.path.rename(old, new) [conditional]
|
||||
# => move old to new because new doesn't exist
|
||||
# 3. dpkg-divert --rename --divert new --add src
|
||||
# => dont move src to new because new exists
|
||||
# Results:
|
||||
# - old does not exist anymore
|
||||
# - src is still the same tweaked file
|
||||
# - new exists with default contents
|
||||
# => idempotency for next times, and no breakage
|
||||
#
|
||||
if rename and old_exists and not new_exists:
|
||||
os.rename(old, new)
|
||||
|
||||
rc, stdout, stderr = module.run_command(COMMANDLINE)
|
||||
rc == 0 and module.exit_json(changed=True, stdout=stdout, stderr=stderr,
|
||||
cmd=[forcerm, cmd], msg=[rmout, stdout])
|
||||
|
||||
# Damn! FORCEREMOVE succeeded and COMMANDLINE failed. Try to restore old
|
||||
# state and end up with a 'failed' status anyway.
|
||||
if (rename and (old_exists and not os.path.isfile(old)) and
|
||||
(os.path.isfile(new) and not new_exists)):
|
||||
os.rename(new, old)
|
||||
|
||||
RESTORE = [ENVIRONMENT, 'LC_ALL=C', DPKG_DIVERT, '--divert', old, path]
|
||||
old_pkg = listpackage.rstrip()
|
||||
if old_pkg == "LOCAL":
|
||||
RESTORE.insert(3, '--local')
|
||||
else:
|
||||
RESTORE.insert(3, '--package')
|
||||
RESTORE.insert(4, old_pkg)
|
||||
rename and RESTORE.insert(3, '--rename')
|
||||
|
||||
module.run_command(RESTORE, check_rc=True)
|
||||
module.exit_json(failed=True, changed=True, stdout=stdout,
|
||||
stderr=stderr, cmd=[forcerm, cmd])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,408 @@
|
|||
# Copyright (c) 2016, Peter Sagerson <psagers@ignorare.net>
|
||||
# Copyright (c) 2016, Jiri Tyr <jiri.tyr@gmail.com>
|
||||
# Copyright (c) 2017, Alexander Korinek <noles@a3k.net>
|
||||
# Copyright (c) 2019, Maciej Delmanowski <drybjed@gmail.com>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# GNU General Public License v3.0+
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native, to_bytes
|
||||
|
||||
import traceback
|
||||
import re
|
||||
|
||||
try:
|
||||
import ldap
|
||||
import ldap.sasl
|
||||
|
||||
HAS_LDAP = True
|
||||
except ImportError:
|
||||
HAS_LDAP = False
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = """
|
||||
---
|
||||
module: ldap_attrs
|
||||
short_description: Add or remove multiple LDAP attribute values.
|
||||
description:
|
||||
- Add or remove multiple LDAP attribute values.
|
||||
notes:
|
||||
- This only deals with attributes on existing entries. To add or remove
|
||||
whole entries, see M(ldap_entry).
|
||||
- The default authentication settings will attempt to use a SASL EXTERNAL
|
||||
bind over a UNIX domain socket. This works well with the default Ubuntu
|
||||
install for example, which includes a cn=peercred,cn=external,cn=auth ACL
|
||||
rule allowing root to modify the server configuration. If you need to use
|
||||
a simple bind to access your server, pass the credentials in I(bind_dn)
|
||||
and I(bind_pw).
|
||||
- For I(state=present) and I(state=absent), all value comparisons are
|
||||
performed on the server for maximum accuracy. For I(state=exact), values
|
||||
have to be compared in Python, which obviously ignores LDAP matching
|
||||
rules. This should work out in most cases, but it is theoretically
|
||||
possible to see spurious changes when target and actual values are
|
||||
semantically identical but lexically distinct.
|
||||
version_added: '2.5'
|
||||
author:
|
||||
- Jiri Tyr (@jtyr)
|
||||
- Alexander Korinek (@noles)
|
||||
requirements:
|
||||
- python-ldap
|
||||
options:
|
||||
bind_dn:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- A DN to bind with. If this is omitted, we'll try a SASL bind with
|
||||
the EXTERNAL mechanism. If this is blank, we'll use an anonymous
|
||||
bind.
|
||||
bind_pw:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- The password to use with I(bind_dn).
|
||||
dn:
|
||||
required: true
|
||||
description:
|
||||
- The DN of the entry to modify.
|
||||
server_uri:
|
||||
required: false
|
||||
default: ldapi:///
|
||||
description:
|
||||
- A URI to the LDAP server. The default value lets the underlying
|
||||
LDAP client library look for a UNIX domain socket in its default
|
||||
location.
|
||||
start_tls:
|
||||
required: false
|
||||
choices: ['yes', 'no']
|
||||
default: 'no'
|
||||
description:
|
||||
- If true, we'll use the START_TLS LDAP extension.
|
||||
state:
|
||||
required: false
|
||||
choices: [present, absent, exact]
|
||||
default: present
|
||||
description:
|
||||
- The state of the attribute values. If C(present), all given
|
||||
values will be added if they're missing. If C(absent), all given
|
||||
values will be removed if present. If C(exact), the set of values
|
||||
will be forced to exactly those provided and no others. If
|
||||
I(state=exact) and I(value) is empty, all values for this
|
||||
attribute will be removed.
|
||||
attributes:
|
||||
required: true
|
||||
description:
|
||||
- The attribute(s) and value(s) to add or remove. The complex argument
|
||||
format is required in order to pass a list of strings (see examples).
|
||||
ordered:
|
||||
required: false
|
||||
choices: ['yes', 'no']
|
||||
default: 'no'
|
||||
description:
|
||||
- If C(yes), prepend list values with X-ORDERED index numbers in all
|
||||
attributes specified in the current task. This is useful mostly with
|
||||
I(olcAccess) attribute to easily manage LDAP Access Control Lists.
|
||||
validate_certs:
|
||||
required: false
|
||||
choices: ['yes', 'no']
|
||||
default: 'yes'
|
||||
description:
|
||||
- If C(no), SSL certificates will not be validated. This should only be
|
||||
used on sites using self-signed certificates.
|
||||
"""
|
||||
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Configure directory number 1 for example.com
|
||||
ldap_attrs:
|
||||
dn: olcDatabase={1}hdb,cn=config
|
||||
attributes:
|
||||
olcSuffix: dc=example,dc=com
|
||||
state: exact
|
||||
|
||||
# The complex argument format is required here to pass a list of ACL strings.
|
||||
- name: Set up the ACL
|
||||
ldap_attrs:
|
||||
dn: olcDatabase={1}hdb,cn=config
|
||||
attributes:
|
||||
olcAccess:
|
||||
- >-
|
||||
{0}to attrs=userPassword,shadowLastChange
|
||||
by self write
|
||||
by anonymous auth
|
||||
by dn="cn=admin,dc=example,dc=com" write
|
||||
by * none'
|
||||
- >-
|
||||
{1}to dn.base="dc=example,dc=com"
|
||||
by dn="cn=admin,dc=example,dc=com" write
|
||||
by * read
|
||||
state: exact
|
||||
|
||||
# An alternative approach with automatic X-ORDERED numbering
|
||||
- name: Set up the ACL
|
||||
ldap_attrs:
|
||||
dn: olcDatabase={1}hdb,cn=config
|
||||
attributes:
|
||||
olcAccess:
|
||||
- >-
|
||||
to attrs=userPassword,shadowLastChange
|
||||
by self write
|
||||
by anonymous auth
|
||||
by dn="cn=admin,dc=example,dc=com" write
|
||||
by * none'
|
||||
- >-
|
||||
to dn.base="dc=example,dc=com"
|
||||
by dn="cn=admin,dc=example,dc=com" write
|
||||
by * read
|
||||
ordered: yes
|
||||
state: exact
|
||||
|
||||
- name: Declare some indexes
|
||||
ldap_attrs:
|
||||
dn: olcDatabase={1}hdb,cn=config
|
||||
attributes:
|
||||
olcDbIndex:
|
||||
- objectClass eq
|
||||
- uid eq
|
||||
|
||||
- name: Set up a root user, which we can use later to bootstrap the directory
|
||||
ldap_attrs:
|
||||
dn: olcDatabase={1}hdb,cn=config
|
||||
attributes:
|
||||
olcRootDN: cn=root,dc=example,dc=com
|
||||
olcRootPW: "{SSHA}tabyipcHzhwESzRaGA7oQ/SDoBZQOGND"
|
||||
state: exact
|
||||
|
||||
- name: Get rid of an unneeded attribute
|
||||
ldap_attrs:
|
||||
dn: uid=jdoe,ou=people,dc=example,dc=com
|
||||
attributes:
|
||||
shadowExpire: ""
|
||||
state: exact
|
||||
server_uri: ldap://localhost/
|
||||
bind_dn: cn=admin,dc=example,dc=com
|
||||
bind_pw: password
|
||||
|
||||
#
|
||||
# The same as in the previous example but with the authentication details
|
||||
# stored in the ldap_auth variable:
|
||||
#
|
||||
# ldap_auth:
|
||||
# server_uri: ldap://localhost/
|
||||
# bind_dn: cn=admin,dc=example,dc=com
|
||||
# bind_pw: password
|
||||
- name: Get rid of an unneeded attribute
|
||||
ldap_attrs:
|
||||
dn: uid=jdoe,ou=people,dc=example,dc=com
|
||||
attributes:
|
||||
shadowExpire: ""
|
||||
state: exact
|
||||
params: "{{ ldap_auth }}"
|
||||
"""
|
||||
|
||||
|
||||
RETURN = """
|
||||
modlist:
|
||||
description: list of modified parameters
|
||||
returned: success
|
||||
type: list
|
||||
sample: '[[2, "olcRootDN", ["cn=root,dc=example,dc=com"]]]'
|
||||
"""
|
||||
|
||||
|
||||
class LdapAttr(object):
|
||||
def __init__(self, module):
|
||||
# Shortcuts
|
||||
self.module = module
|
||||
self.bind_dn = self.module.params['bind_dn']
|
||||
self.bind_pw = self.module.params['bind_pw']
|
||||
self.dn = self.module.params['dn']
|
||||
self.server_uri = self.module.params['server_uri']
|
||||
self.start_tls = self.module.params['start_tls']
|
||||
self.state = self.module.params['state']
|
||||
self.verify_cert = self.module.params['validate_certs']
|
||||
self.attrs = self.module.params['attributes']
|
||||
self.ordered = self.module.params['ordered']
|
||||
|
||||
# Establish connection
|
||||
self.connection = self._connect_to_ldap()
|
||||
|
||||
def _order_values(self, values):
|
||||
""" Prepend X-ORDERED index numbers to attribute's values. """
|
||||
ordered_values = []
|
||||
|
||||
if isinstance(values, list):
|
||||
for index, value in enumerate(values):
|
||||
cleaned_value = re.sub(r'^\{\d+\}', '', value)
|
||||
ordered_values.append('{' + str(index) + '}' + cleaned_value)
|
||||
|
||||
return ordered_values
|
||||
|
||||
def _normalize_values(self, values):
|
||||
""" Normalize attribute's values. """
|
||||
norm_values = []
|
||||
|
||||
if isinstance(values, list):
|
||||
if self.ordered:
|
||||
norm_values = list(map(to_bytes,
|
||||
self._order_values(list(map(str,
|
||||
values)))))
|
||||
else:
|
||||
norm_values = list(map(to_bytes, values))
|
||||
elif values != "":
|
||||
norm_values = [to_bytes(str(values))]
|
||||
|
||||
return norm_values
|
||||
|
||||
def add(self):
|
||||
modlist = []
|
||||
for name, values in self.module.params['attributes'].items():
|
||||
norm_values = self._normalize_values(values)
|
||||
for value in norm_values:
|
||||
if self._is_value_absent(name, value):
|
||||
modlist.append((ldap.MOD_ADD, name, value))
|
||||
|
||||
return modlist
|
||||
|
||||
def delete(self):
|
||||
modlist = []
|
||||
for name, values in self.module.params['attributes'].items():
|
||||
norm_values = self._normalize_values(values)
|
||||
for value in norm_values:
|
||||
if self._is_value_present(name, value):
|
||||
modlist.append((ldap.MOD_DELETE, name, value))
|
||||
|
||||
return modlist
|
||||
|
||||
def exact(self):
|
||||
modlist = []
|
||||
for name, values in self.module.params['attributes'].items():
|
||||
norm_values = self._normalize_values(values)
|
||||
try:
|
||||
results = self.connection.search_s(
|
||||
self.dn, ldap.SCOPE_BASE, attrlist=[name])
|
||||
except ldap.LDAPError as e:
|
||||
self.module.fail_json(
|
||||
msg="Cannot search for attribute %s" % name,
|
||||
details=to_native(e))
|
||||
|
||||
current = results[0][1].get(name, [])
|
||||
|
||||
if frozenset(norm_values) != frozenset(current):
|
||||
if len(current) == 0:
|
||||
modlist.append((ldap.MOD_ADD, name, norm_values))
|
||||
elif len(norm_values) == 0:
|
||||
modlist.append((ldap.MOD_DELETE, name, None))
|
||||
else:
|
||||
modlist.append((ldap.MOD_REPLACE, name, norm_values))
|
||||
|
||||
return modlist
|
||||
|
||||
def _is_value_present(self, name, value):
|
||||
""" True if the target attribute has the given value. """
|
||||
try:
|
||||
is_present = bool(
|
||||
self.connection.compare_s(self.dn, name, value))
|
||||
except ldap.NO_SUCH_ATTRIBUTE:
|
||||
is_present = False
|
||||
|
||||
return is_present
|
||||
|
||||
def _is_value_absent(self, name, value):
|
||||
""" True if the target attribute doesn't have the given value. """
|
||||
return not self._is_value_present(name, value)
|
||||
|
||||
def _connect_to_ldap(self):
|
||||
if not self.verify_cert:
|
||||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
||||
|
||||
connection = ldap.initialize(self.server_uri)
|
||||
|
||||
if self.start_tls:
|
||||
try:
|
||||
connection.start_tls_s()
|
||||
except ldap.LDAPError as e:
|
||||
self.module.fail_json(msg="Cannot start TLS.",
|
||||
details=to_native(e))
|
||||
|
||||
try:
|
||||
if self.bind_dn is not None:
|
||||
connection.simple_bind_s(self.bind_dn, self.bind_pw)
|
||||
else:
|
||||
connection.sasl_interactive_bind_s('', ldap.sasl.external())
|
||||
except ldap.LDAPError as e:
|
||||
self.module.fail_json(
|
||||
msg="Cannot bind to the server.", details=to_native(e))
|
||||
|
||||
return connection
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec={
|
||||
'bind_dn': dict(default=None),
|
||||
'bind_pw': dict(default='', no_log=True),
|
||||
'dn': dict(required=True),
|
||||
'params': dict(type='dict'),
|
||||
'server_uri': dict(default='ldapi:///'),
|
||||
'start_tls': dict(default=False, type='bool'),
|
||||
'state': dict(
|
||||
default='present',
|
||||
choices=['present', 'absent', 'exact']),
|
||||
'attributes': dict(required=True, type='dict'),
|
||||
'ordered': dict(default=False, type='bool'),
|
||||
'validate_certs': dict(default=True, type='bool'),
|
||||
},
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
if not HAS_LDAP:
|
||||
module.fail_json(
|
||||
msg="Missing required 'ldap' module (pip install python-ldap)")
|
||||
|
||||
# Update module parameters with user's parameters if defined
|
||||
if 'params' in module.params and isinstance(module.params['params'], dict):
|
||||
module.params.update(module.params['params'])
|
||||
# Remove the params
|
||||
module.params.pop('params', None)
|
||||
|
||||
# Instantiate the LdapAttr object
|
||||
ldap = LdapAttr(module)
|
||||
|
||||
state = module.params['state']
|
||||
|
||||
# Perform action
|
||||
if state == 'present':
|
||||
modlist = ldap.add()
|
||||
elif state == 'absent':
|
||||
modlist = ldap.delete()
|
||||
elif state == 'exact':
|
||||
modlist = ldap.exact()
|
||||
|
||||
changed = False
|
||||
|
||||
if len(modlist) > 0:
|
||||
changed = True
|
||||
|
||||
if not module.check_mode:
|
||||
try:
|
||||
ldap.connection.modify_s(ldap.dn, modlist)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Attribute action failed.",
|
||||
details=to_native(e),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
module.exit_json(changed=changed, modlist=modlist)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue