Vendor Galaxy Roles and Collections
This commit is contained in:
parent
c1e1897cda
commit
2aed20393f
3553 changed files with 387444 additions and 2 deletions
|
|
@ -0,0 +1,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)))
|
||||
|
|
@ -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)))
|
||||
1495
ansible_collections/debops/debops/plugins/connection/lxc_ssh.py
Normal file
1495
ansible_collections/debops/debops/plugins/connection/lxc_ssh.py
Normal file
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
|
|
@ -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}
|
||||
|
|
@ -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
|
||||
}
|
||||
152
ansible_collections/debops/debops/plugins/filter/split.py
Normal file
152
ansible_collections/debops/debops/plugins/filter/split.py
Normal 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,
|
||||
}
|
||||
53
ansible_collections/debops/debops/plugins/filter/toml.py
Normal file
53
ansible_collections/debops/debops/plugins/filter/toml.py
Normal 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
|
||||
}
|
||||
117
ansible_collections/debops/debops/plugins/lookup/dig_srv.py
Normal file
117
ansible_collections/debops/debops/plugins/lookup/dig_srv.py
Normal 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
|
||||
195
ansible_collections/debops/debops/plugins/lookup/file_src.py
Normal file
195
ansible_collections/debops/debops/plugins/lookup/file_src.py
Normal 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
|
||||
66
ansible_collections/debops/debops/plugins/lookup/lists.py
Normal file
66
ansible_collections/debops/debops/plugins/lookup/lists.py
Normal 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
|
||||
197
ansible_collections/debops/debops/plugins/lookup/task_src.py
Normal file
197
ansible_collections/debops/debops/plugins/lookup/task_src.py
Normal 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
|
||||
199
ansible_collections/debops/debops/plugins/lookup/template_src.py
Normal file
199
ansible_collections/debops/debops/plugins/lookup/template_src.py
Normal 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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
116
ansible_collections/debops/debops/plugins/modules/cran.py
Normal file
116
ansible_collections/debops/debops/plugins/modules/cran.py
Normal 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()
|
||||
409
ansible_collections/debops/debops/plugins/modules/dpkg_divert.py
Normal file
409
ansible_collections/debops/debops/plugins/modules/dpkg_divert.py
Normal 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()
|
||||
408
ansible_collections/debops/debops/plugins/modules/ldap_attrs.py
Normal file
408
ansible_collections/debops/debops/plugins/modules/ldap_attrs.py
Normal 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()
|
||||
Loading…
Reference in a new issue