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,75 @@
# profile_tasks.py: an Ansible plugin for timing tasks
# Copyright (C) 2014 Jharrod LaFon <jharrod.lafon@gmail.com>
# SPDX-License-Identifier: MIT
# https://github.com/jlafon/ansible-profile/
# Included with permission
# The MIT License (MIT)
#
# Copyright (c) 2014 Jharrod LaFon
#
# 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.plugins.callback import CallbackBase
import time
class CallbackModule(CallbackBase):
"""
A plugin for timing tasks
"""
def __init__(self):
self.stats = {}
self.current = None
def playbook_on_task_start(self, name, is_conditional):
"""
Logs the start of each task
"""
if self.current is not None:
# Record the running time of the last executed task
self.stats[self.current] = time.time() - self.stats[self.current]
# Record the start time of the current task
self.current = name
self.stats[self.current] = time.time()
def playbook_on_stats(self, stats):
"""
Prints the timings
"""
# Record the timing of the very last task
if self.current is not None:
self.stats[self.current] = time.time() - self.stats[self.current]
# Sort the tasks by their running time
results = sorted(self.stats.items(),
key=lambda value: value[1], reverse=True)
# Just keep the top 10
results = results[:10]
# Print the timings
for name, elapsed in results:
print("{0:-<70}{1:->9}".format(
'{0} '.format(name),
' {0:.02f}s'.format(elapsed)))

View file

@ -0,0 +1,75 @@
# profile_tasks.py: an Ansible plugin for timing tasks
# Copyright (C) 2014 Jharrod LaFon <jharrod.lafon@gmail.com>
# SPDX-License-Identifier: MIT
# https://github.com/jlafon/ansible-profile/
# Included with permission
# The MIT License (MIT)
#
# Copyright (c) 2014 Jharrod LaFon
#
# 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.plugins.callback import CallbackBase
import time
class CallbackModule(CallbackBase):
"""
A plugin for timing tasks
"""
def __init__(self):
self.stats = {}
self.current = None
def playbook_on_task_start(self, name, is_conditional):
"""
Logs the start of each task
"""
if self.current is not None:
# Record the running time of the last executed task
self.stats[self.current] = time.time() - self.stats[self.current]
# Record the start time of the current task
self.current = name
self.stats[self.current] = time.time()
def playbook_on_stats(self, stats):
"""
Prints the timings
"""
# Record the timing of the very last task
if self.current is not None:
self.stats[self.current] = time.time() - self.stats[self.current]
# Sort the tasks by their running time
results = sorted(self.stats.items(),
key=lambda value: value[1], reverse=True)
# Just keep the top 10
results = results[:10]
# Print the timings
for name, elapsed in results:
print("{0:-<70}{1:->9}".format(
'{0} '.format(name),
' {0:.02f}s'.format(elapsed)))

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,193 @@
# Copyright (C) 2017 Maciej Delmanowski <drybjed@gmail.com>
# Copyright (C) 2017 DebOps <https://debops.org/>
# SPDX-License-Identifier: GPL-3.0-or-later
# This file is part of DebOps.
#
# DebOps is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# DebOps is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with DebOps. If not, see <https://www.gnu.org/licenses/>.
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
from operator import itemgetter
try:
unicode = unicode
except NameError:
# py3
unicode = str
__metaclass__ = type
def _mangle_recipients(name, *args):
"""Modify the recipient address if it's the same as alias
to prevent mail forwarding loops
Ref: https://serverfault.com/questions/471604/
"""
input_args = []
# Flatten the input list
for sublist in list(args):
for item in sublist:
input_args.append(item)
if name in input_args:
return [w.replace(name, '\\' + name) for w in input_args]
else:
return input_args
def _update_recipients(current_data, new_data, *args, **kwargs):
"""Replace the current list of recipients with a new one"""
for selector in args:
if selector in new_data:
new_recipients = ([new_data.get(selector)]
if isinstance(new_data.get(selector),
(str, unicode))
else new_data.get(selector))
current_data.update({'recipients':
_mangle_recipients(current_data.get('name'),
new_recipients)})
def _add_recipients(current_data, new_data, *args, **kwargs):
"""Add mail recipients to an existing list of recipients"""
for selector in args:
if selector in new_data:
current_recipients = current_data.get('recipients', [])
current_recipients.extend([new_data.get(selector)]
if isinstance(new_data.get(selector),
(str, unicode))
else new_data.get(selector))
current_data.update({'recipients':
_mangle_recipients(current_data.get('name'),
current_recipients)})
def _del_recipients(current_data, new_data, *args, **kwargs):
"""Remove mail recipients from an existing list"""
for selector in args:
if selector in new_data:
deleted_recipients = ([new_data.get(selector)]
if isinstance(new_data.get(selector),
(str, unicode))
else new_data.get(selector))
current_recipients = current_data.get('recipients', [])
new_recipients = [x for x in current_recipients
if x not in deleted_recipients]
current_data.update({'recipients': new_recipients})
def etc_aliases_parse_recipients(*args, **kwargs):
"""Return a parsed list of mail aliases and recipients"""
input_args = []
parsed_aliases = {}
# Flatten the input list
for sublist in list(args):
for item in sublist:
input_args.append(item)
for element in input_args:
if isinstance(element, dict):
if (any(x in element for x in ['name', 'alias']) and
element.get('state', 'present') != 'ignore'):
alias_name = element.get('alias', element.get('name'))
current_alias = (parsed_aliases[alias_name].copy()
if alias_name in parsed_aliases
else {})
current_alias.update({
'name': alias_name, # in case of a new entry
'state': element.get('state',
current_alias.get('state',
'present')),
'weight': int(element.get('weight',
current_alias.get('weight', 0))),
'section': element.get('section',
current_alias.get('section',
'unknown'))
})
_update_recipients(current_alias, element, 'dest', 'to')
_add_recipients(current_alias, element,
'add_dest', 'add_to', 'cc', 'bcc')
_del_recipients(current_alias, element,
'del_dest', 'del_to')
if 'real_name' in element or 'real_alias' in element:
current_alias['real_name'] = (
element.get('real_name',
element.get('real_alias')))
if 'comment' in element:
current_alias['comment'] = element.get('comment')
if not current_alias.get('recipients', ''):
current_alias['state'] = 'comment'
parsed_aliases.update({alias_name: current_alias})
# These parameters are special and should not be interpreted
# directly as mail aliases.
elif not all(x in ['name', 'alias', 'state', 'comment',
'section', 'weight', 'dest', 'to',
'add_dest', 'add_to', 'cc', 'bcc',
'del_dest', 'del_to']
for x in element):
for key, value in element.items():
current_alias = parsed_aliases.get(key, {}).copy()
current_alias.update({
'name': key,
'recipients': (_mangle_recipients(
current_alias.get('name'), [value]
if isinstance(value, (str, unicode))
else value)),
'state': 'present',
'weight': int(element.get('weight',
current_alias.get('weight', 0))),
'section': current_alias.get('section', 'unknown')
})
current_alias.update({
'recipients': (_mangle_recipients(
current_alias.get('name'),
([value]
if isinstance(value, (str, unicode))
else value)))
})
if not current_alias.get('recipients', ''):
current_alias['state'] = 'comment'
parsed_aliases[key] = current_alias
# Expand the dictionary of aliases into a list,
# and return sorted by weight.
return sorted(parsed_aliases.values(), key=itemgetter('weight', 'name'))
class FilterModule(object):
"""Register custom filter plugins in Ansible"""
def filters(self):
return {'etc_aliases_parse_recipients': etc_aliases_parse_recipients}

