Vendor Galaxy Roles and Collections
Some checks failed
/ Ansible Lint (push) Failing after 5m45s
/ Ansible Lint (pull_request) Failing after 4m59s

This commit is contained in:
Stefan Bethke 2026-02-06 22:07:16 +01:00
commit 2aed20393f
3553 changed files with 387444 additions and 2 deletions

View file

@ -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()

View file

@ -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()

View file

@ -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()