diff --git a/.azure-pipelines/azure-pipelines.yml b/.azure-pipelines/azure-pipelines.yml index c0be539..e2ac310 100644 --- a/.azure-pipelines/azure-pipelines.yml +++ b/.azure-pipelines/azure-pipelines.yml @@ -50,13 +50,31 @@ stages: - template: templates/matrix.yml parameters: testFormat: devel/linux/{0}/1 + targets: + - name: CentOS 7 + test: centos7 + - name: Fedora 34 + test: fedora34 + - name: Fedora 35 + test: fedora35 + - name: openSUSE 15 py3 + test: opensuse15 + - name: Ubuntu 18.04 + test: ubuntu1804 + - name: Ubuntu 20.04 + test: ubuntu2004 + - stage: Docker_2_12 + displayName: Docker 2.12 + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + testFormat: 2.12/linux/{0}/1 targets: - name: CentOS 6 test: centos6 - name: CentOS 7 test: centos7 - - name: CentOS 8 - test: centos8 - name: Fedora 33 test: fedora33 - name: Fedora 34 @@ -81,8 +99,6 @@ stages: test: centos6 - name: CentOS 7 test: centos7 - - name: CentOS 8 - test: centos8 - name: Fedora 32 test: fedora32 - name: Fedora 33 @@ -107,8 +123,6 @@ stages: test: centos6 - name: CentOS 7 test: centos7 - - name: CentOS 8 - test: centos8 - name: Fedora 30 test: fedora30 - name: Fedora 31 @@ -133,8 +147,6 @@ stages: test: centos6 - name: CentOS 7 test: centos7 - - name: CentOS 8 - test: centos8 - name: Fedora 30 test: fedora30 - name: Fedora 31 @@ -156,6 +168,24 @@ stages: - template: templates/matrix.yml parameters: testFormat: devel/{0}/1 + targets: + - name: MacOS 12.0 + test: macos/12.0 + - name: RHEL 7.9 + test: rhel/7.9 + - name: RHEL 8.5 + test: rhel/8.5 + - name: FreeBSD 12.3 + test: freebsd/12.3 + - name: FreeBSD 13.0 + test: freebsd/13.0 + - stage: Remote_2_12 + displayName: Remote 2.12 + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + testFormat: 2.12/{0}/1 targets: - name: MacOS 11.1 test: macos/11.1 @@ -195,8 +225,8 @@ stages: targets: - name: OS X 10.11 test: osx/10.11 - - name: RHEL 7.6 - test: rhel/7.6 + - name: RHEL 7.9 + test: rhel/7.9 - name: RHEL 8.2 test: rhel/8.2 - name: FreeBSD 11.1 @@ -214,8 +244,8 @@ stages: targets: - name: OS X 10.11 test: osx/10.11 - - name: RHEL 7.6 - test: rhel/7.6 + - name: RHEL 7.9 + test: rhel/7.9 - name: RHEL 8.1 test: rhel/8.1 - name: FreeBSD 11.1 @@ -230,9 +260,11 @@ stages: - Remote_2_9 - Docker_2_9 - Remote_2_10 - - Remote_2_11 - Docker_2_10 + - Remote_2_11 - Docker_2_11 + - Remote_2_12 + - Docker_2_12 - Remote_devel - Docker_devel jobs: diff --git a/README.md b/README.md index 2c7c169..9144f77 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,8 @@ None -* ansible-core 2.12 (devel) +* ansible-core 2.13 (devel) +* ansible-core 2.12 (stable) * ansible-core 2.11 (stable) * ansible-base 2.10 (stable) * ansible 2.9 (stable) diff --git a/bindep.txt b/bindep.txt index 6632897..62151cb 100644 --- a/bindep.txt +++ b/bindep.txt @@ -1,4 +1,4 @@ # This is a cross-platform list tracking distribution packages needed by tests; # see https://docs.openstack.org/infra/bindep/ for additional information. -rsync [platform:centos-8 platform:rhel-8] +rsync [platform:rhel-8] diff --git a/changelogs/fragments/272-copy_ignore_txt.yml b/changelogs/fragments/272-copy_ignore_txt.yml new file mode 100644 index 0000000..7537d54 --- /dev/null +++ b/changelogs/fragments/272-copy_ignore_txt.yml @@ -0,0 +1,3 @@ +--- +trivial: + - Copy ignore-2.12.txt to ignore-2.13.txt. diff --git a/changelogs/fragments/277_fix_integration_test_on_devel.yml b/changelogs/fragments/277_fix_integration_test_on_devel.yml new file mode 100644 index 0000000..d2b4c69 --- /dev/null +++ b/changelogs/fragments/277_fix_integration_test_on_devel.yml @@ -0,0 +1,3 @@ +--- +trivial: +- Fix integration tests of synchronize and sysctl to address chaging behavior on devel branch (https://github.com/ansible-collections/overview/issues/45). diff --git a/changelogs/fragments/282_fix_unit_test_for_synchronize.yml b/changelogs/fragments/282_fix_unit_test_for_synchronize.yml new file mode 100644 index 0000000..23521af --- /dev/null +++ b/changelogs/fragments/282_fix_unit_test_for_synchronize.yml @@ -0,0 +1,3 @@ +--- +trivial: +- Fix unit tests of synchronize action plugin to use yaml.safe_load(). diff --git a/changelogs/fragments/287_firewalld_requirements.yml b/changelogs/fragments/287_firewalld_requirements.yml new file mode 100644 index 0000000..621701b --- /dev/null +++ b/changelogs/fragments/287_firewalld_requirements.yml @@ -0,0 +1,3 @@ +--- +trivial: +- firewalld - add python-firewall to requirements (https://github.com/ansible-collections/ansible.posix/issues/286). diff --git a/changelogs/fragments/288_mounts_options.yml b/changelogs/fragments/288_mounts_options.yml new file mode 100644 index 0000000..23b6edd --- /dev/null +++ b/changelogs/fragments/288_mounts_options.yml @@ -0,0 +1,3 @@ +--- +trivial: +- mount - remove deprecated option from nfs example diff --git a/changelogs/fragments/297_firewalld_exclusive_options_handling.yml b/changelogs/fragments/297_firewalld_exclusive_options_handling.yml new file mode 100644 index 0000000..4727000 --- /dev/null +++ b/changelogs/fragments/297_firewalld_exclusive_options_handling.yml @@ -0,0 +1,3 @@ +--- +bugfixes: +- firewalld - Refine the handling of exclusive options (https://github.com/ansible-collections/ansible.posix/issues/255). diff --git a/changelogs/fragments/299_seboolean_python3.yml b/changelogs/fragments/299_seboolean_python3.yml new file mode 100644 index 0000000..5680d99 --- /dev/null +++ b/changelogs/fragments/299_seboolean_python3.yml @@ -0,0 +1,3 @@ +--- +bugfixes: +- seboolean - add ``python3-libsemanage`` package dependency for RHEL8+ systems. diff --git a/changelogs/fragments/302_shippable_exit_code.yml b/changelogs/fragments/302_shippable_exit_code.yml new file mode 100644 index 0000000..d1dae9b --- /dev/null +++ b/changelogs/fragments/302_shippable_exit_code.yml @@ -0,0 +1,3 @@ +--- +trivial: +- CI tests - fix exit code to address shellckeck test issue (https://github.com/ansible-collections/ansible.posix/issues/301). diff --git a/changelogs/fragments/304_pep632.yml b/changelogs/fragments/304_pep632.yml new file mode 100644 index 0000000..6c92fdc --- /dev/null +++ b/changelogs/fragments/304_pep632.yml @@ -0,0 +1,3 @@ +--- +bugfixes: +- Use vendored version of ``distutils.version`` instead of the deprecated Python standard library to address PEP 632 (https://github.com/ansible-collections/ansible.posix/issues/303). diff --git a/changelogs/fragments/shell_escape_full_path_for_rsync.yml b/changelogs/fragments/shell_escape_full_path_for_rsync.yml new file mode 100644 index 0000000..d37bee7 --- /dev/null +++ b/changelogs/fragments/shell_escape_full_path_for_rsync.yml @@ -0,0 +1,3 @@ +--- +bugfixes: + - Fix for whitespace in source full path causing error ```code 23) at main.c(1330) [sender=3.2.3]``` (https://github.com/ansible-collections/ansible.posix/pull/278) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..69cb760 --- /dev/null +++ b/codecov.yml @@ -0,0 +1 @@ +comment: false diff --git a/plugins/module_utils/_version.py b/plugins/module_utils/_version.py new file mode 100644 index 0000000..4ee5820 --- /dev/null +++ b/plugins/module_utils/_version.py @@ -0,0 +1,344 @@ +# Vendored copy of distutils/version.py from CPython 3.9.5 +# +# Implements multiple version numbering conventions for the +# Python Module Distribution Utilities. +# +# PSF License (see licenses/PSF-license.txt or https://opensource.org/licenses/Python-2.0) +# + +"""Provides classes to represent module version numbers (one class for +each style of version numbering). There are currently two such classes +implemented: StrictVersion and LooseVersion. + +Every version number class implements the following interface: + * the 'parse' method takes a string and parses it to some internal + representation; if the string is an invalid version number, + 'parse' raises a ValueError exception + * the class constructor takes an optional string argument which, + if supplied, is passed to 'parse' + * __str__ reconstructs the string that was passed to 'parse' (or + an equivalent string -- ie. one that will generate an equivalent + version number instance) + * __repr__ generates Python code to recreate the version number instance + * _cmp compares the current instance with either another instance + of the same class or a string (which will be parsed to an instance + of the same class, thus must follow the same rules) +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import re + +try: + RE_FLAGS = re.VERBOSE | re.ASCII +except AttributeError: + RE_FLAGS = re.VERBOSE + + +class Version: + """Abstract base class for version numbering classes. Just provides + constructor (__init__) and reproducer (__repr__), because those + seem to be the same for all version numbering classes; and route + rich comparisons to _cmp. + """ + + def __init__(self, vstring=None): + if vstring: + self.parse(vstring) + + def __repr__(self): + return "%s ('%s')" % (self.__class__.__name__, str(self)) + + def __eq__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c == 0 + + def __lt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c < 0 + + def __le__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c <= 0 + + def __gt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c > 0 + + def __ge__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c >= 0 + + +# Interface for version-number classes -- must be implemented +# by the following classes (the concrete ones -- Version should +# be treated as an abstract class). +# __init__ (string) - create and take same action as 'parse' +# (string parameter is optional) +# parse (string) - convert a string representation to whatever +# internal representation is appropriate for +# this style of version numbering +# __str__ (self) - convert back to a string; should be very similar +# (if not identical to) the string supplied to parse +# __repr__ (self) - generate Python code to recreate +# the instance +# _cmp (self, other) - compare two version numbers ('other' may +# be an unparsed version string, or another +# instance of your version class) + + +class StrictVersion(Version): + """Version numbering for anal retentives and software idealists. + Implements the standard interface for version number classes as + described above. A version number consists of two or three + dot-separated numeric components, with an optional "pre-release" tag + on the end. The pre-release tag consists of the letter 'a' or 'b' + followed by a number. If the numeric components of two version + numbers are equal, then one with a pre-release tag will always + be deemed earlier (lesser) than one without. + + The following are valid version numbers (shown in the order that + would be obtained by sorting according to the supplied cmp function): + + 0.4 0.4.0 (these two are equivalent) + 0.4.1 + 0.5a1 + 0.5b3 + 0.5 + 0.9.6 + 1.0 + 1.0.4a3 + 1.0.4b1 + 1.0.4 + + The following are examples of invalid version numbers: + + 1 + 2.7.2.2 + 1.3.a4 + 1.3pl1 + 1.3c4 + + The rationale for this version numbering system will be explained + in the distutils documentation. + """ + + version_re = re.compile(r"^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$", RE_FLAGS) + + def parse(self, vstring): + match = self.version_re.match(vstring) + if not match: + raise ValueError("invalid version number '%s'" % vstring) + + (major, minor, patch, prerelease, prerelease_num) = match.group(1, 2, 4, 5, 6) + + if patch: + self.version = tuple(map(int, [major, minor, patch])) + else: + self.version = tuple(map(int, [major, minor])) + (0,) + + if prerelease: + self.prerelease = (prerelease[0], int(prerelease_num)) + else: + self.prerelease = None + + def __str__(self): + if self.version[2] == 0: + vstring = ".".join(map(str, self.version[0:2])) + else: + vstring = ".".join(map(str, self.version)) + + if self.prerelease: + vstring = vstring + self.prerelease[0] + str(self.prerelease[1]) + + return vstring + + def _cmp(self, other): + if isinstance(other, str): + other = StrictVersion(other) + elif not isinstance(other, StrictVersion): + return NotImplemented + + if self.version != other.version: + # numeric versions don't match + # prerelease stuff doesn't matter + if self.version < other.version: + return -1 + else: + return 1 + + # have to compare prerelease + # case 1: neither has prerelease; they're equal + # case 2: self has prerelease, other doesn't; other is greater + # case 3: self doesn't have prerelease, other does: self is greater + # case 4: both have prerelease: must compare them! + + if not self.prerelease and not other.prerelease: + return 0 + elif self.prerelease and not other.prerelease: + return -1 + elif not self.prerelease and other.prerelease: + return 1 + elif self.prerelease and other.prerelease: + if self.prerelease == other.prerelease: + return 0 + elif self.prerelease < other.prerelease: + return -1 + else: + return 1 + else: + raise AssertionError("never get here") + + +# end class StrictVersion + +# The rules according to Greg Stein: +# 1) a version number has 1 or more numbers separated by a period or by +# sequences of letters. If only periods, then these are compared +# left-to-right to determine an ordering. +# 2) sequences of letters are part of the tuple for comparison and are +# compared lexicographically +# 3) recognize the numeric components may have leading zeroes +# +# The LooseVersion class below implements these rules: a version number +# string is split up into a tuple of integer and string components, and +# comparison is a simple tuple comparison. This means that version +# numbers behave in a predictable and obvious way, but a way that might +# not necessarily be how people *want* version numbers to behave. There +# wouldn't be a problem if people could stick to purely numeric version +# numbers: just split on period and compare the numbers as tuples. +# However, people insist on putting letters into their version numbers; +# the most common purpose seems to be: +# - indicating a "pre-release" version +# ('alpha', 'beta', 'a', 'b', 'pre', 'p') +# - indicating a post-release patch ('p', 'pl', 'patch') +# but of course this can't cover all version number schemes, and there's +# no way to know what a programmer means without asking him. +# +# The problem is what to do with letters (and other non-numeric +# characters) in a version number. The current implementation does the +# obvious and predictable thing: keep them as strings and compare +# lexically within a tuple comparison. This has the desired effect if +# an appended letter sequence implies something "post-release": +# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002". +# +# However, if letters in a version number imply a pre-release version, +# the "obvious" thing isn't correct. Eg. you would expect that +# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison +# implemented here, this just isn't so. +# +# Two possible solutions come to mind. The first is to tie the +# comparison algorithm to a particular set of semantic rules, as has +# been done in the StrictVersion class above. This works great as long +# as everyone can go along with bondage and discipline. Hopefully a +# (large) subset of Python module programmers will agree that the +# particular flavour of bondage and discipline provided by StrictVersion +# provides enough benefit to be worth using, and will submit their +# version numbering scheme to its domination. The free-thinking +# anarchists in the lot will never give in, though, and something needs +# to be done to accommodate them. +# +# Perhaps a "moderately strict" version class could be implemented that +# lets almost anything slide (syntactically), and makes some heuristic +# assumptions about non-digits in version number strings. This could +# sink into special-case-hell, though; if I was as talented and +# idiosyncratic as Larry Wall, I'd go ahead and implement a class that +# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is +# just as happy dealing with things like "2g6" and "1.13++". I don't +# think I'm smart enough to do it right though. +# +# In any case, I've coded the test suite for this module (see +# ../test/test_version.py) specifically to fail on things like comparing +# "1.2a2" and "1.2". That's not because the *code* is doing anything +# wrong, it's because the simple, obvious design doesn't match my +# complicated, hairy expectations for real-world version numbers. It +# would be a snap to fix the test suite to say, "Yep, LooseVersion does +# the Right Thing" (ie. the code matches the conception). But I'd rather +# have a conception that matches common notions about version numbers. + + +class LooseVersion(Version): + """Version numbering for anarchists and software realists. + Implements the standard interface for version number classes as + described above. A version number consists of a series of numbers, + separated by either periods or strings of letters. When comparing + version numbers, the numeric components will be compared + numerically, and the alphabetic components lexically. The following + are all valid version numbers, in no particular order: + + 1.5.1 + 1.5.2b2 + 161 + 3.10a + 8.02 + 3.4j + 1996.07.12 + 3.2.pl0 + 3.1.1.6 + 2g6 + 11g + 0.960923 + 2.2beta29 + 1.13++ + 5.5.kw + 2.0b1pl0 + + In fact, there is no such thing as an invalid version number under + this scheme; the rules for comparison are simple and predictable, + but may not always give the results you want (for some definition + of "want"). + """ + + component_re = re.compile(r"(\d+ | [a-z]+ | \.)", re.VERBOSE) + + def __init__(self, vstring=None): + if vstring: + self.parse(vstring) + + def parse(self, vstring): + # I've given up on thinking I can reconstruct the version string + # from the parsed tuple -- so I just store the string here for + # use by __str__ + self.vstring = vstring + components = [x for x in self.component_re.split(vstring) if x and x != "."] + for i, obj in enumerate(components): + try: + components[i] = int(obj) + except ValueError: + pass + + self.version = components + + def __str__(self): + return self.vstring + + def __repr__(self): + return "LooseVersion ('%s')" % str(self) + + def _cmp(self, other): + if isinstance(other, str): + other = LooseVersion(other) + elif not isinstance(other, LooseVersion): + return NotImplemented + + if self.version == other.version: + return 0 + if self.version < other.version: + return -1 + if self.version > other.version: + return 1 + + +# end class LooseVersion diff --git a/plugins/module_utils/firewalld.py b/plugins/module_utils/firewalld.py index 7c34db4..c79a126 100644 --- a/plugins/module_utils/firewalld.py +++ b/plugins/module_utils/firewalld.py @@ -4,11 +4,10 @@ # 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_collections.ansible.posix.plugins.module_utils.version import LooseVersion __metaclass__ = type -# Imports and info for sanity checking -from distutils.version import LooseVersion FW_VERSION = None fw = None diff --git a/plugins/module_utils/version.py b/plugins/module_utils/version.py new file mode 100644 index 0000000..26d8c30 --- /dev/null +++ b/plugins/module_utils/version.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Felix Fontein +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +"""Provide version object to compare version numbers.""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +# Once we drop support for Ansible 2.9, ansible-base 2.10, and ansible-core 2.11, we can +# remove the _version.py file, and replace the following import by +# +# from ansible.module_utils.compat.version import LooseVersion + +from ._version import LooseVersion, StrictVersion diff --git a/plugins/modules/firewalld.py b/plugins/modules/firewalld.py index f7a4a67..39a3b18 100644 --- a/plugins/modules/firewalld.py +++ b/plugins/modules/firewalld.py @@ -128,8 +128,11 @@ notes: The module will not take care of this for you implicitly because that would undo any previously performed immediate actions which were not permanent. Therefore, if you require immediate access to a newly created zone it is recommended you reload firewalld immediately after the zone creation returns with a changed state and before you perform any other immediate, non-permanent actions on that zone. + - This module needs C(python-firewall) or C(python3-firewall) on managed nodes. + It is usually provided as a subset with C(firewalld) from the OS distributor for the OS default Python interpreter. requirements: - firewalld >= 0.2.11 +- python-firewall >= 0.2.11 author: - Adam Miller (@maxamillion) ''' @@ -757,6 +760,10 @@ def main(): target=('zone',), source=('permanent',), ), + mutually_exclusive=[ + ['icmp_block', 'icmp_block_inversion', 'service', 'port', 'port_forward', 'rich_rule', + 'interface', 'masquerade', 'source', 'target'] + ], ) permanent = module.params['permanent'] @@ -813,33 +820,11 @@ def main(): if 'toaddr' in port_forward: port_forward_toaddr = port_forward['toaddr'] - modification_count = 0 - if icmp_block is not None: - modification_count += 1 - if icmp_block_inversion is not None: - modification_count += 1 - if service is not None: - modification_count += 1 - if port is not None: - modification_count += 1 - if port_forward is not None: - modification_count += 1 - if rich_rule is not None: - modification_count += 1 - if interface is not None: - modification_count += 1 - if masquerade is not None: - modification_count += 1 - if source is not None: - modification_count += 1 - if target is not None: - modification_count += 1 - - if modification_count > 1: - module.fail_json( - msg='can only operate on port, service, rich_rule, masquerade, icmp_block, icmp_block_inversion, interface or source at once' - ) - elif (modification_count > 0) and (desired_state in ['absent', 'present']) and (target is None): + modification = False + if any([icmp_block, icmp_block_inversion, service, port, port_forward, rich_rule, + interface, masquerade, source, target]): + modification = True + if modification and desired_state in ['absent', 'present'] and target is None: module.fail_json( msg='absent and present state can only be used in zone level operations' ) @@ -1024,7 +1009,7 @@ def main(): msgs = msgs + transaction_msgs ''' If there are no changes within the zone we are operating on the zone itself ''' - if modification_count == 0 and desired_state in ['absent', 'present']: + if not modification and desired_state in ['absent', 'present']: transaction = ZoneTransaction( module, diff --git a/plugins/modules/firewalld_info.py b/plugins/modules/firewalld_info.py index 68caa1f..6b1535b 100644 --- a/plugins/modules/firewalld_info.py +++ b/plugins/modules/firewalld_info.py @@ -204,8 +204,9 @@ firewalld_info: ''' from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.six import raise_from from ansible.module_utils._text import to_native -from distutils.version import StrictVersion +from ansible_collections.ansible.posix.plugins.module_utils.version import StrictVersion try: diff --git a/plugins/modules/mount.py b/plugins/modules/mount.py index dce7b2a..8b28f12 100644 --- a/plugins/modules/mount.py +++ b/plugins/modules/mount.py @@ -172,7 +172,7 @@ EXAMPLES = r''' ansible.posix.mount: src: 192.168.1.100:/nfs/ssd/shared_data path: /mnt/shared_data - opts: rw,sync,hard,intr + opts: rw,sync,hard state: mounted fstype: nfs @@ -180,7 +180,7 @@ EXAMPLES = r''' ansible.posix.mount: src: 192.168.1.100:/nfs/ssd/shared_data path: /mnt/shared_data - opts: rw,sync,hard,intr + opts: rw,sync,hard boot: no state: mounted fstype: nfs diff --git a/plugins/modules/seboolean.py b/plugins/modules/seboolean.py index ef1b8c6..f4d8cf4 100644 --- a/plugins/modules/seboolean.py +++ b/plugins/modules/seboolean.py @@ -40,6 +40,7 @@ notes: requirements: - libselinux-python - libsemanage-python +- python3-libsemanage author: - Stephen Fromm (@sfromm) ''' @@ -284,7 +285,7 @@ def main(): module.fail_json(msg=missing_required_lib('libselinux-python'), exception=SELINUX_IMP_ERR) if not HAVE_SEMANAGE: - module.fail_json(msg=missing_required_lib('libsemanage-python'), exception=SEMANAGE_IMP_ERR) + module.fail_json(msg=missing_required_lib('libsemanage-python or python3-libsemanage'), exception=SEMANAGE_IMP_ERR) ignore_selinux_state = module.params['ignore_selinux_state'] diff --git a/plugins/modules/synchronize.py b/plugins/modules/synchronize.py index 7b8ae07..6e6d5a4 100644 --- a/plugins/modules/synchronize.py +++ b/plugins/modules/synchronize.py @@ -586,8 +586,8 @@ def main(): if '@' not in dest: dest = os.path.expanduser(dest) - cmd.append(source) - cmd.append(dest) + cmd.append(shlex_quote(source)) + cmd.append(shlex_quote(dest)) cmdstr = ' '.join(cmd) # If we are using password authentication, write the password into the pipe diff --git a/shippable.yml b/shippable.yml index 388d5cf..0eda5dc 100644 --- a/shippable.yml +++ b/shippable.yml @@ -15,7 +15,7 @@ matrix: - env: T=2.9/freebsd/12.0/1 - env: T=2.9/linux/centos6/1 - env: T=2.9/linux/centos7/1 - - env: T=2.9/linux/centos8/1 +# - env: T=2.9/linux/centos8/1 - env: T=2.9/linux/fedora30/1 - env: T=2.9/linux/fedora31/1 - env: T=2.9/linux/opensuse15py2/1 @@ -30,7 +30,7 @@ matrix: - env: T=2.10/freebsd/12.1/1 - env: T=2.10/linux/centos6/1 - env: T=2.10/linux/centos7/1 - - env: T=2.10/linux/centos8/1 +# - env: T=2.10/linux/centos8/1 - env: T=2.10/linux/fedora30/1 - env: T=2.10/linux/fedora31/1 - env: T=2.10/linux/opensuse15py2/1 @@ -45,7 +45,7 @@ matrix: - env: T=devel/freebsd/12.1/1 - env: T=devel/linux/centos6/1 - env: T=devel/linux/centos7/1 - - env: T=devel/linux/centos8/1 +# - env: T=devel/linux/centos8/1 - env: T=devel/linux/fedora30/1 - env: T=devel/linux/fedora31/1 - env: T=devel/linux/opensuse15py2/1 diff --git a/tests/integration/targets/acl/tasks/acl.yml b/tests/integration/targets/acl/tasks/acl.yml index 7770ed4..30cfebb 100644 --- a/tests/integration/targets/acl/tasks/acl.yml +++ b/tests/integration/targets/acl/tasks/acl.yml @@ -23,6 +23,16 @@ group: name: "{{ test_group }}" +- name: Clean up working directory and files + file: + path: "{{ output_dir }}" + state: absent + +- name: Create working directory + file: + path: "{{ output_dir }}" + state: directory + - name: Create ansible file file: path: "{{ test_file }}" diff --git a/tests/integration/targets/firewalld/tasks/source_test_cases.yml b/tests/integration/targets/firewalld/tasks/source_test_cases.yml index f7c4f00..172a47e 100644 --- a/tests/integration/targets/firewalld/tasks/source_test_cases.yml +++ b/tests/integration/targets/firewalld/tasks/source_test_cases.yml @@ -82,4 +82,4 @@ assert: that: - result is not changed - - "result.msg == 'can only operate on port, service, rich_rule, masquerade, icmp_block, icmp_block_inversion, interface or source at once'" + - "result.msg == 'parameters are mutually exclusive: icmp_block|icmp_block_inversion|service|port|port_forward|rich_rule|interface|masquerade|source|target'" diff --git a/tests/integration/targets/selinux/tasks/selinux.yml b/tests/integration/targets/selinux/tasks/selinux.yml index 2d8cfdd..d936ec6 100644 --- a/tests/integration/targets/selinux/tasks/selinux.yml +++ b/tests/integration/targets/selinux/tasks/selinux.yml @@ -26,10 +26,19 @@ state: present - name: TEST 1 | Get current SELinux config file contents + slurp: + src: /etc/sysconfig/selinux + register: selinux_config_original_base64 + +- name: TEST 1 | Register SELinux config and SELinux status set_fact: - selinux_config_original: "{{ lookup('file', '/etc/sysconfig/selinux').split('\n') }}" + selinux_config_original_raw: "{{ selinux_config_original_base64.content | b64decode }}" before_test_sestatus: "{{ ansible_selinux }}" +- name: TEST 1 | Split by line and register original config + set_fact: + selinux_config_original: "{{ selinux_config_original_raw.split('\n') }}" + - debug: var: "{{ item }}" verbosity: 1 @@ -95,8 +104,17 @@ - _disable_test2.reboot_required - name: TEST 1 | Get modified config file + slurp: + src: /etc/sysconfig/selinux + register: selinux_config_after_base64 + +- name: TEST 1 | Register modified config set_fact: - selinux_config_after: "{{ lookup('file', '/etc/sysconfig/selinux').split('\n') }}" + selinux_config_after_raw: "{{ selinux_config_after_base64.content | b64decode }}" + +- name: TEST 1 | Split by line and register modified config + set_fact: + selinux_config_after: "{{ selinux_config_after_raw.split('\n') }}" - debug: var: selinux_config_after @@ -209,8 +227,17 @@ - not _state_test2.reboot_required - name: TEST 2 | Get modified config file + slurp: + src: /etc/sysconfig/selinux + register: selinux_config_after_base64 + +- name: TEST 2 | Register modified config set_fact: - selinux_config_after: "{{ lookup('file', '/etc/sysconfig/selinux').split('\n') }}" + selinux_config_after_raw: "{{ selinux_config_after_base64.content | b64decode }}" + +- name: TEST 2 | Split by line and register modified config + set_fact: + selinux_config_after: "{{ selinux_config_after_raw.split('\n') }}" - debug: var: selinux_config_after diff --git a/tests/integration/targets/synchronize/tasks/main.yml b/tests/integration/targets/synchronize/tasks/main.yml index ac1aa03..125a406 100644 --- a/tests/integration/targets/synchronize/tasks/main.yml +++ b/tests/integration/targets/synchronize/tasks/main.yml @@ -2,16 +2,29 @@ package: name: rsync when: ansible_distribution != "MacOSX" -- name: cleanup old files - shell: rm -rf {{output_dir}}/* +- name: Clean up the working directory and files + file: + path: '{{ output_dir }}' + state: absent +- name: Create the working directory + file: + path: '{{ output_dir }}' + state: directory - name: create test new files - copy: dest={{output_dir}}/{{item}} mode=0644 content="hello world" + copy: + dest: '{{output_dir}}/{{item}}' + mode: '0644' + content: 'hello world' with_items: - foo.txt - bar.txt + - name: synchronize file to new filename - synchronize: src={{output_dir}}/foo.txt dest={{output_dir}}/foo.result + synchronize: + src: '{{output_dir}}/foo.txt' + dest: '{{output_dir}}/foo.result' register: sync_result + delegate_to: '{{ inventory_hostname }}' - assert: that: - '''changed'' in sync_result' @@ -31,9 +44,13 @@ that: - stat_result.stat.exists == True - stat_result.stat.checksum == '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed' + - name: test that the file is not copied a second time - synchronize: src={{output_dir}}/foo.txt dest={{output_dir}}/foo.result + synchronize: + src='{{output_dir}}/foo.txt' + dest='{{output_dir}}/foo.result' register: sync_result + delegate_to: '{{ inventory_hostname }}' - assert: that: - sync_result.changed == False @@ -44,12 +61,14 @@ with_items: - foo.result - bar.result + - name: Synchronize using the mode=push param synchronize: src: '{{output_dir}}/foo.txt' dest: '{{output_dir}}/foo.result' mode: push register: sync_result + delegate_to: '{{ inventory_hostname }}' - assert: that: - '''changed'' in sync_result' @@ -69,12 +88,14 @@ that: - stat_result.stat.exists == True - stat_result.stat.checksum == '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed' + - name: test that the file is not copied a second time synchronize: src: '{{output_dir}}/foo.txt' dest: '{{output_dir}}/foo.result' mode: push register: sync_result + delegate_to: '{{ inventory_hostname }}' - assert: that: - sync_result.changed == False @@ -85,12 +106,14 @@ with_items: - foo.result - bar.result + - name: Synchronize using the mode=pull param synchronize: src: '{{output_dir}}/foo.txt' dest: '{{output_dir}}/foo.result' mode: pull register: sync_result + delegate_to: '{{ inventory_hostname }}' - assert: that: - '''changed'' in sync_result' @@ -110,12 +133,14 @@ that: - stat_result.stat.exists == True - stat_result.stat.checksum == '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed' + - name: test that the file is not copied a second time synchronize: src: '{{output_dir}}/foo.txt' dest: '{{output_dir}}/foo.result' mode: pull register: sync_result + delegate_to: '{{ inventory_hostname }}' - assert: that: - sync_result.changed == False @@ -126,12 +151,16 @@ with_items: - foo.result - bar.result + - name: synchronize files using with_items (issue#5965) - synchronize: src={{output_dir}}/{{item}} dest={{output_dir}}/{{item}}.result + synchronize: + src: '{{output_dir}}/{{item}}' + dest: '{{output_dir}}/{{item}}.result' with_items: - foo.txt - bar.txt register: sync_result + delegate_to: '{{ inventory_hostname }}' - assert: that: - sync_result.changed @@ -151,9 +180,14 @@ with_items: - foo.txt - bar.txt + - name: synchronize files using rsync_path (issue#7182) - synchronize: src={{output_dir}}/foo.txt dest={{output_dir}}/foo.rsync_path rsync_path="sudo rsync" + synchronize: + src: '{{output_dir}}/foo.txt' + dest: '{{output_dir}}/foo.rsync_path' + rsync_path: 'sudo rsync' register: sync_result + delegate_to: '{{ inventory_hostname }}' - assert: that: - '''changed'' in sync_result' @@ -186,6 +220,7 @@ dest: '{{output_dir}}/{{item}}/foo.txt' with_items: - directory_a + delegate_to: '{{ inventory_hostname }}' - name: synchronize files using link_dest synchronize: src: '{{output_dir}}/directory_a/foo.txt' @@ -193,6 +228,7 @@ link_dest: - '{{output_dir}}/directory_a' register: sync_result + delegate_to: '{{ inventory_hostname }}' - name: get stat information for directory_a stat: path: '{{ output_dir }}/directory_a/foo.txt' @@ -214,6 +250,8 @@ - '{{output_dir}}' register: sync_result ignore_errors: true + delegate_to: '{{ inventory_hostname }}' + - assert: that: - sync_result is not changed @@ -227,3 +265,46 @@ - directory_a/foo.txt - directory_a - directory_b + +- name: setup - test for source with working dir with spaces in path + file: + state: directory + path: '{{output_dir}}/{{item}}' + delegate_to: '{{ inventory_hostname }}' + with_items: + - 'directory a' + - 'directory b' +- name: setup - create test new files + copy: + dest: '{{output_dir}}/directory a/{{item}}' + mode: '0644' + content: 'hello world' + with_items: + - foo.txt + delegate_to: '{{ inventory_hostname }}' +- name: copy source with spaces in dir path + synchronize: + src: '{{output_dir}}/directory a/foo.txt' + dest: '{{output_dir}}/directory b/' + delegate_to: '{{ inventory_hostname }}' + register: sync_result + ignore_errors: true +- name: get stat information for directory_b + stat: + path: '{{ output_dir }}/directory b/foo.txt' + register: stat_result_b +- assert: + that: + - '''changed'' in sync_result' + - sync_result.changed == true + - stat_result_b.stat.exists == True + - stat_result_b.stat.checksum == '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed' +- name: Cleanup + file: + state: absent + path: '{{output_dir}}/{{item}}' + with_items: + - 'directory b/foo.txt' + - 'directory a/foo.txt' + - 'directory a' + - 'directory b' diff --git a/tests/integration/targets/sysctl/tasks/main.yml b/tests/integration/targets/sysctl/tasks/main.yml index c8532d3..c9a63c4 100644 --- a/tests/integration/targets/sysctl/tasks/main.yml +++ b/tests/integration/targets/sysctl/tasks/main.yml @@ -123,10 +123,10 @@ that: - sysctl_test2_change_test is not changed - - name: Try sysctl with an invalid value + - name: Try sysctl with an invalid name sysctl: - name: net.ipv4.ip_forward - value: foo + name: test.invalid + value: 1 register: sysctl_test3 ignore_errors: yes @@ -196,10 +196,10 @@ - sysctl_no_value is failed - "sysctl_no_value.msg == 'value cannot be None'" - - name: Try sysctl with an invalid value + - name: Try sysctl with an invalid name sysctl: - name: net.ipv4.ip_forward - value: foo + name: test.invalid + value: 1 sysctl_set: yes register: sysctl_test4 ignore_errors: yes diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt new file mode 100644 index 0000000..0b6905e --- /dev/null +++ b/tests/sanity/ignore-2.13.txt @@ -0,0 +1,8 @@ +plugins/modules/synchronize.py pylint:disallowed-name +plugins/modules/synchronize.py use-argspec-type-path +plugins/modules/synchronize.py validate-modules:doc-default-does-not-match-spec +plugins/modules/synchronize.py validate-modules:nonexistent-parameter-documented +plugins/modules/synchronize.py validate-modules:parameter-type-not-in-doc +plugins/modules/synchronize.py validate-modules:undocumented-parameter +tests/utils/shippable/check_matrix.py replace-urlopen +tests/utils/shippable/timing.py shebang diff --git a/tests/unit/plugins/action/test_synchronize.py b/tests/unit/plugins/action/test_synchronize.py index 39d9697..bc1efca 100644 --- a/tests/unit/plugins/action/test_synchronize.py +++ b/tests/unit/plugins/action/test_synchronize.py @@ -125,7 +125,7 @@ class SynchronizeTester(object): metapath = os.path.join(fixturepath, 'meta.yaml') with open(metapath, 'rb') as f: fdata = f.read() - test_meta = yaml.load(fdata) + test_meta = yaml.safe_load(fdata) # load initial play context vars if '_play_context' in test_meta: diff --git a/tests/utils/shippable/shippable.sh b/tests/utils/shippable/shippable.sh index 3a56ac6..14f2e57 100755 --- a/tests/utils/shippable/shippable.sh +++ b/tests/utils/shippable/shippable.sh @@ -50,7 +50,7 @@ function retry echo "@* -> ${result}" done echo "Command '@*' failed 3 times!" - exit -1 + exit 255 } command -v pip