View file

@ -0,0 +1,60 @@
# Copyright (C) 2015 Maciej Delmanowski <drybjed@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <https://www.gnu.org/licenses/>.
from ansible import errors
try:
import fnmatch
except Exception as e:
raise errors.AnsibleFilterError('fnmatch python library not found')
def globmatch_filter(value, pattern):
''' Return string or list of items matching given glob pattern(s). '''
if not isinstance(pattern, (list, tuple)):
pattern = [pattern]
if isinstance(value, (list, tuple)):
_ret = []
for element in pattern:
for entry in value:
if fnmatch.fnmatch(str(entry), str(element)):
if entry not in _ret:
_ret.append(entry)
if _ret:
return _ret
else:
return list()
else:
for element in pattern:
if fnmatch.fnmatch(str(value), str(element)):
return value
class FilterModule(object):
''' Return string or list of items matching given glob pattern(s). '''
def filters(self):
return {
'globmatch': globmatch_filter
}

View file

@ -0,0 +1,152 @@
# (c) 2014 Tim Raasveld <info@webtrein.nl>
# https://github.com/timraasveld/ansible-string-split-filter/
# (c) 2014 Maciej Delmanowski <drybjed@gmail.com>
# https://debops.org/
# SPDX-License-Identifier: CC0-1.0
# License: CC0 1.0 Universal
#
# Statement of Purpose
#
# The laws of most jurisdictions throughout the world automatically confer
# exclusive Copyright and Related Rights (defined below) upon the creator and
# subsequent owner(s) (each and all, an "owner") of an original work of
# authorship and/or a database (each, a "Work").
#
# Certain owners wish to permanently relinquish those rights to a Work for the
# purpose of contributing to a commons of creative, cultural and scientific
# works ("Commons") that the public can reliably and without fear of later
# claims of infringement build upon, modify, incorporate in other works, reuse
# and redistribute as freely as possible in any form whatsoever and for any
# purposes, including without limitation commercial purposes. These owners may
# contribute to the Commons to promote the ideal of a free culture and the
# further production of creative, cultural and scientific works, or to gain
# reputation or greater distribution for their Work in part through the use and
# efforts of others.
#
# For these and/or other purposes and motivations, and without any expectation
# of additional consideration or compensation, the person associating CC0 with
# a Work (the "Affirmer"), to the extent that he or she is an owner of
# Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to
# the Work and publicly distribute the Work under its terms, with knowledge of
# his or her Copyright and Related Rights in the Work and the meaning and
# intended legal effect of CC0 on those rights.
#
# 1. Copyright and Related Rights. A Work made available under CC0 may be
# protected by copyright and related or neighboring rights ("Copyright and
# Related Rights"). Copyright and Related Rights include, but are not limited
# to, the following:
#
# i. the right to reproduce, adapt, distribute, perform, display,
# communicate, and translate a Work;
#
# ii. moral rights retained by the original author(s) and/or performer(s);
#
# iii. publicity and privacy rights pertaining to a person's image or
# likeness depicted in a Work;
#
# iv. rights protecting against unfair competition in regards to a Work,
# subject to the limitations in paragraph 4(a), below;
#
# v. rights protecting the extraction, dissemination, use and reuse of data
# in a Work;
#
# vi. database rights (such as those arising under Directive 96/9/EC of the
# European Parliament and of the Council of 11 March 1996 on the legal
# protection of databases, and under any national implementation thereof,
# including any amended or successor version of such directive); and
#
# vii. other similar, equivalent or corresponding rights throughout the world
# based on applicable law or treaty, and any national implementations
# thereof.
#
# 2. Waiver. To the greatest extent permitted by, but not in contravention of,
# applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
# unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
# and Related Rights and associated claims and causes of action, whether now
# known or unknown (including existing as well as future claims and causes of
# action), in the Work (i) in all territories worldwide, (ii) for the maximum
# duration provided by applicable law or treaty (including future time
# extensions), (iii) in any current or future medium and for any number of
# copies, and (iv) for any purpose whatsoever, including without limitation
# commercial, advertising or promotional purposes (the "Waiver"). Affirmer
# makes the Waiver for the benefit of each member of the public at large and to
# the detriment of Affirmer's heirs and successors, fully intending that such
# Waiver shall not be subject to revocation, rescission, cancellation,
# termination, or any other legal or equitable action to disrupt the quiet
# enjoyment of the Work by the public as contemplated by Affirmer's express
# Statement of Purpose.
#
# 3. Public License Fallback. Should any part of the Waiver for any reason be
# judged legally invalid or ineffective under applicable law, then the Waiver
# shall be preserved to the maximum extent permitted taking into account
# Affirmer's express Statement of Purpose. In addition, to the extent the
# Waiver is so judged Affirmer hereby grants to each affected person
# a royalty-free, non transferable, non sublicensable, non exclusive,
# irrevocable and unconditional license to exercise Affirmer's Copyright and
# Related Rights in the Work (i) in all territories worldwide, (ii) for the
# maximum duration provided by applicable law or treaty (including future time
# extensions), (iii) in any current or future medium and for any number of
# copies, and (iv) for any purpose whatsoever, including without limitation
# commercial, advertising or promotional purposes (the "License"). The License
# shall be deemed effective as of the date CC0 was applied by Affirmer to the
# Work. Should any part of the License for any reason be judged legally invalid
# or ineffective under applicable law, such partial invalidity or
# ineffectiveness shall not invalidate the remainder of the License, and in
# such case Affirmer hereby affirms that he or she will not (i) exercise any of
# his or her remaining Copyright and Related Rights in the Work or (ii) assert
# any associated claims and causes of action with respect to the Work, in
# either case contrary to Affirmer's express Statement of Purpose.
#
# 4. Limitations and Disclaimers.
#
# a. No trademark or patent rights held by Affirmer are waived, abandoned,
# surrendered, licensed or otherwise affected by this document.
#
# b. Affirmer offers the Work as-is and makes no representations or
# warranties of any kind concerning the Work, express, implied, statutory or
# otherwise, including without limitation warranties of title,
# merchantability, fitness for a particular purpose, non infringement, or the
# absence of latent or other defects, accuracy, or the present or absence of
# errors, whether or not discoverable, all to the greatest extent permissible
# under applicable law.
#
# c. Affirmer disclaims responsibility for clearing rights of other persons
# that may apply to the Work or any use thereof, including without limitation
# any person's Copyright and Related Rights in the Work. Further, Affirmer
# disclaims responsibility for obtaining any necessary consents, permissions
# or other rights required for any use of the Work.
#
# d. Affirmer understands and acknowledges that Creative Commons is not a
# party to this document and has no duty or obligation with respect to this
# CC0 or use of the Work.
#
# For more information, please see
# <http://creativecommons.org/publicdomain/zero/1.0/>
import re
def split_string(string, separator=None, maxsplit=-1):
try:
return string.split(separator, maxsplit)
except Exception:
return list(string)
def split_regex(string, seperator_pattern):
try:
return re.split(seperator_pattern, string)
except Exception:
return list(string)
class FilterModule(object):
''' A filter to split a string into a list. '''
def filters(self):
return {
'split': split_string,
'split_regex': split_regex,
}

