summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/routeros/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
commit975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch)
tree89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/community/routeros/tests
parentInitial commit. (diff)
downloadansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz
ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/routeros/tests')
-rw-r--r--ansible_collections/community/routeros/tests/config.yml9
-rw-r--r--ansible_collections/community/routeros/tests/ee/all.yml18
-rw-r--r--ansible_collections/community/routeros/tests/ee/roles/filter_quoting/aliases6
-rw-r--r--ansible_collections/community/routeros/tests/ee/roles/filter_quoting/tasks/main.yml63
-rw-r--r--ansible_collections/community/routeros/tests/ee/roles/smoke/tasks/main.yml43
-rw-r--r--ansible_collections/community/routeros/tests/integration/requirements.yml7
-rw-r--r--ansible_collections/community/routeros/tests/integration/targets/filter_quoting/aliases6
-rw-r--r--ansible_collections/community/routeros/tests/integration/targets/filter_quoting/tasks/main.yml63
-rw-r--r--ansible_collections/community/routeros/tests/sanity/extra/extra-docs.json13
-rw-r--r--ansible_collections/community/routeros/tests/sanity/extra/extra-docs.json.license3
-rwxr-xr-xansible_collections/community/routeros/tests/sanity/extra/extra-docs.py29
-rw-r--r--ansible_collections/community/routeros/tests/sanity/extra/licenses.json4
-rw-r--r--ansible_collections/community/routeros/tests/sanity/extra/licenses.json.license3
-rwxr-xr-xansible_collections/community/routeros/tests/sanity/extra/licenses.py110
-rw-r--r--ansible_collections/community/routeros/tests/sanity/extra/licenses.py.license3
-rw-r--r--ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.json7
-rw-r--r--ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.json.license3
-rwxr-xr-xansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.py58
-rw-r--r--ansible_collections/community/routeros/tests/sanity/extra/update-docs.json8
-rw-r--r--ansible_collections/community/routeros/tests/sanity/extra/update-docs.json.license3
-rw-r--r--ansible_collections/community/routeros/tests/sanity/extra/update-docs.py21
-rw-r--r--ansible_collections/community/routeros/tests/sanity/ignore-2.10.txt6
-rw-r--r--ansible_collections/community/routeros/tests/sanity/ignore-2.10.txt.license3
-rw-r--r--ansible_collections/community/routeros/tests/sanity/ignore-2.11.txt6
-rw-r--r--ansible_collections/community/routeros/tests/sanity/ignore-2.11.txt.license3
-rw-r--r--ansible_collections/community/routeros/tests/sanity/ignore-2.12.txt1
-rw-r--r--ansible_collections/community/routeros/tests/sanity/ignore-2.12.txt.license3
-rw-r--r--ansible_collections/community/routeros/tests/sanity/ignore-2.13.txt1
-rw-r--r--ansible_collections/community/routeros/tests/sanity/ignore-2.13.txt.license3
-rw-r--r--ansible_collections/community/routeros/tests/sanity/ignore-2.14.txt1
-rw-r--r--ansible_collections/community/routeros/tests/sanity/ignore-2.14.txt.license3
-rw-r--r--ansible_collections/community/routeros/tests/sanity/ignore-2.15.txt1
-rw-r--r--ansible_collections/community/routeros/tests/sanity/ignore-2.15.txt.license3
-rw-r--r--ansible_collections/community/routeros/tests/sanity/ignore-2.16.txt1
-rw-r--r--ansible_collections/community/routeros/tests/sanity/ignore-2.16.txt.license3
-rw-r--r--ansible_collections/community/routeros/tests/sanity/ignore-2.9.txt6
-rw-r--r--ansible_collections/community/routeros/tests/sanity/ignore-2.9.txt.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/compat/__init__.py0
-rw-r--r--ansible_collections/community/routeros/tests/unit/compat/builtins.py20
-rw-r--r--ansible_collections/community/routeros/tests/unit/compat/mock.py109
-rw-r--r--ansible_collections/community/routeros/tests/unit/compat/unittest.py25
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/module_utils/test__api_data.py114
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/module_utils/test_quoting.py274
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fake_api.py243
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export24
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export_verbose26
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export_verbose.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging34
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging10
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging15
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging19
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv61
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv6.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging10
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging13
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging7
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging10
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging1
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging16
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging7
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_package_print106
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_package_print.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_resource_print17
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_resource_print.license3
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/routeros_module.py75
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/test_api.py308
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_facts.py752
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_find_and_modify.py651
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_info.py811
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_modify.py1972
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/test_command.py100
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/test_facts.py335
-rw-r--r--ansible_collections/community/routeros/tests/unit/plugins/modules/utils.py54
-rw-r--r--ansible_collections/community/routeros/tests/unit/requirements.txt6
-rw-r--r--ansible_collections/community/routeros/tests/unit/requirements.yml7
91 files changed, 6759 insertions, 0 deletions
diff --git a/ansible_collections/community/routeros/tests/config.yml b/ansible_collections/community/routeros/tests/config.yml
new file mode 100644
index 000000000..38590f2e4
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/config.yml
@@ -0,0 +1,9 @@
+---
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# See template for more information:
+# https://github.com/ansible/ansible/blob/devel/test/lib/ansible_test/config/config.yml
+modules:
+ python_requires: default
diff --git a/ansible_collections/community/routeros/tests/ee/all.yml b/ansible_collections/community/routeros/tests/ee/all.yml
new file mode 100644
index 000000000..26f198b4f
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/ee/all.yml
@@ -0,0 +1,18 @@
+---
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+- hosts: localhost
+ tasks:
+ - name: Find all roles
+ find:
+ paths:
+ - "{{ (playbook_dir | default('.')) ~ '/roles' }}"
+ file_type: directory
+ depth: 1
+ register: result
+ - name: Include all roles
+ include_role:
+ name: "{{ item }}"
+ loop: "{{ result.files | map(attribute='path') | map('regex_replace', '.*/', '') | sort }}"
diff --git a/ansible_collections/community/routeros/tests/ee/roles/filter_quoting/aliases b/ansible_collections/community/routeros/tests/ee/roles/filter_quoting/aliases
new file mode 100644
index 000000000..ddba81818
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/ee/roles/filter_quoting/aliases
@@ -0,0 +1,6 @@
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+shippable/posix/group1
+skip/python2.6
diff --git a/ansible_collections/community/routeros/tests/ee/roles/filter_quoting/tasks/main.yml b/ansible_collections/community/routeros/tests/ee/roles/filter_quoting/tasks/main.yml
new file mode 100644
index 000000000..e7a2d29a1
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/ee/roles/filter_quoting/tasks/main.yml
@@ -0,0 +1,63 @@
+---
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+- name: "Test split filter"
+ assert:
+ that:
+ - "'' | community.routeros.split == []"
+ - "'foo bar' | community.routeros.split == ['foo', 'bar']"
+ - >
+ 'foo bar="a b c"' | community.routeros.split == ['foo', 'bar=a b c']
+
+- name: "Test split filter error handling"
+ set_fact:
+ test: >-
+ {{ 'a="' | community.routeros.split }}
+ ignore_errors: true
+ register: result
+
+- name: "Verify split filter error handling"
+ assert:
+ that:
+ - >-
+ result.msg == "Unexpected end of string during escaped parameter"
+
+- name: "Test quote_argument filter"
+ assert:
+ that:
+ - >
+ 'a=' | community.routeros.quote_argument == 'a=""'
+ - >
+ 'a=b' | community.routeros.quote_argument == 'a=b'
+ - >
+ 'a=b c' | community.routeros.quote_argument == 'a="b\\_c"'
+ - >
+ 'a=""' | community.routeros.quote_argument == 'a="\\"\\""'
+
+- name: "Test quote_argument_value filter"
+ assert:
+ that:
+ - >
+ '' | community.routeros.quote_argument_value == '""'
+ - >
+ 'foo' | community.routeros.quote_argument_value == 'foo'
+ - >
+ '"foo bar"' | community.routeros.quote_argument_value == '"\\"foo\\_bar\\""'
+
+- name: "Test join filter"
+ assert:
+ that:
+ - >
+ ['a=', 'b=c d'] | community.routeros.join == 'a="" b="c\\_d"'
+
+- name: "Test list_to_dict filter"
+ assert:
+ that:
+ - >
+ ['a=', 'b=c'] | community.routeros.list_to_dict == {'a': '', 'b': 'c'}
+ - >
+ ['a=', 'b=c'] | community.routeros.list_to_dict(skip_empty_values=True) == {'b': 'c'}
+ - >
+ ['a', 'b=c'] | community.routeros.list_to_dict(require_assignment=False) == {'a': none, 'b': 'c'}
diff --git a/ansible_collections/community/routeros/tests/ee/roles/smoke/tasks/main.yml b/ansible_collections/community/routeros/tests/ee/roles/smoke/tasks/main.yml
new file mode 100644
index 000000000..b992c8e18
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/ee/roles/smoke/tasks/main.yml
@@ -0,0 +1,43 @@
+---
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+- name: Run api module
+ community.routeros.api:
+ username: foo
+ password: bar
+ hostname: localhost
+ path: ip address
+ ignore_errors: true
+ register: result
+
+- name: Validate result
+ assert:
+ that:
+ - result is failed
+ - result.msg in potential_errors
+ vars:
+ potential_errors:
+ - "Error while connecting: [Errno 111] Connection refused"
+ - "Error while connecting: [Errno 99] Cannot assign requested address"
+
+- name: Run command module
+ community.routeros.command:
+ commands:
+ - /ip address print
+ vars:
+ ansible_host: localhost
+ ansible_connection: ansible.netcommon.network_cli
+ ansible_network_os: community.routeros.routeros
+ ansible_user: foo
+ ansible_ssh_pass: bar
+ ansible_ssh_port: 12349
+ ignore_errors: true
+ register: result
+
+- name: Validate result
+ assert:
+ that:
+ - result is failed
+ - "'Unable to connect to port 12349 ' in result.msg or 'ssh connect failed: Connection refused' in result.msg"
diff --git a/ansible_collections/community/routeros/tests/integration/requirements.yml b/ansible_collections/community/routeros/tests/integration/requirements.yml
new file mode 100644
index 000000000..6a22736b5
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/integration/requirements.yml
@@ -0,0 +1,7 @@
+---
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+collections:
+- ansible.netcommon
diff --git a/ansible_collections/community/routeros/tests/integration/targets/filter_quoting/aliases b/ansible_collections/community/routeros/tests/integration/targets/filter_quoting/aliases
new file mode 100644
index 000000000..ddba81818
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/integration/targets/filter_quoting/aliases
@@ -0,0 +1,6 @@
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+shippable/posix/group1
+skip/python2.6
diff --git a/ansible_collections/community/routeros/tests/integration/targets/filter_quoting/tasks/main.yml b/ansible_collections/community/routeros/tests/integration/targets/filter_quoting/tasks/main.yml
new file mode 100644
index 000000000..e7a2d29a1
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/integration/targets/filter_quoting/tasks/main.yml
@@ -0,0 +1,63 @@
+---
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+- name: "Test split filter"
+ assert:
+ that:
+ - "'' | community.routeros.split == []"
+ - "'foo bar' | community.routeros.split == ['foo', 'bar']"
+ - >
+ 'foo bar="a b c"' | community.routeros.split == ['foo', 'bar=a b c']
+
+- name: "Test split filter error handling"
+ set_fact:
+ test: >-
+ {{ 'a="' | community.routeros.split }}
+ ignore_errors: true
+ register: result
+
+- name: "Verify split filter error handling"
+ assert:
+ that:
+ - >-
+ result.msg == "Unexpected end of string during escaped parameter"
+
+- name: "Test quote_argument filter"
+ assert:
+ that:
+ - >
+ 'a=' | community.routeros.quote_argument == 'a=""'
+ - >
+ 'a=b' | community.routeros.quote_argument == 'a=b'
+ - >
+ 'a=b c' | community.routeros.quote_argument == 'a="b\\_c"'
+ - >
+ 'a=""' | community.routeros.quote_argument == 'a="\\"\\""'
+
+- name: "Test quote_argument_value filter"
+ assert:
+ that:
+ - >
+ '' | community.routeros.quote_argument_value == '""'
+ - >
+ 'foo' | community.routeros.quote_argument_value == 'foo'
+ - >
+ '"foo bar"' | community.routeros.quote_argument_value == '"\\"foo\\_bar\\""'
+
+- name: "Test join filter"
+ assert:
+ that:
+ - >
+ ['a=', 'b=c d'] | community.routeros.join == 'a="" b="c\\_d"'
+
+- name: "Test list_to_dict filter"
+ assert:
+ that:
+ - >
+ ['a=', 'b=c'] | community.routeros.list_to_dict == {'a': '', 'b': 'c'}
+ - >
+ ['a=', 'b=c'] | community.routeros.list_to_dict(skip_empty_values=True) == {'b': 'c'}
+ - >
+ ['a', 'b=c'] | community.routeros.list_to_dict(require_assignment=False) == {'a': none, 'b': 'c'}
diff --git a/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.json b/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.json
new file mode 100644
index 000000000..9a28d174f
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.json
@@ -0,0 +1,13 @@
+{
+ "include_symlinks": false,
+ "prefixes": [
+ "docs/docsite/",
+ "plugins/",
+ "roles/"
+ ],
+ "output": "path-line-column-message",
+ "requirements": [
+ "ansible-core",
+ "antsibull-docs"
+ ]
+}
diff --git a/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.json.license b/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.json.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.json.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.py b/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.py
new file mode 100755
index 000000000..c636beb08
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+"""Check extra collection docs with antsibull-docs."""
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import sys
+import subprocess
+
+
+def main():
+ """Main entry point."""
+ env = os.environ.copy()
+ suffix = ':{env}'.format(env=env["ANSIBLE_COLLECTIONS_PATH"]) if 'ANSIBLE_COLLECTIONS_PATH' in env else ''
+ env['ANSIBLE_COLLECTIONS_PATH'] = '{root}{suffix}'.format(root=os.path.dirname(os.path.dirname(os.path.dirname(os.getcwd()))), suffix=suffix)
+ p = subprocess.run(
+ ['antsibull-docs', 'lint-collection-docs', '--plugin-docs', '--disallow-semantic-markup', '--skip-rstcheck', '.'],
+ env=env,
+ check=False,
+ )
+ if p.returncode not in (0, 3):
+ print('{0}:0:0: unexpected return code {1}'.format(sys.argv[0], p.returncode))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/routeros/tests/sanity/extra/licenses.json b/ansible_collections/community/routeros/tests/sanity/extra/licenses.json
new file mode 100644
index 000000000..50e47ca88
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/extra/licenses.json
@@ -0,0 +1,4 @@
+{
+ "include_symlinks": false,
+ "output": "path-message"
+}
diff --git a/ansible_collections/community/routeros/tests/sanity/extra/licenses.json.license b/ansible_collections/community/routeros/tests/sanity/extra/licenses.json.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/extra/licenses.json.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/sanity/extra/licenses.py b/ansible_collections/community/routeros/tests/sanity/extra/licenses.py
new file mode 100755
index 000000000..80eb795ef
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/extra/licenses.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+"""Prevent files without a correct license identifier from being added to the source tree."""
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import glob
+import sys
+
+
+def format_license_list(licenses):
+ if not licenses:
+ return '(empty)'
+ return ', '.join(['"%s"' % license for license in licenses])
+
+
+def find_licenses(filename, relax=False):
+ spdx_license_identifiers = []
+ other_license_identifiers = []
+ has_copyright = False
+ try:
+ with open(filename, 'r', encoding='utf-8') as f:
+ for line in f:
+ line = line.rstrip()
+ if 'Copyright ' in line:
+ has_copyright = True
+ if 'Copyright: ' in line:
+ print('%s: found copyright line with "Copyright:". Please remove the colon.' % (filename, ))
+ if 'SPDX-FileCopyrightText: ' in line:
+ has_copyright = True
+ idx = line.find('SPDX-License-Identifier: ')
+ if idx >= 0:
+ lic_id = line[idx + len('SPDX-License-Identifier: '):]
+ spdx_license_identifiers.extend(lic_id.split(' OR '))
+ if 'GNU General Public License' in line:
+ if 'v3.0+' in line:
+ other_license_identifiers.append('GPL-3.0-or-later')
+ if 'version 3 or later' in line:
+ other_license_identifiers.append('GPL-3.0-or-later')
+ if 'Simplified BSD License' in line:
+ other_license_identifiers.append('BSD-2-Clause')
+ if 'Apache License 2.0' in line:
+ other_license_identifiers.append('Apache-2.0')
+ if 'PSF License' in line or 'Python-2.0' in line:
+ other_license_identifiers.append('PSF-2.0')
+ if 'MIT License' in line:
+ other_license_identifiers.append('MIT')
+ except Exception as exc:
+ print('%s: error while processing file: %s' % (filename, exc))
+ if len(set(spdx_license_identifiers)) < len(spdx_license_identifiers):
+ print('%s: found identical SPDX-License-Identifier values' % (filename, ))
+ if other_license_identifiers and set(other_license_identifiers) != set(spdx_license_identifiers):
+ print('%s: SPDX-License-Identifier yielded the license list %s, while manual guessing yielded the license list %s' % (
+ filename, format_license_list(spdx_license_identifiers), format_license_list(other_license_identifiers)))
+ if not has_copyright and not relax:
+ print('%s: found no copyright notice' % (filename, ))
+ return sorted(spdx_license_identifiers)
+
+
+def main():
+ """Main entry point."""
+ paths = sys.argv[1:] or sys.stdin.read().splitlines()
+
+ # The following paths are allowed to have no license identifier
+ no_comments_allowed = [
+ 'changelogs/fragments/*.yml',
+ 'changelogs/fragments/*.yaml',
+ ]
+
+ # These files are completely ignored
+ ignore_paths = [
+ '.ansible-test-timeout.json',
+ '.reuse/dep5',
+ 'LICENSES/*.txt',
+ 'COPYING',
+ ]
+
+ no_comments_allowed = [fn for pattern in no_comments_allowed for fn in glob.glob(pattern)]
+ ignore_paths = [fn for pattern in ignore_paths for fn in glob.glob(pattern)]
+
+ valid_licenses = [license_file[len('LICENSES/'):-len('.txt')] for license_file in glob.glob('LICENSES/*.txt')]
+
+ for path in paths:
+ if path.startswith('./'):
+ path = path[2:]
+ if path in ignore_paths or path.startswith('tests/output/'):
+ continue
+ if os.stat(path).st_size == 0:
+ continue
+ if not path.endswith('.license') and os.path.exists(path + '.license'):
+ path = path + '.license'
+ valid_licenses_for_path = valid_licenses
+ if path.startswith('plugins/') and not path.startswith(('plugins/modules/', 'plugins/module_utils/')):
+ valid_licenses_for_path = [license for license in valid_licenses if license == 'GPL-3.0-or-later']
+ licenses = find_licenses(path, relax=path in no_comments_allowed)
+ if not licenses:
+ if path not in no_comments_allowed:
+ print('%s: must have at least one license' % (path, ))
+ else:
+ for license in licenses:
+ if license not in valid_licenses_for_path:
+ print('%s: found not allowed license "%s", must be one of %s' % (
+ path, license, format_license_list(valid_licenses_for_path)))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/routeros/tests/sanity/extra/licenses.py.license b/ansible_collections/community/routeros/tests/sanity/extra/licenses.py.license
new file mode 100644
index 000000000..6c4958feb
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/extra/licenses.py.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: 2022, Felix Fontein <felix@fontein.de>
diff --git a/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.json b/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.json
new file mode 100644
index 000000000..c789a7fd3
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.json
@@ -0,0 +1,7 @@
+{
+ "include_symlinks": true,
+ "prefixes": [
+ "plugins/"
+ ],
+ "output": "path-message"
+}
diff --git a/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.json.license b/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.json.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.json.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.py b/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.py
new file mode 100755
index 000000000..b39df83a1
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+"""Prevent unwanted files from being added to the source tree."""
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import os.path
+import sys
+
+
+def main():
+ """Main entry point."""
+ paths = sys.argv[1:] or sys.stdin.read().splitlines()
+
+ allowed_extensions = (
+ '.cs',
+ '.ps1',
+ '.psm1',
+ '.py',
+ )
+
+ skip_paths = set([
+ ])
+
+ skip_directories = (
+ )
+
+ yaml_directories = (
+ 'plugins/test/',
+ 'plugins/filter/',
+ )
+
+ for path in paths:
+ if path in skip_paths:
+ continue
+
+ if any(path.startswith(skip_directory) for skip_directory in skip_directories):
+ continue
+
+ if os.path.islink(path):
+ print('%s: is a symbolic link' % (path, ))
+ elif not os.path.isfile(path):
+ print('%s: is not a regular file' % (path, ))
+
+ ext = os.path.splitext(path)[1]
+
+ if ext in ('.yml', ) and any(path.startswith(yaml_directory) for yaml_directory in yaml_directories):
+ continue
+
+ if ext not in allowed_extensions:
+ print('%s: extension must be one of: %s' % (path, ', '.join(allowed_extensions)))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/routeros/tests/sanity/extra/update-docs.json b/ansible_collections/community/routeros/tests/sanity/extra/update-docs.json
new file mode 100644
index 000000000..029699f0f
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/extra/update-docs.json
@@ -0,0 +1,8 @@
+{
+ "include_symlinks": false,
+ "prefixes": [
+ "docs/docsite/rst/api-guide.rst",
+ "plugins/modules/"
+ ],
+ "output": "path-line-column-message"
+}
diff --git a/ansible_collections/community/routeros/tests/sanity/extra/update-docs.json.license b/ansible_collections/community/routeros/tests/sanity/extra/update-docs.json.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/extra/update-docs.json.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/sanity/extra/update-docs.py b/ansible_collections/community/routeros/tests/sanity/extra/update-docs.py
new file mode 100644
index 000000000..68e2edf87
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/extra/update-docs.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+"""Check whether update-docs.py modifies something."""
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+import subprocess
+
+
+def main():
+ """Main entry point."""
+ p = subprocess.run(['./update-docs.py'], check=False)
+ if p.returncode not in (0, 1):
+ print('{0}:0:0: unexpected return code {1}'.format(sys.argv[0], p.returncode))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.10.txt b/ansible_collections/community/routeros/tests/sanity/ignore-2.10.txt
new file mode 100644
index 000000000..876765a85
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.10.txt
@@ -0,0 +1,6 @@
+update-docs.py compile-2.6
+update-docs.py compile-2.7
+update-docs.py compile-3.5
+update-docs.py future-import-boilerplate
+update-docs.py metaclass-boilerplate
+update-docs.py shebang
diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.10.txt.license b/ansible_collections/community/routeros/tests/sanity/ignore-2.10.txt.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.10.txt.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.11.txt b/ansible_collections/community/routeros/tests/sanity/ignore-2.11.txt
new file mode 100644
index 000000000..876765a85
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.11.txt
@@ -0,0 +1,6 @@
+update-docs.py compile-2.6
+update-docs.py compile-2.7
+update-docs.py compile-3.5
+update-docs.py future-import-boilerplate
+update-docs.py metaclass-boilerplate
+update-docs.py shebang
diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.11.txt.license b/ansible_collections/community/routeros/tests/sanity/ignore-2.11.txt.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.11.txt.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.12.txt b/ansible_collections/community/routeros/tests/sanity/ignore-2.12.txt
new file mode 100644
index 000000000..ce635c32c
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.12.txt
@@ -0,0 +1 @@
+update-docs.py shebang
diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.12.txt.license b/ansible_collections/community/routeros/tests/sanity/ignore-2.12.txt.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.12.txt.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.13.txt b/ansible_collections/community/routeros/tests/sanity/ignore-2.13.txt
new file mode 100644
index 000000000..ce635c32c
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.13.txt
@@ -0,0 +1 @@
+update-docs.py shebang
diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.13.txt.license b/ansible_collections/community/routeros/tests/sanity/ignore-2.13.txt.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.13.txt.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.14.txt b/ansible_collections/community/routeros/tests/sanity/ignore-2.14.txt
new file mode 100644
index 000000000..ce635c32c
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.14.txt
@@ -0,0 +1 @@
+update-docs.py shebang
diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.14.txt.license b/ansible_collections/community/routeros/tests/sanity/ignore-2.14.txt.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.14.txt.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.15.txt b/ansible_collections/community/routeros/tests/sanity/ignore-2.15.txt
new file mode 100644
index 000000000..ce635c32c
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.15.txt
@@ -0,0 +1 @@
+update-docs.py shebang
diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.15.txt.license b/ansible_collections/community/routeros/tests/sanity/ignore-2.15.txt.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.15.txt.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.16.txt b/ansible_collections/community/routeros/tests/sanity/ignore-2.16.txt
new file mode 100644
index 000000000..ce635c32c
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.16.txt
@@ -0,0 +1 @@
+update-docs.py shebang
diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.16.txt.license b/ansible_collections/community/routeros/tests/sanity/ignore-2.16.txt.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.16.txt.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.9.txt b/ansible_collections/community/routeros/tests/sanity/ignore-2.9.txt
new file mode 100644
index 000000000..876765a85
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.9.txt
@@ -0,0 +1,6 @@
+update-docs.py compile-2.6
+update-docs.py compile-2.7
+update-docs.py compile-3.5
+update-docs.py future-import-boilerplate
+update-docs.py metaclass-boilerplate
+update-docs.py shebang
diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.9.txt.license b/ansible_collections/community/routeros/tests/sanity/ignore-2.9.txt.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.9.txt.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/compat/__init__.py b/ansible_collections/community/routeros/tests/unit/compat/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/compat/__init__.py
diff --git a/ansible_collections/community/routeros/tests/unit/compat/builtins.py b/ansible_collections/community/routeros/tests/unit/compat/builtins.py
new file mode 100644
index 000000000..d548601d4
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/compat/builtins.py
@@ -0,0 +1,20 @@
+# Copyright (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt 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
+
+#
+# Compat for python2.7
+#
+
+# One unittest needs to import builtins via __import__() so we need to have
+# the string that represents it
+try:
+ import __builtin__ # noqa: F401, pylint: disable=unused-import
+except ImportError:
+ BUILTINS = 'builtins'
+else:
+ BUILTINS = '__builtin__'
diff --git a/ansible_collections/community/routeros/tests/unit/compat/mock.py b/ansible_collections/community/routeros/tests/unit/compat/mock.py
new file mode 100644
index 000000000..bdbea945e
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/compat/mock.py
@@ -0,0 +1,109 @@
+# Copyright (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt 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
+
+'''
+Compat module for Python3.x's unittest.mock module
+'''
+import sys
+
+# Python 2.7
+
+# Note: Could use the pypi mock library on python3.x as well as python2.x. It
+# is the same as the python3 stdlib mock library
+
+try:
+ # Allow wildcard import because we really do want to import all of mock's
+ # symbols into this compat shim
+ # pylint: disable=wildcard-import,unused-wildcard-import
+ from unittest.mock import * # noqa: F401, pylint: disable=unused-import
+except ImportError:
+ # Python 2
+ # pylint: disable=wildcard-import,unused-wildcard-import
+ try:
+ from mock import * # noqa: F401, pylint: disable=unused-import
+ except ImportError:
+ print('You need the mock library installed on python2.x to run tests')
+
+
+# Prior to 3.4.4, mock_open cannot handle binary read_data
+if sys.version_info >= (3,) and sys.version_info < (3, 4, 4):
+ file_spec = None
+
+ def _iterate_read_data(read_data):
+ # Helper for mock_open:
+ # Retrieve lines from read_data via a generator so that separate calls to
+ # readline, read, and readlines are properly interleaved
+ sep = b'\n' if isinstance(read_data, bytes) else '\n'
+ data_as_list = [l + sep for l in read_data.split(sep)]
+
+ if data_as_list[-1] == sep:
+ # If the last line ended in a newline, the list comprehension will have an
+ # extra entry that's just a newline. Remove this.
+ data_as_list = data_as_list[:-1]
+ else:
+ # If there wasn't an extra newline by itself, then the file being
+ # emulated doesn't have a newline to end the last line remove the
+ # newline that our naive format() added
+ data_as_list[-1] = data_as_list[-1][:-1]
+
+ for line in data_as_list:
+ yield line
+
+ def mock_open(mock=None, read_data=''):
+ """
+ A helper function to create a mock to replace the use of `open`. It works
+ for `open` called directly or used as a context manager.
+
+ The `mock` argument is the mock object to configure. If `None` (the
+ default) then a `MagicMock` will be created for you, with the API limited
+ to methods or attributes available on standard file handles.
+
+ `read_data` is a string for the `read` methoddline`, and `readlines` of the
+ file handle to return. This is an empty string by default.
+ """
+ def _readlines_side_effect(*args, **kwargs):
+ if handle.readlines.return_value is not None:
+ return handle.readlines.return_value
+ return list(_data)
+
+ def _read_side_effect(*args, **kwargs):
+ if handle.read.return_value is not None:
+ return handle.read.return_value
+ return type(read_data)().join(_data)
+
+ def _readline_side_effect():
+ if handle.readline.return_value is not None:
+ while True:
+ yield handle.readline.return_value
+ for line in _data:
+ yield line
+
+ global file_spec
+ if file_spec is None:
+ import _io
+ file_spec = list(set(dir(_io.TextIOWrapper)).union(set(dir(_io.BytesIO))))
+
+ if mock is None:
+ mock = MagicMock(name='open', spec=open)
+
+ handle = MagicMock(spec=file_spec)
+ handle.__enter__.return_value = handle
+
+ _data = _iterate_read_data(read_data)
+
+ handle.write.return_value = None
+ handle.read.return_value = None
+ handle.readline.return_value = None
+ handle.readlines.return_value = None
+
+ handle.read.side_effect = _read_side_effect
+ handle.readline.side_effect = _readline_side_effect()
+ handle.readlines.side_effect = _readlines_side_effect
+
+ mock.return_value = handle
+ return mock
diff --git a/ansible_collections/community/routeros/tests/unit/compat/unittest.py b/ansible_collections/community/routeros/tests/unit/compat/unittest.py
new file mode 100644
index 000000000..d50bab86f
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/compat/unittest.py
@@ -0,0 +1,25 @@
+# Copyright (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt 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
+
+'''
+Compat module for Python2.7's unittest module
+'''
+
+import sys
+
+# Allow wildcard import because we really do want to import all of
+# unittests's symbols into this compat shim
+# pylint: disable=wildcard-import,unused-wildcard-import
+if sys.version_info < (2, 7):
+ try:
+ # Need unittest2 on python2.6
+ from unittest2 import * # noqa: F401, pylint: disable=unused-import
+ except ImportError:
+ print('You need unittest2 installed on python2.6.x to run tests')
+else:
+ from unittest import * # noqa: F401, pylint: disable=unused-import
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/module_utils/test__api_data.py b/ansible_collections/community/routeros/tests/unit/plugins/module_utils/test__api_data.py
new file mode 100644
index 000000000..1250fdaa5
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/module_utils/test__api_data.py
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021, Felix Fontein (@felixfontein) <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import pytest
+
+from ansible_collections.community.routeros.plugins.module_utils._api_data import (
+ APIData,
+ KeyInfo,
+ split_path,
+ join_path,
+)
+
+
+def test_api_data_errors():
+ with pytest.raises(ValueError) as exc:
+ APIData()
+ assert exc.value.args[0] == 'fields must be provided'
+
+ values = [
+ ('primary_keys', []),
+ ('stratify_keys', []),
+ ('has_identifier', True),
+ ('single_value', True),
+ ('unknown_mechanism', True),
+ ]
+
+ for index, (param, param_value) in enumerate(values):
+ for param2, param2_value in values[index + 1:]:
+ with pytest.raises(ValueError) as exc:
+ APIData(**{param: param_value, param2: param2_value})
+ assert exc.value.args[0] == 'primary_keys, stratify_keys, has_identifier, single_value, and unknown_mechanism are mutually exclusive'
+
+ with pytest.raises(ValueError) as exc:
+ APIData(unknown_mechanism=True, fully_understood=True)
+ assert exc.value.args[0] == 'unknown_mechanism and fully_understood cannot be combined'
+
+ with pytest.raises(ValueError) as exc:
+ APIData(unknown_mechanism=True, fixed_entries=True)
+ assert exc.value.args[0] == 'fixed_entries can only be used with primary_keys'
+
+ with pytest.raises(ValueError) as exc:
+ APIData(primary_keys=['foo'], fields={})
+ assert exc.value.args[0] == 'Primary key foo must be in fields!'
+
+ with pytest.raises(ValueError) as exc:
+ APIData(stratify_keys=['foo'], fields={})
+ assert exc.value.args[0] == 'Stratify key foo must be in fields!'
+
+ with pytest.raises(ValueError) as exc:
+ APIData(required_one_of=['foo'], fields={})
+ assert exc.value.args[0] == 'Require one of element at index #1 must be a list!'
+
+ with pytest.raises(ValueError) as exc:
+ APIData(required_one_of=[['foo']], fields={})
+ assert exc.value.args[0] == 'Require one of key foo must be in fields!'
+
+ with pytest.raises(ValueError) as exc:
+ APIData(mutually_exclusive=['foo'], fields={})
+ assert exc.value.args[0] == 'Mutually exclusive element at index #1 must be a list!'
+
+ with pytest.raises(ValueError) as exc:
+ APIData(mutually_exclusive=[['foo']], fields={})
+ assert exc.value.args[0] == 'Mutually exclusive key foo must be in fields!'
+
+
+def test_key_info_errors():
+ values = [
+ ('required', True),
+ ('default', ''),
+ ('automatically_computed_from', ()),
+ ('can_disable', True),
+ ]
+
+ params_allowed_together = [
+ 'default',
+ 'can_disable',
+ ]
+
+ emsg = 'required, default, automatically_computed_from, and can_disable are mutually exclusive besides default and can_disable which can be set together'
+ for index, (param, param_value) in enumerate(values):
+ for param2, param2_value in values[index + 1:]:
+ if param in params_allowed_together and param2 in params_allowed_together:
+ continue
+ with pytest.raises(ValueError) as exc:
+ KeyInfo(**{param: param_value, param2: param2_value})
+ assert exc.value.args[0] == emsg
+
+ with pytest.raises(ValueError) as exc:
+ KeyInfo('foo')
+ assert exc.value.args[0] == 'KeyInfo() does not have positional arguments'
+
+ with pytest.raises(ValueError) as exc:
+ KeyInfo(remove_value='')
+ assert exc.value.args[0] == 'remove_value can only be specified if can_disable=True'
+
+
+SPLITTED_PATHS = [
+ ('', [], ''),
+ (' ip ', ['ip'], 'ip'),
+ ('ip', ['ip'], 'ip'),
+ (' ip \t\n\raddress ', ['ip', 'address'], 'ip address'),
+]
+
+
+@pytest.mark.parametrize("joined_input, splitted, joined_output", SPLITTED_PATHS)
+def test_join_split_path(joined_input, splitted, joined_output):
+ assert split_path(joined_input) == splitted
+ assert join_path(splitted) == joined_output
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/module_utils/test_quoting.py b/ansible_collections/community/routeros/tests/unit/plugins/module_utils/test_quoting.py
new file mode 100644
index 000000000..6d29d507c
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/module_utils/test_quoting.py
@@ -0,0 +1,274 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021, Felix Fontein (@felixfontein) <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import pytest
+
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.routeros.plugins.module_utils.quoting import (
+ ParseError,
+ convert_list_to_dictionary,
+ join_routeros_command,
+ parse_argument_value,
+ quote_routeros_argument,
+ quote_routeros_argument_value,
+ split_routeros_command,
+)
+
+
+TEST_PARSE_ARGUMENT_VALUE = [
+ ('a', {}, ('a', 1)),
+ ('a ', {'must_match_everything': False}, ('a', 1)),
+ (r'"a b"', {}, ('a b', 5)),
+ (r'"b\"f"', {}, ('b"f', 6)),
+ (r'"\01"', {}, ('\x01', 5)),
+ (r'"\1F"', {}, ('\x1f', 5)),
+ (r'"\FF"', {}, (to_native(b'\xff'), 5)),
+ (r'"\"e"', {}, ('"e', 5)),
+ (r'"\""', {}, ('"', 4)),
+ (r'"\\"', {}, ('\\', 4)),
+ (r'"\?"', {}, ('?', 4)),
+ (r'"\$"', {}, ('$', 4)),
+ (r'"\_"', {}, (' ', 4)),
+ (r'"\a"', {}, ('\a', 4)),
+ (r'"\b"', {}, ('\b', 4)),
+ (r'"\f"', {}, (to_native(b'\xff'), 4)),
+ (r'"\n"', {}, ('\n', 4)),
+ (r'"\r"', {}, ('\r', 4)),
+ (r'"\t"', {}, ('\t', 4)),
+ (r'"\v"', {}, ('\v', 4)),
+ (r'"b=c"', {}, ('b=c', 5)),
+ (r'""', {}, ('', 2)),
+ (r'"" ', {'must_match_everything': False}, ('', 2)),
+ ("'e", {'start_index': 1}, ('e', 2)),
+]
+
+
+@pytest.mark.parametrize("command, kwargs, result", TEST_PARSE_ARGUMENT_VALUE)
+def test_parse_argument_value(command, kwargs, result):
+ result_ = parse_argument_value(command, **kwargs)
+ print(result_, result)
+ assert result_ == result
+
+
+TEST_PARSE_ARGUMENT_VALUE_ERRORS = [
+ (r'"e', {}, 'Unexpected end of string during escaped parameter'),
+ ("'e", {}, '"\'" can only be used inside double quotes'),
+ (r'\FF', {}, 'Escape sequences can only be used inside double quotes'),
+ (r'\"e', {}, 'Escape sequences can only be used inside double quotes'),
+ ('e=f', {}, '"=" can only be used inside double quotes'),
+ ('e$', {}, '"$" can only be used inside double quotes'),
+ ('e(', {}, '"(" can only be used inside double quotes'),
+ ('e)', {}, '")" can only be used inside double quotes'),
+ ('e[', {}, '"[" can only be used inside double quotes'),
+ ('e{', {}, '"{" can only be used inside double quotes'),
+ ('e`', {}, '"`" can only be used inside double quotes'),
+ ('?', {}, '"?" can only be used in escaped form'),
+ (r'b"', {}, '\'"\' must not appear in an unquoted value'),
+ (r'""a', {}, "Ending '\"' must be followed by space or end of string"),
+ (r'"" ', {}, "Unexpected data at end of value"),
+ ('"\\', {}, r"'\' must not be at the end of the line"),
+ (r'"\A', {}, r'Hex escape sequence cut off at end of line'),
+ (r'"\Z"', {}, r"Invalid escape sequence '\Z'"),
+ (r'"\Aa"', {}, r"Invalid hex escape sequence '\Aa'"),
+]
+
+
+@pytest.mark.parametrize("command, kwargs, message", TEST_PARSE_ARGUMENT_VALUE_ERRORS)
+def test_parse_argument_value_errors(command, kwargs, message):
+ with pytest.raises(ParseError) as exc:
+ parse_argument_value(command, **kwargs)
+ print(exc.value.args[0], message)
+ assert exc.value.args[0] == message
+
+
+TEST_SPLIT_ROUTEROS_COMMAND = [
+ ('', []),
+ (' ', []),
+ (r'a b c', ['a', 'b', 'c']),
+ (r'a=b c d=e', ['a=b', 'c', 'd=e']),
+ (r'a="b f" c d=e', ['a=b f', 'c', 'd=e']),
+ (r'a="b\"f" c="\FF" d="\"e"', ['a=b"f', to_native(b'c=\xff'), 'd="e']),
+ (r'a="b=c"', ['a=b=c']),
+ (r'a=b ', ['a=b']),
+]
+
+
+@pytest.mark.parametrize("command, result", TEST_SPLIT_ROUTEROS_COMMAND)
+def test_split_routeros_command(command, result):
+ result_ = split_routeros_command(command)
+ print(result_, result)
+ assert result_ == result
+
+
+TEST_SPLIT_ROUTEROS_COMMAND_ERRORS = [
+ (r'a=', 'Expected value, but found end of string'),
+ (r'a="b\"f" d="e', 'Unexpected end of string during escaped parameter'),
+ ('d=\'e', '"\'" can only be used inside double quotes'),
+ (r'c\FF', r'Found unexpected "\"'),
+ (r'd=\"e', 'Escape sequences can only be used inside double quotes'),
+ ('d=e=f', '"=" can only be used inside double quotes'),
+ ('d=e$', '"$" can only be used inside double quotes'),
+ ('d=e(', '"(" can only be used inside double quotes'),
+ ('d=e)', '")" can only be used inside double quotes'),
+ ('d=e[', '"[" can only be used inside double quotes'),
+ ('d=e{', '"{" can only be used inside double quotes'),
+ ('d=e`', '"`" can only be used inside double quotes'),
+ ('d=?', '"?" can only be used in escaped form'),
+ (r'a=b"', '\'"\' must not appear in an unquoted value'),
+ (r'a=""a', "Ending '\"' must be followed by space or end of string"),
+ ('a="\\', r"'\' must not be at the end of the line"),
+ (r'a="\Z', r"Invalid escape sequence '\Z'"),
+ (r'a="\Aa', r"Invalid hex escape sequence '\Aa'"),
+]
+
+
+@pytest.mark.parametrize("command, message", TEST_SPLIT_ROUTEROS_COMMAND_ERRORS)
+def test_split_routeros_command_errors(command, message):
+ with pytest.raises(ParseError) as exc:
+ split_routeros_command(command)
+ print(exc.value.args[0], message)
+ assert exc.value.args[0] == message
+
+
+TEST_CONVERT_LIST_TO_DICTIONARY = [
+ (['a=b', 'c=d=e', 'e='], {}, {'a': 'b', 'c': 'd=e', 'e': ''}),
+ (['a=b', 'c=d=e', 'e='], {'skip_empty_values': False}, {'a': 'b', 'c': 'd=e', 'e': ''}),
+ (['a=b', 'c=d=e', 'e='], {'skip_empty_values': True}, {'a': 'b', 'c': 'd=e'}),
+ (['a=b', 'c=d=e', 'e=', 'f'], {'require_assignment': False}, {'a': 'b', 'c': 'd=e', 'e': '', 'f': None}),
+]
+
+
+@pytest.mark.parametrize("list, kwargs, expected_dict", TEST_CONVERT_LIST_TO_DICTIONARY)
+def test_convert_list_to_dictionary(list, kwargs, expected_dict):
+ result = convert_list_to_dictionary(list, **kwargs)
+ print(result, expected_dict)
+ assert result == expected_dict
+
+
+TEST_CONVERT_LIST_TO_DICTIONARY_ERRORS = [
+ (['a=b', 'c=d=e', 'e=', 'f'], {}, "missing '=' after 'f'"),
+]
+
+
+@pytest.mark.parametrize("list, kwargs, message", TEST_CONVERT_LIST_TO_DICTIONARY_ERRORS)
+def test_convert_list_to_dictionary_errors(list, kwargs, message):
+ with pytest.raises(ParseError) as exc:
+ result = convert_list_to_dictionary(list, **kwargs)
+ print(exc.value.args[0], message)
+ assert exc.value.args[0] == message
+
+
+TEST_JOIN_ROUTEROS_COMMAND = [
+ (['a=b', 'c=d=e', 'e=', 'f', 'g=h i j', 'h="h"'], r'a=b c="d=e" e="" f g="h\_i\_j" h="\"h\""'),
+]
+
+
+@pytest.mark.parametrize("list, expected", TEST_JOIN_ROUTEROS_COMMAND)
+def test_join_routeros_command(list, expected):
+ result = join_routeros_command(list)
+ print(result, expected)
+ assert result == expected
+
+
+TEST_QUOTE_ROUTEROS_ARGUMENT = [
+ (r'', r''),
+ (r'a', r'a'),
+ (r'a=b', r'a=b'),
+ (r'a=b c', r'a="b\_c"'),
+ (r'a="b c"', r'a="\"b\_c\""'),
+ (r"a='b", "a=\"'b\""),
+ (r"a=b'", "a=\"b'\""),
+ (r'a=""', r'a="\"\""'),
+]
+
+
+@pytest.mark.parametrize("argument, expected", TEST_QUOTE_ROUTEROS_ARGUMENT)
+def test_quote_routeros_argument(argument, expected):
+ result = quote_routeros_argument(argument)
+ print(result, expected)
+ assert result == expected
+
+
+TEST_QUOTE_ROUTEROS_ARGUMENT_ERRORS = [
+ ('a b', 'Attribute names must not contain spaces'),
+ ('a b=c', 'Attribute names must not contain spaces'),
+]
+
+
+@pytest.mark.parametrize("argument, message", TEST_QUOTE_ROUTEROS_ARGUMENT_ERRORS)
+def test_quote_routeros_argument_errors(argument, message):
+ with pytest.raises(ParseError) as exc:
+ result = quote_routeros_argument(argument)
+ print(exc.value.args[0], message)
+ assert exc.value.args[0] == message
+
+
+TEST_QUOTE_ROUTEROS_ARGUMENT_VALUE = [
+ (r'', r'""'),
+ (r";", r'";"'),
+ (r" ", r'"\_"'),
+ (r"=", r'"="'),
+ (r'a', r'a'),
+ (r'a=b', r'"a=b"'),
+ (r'b c', r'"b\_c"'),
+ (r'"b c"', r'"\"b\_c\""'),
+ ("'b", "\"'b\""),
+ ("b'", "\"b'\""),
+ ('"', r'"\""'),
+ ('\\', r'"\\"'),
+ ('?', r'"\?"'),
+ ('$', r'"\$"'),
+ ('_', r'_'),
+ (' ', r'"\_"'),
+ ('\a', r'"\a"'),
+ ('\b', r'"\b"'),
+ # (to_native(b'\xff'), r'"\f"'),
+ ('\n', r'"\n"'),
+ ('\r', r'"\r"'),
+ ('\t', r'"\t"'),
+ ('\v', r'"\v"'),
+ ('\x01', r'"\01"'),
+ ('\x1f', r'"\1F"'),
+]
+
+
+@pytest.mark.parametrize("argument, expected", TEST_QUOTE_ROUTEROS_ARGUMENT_VALUE)
+def test_quote_routeros_argument_value(argument, expected):
+ result = quote_routeros_argument_value(argument)
+ print(result, expected)
+ assert result == expected
+
+
+TEST_ROUNDTRIP = [
+ {'a': 'b', 'c': 'd'},
+ {'script': ''':local host value=[/system identity get name];
+:local date value=[/system clock get date];
+:local day [ :pick $date 4 6 ];
+:local month [ :pick $date 0 3 ];
+:local year [ :pick $date 7 11 ];
+:local name value=($host."-".$day."-".$month."-".$year);
+/system backup save name=$name;
+/export file=$name;
+/tool fetch address="192.168.1.1" user=ros password="PASSWORD" mode=ftp dst-path=("/mikrotik/rsc/".$name.".rsc") src-path=($name.".rsc") upload=yes;
+/tool fetch address="192.168.1.1" user=ros password="PASSWORD" mode=ftp dst-path=("/mikrotik/backup/".$name.".backup") src-path=($name.".backup") upload=yes;
+'''},
+]
+
+
+@pytest.mark.parametrize("dictionary", TEST_ROUNDTRIP)
+def test_roundtrip(dictionary):
+ argument_list = ['%s=%s' % (k, v) for k, v in dictionary.items()]
+ command = join_routeros_command(argument_list)
+ resplit_list = split_routeros_command(command)
+ print(resplit_list, argument_list)
+ assert resplit_list == argument_list
+ re_dictionary = convert_list_to_dictionary(resplit_list)
+ print(re_dictionary, dictionary)
+ assert re_dictionary == dictionary
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fake_api.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/fake_api.py
new file mode 100644
index 000000000..a5ddb3180
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fake_api.py
@@ -0,0 +1,243 @@
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt 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
+
+from ansible_collections.community.routeros.plugins.module_utils._api_data import PATHS
+
+
+class FakeLibRouterosError(Exception):
+ def __init__(self, message):
+ self.message = message
+ super(FakeLibRouterosError, self).__init__(self.message)
+
+
+class TrapError(FakeLibRouterosError):
+ def __init__(self, message="failure: already have interface with such name"):
+ super(TrapError, self).__init__(message)
+
+
+# fixtures
+class fake_ros_api(object):
+ def __init__(self, api, path):
+ pass
+
+ @classmethod
+ def path(cls, api, path):
+ fake_bridge = [{".id": "*DC", "name": "b2", "mtu": "auto", "actual-mtu": 1500,
+ "l2mtu": 65535, "arp": "enabled", "arp-timeout": "auto",
+ "mac-address": "3A:C1:90:D6:E8:44", "protocol-mode": "rstp",
+ "fast-forward": "true", "igmp-snooping": "false",
+ "auto-mac": "true", "ageing-time": "5m", "priority":
+ "0x8000", "max-message-age": "20s", "forward-delay": "15s",
+ "transmit-hold-count": 6, "vlan-filtering": "false",
+ "dhcp-snooping": "false", "running": "true", "disabled": "false"}]
+ return fake_bridge
+
+ @classmethod
+ def arbitrary(cls, api, path):
+ def retr(self, *args, **kwargs):
+ if 'name' not in kwargs.keys():
+ raise TrapError(message="no such command")
+ dummy_test_string = '/interface/bridge add name=unit_test_brige_arbitrary'
+ result = "/%s/%s add name=%s" % (path[0], path[1], kwargs['name'])
+ return [result]
+ return retr
+
+ def add(self, name):
+ if name == "unit_test_brige_exist":
+ raise TrapError
+ return '*A1'
+
+ def remove(self, id):
+ if id != "*A1":
+ raise TrapError(message="no such item (4)")
+ return '*A1'
+
+ def update(self, **kwargs):
+ if kwargs['.id'] != "*A1" or 'name' not in kwargs.keys():
+ raise TrapError(message="no such item (4)")
+ return ["updated: {'.id': '%s' % kwargs['.id'], 'name': '%s' % kwargs['name']}"]
+
+ def select(self, *args):
+ dummy_bridge = [{".id": "*A1", "name": "dummy_bridge_A1"},
+ {".id": "*A2", "name": "dummy_bridge_A2"},
+ {".id": "*A3", "name": "dummy_bridge_A3"}]
+
+ result = []
+ for dummy in dummy_bridge:
+ found = {}
+ for search in args:
+ if search in dummy.keys():
+ found[search] = dummy[search]
+ else:
+ continue
+ if len(found.keys()) == 2:
+ result.append(found)
+
+ if result:
+ return result
+ else:
+ return []
+
+ @classmethod
+ def select_where(cls, api, path):
+ api_path = Where()
+ return api_path
+
+
+class Where(object):
+ def __init__(self):
+ pass
+
+ def select(self, *args):
+ return self
+
+ def where(self, *args):
+ return [{".id": "*A1", "name": "dummy_bridge_A1"}]
+
+
+class Key(object):
+ def __init__(self, name):
+ self.name = name
+ self.str_return()
+
+ def str_return(self):
+ return str(self.name)
+
+
+class Or(object):
+ def __init__(self, *args):
+ self.args = args
+ self.str_return()
+
+ def str_return(self):
+ return repr(self.args)
+
+
+def _normalize_entry(entry, path_info, on_create=False):
+ for key, data in path_info.fields.items():
+ if key not in entry and data.default is not None and (not data.can_disable or on_create):
+ entry[key] = data.default
+ if data.can_disable:
+ if key in entry and entry[key] in (None, data.remove_value):
+ del entry[key]
+ if ('!%s' % key) in entry:
+ entry.pop(key, None)
+ del entry['!%s' % key]
+ if data.absent_value is not None and key in entry and entry[key] == data.absent_value:
+ del entry[key]
+
+
+def massage_expected_result_data(values, path, keep_all=False, remove_dynamic=False, remove_builtin=False):
+ path_info = PATHS[path]
+ if remove_dynamic:
+ values = [entry for entry in values if not entry.get('dynamic', False)]
+ if remove_builtin:
+ values = [entry for entry in values if not entry.get('builtin', False)]
+ values = [entry.copy() for entry in values]
+ for entry in values:
+ _normalize_entry(entry, path_info)
+ if not keep_all:
+ for key in list(entry):
+ if key == '.id' or key in path_info.fields:
+ continue
+ del entry[key]
+ for key, data in path_info.fields.items():
+ if data.absent_value is not None and key not in entry:
+ entry[key] = data.absent_value
+ return values
+
+
+class Path(object):
+ def __init__(self, path, initial_values, read_only=False):
+ self._path = path
+ self._path_info = PATHS[path]
+ self._values = [entry.copy() for entry in initial_values]
+ for entry in self._values:
+ _normalize_entry(entry, self._path_info)
+ self._new_id_counter = 0
+ self._read_only = read_only
+
+ def __iter__(self):
+ return [entry.copy() for entry in self._values].__iter__()
+
+ def _find_id(self, id, required=False):
+ for index, entry in enumerate(self._values):
+ if entry['.id'] == id:
+ return index
+ if required:
+ raise FakeLibRouterosError('Cannot find key "%s"' % id)
+ return None
+
+ def add(self, **kwargs):
+ if self._path_info.fixed_entries or self._path_info.single_value:
+ raise Exception('Cannot add entries')
+ if self._read_only:
+ raise Exception('Modifying read-only path: add %s' % repr(kwargs))
+ if '.id' in kwargs:
+ raise Exception('Trying to create new entry with ".id" field: %s' % repr(kwargs))
+ if 'dynamic' in kwargs or 'builtin' in kwargs:
+ raise Exception('Trying to add a dynamic or builtin entry')
+ self._new_id_counter += 1
+ id = '*NEW%d' % self._new_id_counter
+ entry = {
+ '.id': id,
+ }
+ entry.update(kwargs)
+ _normalize_entry(entry, self._path_info, on_create=True)
+ self._values.append(entry)
+ return id
+
+ def remove(self, *args):
+ if self._path_info.fixed_entries or self._path_info.single_value:
+ raise Exception('Cannot remove entries')
+ if self._read_only:
+ raise Exception('Modifying read-only path: remove %s' % repr(args))
+ for id in args:
+ index = self._find_id(id, required=True)
+ entry = self._values[index]
+ if entry.get('dynamic', False) or entry.get('builtin', False):
+ raise Exception('Trying to remove a dynamic or builtin entry')
+ del self._values[index]
+
+ def update(self, **kwargs):
+ if self._read_only:
+ raise Exception('Modifying read-only path: update %s' % repr(kwargs))
+ if 'dynamic' in kwargs or 'builtin' in kwargs:
+ raise Exception('Trying to update dynamic builtin fields')
+ if self._path_info.single_value:
+ index = 0
+ else:
+ index = self._find_id(kwargs['.id'], required=True)
+ entry = self._values[index]
+ if entry.get('dynamic', False) or entry.get('builtin', False):
+ raise Exception('Trying to update a dynamic or builtin entry')
+ entry.update(kwargs)
+ _normalize_entry(entry, self._path_info)
+
+ def __call__(self, command, *args, **kwargs):
+ if self._read_only:
+ raise Exception('Modifying read-only path: "%s" %s %s' % (command, repr(args), repr(kwargs)))
+ if command != 'move':
+ raise FakeLibRouterosError('Unsupported command "%s"' % command)
+ if self._path_info.fixed_entries or self._path_info.single_value:
+ raise Exception('Cannot move entries')
+ yield None # make sure that nothing happens if the result isn't consumed
+ source_index = self._find_id(kwargs.pop('numbers'), required=True)
+ entry = self._values.pop(source_index)
+ dest_index = self._find_id(kwargs.pop('destination'), required=True)
+ self._values.insert(dest_index, entry)
+
+
+def create_fake_path(path, initial_values, read_only=False):
+ def create(api, called_path):
+ called_path = tuple(called_path)
+ if path != called_path:
+ raise AssertionError('Expected {path}, got {called_path}'.format(path=path, called_path=called_path))
+ return Path(path, initial_values, read_only=read_only)
+
+ return create
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export
new file mode 100644
index 000000000..d1ac49140
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export
@@ -0,0 +1,24 @@
+# sep/25/2018 10:10:52 by RouterOS 6.42.5
+# software id = 9EER-511K
+#
+#
+#
+/interface wireless security-profiles
+set [ find default=yes ] supplicant-identity=MikroTik
+/tool user-manager customer
+set admin access=own-routers,own-users,own-profiles,own-limits,config-payment-gw
+/ip address
+add address=192.168.88.1/24 comment=defconf interface=ether1 network=192.168.88.0
+/ip dhcp-client
+add dhcp-options=hostname,clientid disabled=no interface=ether1
+/system lcd page
+set time disabled=yes display-time=5s
+set resources disabled=yes display-time=5s
+set uptime disabled=yes display-time=5s
+set packets disabled=yes display-time=5s
+set bits disabled=yes display-time=5s
+set version disabled=yes display-time=5s
+set identity disabled=yes display-time=5s
+set ether1 disabled=yes display-time=5s
+/tool user-manager database
+set db-path=user-manager
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export_verbose b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export_verbose
new file mode 100644
index 000000000..0f49fefe3
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export_verbose
@@ -0,0 +1,26 @@
+# sep/25/2018 10:10:52 by RouterOS 6.42.5
+# software id = 9EER-511K
+#
+#
+#
+/interface wireless security-profiles
+set [ find default=yes ] supplicant-identity=MikroTik
+/tool user-manager customer
+set admin access=own-routers,own-users,own-profiles,own-limits,config-payment-gw
+/ip address
+add address=192.168.88.1/24 comment=defconf interface=ether1 network=192.168.88.0
+/ip dhcp-client
+add dhcp-options=hostname,clientid disabled=no interface=ether1
+/system lcd
+set contrast=0 enabled=no port=parallel type=24x4
+/system lcd page
+set time disabled=yes display-time=5s
+set resources disabled=yes display-time=5s
+set uptime disabled=yes display-time=5s
+set packets disabled=yes display-time=5s
+set bits disabled=yes display-time=5s
+set version disabled=yes display-time=5s
+set identity disabled=yes display-time=5s
+set ether1 disabled=yes display-time=5s
+/tool user-manager database
+set db-path=user-manager
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export_verbose.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export_verbose.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export_verbose.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging
new file mode 100644
index 000000000..9ccddb292
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging
@@ -0,0 +1,34 @@
+Flags: D - dynamic, X - disabled, R - running, S - slave
+ 0 R name="ether1" default-name="ether1" type="ether" mtu=1500 actual-mtu=1500
+ mac-address=00:1C:42:36:52:90 last-link-up-time=sep/25/2018 06:30:04
+ link-downs=0
+ 1 R name="ether2" default-name="ether2" type="ether" mtu=1500 actual-mtu=1500
+ mac-address=00:1C:42:36:52:91 last-link-up-time=sep/25/2018 06:30:04
+ link-downs=0
+ 2 R name="ether3" default-name="ether3" type="ether" mtu=1500 actual-mtu=1500
+ mac-address=00:1C:42:36:52:92 last-link-up-time=sep/25/2018 06:30:04
+ link-downs=0
+ 3 R name="ether4" default-name="ether4" type="ether" mtu=1500 actual-mtu=1500
+ mac-address=00:1C:42:36:52:93 last-link-up-time=sep/25/2018 06:30:04
+ link-downs=0
+ 4 R name="ether5" default-name="ether5" type="ether" mtu=1500 actual-mtu=1500
+ mac-address=00:1C:42:36:52:94 last-link-up-time=sep/25/2018 06:30:04
+ link-downs=0
+ 5 R name="ether6" default-name="ether6" type="ether" mtu=1500 actual-mtu=1500
+ mac-address=00:1C:42:36:52:95 last-link-up-time=sep/25/2018 06:30:04
+ link-downs=0
+ 6 R name="ether7" default-name="ether7" type="ether" mtu=1500 actual-mtu=1500
+ mac-address=00:1C:42:36:52:96 last-link-up-time=sep/25/2018 06:30:04
+ link-downs=0
+ 7 R name="ether8" default-name="ether8" type="ether" mtu=1500 actual-mtu=1500
+ mac-address=00:1C:42:36:52:97 last-link-up-time=sep/25/2018 06:30:04
+ link-downs=0
+ 8 R name="ether9" default-name="ether9" type="ether" mtu=1500 actual-mtu=1500
+ mac-address=00:1C:42:36:52:98 last-link-up-time=sep/25/2018 06:30:04
+ link-downs=0
+ 9 R name="ether10" default-name="ether10" type="ether" mtu=1500 actual-mtu=1500
+ mac-address=00:1C:42:36:52:99 last-link-up-time=sep/25/2018 06:30:04
+ link-downs=0
+10 R name="pppoe" default-name="pppoe" type="ppp" mtu=1500 actual-mtu=1500
+ mac-address=00:1C:42:36:52:00 last-link-up-time=sep/25/2018 06:30:04
+ link-downs=0
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging
new file mode 100644
index 000000000..d4fd2bcd2
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging
@@ -0,0 +1,10 @@
+Flags: X - disabled, I - invalid, D - dynamic
+ 0 ;;; defconf
+ address=192.168.88.1/24 network=192.168.88.0 interface=ether1
+ actual-interface=ether1
+
+ 1 D address=10.37.129.3/24 network=10.37.129.0 interface=ether1
+ actual-interface=ether1
+
+ 2 D address=10.37.0.0/24 network=10.37.0.1 interface=pppoe
+ actual-interface=pppoe
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging
new file mode 100644
index 000000000..906dfb750
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging
@@ -0,0 +1,15 @@
+ 0 interface=ether2-master address=10.37.129.3 address4=10.37.129.3 mac-address=D4:CA:6D:C6:16:4C identity="router1" platform="MikroTik" version="6.42.2 (stable)" unpack=none age=59s
+ uptime=3w19h11m36s software-id="1234-1234" board="RBwAPG-5HacT2HnD" interface-name="bridge" system-description="MikroTik RouterOS 6.42.2 (stable) RBwAPG-5HacT2HnD"
+ system-caps="" system-caps-enabled=""
+
+ 1 interface=ether3 address=10.37.129.4 address4=10.37.129.4 mac-address=D4:CA:6D:C6:18:2F identity="router2" platform="MikroTik" version="6.42.2 (stable)" unpack=none age=54s
+ uptime=3w19h11m30s software-id="1234-1234" board="RBwAPG-5HacT2HnD" ipv6=no interface-name="bridge" system-description="MikroTik RouterOS 6.42.2 (stable) RBwAPG-5HacT2HnD"
+ system-caps="" system-caps-enabled=""
+
+ 2 interface=ether5 address=10.37.129.5 address4=10.37.129.5 mac-address=B8:69:F4:37:F0:C8 identity="router3" platform="MikroTik" version="6.40.8 (bugfix)" unpack=none age=43s
+ uptime=3d14h25m31s software-id="1234-1234" board="RB960PGS" interface-name="ether1" system-description="MikroTik RouterOS 6.40.8 (bugfix) RB960PGS" system-caps=""
+ system-caps-enabled=""
+
+ 3 interface=ether10 address=10.37.129.6 address4=10.37.129.6 mac-address=6C:3B:6B:A1:0B:63 identity="router4" platform="MikroTik" version="6.42.2 (stable)" unpack=none age=54s
+ uptime=3w6d1h11m44s software-id="1234-1234" board="RBSXTLTE3-7" interface-name="bridge" system-description="MikroTik RouterOS 6.42.2 (stable) RBSXTLTE3-7" system-caps=""
+ system-caps-enabled=""
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging
new file mode 100644
index 000000000..6c2e558ef
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging
@@ -0,0 +1,19 @@
+Flags: X - disabled, A - active, D - dynamic,
+C - connect, S - static, r - rip, b - bgp, o - ospf, m - mme,
+B - blackhole, U - unreachable, P - prohibit
+ 0 ADC dst-address=10.10.66.0/30 pref-src=10.10.66.1 gateway=bridge1
+ gateway-status=bridge1 reachable distance=0 scope=10
+ routing-mark=altegro
+
+ 2 A S dst-address=0.0.0.0/0 gateway=85.15.75.109
+ gateway-status=85.15.75.109 reachable via Internet-VTK distance=1
+ scope=30 target-scope=10
+
+ 3 ADC dst-address=10.10.1.0/30 pref-src=10.10.1.1 gateway=GRE_TYRMA
+ gateway-status=GRE_TYRMA reachable distance=0 scope=10
+
+ 4 DC dst-address=10.10.1.4/30 pref-src=10.10.1.5 gateway=RB2011
+ gateway-status=RB2011 unreachable distance=255 scope=10
+
+ 5 ADC dst-address=10.10.2.0/30 pref-src=10.10.2.1 gateway=VLAN_SAT.ROUTER
+ gateway-status=VLAN_SAT.ROUTER reachable distance=0 scope=10
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging
new file mode 100644
index 000000000..c18e9ea57
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging
@@ -0,0 +1,3 @@
+Flags: X - disabled, I - invalid, D - dynamic, G - global, L - link-local
+ 0 DL address=fe80::21c:42ff:fe36:5290/64 from-pool="" interface=ether1
+ actual-interface=ether1 eui-64=no advertise=no no-dad=no
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv6 b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv6
new file mode 100644
index 000000000..0ea34bc0d
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv6
@@ -0,0 +1 @@
+bad command name address (line 1 column 7)
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv6.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv6.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv6.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging
new file mode 100644
index 000000000..8c560e2a4
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging
@@ -0,0 +1,10 @@
+Flags: * - default, X - disabled
+ 0 *X name="default" as=65530 router-id=0.0.0.0 redistribute-connected=no
+ redistribute-static=no redistribute-rip=no redistribute-ospf=no
+ redistribute-other-bgp=no out-filter="" client-to-client-reflection=yes
+ ignore-as-path-len=no routing-table=""
+
+ 1 name="MAIN_AS_STARKDV" as=64520 router-id=10.10.50.1
+ redistribute-connected=no redistribute-static=no redistribute-rip=no
+ redistribute-ospf=no redistribute-other-bgp=no out-filter=""
+ client-to-client-reflection=yes ignore-as-path-len=no routing-table=""
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging
new file mode 100644
index 000000000..1ae4f5bcf
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging
@@ -0,0 +1,13 @@
+Flags: X - disabled, E - established
+ 0 E name="iBGP_BRAS.TYRMA" instance=MAIN_AS_STARKDV remote-address=10.10.100.1
+ remote-as=64520 tcp-md5-key="" nexthop-choice=default multihop=no
+ route-reflect=yes hold-time=3m ttl=default in-filter="" out-filter=""
+ address-families=ip,l2vpn,vpnv4 update-source=LAN_KHV
+ default-originate=never remove-private-as=no as-override=no passive=no
+ use-bfd=yes
+
+ 1 E name="iBGP_BRAS_SAT" instance=MAIN_AS_STARKDV remote-address=10.10.50.230
+ remote-as=64520 tcp-md5-key="" nexthop-choice=default multihop=no
+ route-reflect=yes hold-time=3m ttl=default in-filter="" out-filter=""
+ address-families=ip default-originate=never remove-private-as=no
+ as-override=no passive=no use-bfd=yes
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging
new file mode 100644
index 000000000..f64fa6d00
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging
@@ -0,0 +1,7 @@
+Flags: L - label-present
+ 0 L route-distinguisher=64520:666 dst-address=10.10.66.8/30 gateway=10.10.100.1
+ interface=GRE_TYRMA in-label=6136 out-label=6136 bgp-local-pref=100
+ bgp-origin=incomplete bgp-ext-communities="RT:64520:666"
+
+ 1 L route-distinguisher=64520:666 dst-address=10.10.66.0/30 interface=bridge1
+ in-label=1790 bgp-ext-communities="RT:64520:666"
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging
new file mode 100644
index 000000000..964046633
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging
@@ -0,0 +1,10 @@
+Flags: X - disabled, * - default
+ 0 * name="default" router-id=10.10.50.1 distribute-default=never redistribute-connected=no
+ redistribute-static=no redistribute-rip=no redistribute-bgp=no redistribute-other-ospf=no
+ metric-default=1 metric-connected=20 metric-static=20 metric-rip=20 metric-bgp=auto
+ metric-other-ospf=auto in-filter=ospf-in out-filter=ospf-out
+
+ 1 name="OSPF_ALTEGRO" router-id=10.10.66.1 distribute-default=never redistribute-connected=no
+ redistribute-static=no redistribute-rip=no redistribute-bgp=no redistribute-other-ospf=no
+ metric-default=1 metric-connected=20 metric-static=20 metric-rip=20 metric-bgp=auto
+ metric-other-ospf=auto in-filter=ospf-in out-filter=ospf-out routing-table=altegro
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging
new file mode 100644
index 000000000..d683b252c
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging
@@ -0,0 +1,3 @@
+0 instance=default router-id=10.10.100.1 address=10.10.1.2 interface=GRE_TYRMA priority=1
+ dr-address=0.0.0.0 backup-dr-address=0.0.0.0 state="Full" state-changes=15 ls-retransmits=0
+ ls-requests=0 db-summaries=0 adjacency=6h8m46s
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging
new file mode 100644
index 000000000..d7dc3ff3e
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging
@@ -0,0 +1 @@
+ name: MikroTik
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging
new file mode 100644
index 000000000..79353f791
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging
@@ -0,0 +1,16 @@
+ uptime: 3h28m52s
+ version: 6.42.5 (stable)
+ build-time: Jun/26/2018 12:12:08
+ free-memory: 988.3MiB
+ total-memory: 1010.8MiB
+ cpu: Intel(R)
+ cpu-count: 2
+ cpu-frequency: 2496MHz
+ cpu-load: 0%
+ free-hdd-space: 63.4GiB
+ total-hdd-space: 63.5GiB
+ write-sect-since-reboot: 4576
+ write-sect-total: 4576
+ architecture-name: x86
+ board-name: x86
+ platform: MikroTik
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging
new file mode 100644
index 000000000..263c95909
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging
@@ -0,0 +1,7 @@
+ routerboard: yes
+ model: RouterBOARD 3011UiAS
+ serial-number: 1234567890
+ firmware-type: ipq8060
+ factory-firmware: 3.41
+ current-firmware: 3.41
+ upgrade-firmware: 6.42.2
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_package_print b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_package_print
new file mode 100644
index 000000000..3f806211e
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_package_print
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MMM MMM KKK TTTTTTTTTTT KKK
+
+ MMMM MMMM KKK TTTTTTTTTTT KKK
+
+ MMM MMMM MMM III KKK KKK RRRRRR OOOOOO TTT III KKK KKK
+
+ MMM MM MMM III KKKKK RRR RRR OOO OOO TTT III KKKKK
+
+ MMM MMM III KKK KKK RRRRRR OOO OOO TTT III KKK KKK
+
+ MMM MMM III KKK KKK RRR RRR OOOOOO TTT III KKK KKK
+
+
+
+ MikroTik RouterOS 6.42.5 (c) 1999-2018 http://www.mikrotik.com/
+
+
+[?] Gives the list of available commands
+
+command [?] Gives help on the command and list of arguments
+
+
+
+[Tab] Completes the command/word. If the input is ambiguous,
+
+ a second [Tab] gives possible options
+
+
+
+/ Move up to base level
+
+.. Move up one level
+
+/command Use command at the base level
+
+
+Z <[?47l[?7h[?5l[?25h
+
+
+
+[admin@MainRouter] >
+[admin@MainRouter] > /system routerboard print
+[admin@MainRouter] > /system routerboard print
+
+ routerboard: yes
+ model: 750GL
+ serial-number: 1234567890AB
+ firmware-type: ar7240
+ factory-firmware: 3.09
+ current-firmware: 6.41.2
+ upgrade-firmware: 6.42.5
+
+
+
+
+
+[admin@MainRouter] >
+[admin@MainRouter] > /system identity print
+[admin@MainRouter] > /system identity print
+
+ name: MikroTik
+
+
+
+
+
+[admin@MainRouter] >
+[admin@MainRouter] > /system package print
+[admin@MainRouter] > /system package print
+
+Flags: X - disabled
+ # NAME VERSION SCHEDULED
+ 0 routeros-mipsbe 6.42.5
+ 1 system 6.42.5
+ 2 ipv6 6.42.5
+ 3 wireless 6.42.5
+ 4 hotspot 6.42.5
+ 5 dhcp 6.42.5
+ 6 mpls 6.42.5
+ 7 routing 6.42.5
+ 8 ppp 6.42.5
+ 9 security 6.42.5
+10 advanced-tools 6.42.5
+
+
+
+
+
+[admin@MainRouter] >
+[admin@MainRouter] > \ No newline at end of file
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_package_print.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_package_print.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_package_print.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_resource_print b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_resource_print
new file mode 100644
index 000000000..63bc3beba
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_resource_print
@@ -0,0 +1,17 @@
+[admin@RB1100test] /system resource> print
+ uptime: 2w1d23h34m57s
+ version: "5.0rc1"
+ free-memory: 385272KiB
+ total-memory: 516708KiB
+ cpu: "e500v2"
+ cpu-count: 1
+ cpu-frequency: 799MHz
+ cpu-load: 9%
+ free-hdd-space: 466328KiB
+ total-hdd-space: 520192KiB
+ write-sect-since-reboot: 1411
+ write-sect-total: 70625
+ bad-blocks: 0.2%
+ architecture-name: "powerpc"
+ board-name: "RB1100"
+ platform: "MikroTik"
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_resource_print.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_resource_print.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_resource_print.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/routeros_module.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/routeros_module.py
new file mode 100644
index 000000000..0ec44f70b
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/routeros_module.py
@@ -0,0 +1,75 @@
+# Copyright (c) 2016 Red Hat Inc.
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt 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 os
+import json
+
+from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase
+
+
+fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
+fixture_data = {}
+
+
+def load_fixture(name):
+ path = os.path.join(fixture_path, name)
+
+ if path in fixture_data:
+ return fixture_data[path]
+
+ with open(path) as f:
+ data = f.read()
+
+ try:
+ data = json.loads(data)
+ except Exception:
+ pass
+
+ fixture_data[path] = data
+ return data
+
+
+class TestRouterosModule(ModuleTestCase):
+
+ def execute_module(self, failed=False, changed=False, commands=None, sort=True, defaults=False):
+
+ self.load_fixtures(commands)
+
+ if failed:
+ result = self.failed()
+ self.assertTrue(result['failed'], result)
+ else:
+ result = self.changed(changed)
+ self.assertEqual(result['changed'], changed, result)
+
+ if commands is not None:
+ if sort:
+ self.assertEqual(sorted(commands), sorted(result['commands']), result['commands'])
+ else:
+ self.assertEqual(commands, result['commands'], result['commands'])
+
+ return result
+
+ def failed(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertTrue(result['failed'], result)
+ return result
+
+ def changed(self, changed=False):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], changed, result)
+ return result
+
+ def load_fixtures(self, commands=None):
+ pass
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api.py
new file mode 100644
index 000000000..4cfdeef1e
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api.py
@@ -0,0 +1,308 @@
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt 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
+
+from ansible_collections.community.routeros.tests.unit.compat.mock import patch, MagicMock
+from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import FakeLibRouterosError, Key, Or, fake_ros_api
+from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase
+from ansible_collections.community.routeros.plugins.modules import api
+
+
+class TestRouterosApiModule(ModuleTestCase):
+
+ def setUp(self):
+ super(TestRouterosApiModule, self).setUp()
+ self.module = api
+ self.module.LibRouterosError = FakeLibRouterosError
+ self.module.connect = MagicMock(new=fake_ros_api)
+ self.module.check_has_library = MagicMock()
+ self.patch_create_api = patch('ansible_collections.community.routeros.plugins.modules.api.create_api', MagicMock(new=fake_ros_api))
+ self.patch_create_api.start()
+ self.module.Key = MagicMock(new=Key)
+ self.module.Or = MagicMock(new=Or)
+ self.config_module_args = {"username": "admin",
+ "password": "pаss",
+ "hostname": "127.0.0.1",
+ "path": "interface bridge"}
+
+ def tearDown(self):
+ self.patch_create_api.stop()
+
+ def test_module_fail_when_required_args_missing(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ set_module_args({})
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.path)
+ def test_api_path(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ set_module_args(self.config_module_args.copy())
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api)
+ def test_api_add(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['add'] = "name=unit_test_brige"
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api)
+ def test_api_add_already_exist(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['add'] = "name=unit_test_brige_exist"
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'][0], 'failure: already have interface with such name')
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api)
+ def test_api_remove(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['remove'] = "*A1"
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api)
+ def test_api_remove_no_id(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['remove'] = "*A2"
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'][0], 'no such item (4)')
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.arbitrary)
+ def test_api_cmd(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['cmd'] = "add name=unit_test_brige_arbitrary"
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.arbitrary)
+ def test_api_cmd_none_existing_cmd(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['cmd'] = "add NONE_EXIST=unit_test_brige_arbitrary"
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'][0], 'no such command')
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api)
+ def test_api_update(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['update'] = ".id=*A1 name=unit_test_brige"
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api)
+ def test_api_update_none_existing_id(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['update'] = ".id=*A2 name=unit_test_brige"
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'][0], 'no such item (4)')
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api)
+ def test_api_query(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['query'] = ".id name"
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['msg'], [
+ {'.id': '*A1', 'name': 'dummy_bridge_A1'},
+ {'.id': '*A2', 'name': 'dummy_bridge_A2'},
+ {'.id': '*A3', 'name': 'dummy_bridge_A3'},
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api)
+ def test_api_query_missing_key(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['query'] = ".id other"
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['msg'], ["no results for 'interface bridge 'query' .id other"])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.select_where)
+ def test_api_query_and_WHERE(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['query'] = ".id name WHERE name == dummy_bridge_A2"
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['msg'], [
+ {'.id': '*A1', 'name': 'dummy_bridge_A1'},
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.select_where)
+ def test_api_query_and_WHERE_no_cond(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['query'] = ".id name WHERE name != dummy_bridge_A2"
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['msg'], [
+ {'.id': '*A1', 'name': 'dummy_bridge_A1'},
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api)
+ def test_api_extended_query(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['extended_query'] = {
+ 'attributes': ['.id', 'name'],
+ }
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['msg'], [
+ {'.id': '*A1', 'name': 'dummy_bridge_A1'},
+ {'.id': '*A2', 'name': 'dummy_bridge_A2'},
+ {'.id': '*A3', 'name': 'dummy_bridge_A3'},
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api)
+ def test_api_extended_query_missing_key(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['extended_query'] = {
+ 'attributes': ['.id', 'other'],
+ }
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['msg'], [])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.select_where)
+ def test_api_extended_query_and_WHERE(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['extended_query'] = {
+ 'attributes': ['.id', 'name'],
+ 'where': [
+ {
+ 'attribute': 'name',
+ 'is': '==',
+ 'value': 'dummy_bridge_A2',
+ },
+ ],
+ }
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['msg'], [
+ {'.id': '*A1', 'name': 'dummy_bridge_A1'},
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.select_where)
+ def test_api_extended_query_and_WHERE_no_cond(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['extended_query'] = {
+ 'attributes': ['.id', 'name'],
+ 'where': [
+ {
+ 'attribute': 'name',
+ 'is': 'not',
+ 'value': 'dummy_bridge_A2',
+ },
+ ],
+ }
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['msg'], [
+ {'.id': '*A1', 'name': 'dummy_bridge_A1'},
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.select_where)
+ def test_api_extended_query_and_WHERE_or(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['extended_query'] = {
+ 'attributes': ['.id', 'name'],
+ 'where': [
+ {
+ 'or': [
+ {
+ 'attribute': 'name',
+ 'is': 'in',
+ 'value': [1, 2],
+ },
+ {
+ 'attribute': 'name',
+ 'is': '!=',
+ 'value': 5,
+ },
+ ],
+ },
+ ],
+ }
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['msg'], [
+ {'.id': '*A1', 'name': 'dummy_bridge_A1'},
+ ])
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_facts.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_facts.py
new file mode 100644
index 000000000..64985f8b6
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_facts.py
@@ -0,0 +1,752 @@
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt 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
+
+from ansible_collections.community.routeros.tests.unit.compat.mock import patch, MagicMock
+from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import FakeLibRouterosError, Key, fake_ros_api
+from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase
+from ansible_collections.community.routeros.plugins.modules import api_facts
+
+
+API_RESPONSES = {
+ ('interface', ): [
+ {
+ '.id': '*1',
+ 'name': 'first-ether',
+ 'default-name': 'ether1',
+ 'type': 'ether',
+ 'mtu': 1500,
+ 'actual-mtu': 1500,
+ 'l2mtu': 1598,
+ 'max-l2mtu': 4074,
+ 'mac-address': '00:11:22:33:44:55',
+ 'last-link-up-time': 'apr/22/2022 07:54:55',
+ 'link-downs': 0,
+ 'rx-byte': 1234,
+ 'tx-byte': 1234,
+ 'rx-packet': 1234,
+ 'tx-packet': 1234,
+ 'rx-drop': 1234,
+ 'tx-drop': 1234,
+ 'tx-queue-drop': 1234,
+ 'rx-error': 1234,
+ 'tx-error': 1234,
+ 'fp-rx-byte': 1234,
+ 'fp-tx-byte': 1234,
+ 'fp-rx-packet': 1234,
+ 'fp-tx-packet': 1234,
+ 'running': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*2',
+ 'name': 'second-ether',
+ 'default-name': 'ether2',
+ 'type': 'ether',
+ 'mtu': 1500,
+ 'actual-mtu': 1500,
+ 'l2mtu': 1598,
+ 'max-l2mtu': 4074,
+ 'mac-address': '00:11:22:33:44:66',
+ 'last-link-up-time': 'apr/22/2022 07:54:55',
+ 'link-downs': 0,
+ 'rx-byte': 1234,
+ 'tx-byte': 1234,
+ 'rx-packet': 1234,
+ 'tx-packet': 1234,
+ 'rx-drop': 1234,
+ 'tx-drop': 1234,
+ 'tx-queue-drop': 1234,
+ 'rx-error': 1234,
+ 'tx-error': 1234,
+ 'fp-rx-byte': 1234,
+ 'fp-tx-byte': 1234,
+ 'fp-rx-packet': 1234,
+ 'fp-tx-packet': 1234,
+ 'running': True,
+ 'slave': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*3',
+ 'name': 'third-ether',
+ 'default-name': 'ether3',
+ 'type': 'ether',
+ 'mtu': 1500,
+ 'actual-mtu': 1500,
+ 'l2mtu': 1598,
+ 'max-l2mtu': 4074,
+ 'mac-address': '00:11:22:33:44:77',
+ 'last-link-up-time': 'apr/22/2022 07:54:55',
+ 'link-downs': 0,
+ 'rx-byte': 1234,
+ 'tx-byte': 1234,
+ 'rx-packet': 1234,
+ 'tx-packet': 1234,
+ 'rx-drop': 1234,
+ 'tx-drop': 1234,
+ 'tx-queue-drop': 1234,
+ 'rx-error': 1234,
+ 'tx-error': 1234,
+ 'fp-rx-byte': 1234,
+ 'fp-tx-byte': 1234,
+ 'fp-rx-packet': 1234,
+ 'fp-tx-packet': 1234,
+ 'running': True,
+ 'slave': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*4',
+ 'name': 'fourth-ether',
+ 'default-name': 'ether4',
+ 'type': 'ether',
+ 'mtu': 1500,
+ 'actual-mtu': 1500,
+ 'l2mtu': 1598,
+ 'max-l2mtu': 4074,
+ 'mac-address': '00:11:22:33:44:88',
+ 'last-link-down-time': 'apr/23/2022 08:22:50',
+ 'last-link-up-time': 'apr/23/2022 08:22:52',
+ 'link-downs': 2,
+ 'rx-byte': 1234,
+ 'tx-byte': 1234,
+ 'rx-packet': 1234,
+ 'tx-packet': 1234,
+ 'rx-drop': 1234,
+ 'tx-drop': 1234,
+ 'tx-queue-drop': 1234,
+ 'rx-error': 1234,
+ 'tx-error': 1234,
+ 'fp-rx-byte': 1234,
+ 'fp-tx-byte': 1234,
+ 'fp-rx-packet': 1234,
+ 'fp-tx-packet': 1234,
+ 'running': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*5',
+ 'name': 'fifth-ether',
+ 'default-name': 'ether5',
+ 'type': 'ether',
+ 'mtu': 1500,
+ 'actual-mtu': 1500,
+ 'l2mtu': 1598,
+ 'max-l2mtu': 4074,
+ 'mac-address': '00:11:22:33:44:99',
+ 'last-link-down-time': 'may/02/2022 18:12:32',
+ 'last-link-up-time': 'may/02/2022 18:08:01',
+ 'link-downs': 14,
+ 'rx-byte': 1234,
+ 'tx-byte': 1234,
+ 'rx-packet': 1234,
+ 'tx-packet': 1234,
+ 'rx-drop': 1234,
+ 'tx-drop': 1234,
+ 'tx-queue-drop': 1234,
+ 'rx-error': 1234,
+ 'tx-error': 1234,
+ 'fp-rx-byte': 1234,
+ 'fp-tx-byte': 1234,
+ 'fp-rx-packet': 1234,
+ 'fp-tx-packet': 1234,
+ 'running': False,
+ 'slave': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*7',
+ 'name': 'my-bridge',
+ 'type': 'bridge',
+ 'mtu': 'auto',
+ 'actual-mtu': 1500,
+ 'l2mtu': 1598,
+ 'mac-address': '00:11:22:33:44:66',
+ 'last-link-up-time': 'apr/22/2022 07:54:48',
+ 'link-downs': 0,
+ 'rx-byte': 1234,
+ 'tx-byte': 1234,
+ 'rx-packet': 1234,
+ 'tx-packet': 1234,
+ 'rx-drop': 1234,
+ 'tx-drop': 1234,
+ 'tx-queue-drop': 1234,
+ 'rx-error': 1234,
+ 'tx-error': 1234,
+ 'fp-rx-byte': 1234,
+ 'fp-tx-byte': 1234,
+ 'fp-rx-packet': 1234,
+ 'fp-tx-packet': 1234,
+ 'running': True,
+ 'disabled': False,
+ },
+ ],
+ ('ip', 'address', ): [
+ {
+ '.id': '*1',
+ 'address': '192.168.1.1/24',
+ 'network': '192.168.1.0',
+ 'interface': 'my-bridge',
+ 'actual-interface': 'my-bridge',
+ 'invalid': False,
+ 'dynamic': False,
+ 'disabled': False,
+ 'comment': 'Wohnung',
+ },
+ {
+ '.id': '*5',
+ 'address': '192.168.2.1/24',
+ 'network': '192.168.2.0',
+ 'interface': 'fourth-ether',
+ 'actual-interface': 'fourth-ether',
+ 'invalid': False,
+ 'dynamic': False,
+ 'disabled': False,
+ 'comment': 'VoIP',
+ },
+ {
+ '.id': '*6',
+ 'address': '1.2.3.4/21',
+ 'network': '84.73.216.0',
+ 'interface': 'first-ether',
+ 'actual-interface': 'first-ether',
+ 'invalid': False,
+ 'dynamic': True,
+ 'disabled': False,
+ },
+ ],
+ ('ipv6', 'address', ): [
+ {
+ '.id': '*1',
+ 'address': 'fe80::1:2:3/64',
+ 'from-pool': '',
+ 'interface': 'my-bridge',
+ 'actual-interface': 'my-bridge',
+ 'eui-64': False,
+ 'advertise': False,
+ 'no-dad': False,
+ 'invalid': False,
+ 'dynamic': True,
+ 'link-local': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*2',
+ 'address': 'fe80::1:2:4/64',
+ 'from-pool': '',
+ 'interface': 'fourth-ether',
+ 'actual-interface': 'fourth-ether',
+ 'eui-64': False,
+ 'advertise': False,
+ 'no-dad': False,
+ 'invalid': False,
+ 'dynamic': True,
+ 'link-local': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*3',
+ 'address': 'fe80::1:2:5/64',
+ 'from-pool': '',
+ 'interface': 'first-ether',
+ 'actual-interface': 'first-ether',
+ 'eui-64': False,
+ 'advertise': False,
+ 'no-dad': False,
+ 'invalid': False,
+ 'dynamic': True,
+ 'link-local': True,
+ 'disabled': False,
+ },
+ ],
+ ('ip', 'neighbor', ): [],
+ ('system', 'identity', ): [
+ {
+ 'name': 'MikroTik',
+ },
+ ],
+ ('system', 'resource', ): [
+ {
+ 'uptime': '2w3d4h5m6s',
+ 'version': '6.49.6 (stable)',
+ 'build-time': 'Apr/07/2022 17:53:31',
+ 'free-memory': 12345678,
+ 'total-memory': 23456789,
+ 'cpu': 'MIPS 24Kc V7.4',
+ 'cpu-count': 1,
+ 'cpu-frequency': 400,
+ 'cpu-load': 48,
+ 'free-hdd-space': 123456789,
+ 'total-hdd-space': 234567890,
+ 'write-sect-since-reboot': 1234,
+ 'write-sect-total': 12345,
+ 'bad-blocks': 0,
+ 'architecture-name': 'mipsbe',
+ 'board-name': 'RB750GL',
+ 'platform': 'MikroTik',
+ },
+ ],
+ ('system', 'routerboard', ): [
+ {
+ 'routerboard': True,
+ 'model': '750GL',
+ 'serial-number': '0123456789AB',
+ 'firmware-type': 'ar7240',
+ 'factory-firmware': '3.09',
+ 'current-firmware': '6.49.6',
+ 'upgrade-firmware': '6.49.6',
+ },
+ ],
+ ('routing', 'bgp', 'peer', ): [],
+ ('routing', 'bgp', 'vpnv4-route', ): [],
+ ('routing', 'bgp', 'instance', ): [
+ {
+ '.id': '*0',
+ 'name': 'default',
+ 'as': 65530,
+ 'router-id': '0.0.0.0',
+ 'redistribute-connected': False,
+ 'redistribute-static': False,
+ 'redistribute-rip': False,
+ 'redistribute-ospf': False,
+ 'redistribute-other-bgp': False,
+ 'out-filter': '',
+ 'client-to-client-reflection': True,
+ 'ignore-as-path-len': False,
+ 'routing-table': '',
+ 'default': True,
+ 'disabled': False,
+ },
+ ],
+ ('ip', 'route', ): [
+ {
+ '.id': '*30000001',
+ 'dst-address': '0.0.0.0/0',
+ 'gateway': '1.2.3.0',
+ 'gateway-status': '1.2.3.0 reachable via first-ether',
+ 'distance': 1,
+ 'scope': 30,
+ 'target-scope': 10,
+ 'vrf-interface': 'first-ether',
+ 'active': True,
+ 'dynamic': True,
+ 'static': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*40162F13',
+ 'dst-address': '84.73.216.0/21',
+ 'pref-src': '1.2.3.4',
+ 'gateway': 'first-ether',
+ 'gateway-status': 'first-ether reachable',
+ 'distance': 0,
+ 'scope': 10,
+ 'active': True,
+ 'dynamic': True,
+ 'connect': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*4016AA23',
+ 'dst-address': '192.168.2.0/24',
+ 'pref-src': '192.168.2.1',
+ 'gateway': 'fourth-ether',
+ 'gateway-status': 'fourth-ether reachable',
+ 'distance': 0,
+ 'scope': 10,
+ 'active': True,
+ 'dynamic': True,
+ 'connect': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*40168E05',
+ 'dst-address': '192.168.1.0/24',
+ 'pref-src': '192.168.1.1',
+ 'gateway': 'my-bridge',
+ 'gateway-status': 'my-bridge reachable',
+ 'distance': 0,
+ 'scope': 10,
+ 'active': True,
+ 'dynamic': True,
+ 'connect': True,
+ 'disabled': False,
+ },
+ ],
+ ('routing', 'ospf', 'instance', ): [
+ {
+ '.id': '*0',
+ 'name': 'default',
+ 'router-id': '0.0.0.0',
+ 'distribute-default': 'never',
+ 'redistribute-connected': False,
+ 'redistribute-static': False,
+ 'redistribute-rip': False,
+ 'redistribute-bgp': False,
+ 'redistribute-other-ospf': False,
+ 'metric-default': 1,
+ 'metric-connected': 20,
+ 'metric-static': 20,
+ 'metric-rip': 20,
+ 'metric-bgp': 'auto',
+ 'metric-other-ospf': 'auto',
+ 'in-filter': 'ospf-in',
+ 'out-filter': 'ospf-out',
+ 'state': 'down',
+ 'default': True,
+ 'disabled': False,
+ },
+ ],
+ ('routing', 'ospf', 'neighbor', ): [],
+}
+
+
+class TestRouterosApiFactsModule(ModuleTestCase):
+
+ def setUp(self):
+ super(TestRouterosApiFactsModule, self).setUp()
+ self.module = api_facts
+ self.module.LibRouterosError = FakeLibRouterosError
+ self.module.connect = MagicMock(new=fake_ros_api)
+ self.module.check_has_library = MagicMock()
+ self.patch_create_api = patch('ansible_collections.community.routeros.plugins.modules.api_facts.create_api', MagicMock(new=fake_ros_api))
+ self.patch_create_api.start()
+ self.patch_query_path = patch('ansible_collections.community.routeros.plugins.modules.api_facts.FactsBase.query_path', self.query_path)
+ self.patch_query_path.start()
+ self.module.Key = MagicMock(new=Key)
+ self.config_module_args = {
+ 'username': 'admin',
+ 'password': 'pаss',
+ 'hostname': '127.0.0.1',
+ }
+
+ def tearDown(self):
+ self.patch_query_path.stop()
+ self.patch_create_api.stop()
+
+ def query_path(self, path):
+ response = API_RESPONSES.get(tuple(path))
+ if response is None:
+ raise Exception('Unexpected command: %s' % repr(path))
+ return response
+
+ def test_module_fail_when_required_args_missing(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ set_module_args({})
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+
+ def test_module_fail_when_invalid_gather_subset(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ module_args = self.config_module_args.copy()
+ module_args['gather_subset'] = ['!foobar']
+ set_module_args(module_args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'], 'Bad subset: foobar')
+
+ def test_full_run(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ set_module_args(self.config_module_args.copy())
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['ansible_facts']['ansible_net_all_ipv4_addresses'], [
+ '192.168.1.1',
+ '192.168.2.1',
+ '1.2.3.4',
+ ])
+ self.assertEqual(result['ansible_facts']['ansible_net_all_ipv6_addresses'], [
+ 'fe80::1:2:3',
+ 'fe80::1:2:4',
+ 'fe80::1:2:5',
+ ])
+ self.assertEqual(result['ansible_facts']['ansible_net_arch'], 'mipsbe')
+ self.assertEqual(result['ansible_facts']['ansible_net_bgp_instance'], {
+ 'default': {
+ 'as': 65530,
+ 'client-to-client-reflection': True,
+ 'default': True,
+ 'disabled': False,
+ 'ignore-as-path-len': False,
+ 'name': 'default',
+ 'out-filter': '',
+ 'redistribute-connected': False,
+ 'redistribute-ospf': False,
+ 'redistribute-other-bgp': False,
+ 'redistribute-rip': False,
+ 'redistribute-static': False,
+ 'router-id': '0.0.0.0',
+ 'routing-table': ''
+ },
+ })
+ self.assertEqual(result['ansible_facts']['ansible_net_bgp_peer'], {})
+ self.assertEqual(result['ansible_facts']['ansible_net_bgp_vpnv4_route'], {})
+ self.assertEqual(result['ansible_facts']['ansible_net_cpu_load'], 48)
+ self.assertEqual(result['ansible_facts']['ansible_net_gather_subset'], [
+ 'default',
+ 'hardware',
+ 'interfaces',
+ 'routing',
+ ])
+ self.assertEqual(result['ansible_facts']['ansible_net_hostname'], 'MikroTik')
+ self.assertEqual(result['ansible_facts']['ansible_net_interfaces'], {
+ 'my-bridge': {
+ 'actual-mtu': 1500,
+ 'disabled': False,
+ 'fp-rx-byte': 1234,
+ 'fp-rx-packet': 1234,
+ 'fp-tx-byte': 1234,
+ 'fp-tx-packet': 1234,
+ 'ipv4': [
+ {
+ 'address': '192.168.1.1',
+ 'subnet': 24
+ }
+ ],
+ 'ipv6': [
+ {
+ 'address': 'fe80::1:2:3',
+ 'subnet': 64
+ }
+ ],
+ 'l2mtu': 1598,
+ 'last-link-up-time': 'apr/22/2022 07:54:48',
+ 'link-downs': 0,
+ 'mac-address': '00:11:22:33:44:66',
+ 'mtu': 'auto',
+ 'name': 'my-bridge',
+ 'running': True,
+ 'rx-byte': 1234,
+ 'rx-drop': 1234,
+ 'rx-error': 1234,
+ 'rx-packet': 1234,
+ 'tx-byte': 1234,
+ 'tx-drop': 1234,
+ 'tx-error': 1234,
+ 'tx-packet': 1234,
+ 'tx-queue-drop': 1234,
+ 'type': 'bridge'
+ },
+ 'first-ether': {
+ 'actual-mtu': 1500,
+ 'default-name': 'ether1',
+ 'disabled': False,
+ 'fp-rx-byte': 1234,
+ 'fp-rx-packet': 1234,
+ 'fp-tx-byte': 1234,
+ 'fp-tx-packet': 1234,
+ 'ipv4': [
+ {
+ 'address': '1.2.3.4',
+ 'subnet': 21
+ }
+ ],
+ 'ipv6': [
+ {
+ 'address': 'fe80::1:2:5',
+ 'subnet': 64
+ }
+ ],
+ 'l2mtu': 1598,
+ 'last-link-up-time': 'apr/22/2022 07:54:55',
+ 'link-downs': 0,
+ 'mac-address': '00:11:22:33:44:55',
+ 'max-l2mtu': 4074,
+ 'mtu': 1500,
+ 'name': 'first-ether',
+ 'running': True,
+ 'rx-byte': 1234,
+ 'rx-drop': 1234,
+ 'rx-error': 1234,
+ 'rx-packet': 1234,
+ 'tx-byte': 1234,
+ 'tx-drop': 1234,
+ 'tx-error': 1234,
+ 'tx-packet': 1234,
+ 'tx-queue-drop': 1234,
+ 'type': 'ether'
+ },
+ 'second-ether': {
+ 'actual-mtu': 1500,
+ 'default-name': 'ether2',
+ 'disabled': False,
+ 'fp-rx-byte': 1234,
+ 'fp-rx-packet': 1234,
+ 'fp-tx-byte': 1234,
+ 'fp-tx-packet': 1234,
+ 'l2mtu': 1598,
+ 'last-link-up-time': 'apr/22/2022 07:54:55',
+ 'link-downs': 0,
+ 'mac-address': '00:11:22:33:44:66',
+ 'max-l2mtu': 4074,
+ 'mtu': 1500,
+ 'name': 'second-ether',
+ 'running': True,
+ 'rx-byte': 1234,
+ 'rx-drop': 1234,
+ 'rx-error': 1234,
+ 'rx-packet': 1234,
+ 'slave': True,
+ 'tx-byte': 1234,
+ 'tx-drop': 1234,
+ 'tx-error': 1234,
+ 'tx-packet': 1234,
+ 'tx-queue-drop': 1234,
+ 'type': 'ether'
+ },
+ 'third-ether': {
+ 'actual-mtu': 1500,
+ 'default-name': 'ether3',
+ 'disabled': False,
+ 'fp-rx-byte': 1234,
+ 'fp-rx-packet': 1234,
+ 'fp-tx-byte': 1234,
+ 'fp-tx-packet': 1234,
+ 'l2mtu': 1598,
+ 'last-link-up-time': 'apr/22/2022 07:54:55',
+ 'link-downs': 0,
+ 'mac-address': '00:11:22:33:44:77',
+ 'max-l2mtu': 4074,
+ 'mtu': 1500,
+ 'name': 'third-ether',
+ 'running': True,
+ 'rx-byte': 1234,
+ 'rx-drop': 1234,
+ 'rx-error': 1234,
+ 'rx-packet': 1234,
+ 'slave': True,
+ 'tx-byte': 1234,
+ 'tx-drop': 1234,
+ 'tx-error': 1234,
+ 'tx-packet': 1234,
+ 'tx-queue-drop': 1234,
+ 'type': 'ether'
+ },
+ 'fourth-ether': {
+ 'actual-mtu': 1500,
+ 'default-name': 'ether4',
+ 'disabled': False,
+ 'fp-rx-byte': 1234,
+ 'fp-rx-packet': 1234,
+ 'fp-tx-byte': 1234,
+ 'fp-tx-packet': 1234,
+ 'ipv4': [
+ {
+ 'address': '192.168.2.1',
+ 'subnet': 24
+ }
+ ],
+ 'ipv6': [
+ {
+ 'address': 'fe80::1:2:4',
+ 'subnet': 64
+ }
+ ],
+ 'l2mtu': 1598,
+ 'last-link-down-time': 'apr/23/2022 08:22:50',
+ 'last-link-up-time': 'apr/23/2022 08:22:52',
+ 'link-downs': 2,
+ 'mac-address': '00:11:22:33:44:88',
+ 'max-l2mtu': 4074,
+ 'mtu': 1500,
+ 'name': 'fourth-ether',
+ 'running': True,
+ 'rx-byte': 1234,
+ 'rx-drop': 1234,
+ 'rx-error': 1234,
+ 'rx-packet': 1234,
+ 'tx-byte': 1234,
+ 'tx-drop': 1234,
+ 'tx-error': 1234,
+ 'tx-packet': 1234,
+ 'tx-queue-drop': 1234,
+ 'type': 'ether'
+ },
+ 'fifth-ether': {
+ 'actual-mtu': 1500,
+ 'default-name': 'ether5',
+ 'disabled': False,
+ 'fp-rx-byte': 1234,
+ 'fp-rx-packet': 1234,
+ 'fp-tx-byte': 1234,
+ 'fp-tx-packet': 1234,
+ 'l2mtu': 1598,
+ 'last-link-down-time': 'may/02/2022 18:12:32',
+ 'last-link-up-time': 'may/02/2022 18:08:01',
+ 'link-downs': 14,
+ 'mac-address': '00:11:22:33:44:99',
+ 'max-l2mtu': 4074,
+ 'mtu': 1500,
+ 'name': 'fifth-ether',
+ 'running': False,
+ 'rx-byte': 1234,
+ 'rx-drop': 1234,
+ 'rx-error': 1234,
+ 'rx-packet': 1234,
+ 'slave': True,
+ 'tx-byte': 1234,
+ 'tx-drop': 1234,
+ 'tx-error': 1234,
+ 'tx-packet': 1234,
+ 'tx-queue-drop': 1234,
+ 'type': 'ether'
+ }
+ })
+ self.assertEqual(result['ansible_facts']['ansible_net_memfree_mb'], 12345678 / 1048576.0)
+ self.assertEqual(result['ansible_facts']['ansible_net_memtotal_mb'], 23456789 / 1048576.0)
+ self.assertEqual(result['ansible_facts']['ansible_net_model'], '750GL')
+ self.assertEqual(result['ansible_facts']['ansible_net_neighbors'], [])
+ self.assertEqual(result['ansible_facts']['ansible_net_ospf_instance'], {
+ 'default': {
+ 'default': True,
+ 'disabled': False,
+ 'distribute-default': 'never',
+ 'in-filter': 'ospf-in',
+ 'metric-bgp': 'auto',
+ 'metric-connected': 20,
+ 'metric-default': 1,
+ 'metric-other-ospf': 'auto',
+ 'metric-rip': 20,
+ 'metric-static': 20,
+ 'name': 'default',
+ 'out-filter': 'ospf-out',
+ 'redistribute-bgp': False,
+ 'redistribute-connected': False,
+ 'redistribute-other-ospf': False,
+ 'redistribute-rip': False,
+ 'redistribute-static': False,
+ 'router-id': '0.0.0.0',
+ 'state': 'down'
+ }
+ })
+ self.assertEqual(result['ansible_facts']['ansible_net_ospf_neighbor'], {})
+ self.assertEqual(result['ansible_facts']['ansible_net_route'], {
+ 'main': {
+ 'active': True,
+ 'connect': True,
+ 'disabled': False,
+ 'distance': 0,
+ 'dst-address': '192.168.1.0/24',
+ 'dynamic': True,
+ 'gateway': 'my-bridge',
+ 'gateway-status': 'my-bridge reachable',
+ 'pref-src': '192.168.1.1',
+ 'scope': 10
+ }
+ })
+ self.assertEqual(result['ansible_facts']['ansible_net_serialnum'], '0123456789AB')
+ self.assertEqual(result['ansible_facts']['ansible_net_spacefree_mb'], 123456789 / 1048576.0)
+ self.assertEqual(result['ansible_facts']['ansible_net_spacetotal_mb'], 234567890 / 1048576.0)
+ self.assertEqual(result['ansible_facts']['ansible_net_uptime'], '2w3d4h5m6s')
+ self.assertEqual(result['ansible_facts']['ansible_net_version'], '6.49.6 (stable)')
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_find_and_modify.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_find_and_modify.py
new file mode 100644
index 000000000..384bc8885
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_find_and_modify.py
@@ -0,0 +1,651 @@
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt 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
+
+from ansible_collections.community.routeros.tests.unit.compat.mock import patch, MagicMock
+from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import (
+ FakeLibRouterosError, fake_ros_api, massage_expected_result_data, create_fake_path,
+)
+from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase
+from ansible_collections.community.routeros.plugins.modules import api_find_and_modify
+
+
+START_IP_DNS_STATIC = [
+ {
+ '.id': '*1',
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'dynamic': False,
+ },
+ {
+ '.id': '*A',
+ 'name': 'router',
+ 'text': 'Router Text Entry',
+ 'dynamic': False,
+ },
+ {
+ '.id': '*7',
+ 'comment': '',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'dynamic': False,
+ },
+]
+
+START_IP_DNS_STATIC_OLD_DATA = massage_expected_result_data(START_IP_DNS_STATIC, ('ip', 'dns', 'static'), keep_all=True)
+
+START_IP_FIREWALL_FILTER = [
+ {
+ '.id': '*2',
+ 'action': 'accept',
+ 'chain': 'input',
+ 'comment': 'defconf',
+ 'protocol': 'icmp',
+ },
+ {
+ '.id': '*3',
+ 'action': 'accept',
+ 'chain': 'input',
+ 'comment': 'defconf',
+ 'connection-state': 'established',
+ },
+ {
+ '.id': '*4',
+ 'action': 'accept',
+ 'chain': 'input',
+ 'comment': 'defconf',
+ 'connection-state': 'related',
+ },
+ {
+ '.id': '*7',
+ 'action': 'drop',
+ 'chain': 'input',
+ 'comment': 'defconf',
+ 'in-interface': 'wan',
+ },
+ {
+ '.id': '*8',
+ 'action': 'accept',
+ 'chain': 'forward',
+ 'comment': 'defconf',
+ 'connection-state': 'established',
+ },
+ {
+ '.id': '*9',
+ 'action': 'accept',
+ 'chain': 'forward',
+ 'comment': 'defconf',
+ 'connection-state': 'related',
+ },
+ {
+ '.id': '*A',
+ 'action': 'drop',
+ 'chain': 'forward',
+ 'comment': 'defconf',
+ 'connection-status': 'invalid',
+ },
+]
+
+START_IP_FIREWALL_FILTER_OLD_DATA = massage_expected_result_data(START_IP_FIREWALL_FILTER, ('ip', 'firewall', 'filter'), keep_all=True)
+
+
+class TestRouterosApiFindAndModifyModule(ModuleTestCase):
+
+ def setUp(self):
+ super(TestRouterosApiFindAndModifyModule, self).setUp()
+ self.module = api_find_and_modify
+ self.module.LibRouterosError = FakeLibRouterosError
+ self.module.connect = MagicMock(new=fake_ros_api)
+ self.module.check_has_library = MagicMock()
+ self.patch_create_api = patch(
+ 'ansible_collections.community.routeros.plugins.modules.api_find_and_modify.create_api',
+ MagicMock(new=fake_ros_api))
+ self.patch_create_api.start()
+ self.config_module_args = {
+ 'username': 'admin',
+ 'password': 'pаss',
+ 'hostname': '127.0.0.1',
+ }
+
+ def tearDown(self):
+ self.patch_create_api.stop()
+
+ def test_module_fail_when_required_args_missing(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ set_module_args({})
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+
+ def test_invalid_disabled_and_enabled_option_in_find(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'find': {
+ 'comment': 'foo',
+ '!comment': None,
+ },
+ 'values': {
+ 'comment': 'bar',
+ },
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'], '`find` must not contain both "comment" and "!comment"!')
+
+ def test_invalid_disabled_option_invalid_value_in_find(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'find': {
+ '!comment': 'gone',
+ },
+ 'values': {
+ 'comment': 'bar',
+ },
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'], 'The value for "!comment" in `find` must not be non-trivial!')
+
+ def test_invalid_disabled_and_enabled_option_in_values(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'find': {},
+ 'values': {
+ 'comment': 'foo',
+ '!comment': None,
+ },
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'], '`values` must not contain both "comment" and "!comment"!')
+
+ def test_invalid_disabled_option_invalid_value_in_values(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'find': {},
+ 'values': {
+ '!comment': 'gone',
+ },
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'], 'The value for "!comment" in `values` must not be non-trivial!')
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
+ def test_change_invalid_zero(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'find': {
+ 'name': 'bam',
+ },
+ 'values': {
+ 'name': 'baz',
+ },
+ 'require_matches_min': 10,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'], 'Found no entries, but allow_no_matches=false')
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
+ def test_change_invalid_too_few(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'find': {
+ 'name': 'router',
+ },
+ 'values': {
+ 'name': 'foobar',
+ },
+ 'require_matches_min': 10,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'], 'Found 2 entries, but expected at least 10')
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
+ def test_change_invalid_too_many(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'find': {
+ 'name': 'router',
+ },
+ 'values': {
+ 'name': 'foobar',
+ },
+ 'require_matches_max': 1,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'], 'Found 2 entries, but expected at most 1')
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
+ def test_change_idempotent_zero_matches_1(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'find': {
+ 'name': 'baz',
+ },
+ 'values': {
+ 'name': 'bam',
+ },
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['match_count'], 0)
+ self.assertEqual(result['modify_count'], 0)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
+ def test_change_idempotent_zero_matches_2(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'find': {
+ 'name': 'baz',
+ },
+ 'values': {
+ 'name': 'bam',
+ },
+ 'require_matches_min': 2,
+ 'allow_no_matches': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['match_count'], 0)
+ self.assertEqual(result['modify_count'], 0)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
+ def test_idempotent_1(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'find': {
+ },
+ 'values': {
+ },
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['match_count'], 3)
+ self.assertEqual(result['modify_count'], 0)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
+ def test_idempotent_2(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'find': {
+ 'name': 'foo',
+ },
+ 'values': {
+ 'comment': None,
+ },
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['match_count'], 1)
+ self.assertEqual(result['modify_count'], 0)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC))
+ def test_change(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'find': {
+ 'name': 'foo',
+ },
+ 'values': {
+ 'comment': 'bar',
+ },
+ '_ansible_diff': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*1',
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'ttl': '1d',
+ 'disabled': False,
+ 'dynamic': False,
+ },
+ {
+ '.id': '*A',
+ 'name': 'router',
+ 'text': 'Router Text Entry',
+ 'ttl': '1d',
+ 'disabled': False,
+ 'dynamic': False,
+ },
+ {
+ '.id': '*7',
+ 'comment': 'bar',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ 'disabled': False,
+ 'dynamic': False,
+ },
+ ])
+ self.assertEqual(result['diff']['before']['values'], [
+ {
+ '.id': '*7',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ 'disabled': False,
+ 'dynamic': False,
+ },
+ ])
+ self.assertEqual(result['diff']['after']['values'], [
+ {
+ '.id': '*7',
+ 'comment': 'bar',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ 'disabled': False,
+ 'dynamic': False,
+ },
+ ])
+ self.assertEqual(result['match_count'], 1)
+ self.assertEqual(result['modify_count'], 1)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC))
+ def test_change_remove_comment_1(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'find': {
+ },
+ 'values': {
+ 'comment': None,
+ },
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*1',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'ttl': '1d',
+ 'disabled': False,
+ 'dynamic': False,
+ },
+ {
+ '.id': '*A',
+ 'name': 'router',
+ 'text': 'Router Text Entry',
+ 'ttl': '1d',
+ 'disabled': False,
+ 'dynamic': False,
+ },
+ {
+ '.id': '*7',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ 'disabled': False,
+ 'dynamic': False,
+ },
+ ])
+ self.assertEqual('diff' in result, False)
+ self.assertEqual(result['match_count'], 3)
+ self.assertEqual(result['modify_count'], 1)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC))
+ def test_change_remove_comment_2(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'find': {
+ },
+ 'values': {
+ 'comment': '',
+ },
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*1',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'ttl': '1d',
+ 'disabled': False,
+ 'dynamic': False,
+ },
+ {
+ '.id': '*A',
+ 'name': 'router',
+ 'text': 'Router Text Entry',
+ 'ttl': '1d',
+ 'disabled': False,
+ 'dynamic': False,
+ },
+ {
+ '.id': '*7',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ 'disabled': False,
+ 'dynamic': False,
+ },
+ ])
+ self.assertEqual(result['match_count'], 3)
+ self.assertEqual(result['modify_count'], 1)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC))
+ def test_change_remove_comment_3(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'find': {
+ },
+ 'values': {
+ '!comment': None,
+ },
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*1',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'ttl': '1d',
+ 'disabled': False,
+ 'dynamic': False,
+ },
+ {
+ '.id': '*A',
+ 'name': 'router',
+ 'text': 'Router Text Entry',
+ 'ttl': '1d',
+ 'disabled': False,
+ 'dynamic': False,
+ },
+ {
+ '.id': '*7',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ 'disabled': False,
+ 'dynamic': False,
+ },
+ ])
+ self.assertEqual(result['match_count'], 3)
+ self.assertEqual(result['modify_count'], 1)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path',
+ new=create_fake_path(('ip', 'firewall', 'filter'), START_IP_FIREWALL_FILTER))
+ def test_change_remove_generic(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip firewall filter',
+ 'find': {
+ 'chain': 'input',
+ '!protocol': '',
+ },
+ 'values': {
+ '!connection-state': None,
+ },
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_FIREWALL_FILTER_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*2',
+ 'action': 'accept',
+ 'chain': 'input',
+ 'comment': 'defconf',
+ 'protocol': 'icmp',
+ },
+ {
+ '.id': '*3',
+ 'action': 'accept',
+ 'chain': 'input',
+ 'comment': 'defconf',
+ },
+ {
+ '.id': '*4',
+ 'action': 'accept',
+ 'chain': 'input',
+ 'comment': 'defconf',
+ },
+ {
+ '.id': '*7',
+ 'action': 'drop',
+ 'chain': 'input',
+ 'comment': 'defconf',
+ 'in-interface': 'wan',
+ },
+ {
+ '.id': '*8',
+ 'action': 'accept',
+ 'chain': 'forward',
+ 'comment': 'defconf',
+ 'connection-state': 'established',
+ },
+ {
+ '.id': '*9',
+ 'action': 'accept',
+ 'chain': 'forward',
+ 'comment': 'defconf',
+ 'connection-state': 'related',
+ },
+ {
+ '.id': '*A',
+ 'action': 'drop',
+ 'chain': 'forward',
+ 'comment': 'defconf',
+ 'connection-status': 'invalid',
+ },
+ ])
+ self.assertEqual(result['match_count'], 3)
+ self.assertEqual(result['modify_count'], 2)
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_info.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_info.py
new file mode 100644
index 000000000..2dabc36ef
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_info.py
@@ -0,0 +1,811 @@
+# Copyright (c) 2022, Felix Fontein (@felixfontein) <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt 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
+
+from ansible_collections.community.routeros.tests.unit.compat.mock import patch, MagicMock
+from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import FakeLibRouterosError, Key, fake_ros_api
+from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase
+from ansible_collections.community.routeros.plugins.modules import api_info
+
+
+class TestRouterosApiInfoModule(ModuleTestCase):
+
+ def setUp(self):
+ super(TestRouterosApiInfoModule, self).setUp()
+ self.module = api_info
+ self.module.LibRouterosError = FakeLibRouterosError
+ self.module.connect = MagicMock(new=fake_ros_api)
+ self.module.check_has_library = MagicMock()
+ self.patch_create_api = patch('ansible_collections.community.routeros.plugins.modules.api_info.create_api', MagicMock(new=fake_ros_api))
+ self.patch_create_api.start()
+ self.module.Key = MagicMock(new=Key)
+ self.config_module_args = {
+ 'username': 'admin',
+ 'password': 'pаss',
+ 'hostname': '127.0.0.1',
+ }
+
+ def tearDown(self):
+ self.patch_create_api.stop()
+
+ def test_module_fail_when_required_args_missing(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ set_module_args({})
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+
+ def test_invalid_path(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'something invalid'
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'].startswith('value of path must be one of: '), True)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path')
+ def test_empty_result(self, mock_compose_api_path):
+ mock_compose_api_path.return_value = []
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static'
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['result'], [])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path')
+ def test_regular_result(self, mock_compose_api_path):
+ mock_compose_api_path.return_value = [
+ {
+ 'called-format': 'mac:ssid',
+ 'interim-update': 'enabled',
+ 'mac-caching': 'disabled',
+ 'mac-format': 'XX:XX:XX:XX:XX:XX',
+ 'mac-mode': 'as-username',
+ 'foo': 'bar',
+ '.id': '*1',
+ },
+ ]
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'caps-man aaa',
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['result'], [{
+ 'interim-update': 'enabled',
+ '.id': '*1',
+ }])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path')
+ def test_result_with_defaults(self, mock_compose_api_path):
+ mock_compose_api_path.return_value = [
+ {
+ 'called-format': 'mac:ssid',
+ 'interim-update': 'enabled',
+ 'mac-caching': 'disabled',
+ 'mac-format': 'XX:XX:XX:XX:XX:XX',
+ 'mac-mode': 'as-username',
+ 'foo': 'bar',
+ '.id': '*1',
+ },
+ ]
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'caps-man aaa',
+ 'hide_defaults': False,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['result'], [{
+ 'called-format': 'mac:ssid',
+ 'interim-update': 'enabled',
+ 'mac-caching': 'disabled',
+ 'mac-format': 'XX:XX:XX:XX:XX:XX',
+ 'mac-mode': 'as-username',
+ '.id': '*1',
+ }])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path')
+ def test_full_result(self, mock_compose_api_path):
+ mock_compose_api_path.return_value = [
+ {
+ 'called-format': 'mac:ssid',
+ 'interim-update': 'enabled',
+ 'mac-caching': 'disabled',
+ 'mac-format': 'XX:XX:XX:XX:XX:XX',
+ 'mac-mode': 'as-username',
+ 'foo': 'bar',
+ '.id': '*1',
+ },
+ ]
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'caps-man aaa',
+ 'unfiltered': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['result'], [{
+ 'interim-update': 'enabled',
+ 'foo': 'bar',
+ '.id': '*1',
+ }])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path')
+ def test_disabled_exclamation(self, mock_compose_api_path):
+ mock_compose_api_path.return_value = [
+ {
+ 'chain': 'input',
+ 'in-interface-list': 'LAN',
+ '.id': '*1',
+ 'dynamic': False,
+ },
+ {
+ 'chain': 'forward',
+ 'action': 'drop',
+ 'in-interface': 'sfp1',
+ '.id': '*2',
+ 'dynamic': True,
+ },
+ ]
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip firewall filter',
+ 'handle_disabled': 'exclamation',
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['result'], [{
+ 'chain': 'input',
+ 'in-interface-list': 'LAN',
+ '!action': None,
+ '!comment': None,
+ '!connection-bytes': None,
+ '!connection-limit': None,
+ '!connection-mark': None,
+ '!connection-nat-state': None,
+ '!connection-rate': None,
+ '!connection-state': None,
+ '!connection-type': None,
+ '!content': None,
+ '!disabled': None,
+ '!dscp': None,
+ '!dst-address': None,
+ '!dst-address-list': None,
+ '!dst-address-type': None,
+ '!dst-limit': None,
+ '!dst-port': None,
+ '!fragment': None,
+ '!hotspot': None,
+ '!hw-offload': None,
+ '!icmp-options': None,
+ '!in-bridge-port': None,
+ '!in-bridge-port-list': None,
+ '!in-interface': None,
+ '!ingress-priority': None,
+ '!ipsec-policy': None,
+ '!ipv4-options': None,
+ '!jump-target': None,
+ '!layer7-protocol': None,
+ '!limit': None,
+ '!log': None,
+ '!log-prefix': None,
+ '!nth': None,
+ '!out-bridge-port': None,
+ '!out-bridge-port-list': None,
+ '!out-interface': None,
+ '!out-interface-list': None,
+ '!p2p': None,
+ '!packet-mark': None,
+ '!packet-size': None,
+ '!per-connection-classifier': None,
+ '!port': None,
+ '!priority': None,
+ '!protocol': None,
+ '!psd': None,
+ '!random': None,
+ '!reject-with': None,
+ '!routing-mark': None,
+ '!routing-table': None,
+ '!src-address': None,
+ '!src-address-list': None,
+ '!src-address-type': None,
+ '!src-mac-address': None,
+ '!src-port': None,
+ '!tcp-flags': None,
+ '!tcp-mss': None,
+ '!time': None,
+ '!tls-host': None,
+ '!ttl': None,
+ '.id': '*1',
+ }])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path')
+ def test_disabled_null_value(self, mock_compose_api_path):
+ mock_compose_api_path.return_value = [
+ {
+ 'chain': 'input',
+ 'in-interface-list': 'LAN',
+ '.id': '*1',
+ 'dynamic': False,
+ },
+ ]
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip firewall filter',
+ 'handle_disabled': 'null-value',
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['result'], [{
+ 'chain': 'input',
+ 'in-interface-list': 'LAN',
+ 'action': None,
+ 'comment': None,
+ 'connection-bytes': None,
+ 'connection-limit': None,
+ 'connection-mark': None,
+ 'connection-nat-state': None,
+ 'connection-rate': None,
+ 'connection-state': None,
+ 'connection-type': None,
+ 'content': None,
+ 'disabled': None,
+ 'dscp': None,
+ 'dst-address': None,
+ 'dst-address-list': None,
+ 'dst-address-type': None,
+ 'dst-limit': None,
+ 'dst-port': None,
+ 'fragment': None,
+ 'hotspot': None,
+ 'hw-offload': None,
+ 'icmp-options': None,
+ 'in-bridge-port': None,
+ 'in-bridge-port-list': None,
+ 'in-interface': None,
+ 'ingress-priority': None,
+ 'ipsec-policy': None,
+ 'ipv4-options': None,
+ 'jump-target': None,
+ 'layer7-protocol': None,
+ 'limit': None,
+ 'log': None,
+ 'log-prefix': None,
+ 'nth': None,
+ 'out-bridge-port': None,
+ 'out-bridge-port-list': None,
+ 'out-interface': None,
+ 'out-interface-list': None,
+ 'p2p': None,
+ 'packet-mark': None,
+ 'packet-size': None,
+ 'per-connection-classifier': None,
+ 'port': None,
+ 'priority': None,
+ 'protocol': None,
+ 'psd': None,
+ 'random': None,
+ 'reject-with': None,
+ 'routing-mark': None,
+ 'routing-table': None,
+ 'src-address': None,
+ 'src-address-list': None,
+ 'src-address-type': None,
+ 'src-mac-address': None,
+ 'src-port': None,
+ 'tcp-flags': None,
+ 'tcp-mss': None,
+ 'time': None,
+ 'tls-host': None,
+ 'ttl': None,
+ '.id': '*1',
+ }])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path')
+ def test_disabled_omit(self, mock_compose_api_path):
+ mock_compose_api_path.return_value = [
+ {
+ 'chain': 'input',
+ 'in-interface-list': 'LAN',
+ '.id': '*1',
+ 'dynamic': False,
+ },
+ ]
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip firewall filter',
+ 'handle_disabled': 'omit',
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['result'], [{
+ 'chain': 'input',
+ 'in-interface-list': 'LAN',
+ '.id': '*1',
+ }])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path')
+ def test_dynamic(self, mock_compose_api_path):
+ mock_compose_api_path.return_value = [
+ {
+ 'chain': 'input',
+ 'in-interface-list': 'LAN',
+ 'dynamic': False,
+ '.id': '*1',
+ },
+ {
+ 'chain': 'forward',
+ 'action': 'drop',
+ 'in-interface': 'sfp1',
+ '.id': '*2',
+ 'dynamic': True,
+ },
+ ]
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip firewall filter',
+ 'handle_disabled': 'omit',
+ 'include_dynamic': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['result'], [
+ {
+ 'chain': 'input',
+ 'in-interface-list': 'LAN',
+ '.id': '*1',
+ 'dynamic': False,
+ },
+ {
+ 'chain': 'forward',
+ 'action': 'drop',
+ 'in-interface': 'sfp1',
+ '.id': '*2',
+ 'dynamic': True,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path')
+ def test_builtin_exclude(self, mock_compose_api_path):
+ mock_compose_api_path.return_value = [
+ {
+ '.id': '*2000000',
+ 'name': 'all',
+ 'dynamic': False,
+ 'include': '',
+ 'exclude': '',
+ 'builtin': True,
+ 'comment': 'contains all interfaces',
+ },
+ {
+ '.id': '*2000001',
+ 'name': 'none',
+ 'dynamic': False,
+ 'include': '',
+ 'exclude': '',
+ 'builtin': True,
+ 'comment': 'contains no interfaces',
+ },
+ {
+ '.id': '*2000010',
+ 'name': 'WAN',
+ 'dynamic': False,
+ 'include': '',
+ 'exclude': '',
+ 'builtin': False,
+ 'comment': 'defconf',
+ },
+ ]
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'interface list',
+ 'handle_disabled': 'omit',
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['result'], [
+ {
+ '.id': '*2000010',
+ 'name': 'WAN',
+ 'include': '',
+ 'exclude': '',
+ 'comment': 'defconf',
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path')
+ def test_builtin_include(self, mock_compose_api_path):
+ mock_compose_api_path.return_value = [
+ {
+ '.id': '*2000000',
+ 'name': 'all',
+ 'dynamic': False,
+ 'include': '',
+ 'exclude': '',
+ 'builtin': True,
+ 'comment': 'contains all interfaces',
+ },
+ {
+ '.id': '*2000001',
+ 'name': 'none',
+ 'dynamic': False,
+ 'include': '',
+ 'exclude': '',
+ 'builtin': True,
+ 'comment': 'contains no interfaces',
+ },
+ {
+ '.id': '*2000010',
+ 'name': 'WAN',
+ 'dynamic': False,
+ 'include': '',
+ 'exclude': '',
+ 'builtin': False,
+ 'comment': 'defconf',
+ },
+ ]
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'interface list',
+ 'handle_disabled': 'omit',
+ 'include_builtin': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['result'], [
+ {
+ '.id': '*2000000',
+ 'name': 'all',
+ 'include': '',
+ 'exclude': '',
+ 'builtin': True,
+ 'comment': 'contains all interfaces',
+ },
+ {
+ '.id': '*2000001',
+ 'name': 'none',
+ 'include': '',
+ 'exclude': '',
+ 'builtin': True,
+ 'comment': 'contains no interfaces',
+ },
+ {
+ '.id': '*2000010',
+ 'name': 'WAN',
+ 'include': '',
+ 'exclude': '',
+ 'builtin': False,
+ 'comment': 'defconf',
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path')
+ def test_absent(self, mock_compose_api_path):
+ mock_compose_api_path.return_value = [
+ {
+ '.id': '*1',
+ 'address': '192.168.88.2',
+ 'mac-address': '11:22:33:44:55:66',
+ 'client-id': 'ff:1:2:3:4:5:6:7:8:9:a:b:c:d:e:f:0:1:2',
+ 'address-lists': '',
+ 'server': 'main',
+ 'dhcp-option': '',
+ 'status': 'waiting',
+ 'last-seen': 'never',
+ 'radius': False,
+ 'dynamic': False,
+ 'blocked': False,
+ 'disabled': False,
+ 'comment': 'foo',
+ },
+ {
+ '.id': '*2',
+ 'address': '192.168.88.3',
+ 'mac-address': '11:22:33:44:55:77',
+ 'client-id': '1:2:3:4:5:6:7',
+ 'address-lists': '',
+ 'server': 'main',
+ 'dhcp-option': '',
+ 'status': 'bound',
+ 'expires-after': '3d7m8s',
+ 'last-seen': '1m52s',
+ 'active-address': '192.168.88.14',
+ 'active-mac-address': '11:22:33:44:55:76',
+ 'active-client-id': '1:2:3:4:5:6:7',
+ 'active-server': 'main',
+ 'host-name': 'bar',
+ 'radius': False,
+ 'dynamic': False,
+ 'blocked': False,
+ 'disabled': False,
+ },
+ {
+ '.id': '*3',
+ 'address': '0.0.0.1',
+ 'mac-address': '00:00:00:00:00:01',
+ 'address-lists': '',
+ 'dhcp-option': '',
+ 'status': 'waiting',
+ 'last-seen': 'never',
+ 'radius': False,
+ 'dynamic': False,
+ 'blocked': False,
+ 'disabled': False,
+ },
+ ]
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dhcp-server lease',
+ 'handle_disabled': 'omit',
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['result'], [
+ {
+ '.id': '*1',
+ 'address': '192.168.88.2',
+ 'mac-address': '11:22:33:44:55:66',
+ 'client-id': 'ff:1:2:3:4:5:6:7:8:9:a:b:c:d:e:f:0:1:2',
+ 'server': 'main',
+ 'comment': 'foo',
+ },
+ {
+ '.id': '*2',
+ 'address': '192.168.88.3',
+ 'mac-address': '11:22:33:44:55:77',
+ 'client-id': '1:2:3:4:5:6:7',
+ 'server': 'main',
+ },
+ {
+ '.id': '*3',
+ 'address': '0.0.0.1',
+ 'mac-address': '00:00:00:00:00:01',
+ 'server': 'all',
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path')
+ def test_default_disable_1(self, mock_compose_api_path):
+ mock_compose_api_path.return_value = [
+ {
+ '.id': '*10',
+ 'name': 'gre-tunnel3',
+ 'mtu': 'auto',
+ 'actual-mtu': 65496,
+ 'local-address': '0.0.0.0',
+ 'remote-address': '192.168.1.1',
+ 'dscp': 'inherit',
+ 'clamp-tcp-mss': True,
+ 'dont-fragment': False,
+ 'allow-fast-path': True,
+ 'running': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*11',
+ 'name': 'gre-tunnel4',
+ 'mtu': 'auto',
+ 'actual-mtu': 65496,
+ 'local-address': '0.0.0.0',
+ 'remote-address': '192.168.1.2',
+ 'keepalive': '10s,10',
+ 'dscp': 'inherit',
+ 'clamp-tcp-mss': True,
+ 'dont-fragment': False,
+ 'allow-fast-path': True,
+ 'running': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*12',
+ 'name': 'gre-tunnel5',
+ 'mtu': 'auto',
+ 'actual-mtu': 65496,
+ 'local-address': '192.168.0.1',
+ 'remote-address': '192.168.1.3',
+ 'keepalive': '20s,20',
+ 'dscp': 'inherit',
+ 'clamp-tcp-mss': True,
+ 'dont-fragment': False,
+ 'allow-fast-path': True,
+ 'running': True,
+ 'disabled': False,
+ 'comment': 'foo',
+ },
+ ]
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'interface gre',
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['result'], [
+ {
+ '.id': '*10',
+ 'name': 'gre-tunnel3',
+ 'remote-address': '192.168.1.1',
+ '!comment': None,
+ '!ipsec-secret': None,
+ '!keepalive': None,
+ },
+ {
+ '.id': '*11',
+ 'name': 'gre-tunnel4',
+ 'remote-address': '192.168.1.2',
+ '!comment': None,
+ '!ipsec-secret': None,
+ },
+ {
+ '.id': '*12',
+ 'name': 'gre-tunnel5',
+ 'local-address': '192.168.0.1',
+ 'remote-address': '192.168.1.3',
+ 'keepalive': '20s,20',
+ 'comment': 'foo',
+ '!ipsec-secret': None,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path')
+ def test_default_disable_2(self, mock_compose_api_path):
+ mock_compose_api_path.return_value = [
+ {
+ '.id': '*10',
+ 'name': 'gre-tunnel3',
+ 'mtu': 'auto',
+ 'actual-mtu': 65496,
+ 'local-address': '0.0.0.0',
+ 'remote-address': '192.168.1.1',
+ 'dscp': 'inherit',
+ 'clamp-tcp-mss': True,
+ 'dont-fragment': False,
+ 'allow-fast-path': True,
+ 'running': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*11',
+ 'name': 'gre-tunnel4',
+ 'mtu': 'auto',
+ 'actual-mtu': 65496,
+ 'local-address': '0.0.0.0',
+ 'remote-address': '192.168.1.2',
+ 'keepalive': '10s,10',
+ 'dscp': 'inherit',
+ 'clamp-tcp-mss': True,
+ 'dont-fragment': False,
+ 'allow-fast-path': True,
+ 'running': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*12',
+ 'name': 'gre-tunnel5',
+ 'mtu': 'auto',
+ 'actual-mtu': 65496,
+ 'local-address': '192.168.0.1',
+ 'remote-address': '192.168.1.3',
+ 'keepalive': '20s,20',
+ 'dscp': 'inherit',
+ 'clamp-tcp-mss': True,
+ 'dont-fragment': False,
+ 'allow-fast-path': True,
+ 'running': True,
+ 'disabled': False,
+ 'comment': 'foo',
+ },
+ ]
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'interface gre',
+ 'handle_disabled': 'omit',
+ 'hide_defaults': False,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['result'], [
+ {
+ '.id': '*10',
+ 'name': 'gre-tunnel3',
+ 'mtu': 'auto',
+ 'local-address': '0.0.0.0',
+ 'remote-address': '192.168.1.1',
+ 'dscp': 'inherit',
+ 'clamp-tcp-mss': True,
+ 'dont-fragment': False,
+ 'allow-fast-path': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*11',
+ 'name': 'gre-tunnel4',
+ 'mtu': 'auto',
+ 'local-address': '0.0.0.0',
+ 'remote-address': '192.168.1.2',
+ 'keepalive': '10s,10',
+ 'dscp': 'inherit',
+ 'clamp-tcp-mss': True,
+ 'dont-fragment': False,
+ 'allow-fast-path': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*12',
+ 'name': 'gre-tunnel5',
+ 'mtu': 'auto',
+ 'local-address': '192.168.0.1',
+ 'remote-address': '192.168.1.3',
+ 'keepalive': '20s,20',
+ 'dscp': 'inherit',
+ 'clamp-tcp-mss': True,
+ 'dont-fragment': False,
+ 'allow-fast-path': True,
+ 'disabled': False,
+ 'comment': 'foo',
+ },
+ ])
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_modify.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_modify.py
new file mode 100644
index 000000000..78979733d
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_modify.py
@@ -0,0 +1,1972 @@
+# Copyright (c) 2022, Felix Fontein (@felixfontein) <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt 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
+
+from ansible_collections.community.routeros.tests.unit.compat.mock import patch, MagicMock
+from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import (
+ FakeLibRouterosError, fake_ros_api, massage_expected_result_data, create_fake_path,
+)
+from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase
+from ansible_collections.community.routeros.plugins.modules import api_modify
+
+
+START_IP_DNS_STATIC = [
+ {
+ '.id': '*1',
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'dynamic': False,
+ },
+ {
+ '.id': '*A',
+ 'name': 'router',
+ 'text': 'Router Text Entry',
+ 'dynamic': False,
+ },
+ {
+ '.id': '*7',
+ 'comment': '',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'dynamic': False,
+ },
+ {
+ '.id': '*8',
+ 'comment': '',
+ 'name': 'dynfoo',
+ 'address': '192.168.88.15',
+ 'dynamic': True,
+ },
+]
+
+START_IP_DNS_STATIC_OLD_DATA = massage_expected_result_data(START_IP_DNS_STATIC, ('ip', 'dns', 'static'), remove_dynamic=True)
+
+START_IP_SETTINGS = [
+ {
+ 'accept-redirects': True,
+ 'accept-source-route': False,
+ 'allow-fast-path': True,
+ 'arp-timeout': '30s',
+ 'icmp-rate-limit': 20,
+ 'icmp-rate-mask': '0x1818',
+ 'ip-forward': True,
+ 'max-neighbor-entries': 8192,
+ 'route-cache': True,
+ 'rp-filter': False,
+ 'secure-redirects': True,
+ 'send-redirects': True,
+ 'tcp-syncookies': False,
+ },
+]
+
+START_IP_SETTINGS_OLD_DATA = massage_expected_result_data(START_IP_SETTINGS, ('ip', 'settings'))
+
+START_IP_ADDRESS = [
+ {
+ '.id': '*1',
+ 'address': '192.168.88.0/24',
+ 'interface': 'bridge',
+ 'disabled': False,
+ },
+ {
+ '.id': '*3',
+ 'address': '192.168.1.0/24',
+ 'interface': 'LAN',
+ 'disabled': False,
+ },
+ {
+ '.id': '*F',
+ 'address': '10.0.0.0/16',
+ 'interface': 'WAN',
+ 'disabled': True,
+ },
+]
+
+START_IP_ADDRESS_OLD_DATA = massage_expected_result_data(START_IP_ADDRESS, ('ip', 'address'))
+
+START_IP_DHCP_CLIENT = [
+ {
+ "!comment": None,
+ "!script": None,
+ ".id": "*1",
+ "add-default-route": True,
+ "default-route-distance": 1,
+ "dhcp-options": "hostname,clientid",
+ "disabled": False,
+ "interface": "ether1",
+ "use-peer-dns": True,
+ "use-peer-ntp": True,
+ },
+ {
+ "!comment": None,
+ "!dhcp-options": None,
+ "!script": None,
+ ".id": "*2",
+ "add-default-route": True,
+ "default-route-distance": 1,
+ "disabled": False,
+ "interface": "ether2",
+ "use-peer-dns": True,
+ "use-peer-ntp": True,
+ },
+ {
+ "!comment": None,
+ "!script": None,
+ ".id": "*3",
+ "add-default-route": True,
+ "default-route-distance": 1,
+ "dhcp-options": "hostname",
+ "disabled": False,
+ "interface": "ether3",
+ "use-peer-dns": True,
+ "use-peer-ntp": True,
+ },
+]
+
+START_IP_DHCP_CLIENT_OLD_DATA = massage_expected_result_data(START_IP_DHCP_CLIENT, ('ip', 'dhcp-client'))
+
+START_IP_DHCP_SERVER_LEASE = [
+ {
+ '.id': '*1',
+ 'address': '192.168.88.2',
+ 'mac-address': '11:22:33:44:55:66',
+ 'client-id': 'ff:1:2:3:4:5:6:7:8:9:a:b:c:d:e:f:0:1:2',
+ 'address-lists': '',
+ 'server': 'main',
+ 'dhcp-option': '',
+ 'status': 'waiting',
+ 'last-seen': 'never',
+ 'radius': False,
+ 'dynamic': False,
+ 'blocked': False,
+ 'disabled': False,
+ 'comment': 'foo',
+ },
+ {
+ '.id': '*2',
+ 'address': '192.168.88.3',
+ 'mac-address': '11:22:33:44:55:77',
+ 'client-id': '1:2:3:4:5:6:7',
+ 'address-lists': '',
+ 'server': 'main',
+ 'dhcp-option': '',
+ 'status': 'bound',
+ 'expires-after': '3d7m8s',
+ 'last-seen': '1m52s',
+ 'active-address': '192.168.88.14',
+ 'active-mac-address': '11:22:33:44:55:76',
+ 'active-client-id': '1:2:3:4:5:6:7',
+ 'active-server': 'main',
+ 'host-name': 'bar',
+ 'radius': False,
+ 'dynamic': False,
+ 'blocked': False,
+ 'disabled': False,
+ },
+ {
+ '.id': '*3',
+ 'address': '0.0.0.1',
+ 'mac-address': '00:00:00:00:00:01',
+ 'address-lists': '',
+ 'dhcp-option': '',
+ 'status': 'waiting',
+ 'last-seen': 'never',
+ 'radius': False,
+ 'dynamic': False,
+ 'blocked': False,
+ 'disabled': False,
+ },
+ {
+ '.id': '*4',
+ 'address': '0.0.0.2',
+ 'mac-address': '00:00:00:00:00:02',
+ 'address-lists': '',
+ 'dhcp-option': '',
+ 'status': 'waiting',
+ 'last-seen': 'never',
+ 'radius': False,
+ 'dynamic': False,
+ 'blocked': False,
+ 'disabled': False,
+ },
+]
+
+START_IP_DHCP_SERVER_LEASE_OLD_DATA = massage_expected_result_data(START_IP_DHCP_SERVER_LEASE, ('ip', 'dhcp-server', 'lease'))
+
+START_INTERFACE_LIST = [
+ {
+ '.id': '*2000000',
+ 'name': 'all',
+ 'dynamic': False,
+ 'include': '',
+ 'exclude': '',
+ 'builtin': True,
+ 'comment': 'contains all interfaces',
+ },
+ {
+ '.id': '*2000001',
+ 'name': 'none',
+ 'dynamic': False,
+ 'include': '',
+ 'exclude': '',
+ 'builtin': True,
+ 'comment': 'contains no interfaces',
+ },
+ {
+ '.id': '*2000010',
+ 'name': 'WAN',
+ 'dynamic': False,
+ 'include': '',
+ 'exclude': '',
+ 'builtin': False,
+ 'comment': 'defconf',
+ },
+ {
+ '.id': '*2000011',
+ 'name': 'Foo',
+ 'dynamic': False,
+ 'include': '',
+ 'exclude': '',
+ 'builtin': False,
+ 'comment': '',
+ },
+]
+
+START_INTERFACE_LIST_OLD_DATA = massage_expected_result_data(START_INTERFACE_LIST, ('interface', 'list'), remove_builtin=True)
+
+START_INTERFACE_GRE = [
+ {
+ '.id': '*10',
+ 'name': 'gre-tunnel3',
+ 'mtu': 'auto',
+ 'actual-mtu': 65496,
+ 'local-address': '0.0.0.0',
+ 'remote-address': '192.168.1.1',
+ 'dscp': 'inherit',
+ 'clamp-tcp-mss': True,
+ 'dont-fragment': False,
+ 'allow-fast-path': True,
+ 'running': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*11',
+ 'name': 'gre-tunnel4',
+ 'mtu': 'auto',
+ 'actual-mtu': 65496,
+ 'local-address': '0.0.0.0',
+ 'remote-address': '192.168.1.2',
+ 'keepalive': '10s,10',
+ 'dscp': 'inherit',
+ 'clamp-tcp-mss': True,
+ 'dont-fragment': False,
+ 'allow-fast-path': True,
+ 'running': True,
+ 'disabled': False,
+ },
+ {
+ '.id': '*12',
+ 'name': 'gre-tunnel5',
+ 'mtu': 'auto',
+ 'actual-mtu': 65496,
+ 'local-address': '192.168.0.1',
+ 'remote-address': '192.168.1.3',
+ 'keepalive': '20s,20',
+ 'dscp': 'inherit',
+ 'clamp-tcp-mss': True,
+ 'dont-fragment': False,
+ 'allow-fast-path': True,
+ 'running': True,
+ 'disabled': False,
+ 'comment': 'foo',
+ },
+]
+
+START_INTERFACE_GRE_OLD_DATA = massage_expected_result_data(START_INTERFACE_GRE, ('interface', 'gre'))
+
+
+class TestRouterosApiModifyModule(ModuleTestCase):
+
+ def setUp(self):
+ super(TestRouterosApiModifyModule, self).setUp()
+ self.module = api_modify
+ self.module.LibRouterosError = FakeLibRouterosError
+ self.module.connect = MagicMock(new=fake_ros_api)
+ self.module.check_has_library = MagicMock()
+ self.patch_create_api = patch(
+ 'ansible_collections.community.routeros.plugins.modules.api_modify.create_api',
+ MagicMock(new=fake_ros_api))
+ self.patch_create_api.start()
+ self.config_module_args = {
+ 'username': 'admin',
+ 'password': 'pаss',
+ 'hostname': '127.0.0.1',
+ }
+
+ def tearDown(self):
+ self.patch_create_api.stop()
+
+ def test_module_fail_when_required_args_missing(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ set_module_args({})
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+
+ def test_invalid_path(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'something invalid',
+ 'data': [],
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'].startswith('value of path must be one of: '), True)
+
+ def test_invalid_option(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [{
+ 'name': 'baz',
+ 'foo': 'bar',
+ }],
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'], 'Unknown key "foo" at index 1.')
+
+ def test_invalid_disabled_and_enabled_option(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [{
+ 'name': 'baz',
+ 'comment': 'foo',
+ '!comment': None,
+ }],
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'], 'Not both "comment" and "!comment" must appear at index 1.')
+
+ def test_invalid_disabled_option(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [{
+ 'name': 'foo',
+ '!disabled': None,
+ }],
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'], 'Key "!disabled" must not be disabled (leading "!") at index 1.')
+
+ def test_invalid_disabled_option_value(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [{
+ 'name': 'baz',
+ '!comment': 'foo',
+ }],
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'], 'Disabled key "!comment" must not have a value at index 1.')
+
+ def test_invalid_non_disabled_option_value(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [{
+ 'name': None,
+ }],
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'], 'Key "name" must not be disabled (value null/~/None) at index 1.')
+
+ def test_invalid_required_missing(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dhcp-server',
+ 'data': [{
+ 'interface': 'eth0',
+ }],
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'], 'Every element in data must contain "name". For example, the element at index #1 does not provide it.')
+
+ def test_invalid_required_one_of_missing(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [{
+ 'address': '192.168.88.1',
+ }],
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'], 'Every element in data must contain one of "name", "regexp". For example, the element at index 1 does not provide it.')
+
+ def test_invalid_mutually_exclusive_both(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [{
+ 'name': 'foo',
+ 'regexp': 'bar',
+ 'address': '192.168.88.1',
+ }],
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['failed'], True)
+ self.assertEqual(result['msg'], 'Keys "name", "regexp" cannot be used at the same time at index 1.')
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
+ def test_sync_list_idempotent(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [
+ {
+ '.id': 'bam', # this should be ignored
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ },
+ {
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ },
+ {
+ 'comment': None,
+ 'name': 'router',
+ 'text': 'Router Text Entry',
+ },
+ ],
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
+ def test_sync_list_idempotent_2(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [
+ {
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ },
+ {
+ 'name': 'foo',
+ 'comment': '',
+ 'address': '192.168.88.2',
+ },
+ {
+ 'name': 'router',
+ '!comment': None,
+ 'text': 'Router Text Entry',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
+ def test_sync_list_idempotent_3(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [
+ {
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ },
+ ],
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC))
+ def test_sync_list_add(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [
+ {
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ },
+ {
+ 'name': 'router',
+ 'text': 'Router Text Entry',
+ },
+ {
+ 'name': 'router',
+ 'text': 'Router Text Entry 2',
+ },
+ {
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ },
+ ],
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*1',
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*A',
+ 'name': 'router',
+ 'text': 'Router Text Entry',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*7',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*NEW1',
+ 'name': 'router',
+ 'text': 'Router Text Entry 2',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC))
+ def test_sync_list_modify_1(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [
+ {
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ },
+ {
+ 'name': 'router',
+ 'text': 'Router Text Entry 2',
+ },
+ {
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ },
+ ],
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*1',
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*A',
+ 'name': 'router',
+ 'text': 'Router Text Entry',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*7',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*NEW1',
+ 'name': 'router',
+ 'text': 'Router Text Entry 2',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
+ def test_sync_list_modify_1_check(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [
+ {
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ },
+ {
+ 'name': 'router',
+ 'text': 'Router Text Entry 2',
+ },
+ {
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ },
+ ],
+ '_ansible_check_mode': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*1',
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*A',
+ 'name': 'router',
+ 'text': 'Router Text Entry',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*7',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ 'name': 'router',
+ 'text': 'Router Text Entry 2',
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC))
+ def test_sync_list_modify_2(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [
+ {
+ 'comment': '',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ },
+ {
+ 'name': 'router',
+ 'text': 'Router Text Entry 2',
+ },
+ {
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*1',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*A',
+ 'name': 'router',
+ 'text': 'Router Text Entry 2',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*7',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
+ def test_sync_list_modify_2_check(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [
+ {
+ 'comment': '',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ },
+ {
+ 'name': 'router',
+ 'text': 'Router Text Entry 2',
+ },
+ {
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ '_ansible_check_mode': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*1',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*A',
+ 'name': 'router',
+ 'text': 'Router Text Entry 2',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*7',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC))
+ def test_sync_list_modify_3(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [
+ {
+ '!comment': None,
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ },
+ {
+ 'name': 'router',
+ 'cname': 'router.com.',
+ },
+ {
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*1',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*7',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*NEW1',
+ 'name': 'router',
+ 'cname': 'router.com.',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
+ def test_sync_list_modify_3_check(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [
+ {
+ '!comment': None,
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ },
+ {
+ 'name': 'router',
+ 'cname': 'router.com.',
+ },
+ {
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ '_ansible_check_mode': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*1',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*7',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ 'name': 'router',
+ 'cname': 'router.com.',
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC))
+ def test_sync_list_modify_4(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [
+ {
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ },
+ {
+ 'name': 'router',
+ 'comment': 'defconf',
+ 'text': 'Router Text Entry 2',
+ },
+ {
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*1',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*A',
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'text': 'Router Text Entry 2',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*7',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
+ def test_sync_list_modify_4_check(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [
+ {
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ },
+ {
+ 'name': 'router',
+ 'comment': 'defconf',
+ 'text': 'Router Text Entry 2',
+ },
+ {
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ '_ansible_check_mode': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*1',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*A',
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'text': 'Router Text Entry 2',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*7',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC))
+ def test_sync_list_delete(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [
+ {
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*1',
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
+ def test_sync_list_delete_check(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [
+ {
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ '_ansible_check_mode': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*1',
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC))
+ def test_sync_list_reorder(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [
+ {
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ },
+ {
+ 'name': 'foo',
+ 'text': 'bar',
+ },
+ {
+ 'name': 'router',
+ 'text': 'Router Text Entry',
+ },
+ {
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ 'ensure_order': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*7',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*NEW1',
+ 'name': 'foo',
+ 'text': 'bar',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*A',
+ 'name': 'router',
+ 'text': 'Router Text Entry',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*1',
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True))
+ def test_sync_list_reorder_check(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dns static',
+ 'data': [
+ {
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ },
+ {
+ 'name': 'foo',
+ 'text': 'bar',
+ },
+ {
+ 'name': 'router',
+ 'text': 'Router Text Entry',
+ },
+ {
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ 'ensure_order': True,
+ '_ansible_check_mode': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*7',
+ 'name': 'foo',
+ 'address': '192.168.88.2',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ 'name': 'foo',
+ 'text': 'bar',
+ },
+ {
+ '.id': '*A',
+ 'name': 'router',
+ 'text': 'Router Text Entry',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ {
+ '.id': '*1',
+ 'comment': 'defconf',
+ 'name': 'router',
+ 'address': '192.168.88.1',
+ 'ttl': '1d',
+ 'disabled': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'settings'), START_IP_SETTINGS, read_only=True))
+ def test_sync_value_idempotent(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip settings',
+ 'data': [
+ {
+ 'arp-timeout': '30s',
+ 'icmp-rate-limit': 20,
+ 'icmp-rate-mask': '0x1818',
+ 'ip-forward': True,
+ 'max-neighbor-entries': 8192,
+ },
+ ],
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['old_data'], START_IP_SETTINGS_OLD_DATA)
+ self.assertEqual(result['new_data'], START_IP_SETTINGS_OLD_DATA)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'settings'), START_IP_SETTINGS, read_only=True))
+ def test_sync_value_idempotent_2(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip settings',
+ 'data': [
+ {
+ 'accept-redirects': True,
+ 'icmp-rate-limit': 20,
+ },
+ ],
+ 'handle_entries_content': 'remove',
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['old_data'], START_IP_SETTINGS_OLD_DATA)
+ self.assertEqual(result['new_data'], START_IP_SETTINGS_OLD_DATA)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'settings'), START_IP_SETTINGS))
+ def test_sync_value_modify(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip settings',
+ 'data': [
+ {
+ 'accept-redirects': True,
+ 'accept-source-route': True,
+ 'max-neighbor-entries': 4096,
+ },
+ ],
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_SETTINGS_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ 'accept-redirects': True,
+ 'accept-source-route': True,
+ 'allow-fast-path': True,
+ 'arp-timeout': '30s',
+ 'icmp-rate-limit': 20,
+ 'icmp-rate-mask': '0x1818',
+ 'ip-forward': True,
+ 'max-neighbor-entries': 4096,
+ 'route-cache': True,
+ 'rp-filter': False,
+ 'secure-redirects': True,
+ 'send-redirects': True,
+ 'tcp-syncookies': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'settings'), START_IP_SETTINGS, read_only=True))
+ def test_sync_value_modify_check(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip settings',
+ 'data': [
+ {
+ 'accept-redirects': True,
+ 'accept-source-route': True,
+ 'max-neighbor-entries': 4096,
+ },
+ ],
+ '_ansible_check_mode': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_SETTINGS_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ 'accept-redirects': True,
+ 'accept-source-route': True,
+ 'allow-fast-path': True,
+ 'arp-timeout': '30s',
+ 'icmp-rate-limit': 20,
+ 'icmp-rate-mask': '0x1818',
+ 'ip-forward': True,
+ 'max-neighbor-entries': 4096,
+ 'route-cache': True,
+ 'rp-filter': False,
+ 'secure-redirects': True,
+ 'send-redirects': True,
+ 'tcp-syncookies': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'settings'), START_IP_SETTINGS))
+ def test_sync_value_modify_2(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip settings',
+ 'data': [
+ {
+ 'accept-redirects': True,
+ 'accept-source-route': True,
+ 'max-neighbor-entries': 4096,
+ },
+ ],
+ 'handle_entries_content': 'remove',
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_SETTINGS_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ 'accept-redirects': True,
+ 'accept-source-route': True,
+ 'allow-fast-path': True,
+ 'arp-timeout': '30s',
+ 'icmp-rate-limit': 10,
+ 'icmp-rate-mask': '0x1818',
+ 'ip-forward': True,
+ 'max-neighbor-entries': 4096,
+ 'route-cache': True,
+ 'rp-filter': False,
+ 'secure-redirects': True,
+ 'send-redirects': True,
+ 'tcp-syncookies': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'settings'), START_IP_SETTINGS, read_only=True))
+ def test_sync_value_modify_2_check(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip settings',
+ 'data': [
+ {
+ 'accept-redirects': True,
+ 'accept-source-route': True,
+ 'max-neighbor-entries': 4096,
+ },
+ ],
+ 'handle_entries_content': 'remove',
+ '_ansible_check_mode': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_SETTINGS_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ 'accept-redirects': True,
+ 'accept-source-route': True,
+ 'allow-fast-path': True,
+ 'arp-timeout': '30s',
+ 'icmp-rate-limit': 10,
+ 'icmp-rate-mask': '0x1818',
+ 'ip-forward': True,
+ 'max-neighbor-entries': 4096,
+ 'route-cache': True,
+ 'rp-filter': False,
+ 'secure-redirects': True,
+ 'send-redirects': True,
+ 'tcp-syncookies': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'address'), START_IP_ADDRESS, read_only=True))
+ def test_sync_primary_key_idempotent(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip address',
+ 'data': [
+ {
+ 'address': '192.168.1.0/24',
+ 'interface': 'LAN',
+ 'comment': '',
+ },
+ {
+ 'address': '192.168.88.0/24',
+ 'interface': 'bridge',
+ '!comment': None,
+ },
+ ],
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['old_data'], START_IP_ADDRESS_OLD_DATA)
+ self.assertEqual(result['new_data'], START_IP_ADDRESS_OLD_DATA)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'address'), START_IP_ADDRESS, read_only=True))
+ def test_sync_primary_key_idempotent_2(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip address',
+ 'data': [
+ {
+ 'address': '192.168.88.0/24',
+ 'interface': 'bridge',
+ },
+ {
+ 'address': '10.0.0.0/16',
+ 'interface': 'WAN',
+ 'disabled': True,
+ '!comment': '',
+ },
+ {
+ 'address': '192.168.1.0/24',
+ 'interface': 'LAN',
+ 'disabled': False,
+ 'comment': None,
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['old_data'], START_IP_ADDRESS_OLD_DATA)
+ self.assertEqual(result['new_data'], START_IP_ADDRESS_OLD_DATA)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'address'), START_IP_ADDRESS))
+ def test_sync_primary_key_cru(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip address',
+ 'data': [
+ {
+ 'address': '10.10.0.0/16',
+ 'interface': 'WIFI',
+ },
+ {
+ 'address': '192.168.1.0/24',
+ 'interface': 'LAN',
+ 'disabled': True,
+ },
+ {
+ 'address': '192.168.88.0/24',
+ 'interface': 'bridge',
+ 'comment': 'foo',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_ADDRESS_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*1',
+ 'comment': 'foo',
+ 'address': '192.168.88.0/24',
+ 'interface': 'bridge',
+ 'disabled': False,
+ },
+ {
+ '.id': '*3',
+ 'address': '192.168.1.0/24',
+ 'interface': 'LAN',
+ 'disabled': True,
+ },
+ {
+ '.id': '*NEW1',
+ 'address': '10.10.0.0/16',
+ 'interface': 'WIFI',
+ 'disabled': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'address'), START_IP_ADDRESS, read_only=True))
+ def test_sync_primary_key_cru_check(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip address',
+ 'data': [
+ {
+ 'address': '10.10.0.0/16',
+ 'interface': 'WIFI',
+ },
+ {
+ 'address': '192.168.1.0/24',
+ 'interface': 'LAN',
+ 'disabled': True,
+ },
+ {
+ 'address': '192.168.88.0/24',
+ 'interface': 'bridge',
+ 'comment': 'foo',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ '_ansible_check_mode': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_ADDRESS_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*1',
+ 'comment': 'foo',
+ 'address': '192.168.88.0/24',
+ 'interface': 'bridge',
+ 'disabled': False,
+ },
+ {
+ '.id': '*3',
+ 'address': '192.168.1.0/24',
+ 'interface': 'LAN',
+ 'disabled': True,
+ },
+ {
+ 'address': '10.10.0.0/16',
+ 'interface': 'WIFI',
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'address'), START_IP_ADDRESS))
+ def test_sync_primary_key_cru_reorder(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip address',
+ 'data': [
+ {
+ 'address': '10.10.0.0/16',
+ 'interface': 'WIFI',
+ },
+ {
+ 'address': '192.168.1.0/24',
+ 'interface': 'LAN',
+ 'disabled': True,
+ },
+ {
+ 'address': '192.168.88.0/24',
+ 'interface': 'bridge',
+ 'comment': 'foo',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ 'ensure_order': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_ADDRESS_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ '.id': '*NEW1',
+ 'address': '10.10.0.0/16',
+ 'interface': 'WIFI',
+ 'disabled': False,
+ },
+ {
+ '.id': '*3',
+ 'address': '192.168.1.0/24',
+ 'interface': 'LAN',
+ 'disabled': True,
+ },
+ {
+ '.id': '*1',
+ 'comment': 'foo',
+ 'address': '192.168.88.0/24',
+ 'interface': 'bridge',
+ 'disabled': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'address'), START_IP_ADDRESS, read_only=True))
+ def test_sync_primary_key_cru_reorder_check(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip address',
+ 'data': [
+ {
+ 'address': '10.10.0.0/16',
+ 'interface': 'WIFI',
+ },
+ {
+ 'address': '192.168.1.0/24',
+ 'interface': 'LAN',
+ 'disabled': True,
+ },
+ {
+ 'address': '192.168.88.0/24',
+ 'interface': 'bridge',
+ 'comment': 'foo',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ 'ensure_order': True,
+ '_ansible_check_mode': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], START_IP_ADDRESS_OLD_DATA)
+ self.assertEqual(result['new_data'], [
+ {
+ 'address': '10.10.0.0/16',
+ 'interface': 'WIFI',
+ },
+ {
+ '.id': '*3',
+ 'address': '192.168.1.0/24',
+ 'interface': 'LAN',
+ 'disabled': True,
+ },
+ {
+ '.id': '*1',
+ 'comment': 'foo',
+ 'address': '192.168.88.0/24',
+ 'interface': 'bridge',
+ 'disabled': False,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dhcp-server', 'lease'), START_IP_DHCP_SERVER_LEASE, read_only=True))
+ def test_absent_value(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dhcp-server lease',
+ 'data': [
+ {
+ 'address': '192.168.88.2',
+ 'mac-address': '11:22:33:44:55:66',
+ 'client-id': 'ff:1:2:3:4:5:6:7:8:9:a:b:c:d:e:f:0:1:2',
+ 'server': 'main',
+ 'comment': 'foo',
+ },
+ {
+ 'address': '192.168.88.3',
+ 'mac-address': '11:22:33:44:55:77',
+ 'client-id': '1:2:3:4:5:6:7',
+ 'server': 'main',
+ },
+ {
+ 'address': '0.0.0.1',
+ 'mac-address': '00:00:00:00:00:01',
+ 'server': 'all',
+ },
+ {
+ 'address': '0.0.0.2',
+ 'mac-address': '00:00:00:00:00:02',
+ 'server': 'all',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ 'ensure_order': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['old_data'], START_IP_DHCP_SERVER_LEASE_OLD_DATA)
+ self.assertEqual(result['new_data'], START_IP_DHCP_SERVER_LEASE_OLD_DATA)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dhcp-client'), START_IP_DHCP_CLIENT, read_only=True))
+ def test_default_remove_combination_idempotent(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dhcp-client',
+ 'data': [
+ {
+ 'interface': 'ether1',
+ },
+ {
+ 'interface': 'ether2',
+ 'dhcp-options': None,
+ },
+ {
+ 'interface': 'ether3',
+ 'dhcp-options': 'hostname',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ 'ensure_order': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['old_data'], START_IP_DHCP_CLIENT_OLD_DATA)
+ self.assertEqual(result['new_data'], START_IP_DHCP_CLIENT_OLD_DATA)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('ip', 'dhcp-client'), []))
+ def test_default_remove_combination_create(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'ip dhcp-client',
+ 'data': [
+ {
+ 'interface': 'ether1',
+ },
+ {
+ 'interface': 'ether2',
+ 'dhcp-options': None,
+ },
+ {
+ 'interface': 'ether3',
+ 'dhcp-options': 'hostname',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'handle_entries_content': 'remove',
+ 'ensure_order': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], True)
+ self.assertEqual(result['old_data'], [])
+ self.assertEqual(result['new_data'], [
+ {
+ ".id": "*NEW1",
+ "add-default-route": True,
+ "default-route-distance": 1,
+ "dhcp-options": "hostname,clientid",
+ "disabled": False,
+ "interface": "ether1",
+ "use-peer-dns": True,
+ "use-peer-ntp": True,
+ },
+ {
+ # "!dhcp-options": None,
+ ".id": "*NEW2",
+ "add-default-route": True,
+ "default-route-distance": 1,
+ "disabled": False,
+ "interface": "ether2",
+ "use-peer-dns": True,
+ "use-peer-ntp": True,
+ },
+ {
+ ".id": "*NEW3",
+ "add-default-route": True,
+ "default-route-distance": 1,
+ "dhcp-options": "hostname",
+ "disabled": False,
+ "interface": "ether3",
+ "use-peer-dns": True,
+ "use-peer-ntp": True,
+ },
+ ])
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('interface', 'list'), START_INTERFACE_LIST, read_only=True))
+ def test_absent_entries_builtin(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'interface list',
+ 'data': [
+ {
+ 'name': 'WAN',
+ 'comment': 'defconf',
+ },
+ {
+ 'name': 'Foo',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'ensure_order': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['old_data'], START_INTERFACE_LIST_OLD_DATA)
+ self.assertEqual(result['new_data'], START_INTERFACE_LIST_OLD_DATA)
+
+ @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path',
+ new=create_fake_path(('interface', 'gre'), START_INTERFACE_GRE, read_only=True))
+ def test_idempotent_default_disabled(self):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ args = self.config_module_args.copy()
+ args.update({
+ 'path': 'interface gre',
+ 'data': [
+ {
+ 'name': 'gre-tunnel3',
+ 'remote-address': '192.168.1.1',
+ '!keepalive': None,
+ },
+ {
+ 'name': 'gre-tunnel4',
+ 'remote-address': '192.168.1.2',
+ },
+ {
+ 'name': 'gre-tunnel5',
+ 'local-address': '192.168.0.1',
+ 'remote-address': '192.168.1.3',
+ 'keepalive': '20s,20',
+ 'comment': 'foo',
+ },
+ ],
+ 'handle_absent_entries': 'remove',
+ 'ensure_order': True,
+ })
+ set_module_args(args)
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], False)
+ self.assertEqual(result['old_data'], START_INTERFACE_GRE_OLD_DATA)
+ self.assertEqual(result['new_data'], START_INTERFACE_GRE_OLD_DATA)
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/test_command.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_command.py
new file mode 100644
index 000000000..3fc586586
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_command.py
@@ -0,0 +1,100 @@
+# Copyright (c) 2016 Red Hat Inc.
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt 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 json
+
+from ansible_collections.community.routeros.tests.unit.compat.mock import patch
+from ansible_collections.community.routeros.plugins.modules import command
+from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args
+from .routeros_module import TestRouterosModule, load_fixture
+
+
+class TestRouterosCommandModule(TestRouterosModule):
+
+ module = command
+
+ def setUp(self):
+ super(TestRouterosCommandModule, self).setUp()
+
+ self.mock_run_commands = patch('ansible_collections.community.routeros.plugins.modules.command.run_commands')
+ self.run_commands = self.mock_run_commands.start()
+
+ def tearDown(self):
+ super(TestRouterosCommandModule, self).tearDown()
+ self.mock_run_commands.stop()
+
+ def load_fixtures(self, commands=None):
+
+ def load_from_file(*args, **kwargs):
+ module, commands = args
+ output = list()
+
+ for item in commands:
+ try:
+ obj = json.loads(item)
+ command = obj
+ except ValueError:
+ command = item
+ filename = str(command).replace(' ', '_').replace('/', '')
+ output.append(load_fixture(filename))
+ return output
+
+ self.run_commands.side_effect = load_from_file
+
+ def test_command_simple(self):
+ set_module_args(dict(commands=['/system resource print']))
+ result = self.execute_module(changed=True)
+ self.assertEqual(len(result['stdout']), 1)
+ self.assertTrue('platform: "MikroTik"' in result['stdout'][0])
+
+ def test_command_multiple(self):
+ set_module_args(dict(commands=['/system resource print', '/system resource print']))
+ result = self.execute_module(changed=True)
+ self.assertEqual(len(result['stdout']), 2)
+ self.assertTrue('platform: "MikroTik"' in result['stdout'][0])
+
+ def test_command_wait_for(self):
+ wait_for = 'result[0] contains "MikroTik"'
+ set_module_args(dict(commands=['/system resource print'], wait_for=wait_for))
+ self.execute_module(changed=True)
+
+ def test_command_wait_for_fails(self):
+ wait_for = 'result[0] contains "test string"'
+ set_module_args(dict(commands=['/system resource print'], wait_for=wait_for))
+ self.execute_module(failed=True)
+ self.assertEqual(self.run_commands.call_count, 10)
+
+ def test_command_retries(self):
+ wait_for = 'result[0] contains "test string"'
+ set_module_args(dict(commands=['/system resource print'], wait_for=wait_for, retries=2))
+ self.execute_module(failed=True)
+ self.assertEqual(self.run_commands.call_count, 2)
+
+ def test_command_match_any(self):
+ wait_for = ['result[0] contains "MikroTik"',
+ 'result[0] contains "test string"']
+ set_module_args(dict(commands=['/system resource print'], wait_for=wait_for, match='any'))
+ self.execute_module(changed=True)
+
+ def test_command_match_all(self):
+ wait_for = ['result[0] contains "MikroTik"',
+ 'result[0] contains "RB1100"']
+ set_module_args(dict(commands=['/system resource print'], wait_for=wait_for, match='all'))
+ self.execute_module(changed=True)
+
+ def test_command_match_all_failure(self):
+ wait_for = ['result[0] contains "MikroTik"',
+ 'result[0] contains "test string"']
+ commands = ['/system resource print', '/system resource print']
+ set_module_args(dict(commands=commands, wait_for=wait_for, match='all'))
+ self.execute_module(failed=True)
+
+ def test_command_wait_for_2(self):
+ wait_for = 'result[0] contains "wireless"'
+ set_module_args(dict(commands=['/system package print'], wait_for=wait_for))
+ self.execute_module(changed=True)
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/test_facts.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_facts.py
new file mode 100644
index 000000000..322fdda45
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_facts.py
@@ -0,0 +1,335 @@
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt 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
+
+from ansible_collections.community.routeros.tests.unit.compat.mock import patch
+from ansible_collections.community.routeros.plugins.modules import facts
+from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args
+from .routeros_module import TestRouterosModule, load_fixture
+
+
+class TestRouterosFactsModule(TestRouterosModule):
+
+ module = facts
+
+ def setUp(self):
+ super(TestRouterosFactsModule, self).setUp()
+ self.mock_run_commands = patch('ansible_collections.community.routeros.plugins.modules.facts.run_commands')
+ self.run_commands = self.mock_run_commands.start()
+
+ def tearDown(self):
+ super(TestRouterosFactsModule, self).tearDown()
+ self.mock_run_commands.stop()
+
+ def load_fixtures(self, commands=None):
+ def load_from_file(*args, **kwargs):
+ module = args
+ commands = kwargs['commands']
+ output = list()
+
+ for command in commands:
+ filename = str(command).split(' | ', 1)[0].replace(' ', '_')
+ output.append(load_fixture('facts%s' % filename))
+ return output
+
+ self.run_commands.side_effect = load_from_file
+
+ def test_facts_default(self):
+ set_module_args(dict(gather_subset='default'))
+ result = self.execute_module()
+ self.assertEqual(
+ result['ansible_facts']['ansible_net_hostname'], 'MikroTik'
+ )
+ self.assertEqual(
+ result['ansible_facts']['ansible_net_version'], '6.42.5 (stable)'
+ )
+ self.assertEqual(
+ result['ansible_facts']['ansible_net_model'], 'RouterBOARD 3011UiAS'
+ )
+ self.assertEqual(
+ result['ansible_facts']['ansible_net_serialnum'], '1234567890'
+ )
+ self.assertEqual(
+ result['ansible_facts']['ansible_net_arch'], 'x86'
+ )
+ self.assertEqual(
+ result['ansible_facts']['ansible_net_uptime'], '3h28m52s'
+ )
+
+ def test_facts_hardware(self):
+ set_module_args(dict(gather_subset='hardware'))
+ result = self.execute_module()
+ self.assertEqual(
+ result['ansible_facts']['ansible_net_spacefree_mb'], 64921.6
+ )
+ self.assertEqual(
+ result['ansible_facts']['ansible_net_spacetotal_mb'], 65024.0
+ )
+ self.assertEqual(
+ result['ansible_facts']['ansible_net_memfree_mb'], 988.3
+ )
+ self.assertEqual(
+ result['ansible_facts']['ansible_net_memtotal_mb'], 1010.8
+ )
+
+ def test_facts_config(self):
+ set_module_args(dict(gather_subset='config'))
+ result = self.execute_module()
+ self.assertIsInstance(
+ result['ansible_facts']['ansible_net_config'], str
+ )
+
+ self.assertIsInstance(
+ result['ansible_facts']['ansible_net_config_nonverbose'], str
+ )
+
+ def test_facts_interfaces(self):
+ set_module_args(dict(gather_subset='interfaces'))
+ result = self.execute_module()
+ self.assertIn(
+ result['ansible_facts']['ansible_net_all_ipv4_addresses'][0], ['10.37.129.3', '10.37.0.0', '192.168.88.1']
+ )
+ self.assertEqual(
+ result['ansible_facts']['ansible_net_all_ipv6_addresses'], ['fe80::21c:42ff:fe36:5290']
+ )
+ self.assertEqual(
+ result['ansible_facts']['ansible_net_all_ipv6_addresses'][0],
+ result['ansible_facts']['ansible_net_interfaces']['ether1']['ipv6'][0]['address']
+ )
+ self.assertEqual(
+ len(result['ansible_facts']['ansible_net_interfaces'].keys()), 11
+ )
+ self.assertEqual(
+ len(result['ansible_facts']['ansible_net_neighbors']), 4
+ )
+
+ def test_facts_interfaces_no_ipv6(self):
+ fixture = load_fixture(
+ 'facts/ipv6_address_print_detail_without-paging_no-ipv6'
+ )
+ interfaces = self.module.Interfaces(module=self.module)
+ addresses = interfaces.parse_detail(data=fixture)
+ result = interfaces.populate_addresses(data=addresses, family='ipv6')
+
+ self.assertEqual(result, None)
+
+ def test_facts_routing(self):
+ set_module_args(dict(gather_subset='routing'))
+ result = self.execute_module()
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['name'], ['iBGP_BRAS.TYRMA']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['instance'], ['MAIN_AS_STARKDV']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['remote-address'], ['10.10.100.1']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['remote-as'], ['64520']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['nexthop-choice'], ['default']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['multihop'], ['no']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['route-reflect'], ['yes']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['hold-time'], ['3m']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['ttl'], ['default']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['address-families'], ['ip']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['update-source'], ['LAN_KHV']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['default-originate'], ['never']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['remove-private-as'], ['no']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['as-override'], ['no']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['passive'], ['no']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['use-bfd'], ['yes']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['route-distinguisher'], ['64520:666']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['dst-address'], ['10.10.66.8/30']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['gateway'], ['10.10.100.1']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['interface'], ['GRE_TYRMA']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['in-label'], ['6136']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['out-label'], ['6136']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['bgp-local-pref'], ['100']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['bgp-origin'], ['incomplete']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['bgp-ext-communities'], ['RT:64520:666']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_instance']['default']['name'], ['default']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_instance']['default']['as'], ['65530']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_instance']['default']['router-id'], ['0.0.0.0']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_instance']['default']['redistribute-connected'], ['no']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_instance']['default']['redistribute-static'], ['no']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_instance']['default']['redistribute-rip'], ['no']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_instance']['default']['redistribute-ospf'], ['no']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_instance']['default']['redistribute-other-bgp'], ['no']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_instance']['default']['client-to-client-reflection'], ['yes']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_bgp_instance']['default']['ignore-as-path-len'], ['no']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_route']['altegro']['dst-address'], ['10.10.66.0/30']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_route']['altegro']['pref-src'], ['10.10.66.1']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_route']['altegro']['gateway'], ['bridge1']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_route']['altegro']['gateway-status'], ['bridge1']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_route']['altegro']['distance'], ['0']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_route']['altegro']['scope'], ['10']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_route']['altegro']['routing-mark'], ['altegro']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_instance']['default']['name'], ['default']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_instance']['default']['router-id'], ['10.10.50.1']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_instance']['default']['distribute-default'], ['never']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_instance']['default']['redistribute-connected'], ['no']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_instance']['default']['redistribute-static'], ['no']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_instance']['default']['redistribute-rip'], ['no']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_instance']['default']['redistribute-bgp'], ['no']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_instance']['default']['redistribute-other-ospf'], ['no']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_instance']['default']['metric-default'], ['1']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_instance']['default']['metric-connected'], ['20']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_instance']['default']['metric-static'], ['20']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_instance']['default']['metric-rip'], ['20']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_instance']['default']['metric-bgp'], ['auto']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_instance']['default']['metric-other-ospf'], ['auto']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_instance']['default']['in-filter'], ['ospf-in']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_instance']['default']['out-filter'], ['ospf-out']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_neighbor']['default']['instance'], ['default']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_neighbor']['default']['router-id'], ['10.10.100.1']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_neighbor']['default']['address'], ['10.10.1.2']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_neighbor']['default']['interface'], ['GRE_TYRMA']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_neighbor']['default']['priority'], ['1']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_neighbor']['default']['dr-address'], ['0.0.0.0']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_neighbor']['default']['backup-dr-address'], ['0.0.0.0']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_neighbor']['default']['state'], ['Full']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_neighbor']['default']['state-changes'], ['15']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_neighbor']['default']['ls-retransmits'], ['0']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_neighbor']['default']['ls-requests'], ['0']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_neighbor']['default']['db-summaries'], ['0']
+ )
+ self.assertIn(
+ result['ansible_facts']['ansible_net_ospf_neighbor']['default']['adjacency'], ['6h8m46s']
+ )
diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/utils.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/utils.py
new file mode 100644
index 000000000..419eef114
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/utils.py
@@ -0,0 +1,54 @@
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import json
+
+from ansible_collections.community.routeros.tests.unit.compat import unittest
+from ansible_collections.community.routeros.tests.unit.compat.mock import patch
+from ansible.module_utils import basic
+from ansible.module_utils.common.text.converters import to_bytes
+
+
+def set_module_args(args):
+ if '_ansible_remote_tmp' not in args:
+ args['_ansible_remote_tmp'] = '/tmp'
+ if '_ansible_keep_remote_files' not in args:
+ args['_ansible_keep_remote_files'] = False
+
+ args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
+ basic._ANSIBLE_ARGS = to_bytes(args)
+
+
+class AnsibleExitJson(Exception):
+ pass
+
+
+class AnsibleFailJson(Exception):
+ pass
+
+
+def exit_json(*args, **kwargs):
+ if 'changed' not in kwargs:
+ kwargs['changed'] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs):
+ kwargs['failed'] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class ModuleTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.mock_module = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json)
+ self.mock_module.start()
+ self.mock_sleep = patch('time.sleep')
+ self.mock_sleep.start()
+ set_module_args({})
+ self.addCleanup(self.mock_module.stop)
+ self.addCleanup(self.mock_sleep.stop)
diff --git a/ansible_collections/community/routeros/tests/unit/requirements.txt b/ansible_collections/community/routeros/tests/unit/requirements.txt
new file mode 100644
index 000000000..479f2fc6f
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/requirements.txt
@@ -0,0 +1,6 @@
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+unittest2 ; python_version <= '2.6'
+ordereddict ; python_version <= '2.6'
diff --git a/ansible_collections/community/routeros/tests/unit/requirements.yml b/ansible_collections/community/routeros/tests/unit/requirements.yml
new file mode 100644
index 000000000..6a22736b5
--- /dev/null
+++ b/ansible_collections/community/routeros/tests/unit/requirements.yml
@@ -0,0 +1,7 @@
+---
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+collections:
+- ansible.netcommon