diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c1332ba82..909ffd439 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,11 +36,11 @@ repos: args: ["--filter-files"] - repo: https://github.com/psf/black - rev: 24.4.2 + rev: 24.8.0 hooks: - id: black - repo: https://github.com/pycqa/flake8 - rev: 7.1.0 + rev: 7.1.1 hooks: - id: flake8 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 53f9afdfd..3174e1c67 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,23 @@ Ansible Utils Collection Release Notes .. contents:: Topics +v5.1.0 +====== + +Minor Changes +------------- + +- Allows the cli_parse module to find parser.template_path inside roles or collections when a path relative to the role/collection directory is provided. +- Fix cli_parse module to require a connection. +- Previously, the ansible.utils.ipcut filter only supported IPv6 addresses, leading to confusing error messages when used with IPv4 addresses. This fix ensures that the filter now appropriately handles both IPv4 and IPv6 addresses. +- Removed conditional check for deprecated ansible.netcommon.cli_parse from ansible.utils.cli_parse +- The from_xml filter returns a python dictionary instead of a json string. + +Documentation Changes +--------------------- + +- Add a wildcard mask/hostmask documentation to ipaddr filter doc page to obtain an IP address's wildcard mask/hostmask. + v5.0.0 ====== diff --git a/README.md b/README.md index fd3e39721..db112bdf3 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,19 @@ The Ansible ``ansible.utils`` collection includes a variety of plugins that aid in the management, manipulation and visibility of data for the Ansible playbook developer. +## Communication + +* Join the Ansible forum: + * [Get Help](https://forum.ansible.com/c/help/6): get help or help others. + * [Posts tagged with 'network'](https://forum.ansible.com/tag/network): subscribe to participate in collection-related conversations.``` + * [Ansible Network Automation Working Group](https://forum.ansible.com/g/network-wg/): by joining the team you will automatically get subscribed to the posts tagged with [network](https://forum.ansible.com/tags/network). + * [Social Spaces](https://forum.ansible.com/c/chat/4): gather and interact with fellow enthusiasts. + * [News & Announcements](https://forum.ansible.com/c/news/5): track project-wide announcements including social events. + +* The Ansible [Bullhorn newsletter](https://docs.ansible.com/ansible/devel/community/communication.html#the-bullhorn): used to announce releases and important changes. + +For more information about communication, see the [Ansible communication guide](https://docs.ansible.com/ansible/devel/community/communication.html). + ## Ansible version compatibility diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 040d5fbdc..e54ae4073 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -442,9 +442,35 @@ releases: - Bumping `requires_ansible` to `>=2.15.0`, since previous ansible-core versions are EoL now. release_summary: - "With this release, the minimum required version of `ansible-core` + With this release, the minimum required version of `ansible-core` for this collection is `2.15.0`. The last version known to be compatible with - `ansible-core` versions below `2.15` is v4.1.0." + `ansible-core` versions below `2.15` is v4.1.0. fragments: - bump_215.yaml release_date: "2024-06-10" + 5.1.0: + changes: + doc_changes: + - Add a wildcard mask/hostmask documentation to ipaddr filter doc page to obtain + an IP address's wildcard mask/hostmask. + minor_changes: + - Allows the cli_parse module to find parser.template_path inside roles or collections + when a path relative to the role/collection directory is provided. + - Fix cli_parse module to require a connection. + - Previously, the ansible.utils.ipcut filter only supported IPv6 addresses, + leading to confusing error messages when used with IPv4 addresses. This fix + ensures that the filter now appropriately handles both IPv4 and IPv6 addresses. + - Removed conditional check for deprecated ansible.netcommon.cli_parse from + ansible.utils.cli_parse + - The from_xml filter returns a python dictionary instead of a json string. + fragments: + - 200.yaml + - 203.yaml + - 204.yaml + - 324.yaml + - 358_ipcut.yaml + - add_template_path.yaml + - fix_cli_parse.yaml + - fix_from_xml.yaml + - todo_condition.yml + release_date: "2024-08-05" diff --git a/changelogs/fragments/0-readme.yml b/changelogs/fragments/0-readme.yml new file mode 100644 index 000000000..6ae0307d2 --- /dev/null +++ b/changelogs/fragments/0-readme.yml @@ -0,0 +1,3 @@ +--- +trivial: + - README.md - Add Communication section with Forum information. diff --git a/changelogs/fragments/200.yaml b/changelogs/fragments/200.yaml deleted file mode 100644 index d2f65f317..000000000 --- a/changelogs/fragments/200.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -trivial: - - Add integration tests for replace_keys diff --git a/changelogs/fragments/203.yaml b/changelogs/fragments/203.yaml deleted file mode 100644 index 7200cd8e3..000000000 --- a/changelogs/fragments/203.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -trivial: - - Add integration tests for keep_keys diff --git a/changelogs/fragments/204.yaml b/changelogs/fragments/204.yaml deleted file mode 100644 index 378f5648c..000000000 --- a/changelogs/fragments/204.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -trivial: - - Add integration tests for remove_keys diff --git a/changelogs/fragments/324.yaml b/changelogs/fragments/324.yaml deleted file mode 100644 index dd3f9c1d3..000000000 --- a/changelogs/fragments/324.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -doc_changes: - - Add a wildcard mask/hostmask documentation to ipaddr filter doc page to obtain an IP address's wildcard mask/hostmask. diff --git a/changelogs/fragments/add_template_path.yaml b/changelogs/fragments/add_template_path.yaml deleted file mode 100644 index c408d407a..000000000 --- a/changelogs/fragments/add_template_path.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -minor_changes: - - Allows the cli_parse module to find parser.template_path inside roles or collections when a path relative to the role/collection directory is provided. diff --git a/changelogs/fragments/fix_cli_parse.yaml b/changelogs/fragments/fix_cli_parse.yaml deleted file mode 100644 index 920f639ae..000000000 --- a/changelogs/fragments/fix_cli_parse.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -minor_changes: - - Fix cli_parse module to require a connection. diff --git a/changelogs/fragments/fix_from_xml.yaml b/changelogs/fragments/fix_from_xml.yaml deleted file mode 100644 index 9553d0a95..000000000 --- a/changelogs/fragments/fix_from_xml.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -minor_changes: - - The from_xml filter returns a python dictionary instead of a json string. diff --git a/changelogs/fragments/todo_condition.yml b/changelogs/fragments/todo_condition.yml deleted file mode 100644 index a5fa6a2c0..000000000 --- a/changelogs/fragments/todo_condition.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -minor_changes: - - Removed conditional check for deprecated ansible.netcommon.cli_parse from ansible.utils.cli_parse diff --git a/galaxy.yml b/galaxy.yml index ef5511360..d59b136d0 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -19,4 +19,4 @@ tags: - data - validation - utils -version: 5.0.0 +version: 5.1.0 diff --git a/plugins/filter/ipcut.py b/plugins/filter/ipcut.py index 4e61a45d4..b8d47a7e7 100644 --- a/plugins/filter/ipcut.py +++ b/plugins/filter/ipcut.py @@ -105,10 +105,15 @@ def _ipcut(*args, **kwargs): def ipcut(value, amount): - ipv6_oct = [] try: ip = netaddr.IPAddress(value) - ipv6address = ip.bits().replace(":", "") + if ip.version == 6: + ip_bits = ip.bits().replace(":", "") + elif ip.version == 4: + ip_bits = ip.bits().replace(".", "") + else: + msg = "Unknown IP Address Version: {0}".format(ip.version) + raise AnsibleFilterError(msg) except (netaddr.AddrFormatError, ValueError): msg = "You must pass a valid IP address; {0} is invalid".format(value) raise AnsibleFilterError(msg) @@ -120,20 +125,27 @@ def ipcut(value, amount): raise AnsibleFilterError(msg) else: if amount < 0: - ipsub = ipv6address[amount:] + ipsub = ip_bits[amount:] else: - ipsub = ipv6address[0:amount] - - ipsubfinal = [] - - for i in range(0, len(ipsub), 16): - oct_sub = i + 16 - ipsubfinal.append(ipsub[i:oct_sub]) - - for i in ipsubfinal: - x = hex(int(i, 2)) - ipv6_oct.append(x.replace("0x", "")) - return str(":".join(ipv6_oct)) + ipsub = ip_bits[0:amount] + + if ip.version == 6: + ipv4_oct = [] + for i in range(0, len(ipsub), 16): + oct_sub = i + 16 + ipv4_oct.append( + hex(int(ipsub[i:oct_sub], 2)).replace("0x", ""), + ) + result = str(":".join(ipv4_oct)) + else: # ip.version == 4: + ipv4_oct = [] + for i in range(0, len(ipsub), 8): + oct_sub = i + 8 + ipv4_oct.append( + str(int(ipsub[i:oct_sub], 2)), + ) + result = str(".".join(ipv4_oct)) + return result class FilterModule(object): diff --git a/plugins/filter/next_nth_usable.py b/plugins/filter/next_nth_usable.py index 1f766d02b..492714871 100644 --- a/plugins/filter/next_nth_usable.py +++ b/plugins/filter/next_nth_usable.py @@ -118,17 +118,23 @@ def next_nth_usable(value, offset): Returns the next nth usable ip within a network described by value. """ try: + v = None vtype = ipaddr(value, "type") if vtype == "address": v = ipaddr(value, "cidr") elif vtype == "network": v = ipaddr(value, "subnet") - v = netaddr.IPNetwork(v) + if v is not None: + v = netaddr.IPNetwork(v) + else: + return False except Exception: return False + if not isinstance(offset, int): raise AnsibleFilterError("Must pass in an integer") + if v.size > 1: first_usable, last_usable = _first_last(v) nth_ip = int(netaddr.IPAddress(int(v.ip) + offset)) diff --git a/plugins/filter/previous_nth_usable.py b/plugins/filter/previous_nth_usable.py index 6f767dd22..2533bb77a 100644 --- a/plugins/filter/previous_nth_usable.py +++ b/plugins/filter/previous_nth_usable.py @@ -117,13 +117,18 @@ def previous_nth_usable(value, offset): Returns the previous nth usable ip within a network described by value. """ try: + v = None vtype = ipaddr(value, "type") if vtype == "address": v = ipaddr(value, "cidr") elif vtype == "network": v = ipaddr(value, "subnet") - v = netaddr.IPNetwork(v) + if v is not None: + v = netaddr.IPNetwork(v) + else: + return False + except Exception: return False diff --git a/plugins/plugin_utils/from_xml.py b/plugins/plugin_utils/from_xml.py index eb1194ec3..c2616294d 100644 --- a/plugins/plugin_utils/from_xml.py +++ b/plugins/plugin_utils/from_xml.py @@ -44,7 +44,7 @@ def from_xml(data, engine): if not HAS_XMLTODICT: _raise_error("Missing required library xmltodict") try: - res = xmltodict.parse(data) + res = xmltodict.parse(data, dict_constructor=dict) except Exception: _raise_error("Input Xml is not valid") return res diff --git a/tests/integration/targets/utils_from_xml/tasks/simple.yaml b/tests/integration/targets/utils_from_xml/tasks/simple.yaml index edc529e71..aae1074de 100644 --- a/tests/integration/targets/utils_from_xml/tasks/simple.yaml +++ b/tests/integration/targets/utils_from_xml/tasks/simple.yaml @@ -19,10 +19,12 @@ - name: Integration tests with and without default engine as xmltodict and ansible.builtin.assert: - that: "{{ output == item.test }}" + that: "{{ output == test_item.test }}" loop: - test: "{{ data | ansible.utils.from_xml() }}" - test: "{{ data | ansible.utils.from_xml('xmltodict') }}" + loop_control: + loop_var: test_item - name: Setup invalid xml as input to ansible.utils.from_xml. ansible.builtin.set_fact: diff --git a/tests/integration/targets/utils_keep_keys/tasks/complex1.yaml b/tests/integration/targets/utils_keep_keys/tasks/complex1.yaml index 61050b95f..7bb6df03d 100644 --- a/tests/integration/targets/utils_keep_keys/tasks/complex1.yaml +++ b/tests/integration/targets/utils_keep_keys/tasks/complex1.yaml @@ -4,126 +4,85 @@ file: complex.yaml - name: Test keep_keys - vars: - result: "{{ l1 | ansible.utils.keep_keys(target=['p1', 'p2']) }}" - target: - - { p1: a, p2: a } - - { p1: b, p2: b } - - { p1: c, p2: c } tags: keep_keys block: + - name: Setup data as facts for complex keep_keys integration test l1 + ansible.builtin.set_fact: + l1: + - { p1: a, p2: a, p3: a } + - { p1: b, p2: b, p3: b } + - { p1: c, p2: c, p3: c } + - name: Test keep_keys debug ansible.builtin.debug: - var: result|to_yaml - when: debug_test|d(false)|bool + msg: "{{ l1 | ansible.utils.keep_keys(target=['p1', 'p2']) }}" + register: result + - name: Test keep_keys assert ansible.builtin.assert: - that: result == target + that: result['msg'] == keep['l1'] -- name: Test map keep_keys - vars: - result: "{{ l2 | map('ansible.utils.keep_keys', target=['p1', 'p2']) | list }}" - target: - - - { p1: a, p2: a } - - { p1: b, p2: b } - - { p1: c, p2: c } - - - { p1: a, p2: a } - - { p1: b, p2: b } - - { p1: c, p2: c } - - - { p1: a, p2: a } - - { p1: b, p2: b } - - { p1: c, p2: c } - tags: map_keep_keys - block: - - name: Test map keep_keys debug + - name: Debug l1 for starts_with 'p' ansible.builtin.debug: - var: result|to_yaml - when: debug_test|d(false)|bool - - name: Test map keep_keys assert - ansible.builtin.assert: - that: result == target + msg: "{{ l1 | ansible.utils.keep_keys(target=['p1', 'p2'], matching_parameter='starts_with') }}" + register: result_l1_starts_with_p -- name: Test keep_keys starts_with - vars: - result: "{{ l1 | ansible.utils.keep_keys(target=['p1', 'p2'], matching_parameter='starts_with') }}" - target: - - { p1: a, p2: a } - - { p1: b, p2: b } - - { p1: c, p2: c } - tags: keep_keys_starts_with_2 - block: - - name: Test keep_keys starts_with debug - ansible.builtin.debug: - var: result|to_yaml - when: debug_test|d(false)|bool - - name: Test keep_keys starts_with assert + - name: Assert result dicts l1 for starts_with 'p' ansible.builtin.assert: - that: result == target + that: + - result_l1_starts_with_p['msg'] == keep['l1_starts_with_p'] -- name: Test keep_keys starts_with 'p' - vars: - result: "{{ l1 | ansible.utils.keep_keys(target=['p'], matching_parameter='starts_with') }}" - target: - - { p1: a, p2: a, p3: a } - - { p1: b, p2: b, p3: b } - - { p1: c, p2: c, p3: c } - tags: keep_keys_starts_with_1 - block: - - name: Test keep_keys starts_with 'p' debug + - name: Debug l1 for ends_with ansible.builtin.debug: - var: result|to_yaml - when: debug_test|d(false)|bool - - name: Test keep_keys starts_with 'p' assert + msg: "{{ l1 | ansible.utils.keep_keys(target=['p1', 'p2'], matching_parameter='ends_with') }}" + register: result_l1_ends_with + + - name: Assert result dicts l1 for ends_with ansible.builtin.assert: - that: result == target + that: + - result_l1_ends_with['msg'] == keep['l1_ends_with'] -- name: Test keep_keys ends_with - vars: - result: "{{ l1 | ansible.utils.keep_keys(target=['p1', 'p2'], matching_parameter='ends_with') }}" - target: - - { p1: a, p2: a } - - { p1: b, p2: b } - - { p1: c, p2: c } - tags: keep_keys_ends_with_2 - block: - - name: Test keep_keys end_with debug + - name: Debug l1 for ends_with '2' ansible.builtin.debug: - var: result|to_yaml - when: debug_test|d(false)|bool - - name: Test keep_keys end_with assert + msg: "{{ l1 | ansible.utils.keep_keys(target=['2'], matching_parameter='ends_with') }}" + register: result_l1_ends_with_2 + + - name: Assert result dicts l1 for ends_with '2' ansible.builtin.assert: - that: result == target + that: + - result_l1_ends_with_2['msg'] == keep['l1_ends_with_2'] -- name: Test keep_keys ends_with '2' - vars: - result: "{{ l1 | ansible.utils.keep_keys(target=['2'], matching_parameter='ends_with') }}" - target: - - { p2: a } - - { p2: b } - - { p2: c } - tags: keep_keys_ends_with_1 - block: - - name: Test keep_keys end_with '2' debug + - name: Test keep_keys regex ansible.builtin.debug: - var: result|to_yaml - when: debug_test|d(false)|bool - - name: Test keep_keys end_with '2' assert + msg: "{{ l1 | ansible.utils.keep_keys(target=['p1', 'p2'], matching_parameter='regex') }}" + register: result_l1_regex + + - name: Assert result dicts l1 for regex ansible.builtin.assert: - that: result == target + that: + - result_l1_regex['msg'] == keep['l1_regex'] -- name: Test keep_keys regex - vars: - result: "{{ l1 | ansible.utils.keep_keys(target=['p1', 'p2'], matching_parameter='regex') }}" - target: - - { p1: a, p2: a } - - { p1: b, p2: b } - - { p1: c, p2: c } - tags: keep_keys_regex +- name: Test map keep_keys + tags: map_keep_keys block: - - name: Test keep_keys regex debug + - name: Setup data as facts for complex keep integration test l2 + ansible.builtin.set_fact: + l2: + - - { p1: a, p2: a, p3: a } + - { p1: b, p2: b, p3: b } + - { p1: c, p2: c, p3: c } + - - { p1: a, p2: a, p3: a } + - { p1: b, p2: b, p3: b } + - { p1: c, p2: c, p3: c } + - - { p1: a, p2: a, p3: a } + - { p1: b, p2: b, p3: b } + - { p1: c, p2: c, p3: c } + + - name: Test map keep_keys debug ansible.builtin.debug: - var: result|to_yaml - when: debug_test|d(false)|bool - - name: Test keep_keys regex assert + msg: "{{ l2 | map('ansible.utils.keep_keys', target=['p1', 'p2']) | list }}" + register: result + + - name: Test map keep_keys assert ansible.builtin.assert: - that: result == target + that: result['msg'] == keep['l2'] diff --git a/tests/integration/targets/utils_keep_keys/vars/complex.yaml b/tests/integration/targets/utils_keep_keys/vars/complex.yaml index 8bdec7cf2..aebfc0635 100644 --- a/tests/integration/targets/utils_keep_keys/vars/complex.yaml +++ b/tests/integration/targets/utils_keep_keys/vars/complex.yaml @@ -1,19 +1,36 @@ --- -l1: - - { p1: a, p2: a, p3: a } - - { p1: b, p2: b, p3: b } - - { p1: c, p2: c, p3: c } -l2: - - - { p1: a, p2: a, p3: a } - - { p1: b, p2: b, p3: b } - - { p1: c, p2: c, p3: c } - - - { p1: a, p2: a, p3: a } - - { p1: b, p2: b, p3: b } - - { p1: c, p2: c, p3: c } - - - { p1: a, p2: a, p3: a } - - { p1: b, p2: b, p3: b } - - { p1: c, p2: c, p3: c } -l3: - - { p1_key_o1: a, p2_key_o2: a, p3_key_o3: a } - - { p1_key_o4: b, p2_key_o5: b, p3_key_o6: b } - - { p1_key_o7: c, p2_key_o8: c, p3_key_o9: c } +keep: + l1: + - { p1: a, p2: a } + - { p1: b, p2: b } + - { p1: c, p2: c } + l2: + - - { p1: a, p2: a } + - { p1: b, p2: b } + - { p1: c, p2: c } + - - { p1: a, p2: a } + - { p1: b, p2: b } + - { p1: c, p2: c } + - - { p1: a, p2: a } + - { p1: b, p2: b } + - { p1: c, p2: c } + l3: + - { p1_key_o1: a, p2_key_o2: a, p3_key_o3: a } + - { p1_key_o4: b, p2_key_o5: b, p3_key_o6: b } + - { p1_key_o7: c, p2_key_o8: c, p3_key_o9: c } + l1_starts_with_p: + - { p1: a, p2: a } + - { p1: b, p2: b } + - { p1: c, p2: c } + l1_ends_with: + - { p1: a, p2: a } + - { p1: b, p2: b } + - { p1: c, p2: c } + l1_ends_with_2: + - { p2: a } + - { p2: b } + - { p2: c } + l1_regex: + - { p1: a, p2: a } + - { p1: b, p2: b } + - { p1: c, p2: c } diff --git a/tests/unit/plugins/filter/test_ipcut.py b/tests/unit/plugins/filter/test_ipcut.py index c086833cf..4a6687892 100644 --- a/tests/unit/plugins/filter/test_ipcut.py +++ b/tests/unit/plugins/filter/test_ipcut.py @@ -21,16 +21,30 @@ class TestIpCut(TestCase): def setUp(self): pass - def test_get_last_X_bits(self): + def test_get_last_X_bits_ipv6(self): """Get last X bits of Ipv6 address""" args = ["", "1234:4321:abcd:dcba::17", -80] result = _ipcut(*args) self.assertEqual(result, "dcba:0:0:0:17") - def test_get_first_X_bits(self): + def test_get_first_X_bits_ipv6(self): """Get first X bits of Ipv6 address""" args = ["", "1234:4321:abcd:dcba::17", 64] result = _ipcut(*args) self.assertEqual(result, "1234:4321:abcd:dcba") + + def test_get_last_X_bits_ipv4(self): + """Get last X bits of Ipv4 address""" + + args = ["", "10.2.3.0", -16] + result = _ipcut(*args) + self.assertEqual(result, "3.0") + + def test_get_first_X_bits_ipv4(self): + """Get first X bits of Ipv4 address""" + + args = ["", "10.2.3.0", 24] + result = _ipcut(*args) + self.assertEqual(result, "10.2.3")