View file

@ -0,0 +1,53 @@
# Copyright (C) 2017, Matt Martz <matt@sivel.net>
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import functools
from ansible.plugins.inventory.toml import HAS_TOML, toml_dumps
try:
from ansible.plugins.inventory.toml import toml
except ImportError:
pass
from ansible.errors import AnsibleFilterError
from ansible.module_utils._text import to_text
from ansible.module_utils.common._collections_compat import MutableMapping
from ansible.module_utils.six import string_types
def _check_toml(func):
@functools.wraps(func)
def inner(o):
if not HAS_TOML:
raise AnsibleFilterError('The %s filter plugin requires '
'the python "toml" library' % func.__name__)
return func(o)
return inner
@_check_toml
def from_toml(o):
if not isinstance(o, string_types):
raise AnsibleFilterError('from_toml requires a string, got %s' % type(o))
return toml.loads(to_text(o, errors='surrogate_or_strict'))
@_check_toml
def to_toml(o):
if not isinstance(o, MutableMapping):
raise AnsibleFilterError('to_toml requires a dict, got %s' % type(o))
return to_text(toml_dumps(o), errors='surrogate_or_strict')
class FilterModule(object):
def filters(self):
return {
'to_toml': to_toml,
'from_toml': from_toml
}

View file

@ -0,0 +1,117 @@
# Copyright (C) 2021 David Härdeman <david@hardeman.nu>
# Copyright (C) 2021 DebOps <https://debops.org/>
#
# Based on community.general.dig, which is:
# (c) 2015, Jan-Piet Mens <jpmens(at)gmail.com>
# (c) 2017 Ansible Project
#
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import (absolute_import, division, print_function)
from operator import itemgetter
from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
from ansible.module_utils.common.text.converters import to_native
__metaclass__ = type
try:
import dns.exception
import dns.name
import dns.resolver
import dns.reversename
import dns.rdataclass
from dns.rdatatype import SRV
except ImportError:
raise AnsibleError("dig_srv: dnspython library is not installed")
def make_rdata_dict(rdata):
supported_types = {
SRV: ['priority', 'weight', 'port', 'target'],
}
rd = {}
if rdata.rdtype not in supported_types:
raise AnsibleError("dig_srv: unknown rdtype returned")
fields = supported_types[rdata.rdtype]
for f in fields:
val = rdata.__getattribute__(f)
if isinstance(val, dns.name.Name):
val = dns.name.Name.to_text(val)
if f == "target":
rd[f] = val.rstrip('.')
else:
rd[f] = val
return rd
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
if len(terms) != 3:
raise AnsibleError("dig_srv: three arguments expected")
myres = dns.resolver.Resolver(configure=True)
edns_size = 4096
myres.use_edns(0, ednsflags=dns.flags.DO, payload=edns_size)
domain = terms[0]
if not domain.endswith('.'):
domain += '.'
default_domain = terms[1]
default_port = terms[2]
qtype = 'SRV'
rdclass = dns.rdataclass.from_text('IN')
ret = []
try:
answers = myres.query(domain, qtype, rdclass=rdclass)
for rdata in answers:
try:
rd = make_rdata_dict(rdata)
rd['owner'] = answers.canonical_name.to_text().rstrip('.')
rd['type'] = dns.rdatatype.to_text(rdata.rdtype)
rd['ttl'] = answers.rrset.ttl
rd['class'] = dns.rdataclass.to_text(rdata.rdclass)
rd['dig_srv_src'] = 'dns'
ret.append(rd)
except Exception as e:
raise AnsibleError("dig_srv: can't parse response %s" % to_native(e))
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
ret.append({
"class": "IN",
"owner": domain.rstrip('.'),
"port": default_port,
"priority": 0,
"target": default_domain,
"ttl": 0,
"type": "SRV",
"weight": 0,
"dig_srv_src": "fallback"
})
except dns.resolver.Timeout:
raise AnsibleError("dig_srv: timeout")
except dns.exception.DNSException as e:
raise AnsibleError("dig_srv: unhandled exception %s" % to_native(e))
for r in ret:
r.update({"target_port": r["target"] + ":" + str(r["port"])})
# This is in reverse order of importance, i.e. least important first.
# Note that the TTL field shows the remaining TTL when a RR is cached,
# so sorting on that field is not a good idea.
ret.sort(key=itemgetter("port"))
ret.sort(key=itemgetter("target"))
ret.sort(key=itemgetter("weight"), reverse=True)
ret.sort(key=itemgetter("priority"))
return ret

View file

@ -0,0 +1,195 @@
# (c) 2015, Robert Chady <rchady@sitepen.com>
# Based on `runner/lookup_plugins/file.py` for Ansible
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This file is part of Debops.
# This file is NOT part of Ansible yet.
#
# Debops is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Debops. If not, see <https://www.gnu.org/licenses/>.
'''
This file implements the `file_src` lookup filter for Ansible. In difference
to the `file` filter, this searches values based on the `file-paths`
variable (colon separated) as configured in DebOps.
NOTE: This means this filter relies on DebOps.
'''
__author__ = "Robert Chady <rchady@sitepen.com>"
__copyright__ = "Copyright 2015 by Robert Chady <rchady@sitepen.com>"
__license__ = "GNU General Public LIcense version 3 (GPL v3) or later"
import os
try:
import debops
debops_version = debops.__version__.__version__
conf_section = 'override_paths'
conf_key = 'files_path'
except AttributeError:
try:
from debops import *
from debops.cmds import *
debops_version = '2.3.0'
conf_section = 'paths'
conf_key = 'file-paths'
except ImportError:
pass
except ModuleNotFoundError:
conf_section = ''
conf_key = ''
pass
try:
from ansible.plugins.lookup import LookupBase
except ImportError:
LookupBase = object
from packaging.version import parse
from ansible import __version__ as __ansible_version__
if parse(__ansible_version__) < parse("2.0"):
from ansible import utils, errors
class LookupModule(object):
def __init__(self, basedir, *args, **kwargs):
self.basedir = basedir
def run(self, terms, inject=None, **kwargs):
terms = utils.listify_lookup_plugin_terms(
terms, self.basedir, inject)
ret = []
config = {}
places = []
# this can happen if the variable contains a string,
# strictly not desired for lookup plugins, but users may
# try it, so make it work.
if not isinstance(terms, list):
terms = [terms]
try:
project_config = debops.config.Configuration()
project_dir = debops.projectdir.ProjectDir(
config=project_config)
project_root = project_dir.path
if project_dir.config.get(['project', 'type']) == 'modern':
config = project_dir.config.get([])
else:
config = project_dir.config.get(['views', 'system'])
except NameError:
try:
project_root = find_debops_project(required=False)
config = read_config(project_root)
except NameError:
pass
except NotADirectoryError:
# This is not a DebOps project directory, so continue as normal
pass
if conf_section in config and conf_key in config[conf_section]:
custom_places = (
config[conf_section][conf_key].split(':'))
for custom_path in custom_places:
if os.path.isabs(custom_path):
places.append(custom_path)
else:
places.append(os.path.join(
project_root, custom_path))
for term in terms:
if '_original_file' in inject:
relative_path = utils.path_dwim_relative(
inject['_original_file'], 'files',
'', self.basedir, check=False)
places.append(relative_path)
for path in places:
template = os.path.join(path, term)
if template and os.path.exists(template):
ret.append(template)
break
else:
raise errors.AnsibleError(
"could not locate file in lookup: %s"
% term)
return ret
else:
from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
ret = []
config = {}
places = []
# this can happen if the variable contains a string,
# strictly not desired for lookup plugins, but users may
# try it, so make it work.
if not isinstance(terms, list):
terms = [terms]
try:
project_config = debops.config.Configuration()
project_dir = debops.projectdir.ProjectDir(
config=project_config)
project_root = project_dir.path
if project_dir.config.get(['project', 'type']) == 'modern':
config = project_dir.config.get([])
else:
config = project_dir.config.get(['views', 'system'])
except NameError:
try:
project_root = find_debops_project(required=False)
config = read_config(project_root)
except NameError:
pass
except NotADirectoryError:
# This is not a DebOps project directory, so continue as normal
pass
if conf_section in config and conf_key in config[conf_section]:
custom_places = (
config[conf_section][conf_key].split(':'))
for custom_path in custom_places:
if os.path.isabs(custom_path):
places.append(custom_path)
else:
places.append(os.path.join(
project_root, custom_path))
for term in terms:
if 'role_path' in variables:
relative_path = self._loader.path_dwim_relative(
variables['role_path'], 'files', '')
places.append(relative_path)
for path in places:
template = os.path.join(path, term)
if template and os.path.exists(template):
ret.append(template)
break
else:
raise AnsibleError(
"could not locate file in lookup: %s"
% term)
return ret

View file

@ -0,0 +1,66 @@
# Copyright (C) 2012 Michael DeHaan <michael.dehaan@gmail.com>
# Copyright (C) 2015 Hartmut Goebel <h.goebel@crazy-compilers.com>
# SPDX-License-Identifier: GPL-3.0-or-later
# Based on `runner/lookup_plugins/items.py` for Ansible
#
# This file is part of Debops.
# This file is NOT part of Ansible yet.
#
# Debops is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Debops. If not, see <https://www.gnu.org/licenses/>.
'''
This file implements the `with_lists` lookup filter for Ansible. In
differenceto `with_items`, this one does *not* flatten the lists passed to.
Example:
- debug: msg="{{item.0}} -- {{item.1}} -- {{item.2}}"
with_lists:
- ["General", "Verbosity", "0"]
- ["Mapping", "Nobody-User", "nobody"]
- ["Mapping", "Nobody-Group", "nogroup"]
Output (shortened):
"msg": "General -- Verbosity -- 0"
"msg": "Mapping -- Nobody-User -- nobody"
"msg": "Mapping -- Nobody-Group -- nogroup"
'''
import ansible.utils as utils
import ansible.errors as errors
try:
from ansible.plugins.lookup import LookupBase
except ImportError:
LookupBase = object
class LookupModule(LookupBase):
def __init__(self, basedir=None, **kwargs):
self.basedir = basedir
def run(self, terms, inject=None, **kwargs):
terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject)
if not isinstance(terms, (list, set)):
raise errors.AnsibleError("with_list expects a list or a set")
for i, elem in enumerate(terms):
if not isinstance(elem, (list, tuple)):
raise errors.AnsibleError(
"with_list expects a list (or a set) of lists"
" or tuples, but elem %i is not")
return terms

View file

@ -0,0 +1,197 @@
# (c) 2015, Robert Chady <rchady@sitepen.com>
# Based on `runner/lookup_plugins/file.py` for Ansible
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This file is part of Debops.
# This file is NOT part of Ansible yet.
#
# Debops is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Debops. If not, see <https://www.gnu.org/licenses/>.
'''
This file implements the `task_src` lookup filter for Ansible. In difference
to the `file` filter, this searches values based on the `task-paths`
variable (colon separated) as configured in DebOps.
NOTE: This means this filter relies on DebOps.
'''
__author__ = "Robert Chady <rchady@sitepen.com>"
__copyright__ = "Copyright 2015 by Robert Chady <rchady@sitepen.com>"
__license__ = "GNU General Public LIcense version 3 (GPL v3) or later"
import os
try:
import debops
debops_version = debops.__version__.__version__
conf_section = 'override_paths'
conf_key = 'tasks_path'
except AttributeError:
try:
from debops import *
from debops.cmds import *
debops_version = '2.3.0'
conf_section = 'paths'
conf_key = 'task-paths'
except ImportError:
pass
except ModuleNotFoundError:
conf_section = ''
conf_key = ''
pass
try:
from ansible.plugins.lookup import LookupBase
except ImportError:
LookupBase = object
from packaging.version import parse
from ansible import __version__ as __ansible_version__
if parse(__ansible_version__) < parse("2.0"):
from ansible import utils, errors
class LookupModule(object):
def __init__(self, basedir, *args, **kwargs):
self.basedir = basedir
def run(self, terms, inject=None, **kwargs):
terms = utils.listify_lookup_plugin_terms(
terms, self.basedir, inject)
ret = []
config = {}
places = []
# this can happen if the variable contains a string,
# strictly not desired for lookup plugins, but users may
# try it, so make it work.
if not isinstance(terms, list):
terms = [terms]
try:
project_config = debops.config.Configuration()
project_dir = debops.projectdir.ProjectDir(
config=project_config)
project_root = project_dir.path
if project_dir.config.get(['project', 'type']) == 'modern':
config = project_dir.config.get([])
else:
config = project_dir.config.get(['views', 'system'])
except NameError:
try:
project_root = find_debops_project(required=False)
config = read_config(project_root)
except NameError:
pass
except NotADirectoryError:
# This is not a DebOps project directory, so continue as normal
pass
if conf_section in config and conf_key in config[conf_section]:
custom_places = (
config[conf_section][conf_key].split(':'))
for custom_path in custom_places:
if os.path.isabs(custom_path):
places.append(custom_path)
else:
places.append(os.path.join(
project_root, custom_path))
for term in terms:
if '_original_file' in inject:
relative_path = utils.path_dwim_relative(
inject['_original_file'], 'tasks', '',
self.basedir, check=False)
places.append(relative_path)
for path in places:
template = os.path.join(path, term)
if template and os.path.exists(template):
ret.append(template)
break
else:
raise errors.AnsibleError(
"could not locate file in lookup: %s"
% term)
return ret
else:
from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
ret = []
config = {}
places = []
# this can happen if the variable contains a string,
# strictly not desired for lookup plugins, but users may
# try it, so make it work.
if not isinstance(terms, list):
terms = [terms]
try:
project_config = debops.config.Configuration()
project_dir = debops.projectdir.ProjectDir(
config=project_config)
project_root = project_dir.path
if project_dir.config.get(['project', 'type']) == 'modern':
config = project_dir.config.get([])
else:
config = project_dir.config.get(['views', 'system'])
except NameError:
try:
project_root = find_debops_project(required=False)
config = read_config(project_root)
except NameError:
pass
except NotADirectoryError:
# This is not a DebOps project directory, so continue as normal
pass
if conf_section in config and conf_key in config[conf_section]:
custom_places = (
config[conf_section][conf_key].split(':'))
for custom_path in custom_places:
if os.path.isabs(custom_path):
places.append(custom_path)
else:
places.append(os.path.join(
project_root, custom_path))
for term in terms:
if 'role_path' in variables:
relative_path = (
self._loader.path_dwim_relative(
variables['role_path'],
'tasks', ''))
places.append(relative_path)
for path in places:
template = os.path.join(path, term)
if template and os.path.exists(template):
ret.append(template)
break
else:
raise AnsibleError(
"could not locate file in lookup: %s"
% term)
return ret

View file

@ -0,0 +1,199 @@
# (c) 2015, Robert Chady <rchady@sitepen.com>
# Based on `runner/lookup_plugins/file.py` for Ansible
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This file is part of Debops.
# This file is NOT part of Ansible yet.
#
# Debops is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Debops. If not, see <https://www.gnu.org/licenses/>.
'''
This file implements the `template_src` lookup filter for Ansible. In
difference to the `template` filter, this searches values based on the
`template-paths` variable (colon separated) as configured in DebOps.
NOTE: This means this filter relies on DebOps.
'''
__author__ = "Robert Chady <rchady@sitepen.com>"
__copyright__ = "Copyright 2015 by Robert Chady <rchady@sitepen.com>"
__license__ = "GNU General Public LIcense version 3 (GPL v3) or later"
import os
try:
import debops
debops_version = debops.__version__.__version__
conf_section = 'override_paths'
conf_key = 'templates_path'
except AttributeError:
try:
from debops import *
from debops.cmds import *
debops_version = '2.3.0'
conf_section = 'paths'
conf_key = 'template-paths'
except ImportError:
pass
except ModuleNotFoundError:
conf_section = ''
conf_key = ''
pass
try:
from ansible.plugins.lookup import LookupBase
except ImportError:
LookupBase = object
from packaging.version import parse
from ansible import __version__ as __ansible_version__
if parse(__ansible_version__) < parse("2.0"):
from ansible import utils, errors
class LookupModule(object):
def __init__(self, basedir, *args, **kwargs):
self.basedir = basedir
def run(self, terms, inject=None, **kwargs):
terms = utils.listify_lookup_plugin_terms(
terms, self.basedir, inject)
ret = []
config = {}
places = []
# this can happen if the variable contains a string,
# strictly not desired for lookup plugins, but users may
# try it, so make it work.
if not isinstance(terms, list):
terms = [terms]
try:
project_config = debops.config.Configuration()
project_dir = debops.projectdir.ProjectDir(
config=project_config)
project_root = project_dir.path
if project_dir.config.get(['project', 'type']) == 'modern':
config = project_dir.config.get([])
else:
config = project_dir.config.get(['views', 'system'])
except NameError:
try:
project_root = find_debops_project(required=False)
config = read_config(project_root)
except NameError:
pass
except NotADirectoryError:
# This is not a DebOps project directory, so continue as normal
pass
if conf_section in config and conf_key in config[conf_section]:
custom_places = (
config[conf_section][conf_key].split(':'))
for custom_path in custom_places:
if os.path.isabs(custom_path):
places.append(custom_path)
else:
places.append(os.path.join(
project_root, custom_path))
for term in terms:
if '_original_file' in inject:
relative_path = (
utils.path_dwim_relative(
inject['_original_file'], 'templates',
'', self.basedir, check=False))
places.append(relative_path)
for path in places:
template = os.path.join(path, term)
if template and os.path.exists(template):
ret.append(template)
break
else:
raise errors.AnsibleError(
"could not locate file in lookup: %s"
% term)
return ret
else:
from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
ret = []
config = {}
places = []
# this can happen if the variable contains a string,
# strictly not desired for lookup plugins, but users may
# try it, so make it work.
if not isinstance(terms, list):
terms = [terms]
try:
project_config = debops.config.Configuration()
project_dir = debops.projectdir.ProjectDir(
config=project_config)
project_root = project_dir.path
if project_dir.config.get(['project', 'type']) == 'modern':
config = project_dir.config.get([])
else:
config = project_dir.config.get(['views', 'system'])
except NameError:
try:
project_root = find_debops_project(required=False)
config = read_config(project_root)
except NameError:
pass
except NotADirectoryError:
# This is not a DebOps project directory, so continue as normal
pass
if conf_section in config and conf_key in config[conf_section]:
custom_places = (
config[conf_section][conf_key].split(':'))
for custom_path in custom_places:
if os.path.isabs(custom_path):
places.append(custom_path)
else:
places.append(os.path.join(
project_root, custom_path))
for term in terms:
if 'role_path' in variables:
relative_path = (
self._loader.path_dwim_relative(
variables['role_path'], 'templates',
''))
places.append(relative_path)
for path in places:
template = os.path.join(path, term)
if template and os.path.exists(template):
ret.append(template)
break
else:
raise AnsibleError(
"could not locate file in lookup: %s"
% term)
return ret

View file

@ -0,0 +1,206 @@
# Copyright (C) 2013-2014, Christian Berendt <berendt@b1-systems.de>
# Copyright (C) 2022 Julien Lecomte <julien@lecomte.at>
# Copyright (C) 2022 DebOps <https://debops.org/>
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import re
# import module snippets
from ansible.module_utils.basic import AnsibleModule
DOCUMENTATION = '''
---
module: apache2_module
author:
- Christian Berendt (@berendt)
- Ralf Hertel (@n0trax)
- Robin Roth (@robinro)
- Julien Lecomte
short_description: Enables/disables a module of the Apache2 webserver.
description:
- Enables or disables a specified module of the Apache2 webserver.
options:
name:
type: str
description:
- Name of the module to enable/disable as given to C(a2enmod/a2dismod).
required: true
identifier:
type: str
description:
- Deprecated. Ignored.
required: False
force:
description:
- Force disabling of default modules and override Debian warnings.
required: false
type: bool
default: False
state:
type: str
description:
- Desired state of the module.
choices: ['present', 'absent']
default: present
ignore_configcheck:
description:
- Deprecated. Ignored.
type: bool
default: False
requirements: ["a2enmod","a2dismod"]
notes:
- This does not work on RedHat-based distributions.
Whether it works on others depend on whether the C(a2enmod), C(a2dismod),
and C(a2query) tools are available or not.
'''
EXAMPLES = '''
- name: Enable the Apache2 module wsgi
community.general.apache2_module:
state: present
name: wsgi
- name: Disables the Apache2 module wsgi
community.general.apache2_module:
state: absent
name: wsgi
- name: Disable default modules for Debian
community.general.apache2_module:
state: absent
name: autoindex
force: True
'''
RETURN = '''
result:
description: message about action taken
returned: always
type: str
warnings:
description: list of warning messages
returned: when needed
type: list
rc:
description: return code of underlying command
returned: failed
type: int
stdout:
description: stdout of underlying command
returned: failed
type: str
stderr:
description: stderr of underlying command
returned: failed
type: str
'''
_re_threaded = re.compile(r'threaded: *yes')
def _run_threaded(module):
control_binary = _get_ctl_binary(module)
result, stdout, stderr = module.run_command([control_binary, "-V"])
return bool(_re_threaded.search(stdout))
def _get_ctl_binary(module):
ctl_binary = module.get_bin_path('a2query')
if ctl_binary is not None:
return ctl_binary
module.fail_json(msg="a2query not found. Apache query binary is necessary.")
def _module_is_enabled(module):
control_binary = _get_ctl_binary(module)
result, stdout, stderr = module.run_command([control_binary,
"-m", module.params['name']])
if result in [0, 1, 32]:
return result == 0
else:
error_msg = "Error executing %s: %s" % (control_binary, stderr)
module.fail_json(msg=error_msg)
def _set_state(module, state):
name = module.params['name']
force = module.params['force']
want_enabled = state == 'present'
state_string = {'present': 'enabled', 'absent': 'disabled'}[state]
a2mod_binary = {'present': 'a2enmod', 'absent': 'a2dismod'}[state]
success_msg = "Module %s %s" % (name, state_string)
module.run_command_environ_update = dict(LANG='C', LC_ALL='C',
LC_MESSAGES='C', LC_CTYPE='C')
if module.check_mode:
enabled_state = _module_is_enabled(module)
module.exit_json(changed=(enabled_state == want_enabled),
result=success_msg,
warnings=module.warnings)
a2mod_binary_path = module.get_bin_path(a2mod_binary)
if a2mod_binary_path is None:
module.fail_json(msg="%s not found."
+ " "
+ "Perhaps this system does not use %s to manage apache"
% (a2mod_binary, a2mod_binary))
a2mod_binary_cmd = [a2mod_binary_path]
if not want_enabled and force:
# force exists only for a2dismod on debian
a2mod_binary_cmd.append('-f')
result, stdout, stderr = module.run_command(a2mod_binary_cmd + [name])
if result == 0:
module.exit_json(changed=(' already ' not in stdout),
result=success_msg,
warnings=module.warnings)
else:
msg = (
'Failed to set module {name} to {state}:\n'
'{stdout}\n'
).format(
name=name,
state=state_string,
stdout=stdout,
)
module.fail_json(msg=msg,
rc=result,
stdout=stdout,
stderr=stderr)
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(required=True),
force=dict(type='bool', default=False),
state=dict(default='present', choices=['absent', 'present']),
),
supports_check_mode=True,
)
module.warnings = []
name = module.params['name']
if name == 'cgi' and _run_threaded(module):
module.fail_json(msg="Your MPM seems to be threaded."
+ " "
+ "No automatic actions on module cgi possible.")
if module.params['state'] in ['present', 'absent']:
_set_state(module, module.params['state'])
if __name__ == '__main__':
main()

View file

@ -0,0 +1,200 @@
# Copyright (c) 2014, Ilya Barsukov <barsukov@selectel.ru>, Selectel LLC
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <https://www.gnu.org/licenses/>.
# import module snippets
from ansible.module_utils.basic import *
import os
DOCUMENTATION = """
---
module: btrfs_subvolume
short_description: Provides `create` and `delete` subvolumes methods.
description:
- The M(btrfs_subvolume) module takes the command name followed by
a list of space-delimited arguments.
- The given command will be executed on all selected nodes.
version_added: 0.1
author: Ilya Barsukov
options:
state:
description:
- creates or deletes subvolume
required: true
choices: ["present", "absent"]
path:
description:
- subvolume absolute path
required: true
default: null
qgroups:
required: false
description:
- list of qgroup ids, adds the newly created subvolume to a qgroup
default: []
commit:
required: false
description:
- wait for transaction commit at the end of the operation
or of the each
choices: ["each", "after", "no"]
default: "no"
recursive:
required: false
choices: [true, false]
description:
- create or delete subvolumes recursively
default: false
"""
EXAMPLES = """
# Example for Ansible Playbooks.
- name: Recursive create given subvolume path
btrfs_subvolume:
state: 'present'
path: '/storage/test/test1/test2'
qgroups:
- '1/100'
- '1/101'
recursive: True
- name: Delete given Btrfs subvolume
btrfs_subvolume:
state: 'absent'
path: '/storage/test/test1/test2'
commit: 'each'
"""
def get_subvolumes(path, subs=None):
if subs is None:
subs = []
subvolumes = os.listdir(path)
for sub in subvolumes:
sub = os.path.sep.join([path, sub])
if not os.path.isdir(sub):
continue
subs.append(sub)
subs = get_subvolumes(sub, subs=subs)
return subs
def main():
module = AnsibleModule(
argument_spec=dict(
state=dict(required=True, choices=['present', 'absent'],
type='str'),
path=dict(required=True, default=None, type='str'),
qgroups=dict(default=[], type='list'),
commit=dict(default='no', choices=['after', 'each', 'no'],
type='str'),
recursive=dict(default='False', type='bool')
),
supports_check_mode=True
)
result = {
'changed': False,
'commands': [],
'check': module.check_mode
}
param_path = module.params['path'].rstrip(os.path.sep)
if module.params['state'] == 'present':
# Creating subvolume
if not os.path.exists(param_path) and not module.params['recursive']:
cmd = 'btrfs subvolume create {qgroups} {subvolume}'.format(
qgroups=' -i '.join(['']+module.params['qgroups']),
subvolume=param_path,
)
result['commands'].append(cmd)
if not module.check_mode:
module.run_command(cmd, check_rc=True)
result['changed'] = True
elif module.params['recursive']:
# Check parent subvolumes and create it if they doesn't exist
parents = param_path.split(os.path.sep)
for idx, subvolume in enumerate(parents):
if len(subvolume) == 0:
continue
subvolume = os.path.sep.join(parents[:idx+1])
if not os.path.exists(subvolume):
cmd = ('btrfs subvolume create '
'{qgroups} {subvolume}').format(
qgroups=' -i '.join(['']+module.params['qgroups']),
subvolume=subvolume,
)
result['commands'].append(cmd)
if not module.check_mode:
module.run_command(cmd, check_rc=True)
result['changed'] = True
elif module.params['state'] == 'absent':
# Delete subvolume
commit = ''
if module.params['commit'] != 'no':
commit = '--commit-{}'.format(module.params['commit'])
if os.path.exists(param_path):
if not module.params['recursive']:
cmd = 'btrfs subvolume delete {commit} {subvolume}'.format(
commit=commit, subvolume=param_path
)
result['commands'].append(cmd)
if not module.check_mode:
module.run_command(cmd, check_rc=True)
result['changed'] = True
elif module.params['recursive']:
# reversed parent directories from end to beginning
subvolumes = get_subvolumes(param_path)
subvolumes.insert(0, param_path)
for sub in reversed(subvolumes):
if os.path.exists(sub):
cmd = ('btrfs subvolume delete '
'{commit} {subvolume}').format(
commit=commit, subvolume=sub
)
result['commands'].append(cmd)
if not module.check_mode:
module.run_command(cmd, check_rc=True)
result['changed'] = True
if module.check_mode and result['commands']:
result['changed'] = True
if not module.check_mode:
del result['check']
if not result['commands']:
del result['commands']
module.exit_json(**result)
if __name__ == '__main__':
main()

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