summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/routeros/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'ansible_collections/community/routeros/plugins')
-rw-r--r--ansible_collections/community/routeros/plugins/cliconf/routeros.py62
-rw-r--r--ansible_collections/community/routeros/plugins/doc_fragments/api.py97
-rw-r--r--ansible_collections/community/routeros/plugins/doc_fragments/attributes.py98
-rw-r--r--ansible_collections/community/routeros/plugins/filter/join.yml31
-rw-r--r--ansible_collections/community/routeros/plugins/filter/list_to_dict.yml41
-rw-r--r--ansible_collections/community/routeros/plugins/filter/quote_argument.yml30
-rw-r--r--ansible_collections/community/routeros/plugins/filter/quote_argument_value.yml30
-rw-r--r--ansible_collections/community/routeros/plugins/filter/quoting.py114
-rw-r--r--ansible_collections/community/routeros/plugins/filter/split.yml31
-rw-r--r--ansible_collections/community/routeros/plugins/module_utils/_api_data.py2860
-rw-r--r--ansible_collections/community/routeros/plugins/module_utils/_version.py345
-rw-r--r--ansible_collections/community/routeros/plugins/module_utils/api.py113
-rw-r--r--ansible_collections/community/routeros/plugins/module_utils/quoting.py207
-rw-r--r--ansible_collections/community/routeros/plugins/module_utils/routeros.py153
-rw-r--r--ansible_collections/community/routeros/plugins/module_utils/version.py18
-rw-r--r--ansible_collections/community/routeros/plugins/modules/api.py577
-rw-r--r--ansible_collections/community/routeros/plugins/modules/api_facts.py495
-rw-r--r--ansible_collections/community/routeros/plugins/modules/api_find_and_modify.py327
-rw-r--r--ansible_collections/community/routeros/plugins/modules/api_info.py366
-rw-r--r--ansible_collections/community/routeros/plugins/modules/api_modify.py1030
-rw-r--r--ansible_collections/community/routeros/plugins/modules/command.py210
-rw-r--r--ansible_collections/community/routeros/plugins/modules/facts.py663
-rw-r--r--ansible_collections/community/routeros/plugins/terminal/routeros.py53
23 files changed, 7951 insertions, 0 deletions
diff --git a/ansible_collections/community/routeros/plugins/cliconf/routeros.py b/ansible_collections/community/routeros/plugins/cliconf/routeros.py
new file mode 100644
index 000000000..412627b8e
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/cliconf/routeros.py
@@ -0,0 +1,62 @@
+# Copyright (c) 2017 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
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+author: "Egor Zaitsev (@heuels)"
+name: routeros
+short_description: Use routeros cliconf to run command on MikroTik RouterOS platform
+description:
+ - This routeros plugin provides low level abstraction apis for
+ sending and receiving CLI commands from MikroTik RouterOS network devices.
+'''
+
+import re
+import json
+
+from ansible.module_utils.common.text.converters import to_text
+from ansible.plugins.cliconf import CliconfBase
+
+
+class Cliconf(CliconfBase):
+
+ def get_device_info(self):
+ device_info = {}
+ device_info['network_os'] = 'RouterOS'
+
+ resource = self.get('/system resource print')
+ data = to_text(resource, errors='surrogate_or_strict').strip()
+ match = re.search(r'version: (\S+)', data)
+ if match:
+ device_info['network_os_version'] = match.group(1)
+
+ routerboard = self.get('/system routerboard print')
+ data = to_text(routerboard, errors='surrogate_or_strict').strip()
+ match = re.search(r'model: (.+)$', data, re.M)
+ if match:
+ device_info['network_os_model'] = match.group(1)
+
+ identity = self.get('/system identity print')
+ data = to_text(identity, errors='surrogate_or_strict').strip()
+ match = re.search(r'name: (.+)$', data, re.M)
+ if match:
+ device_info['network_os_hostname'] = match.group(1)
+
+ return device_info
+
+ def get_config(self, source='running', flags=None, format=None):
+ return
+
+ def edit_config(self, command):
+ return
+
+ def get(self, command, prompt=None, answer=None, sendonly=False, newline=True, check_all=False):
+ return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all)
+
+ def get_capabilities(self):
+ result = super(Cliconf, self).get_capabilities()
+ return json.dumps(result)
diff --git a/ansible_collections/community/routeros/plugins/doc_fragments/api.py b/ansible_collections/community/routeros/plugins/doc_fragments/api.py
new file mode 100644
index 000000000..dea374b95
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/doc_fragments/api.py
@@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020, Nikolay Dachev <nikolay@dachev.info>
+# GNU General Public License v3.0+ 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
+
+
+class ModuleDocFragment(object):
+
+ DOCUMENTATION = r'''
+options:
+ hostname:
+ description:
+ - RouterOS hostname API.
+ required: true
+ type: str
+ username:
+ description:
+ - RouterOS login user.
+ required: true
+ type: str
+ password:
+ description:
+ - RouterOS user password.
+ required: true
+ type: str
+ timeout:
+ description:
+ - Timeout for the request.
+ type: int
+ default: 10
+ version_added: 2.3.0
+ tls:
+ description:
+ - If is set TLS will be used for RouterOS API connection.
+ required: false
+ type: bool
+ default: false
+ aliases:
+ - ssl
+ port:
+ description:
+ - RouterOS api port. If I(tls) is set, port will apply to TLS/SSL connection.
+ - Defaults are C(8728) for the HTTP API, and C(8729) for the HTTPS API.
+ type: int
+ force_no_cert:
+ description:
+ - Set to C(true) to connect without a certificate when I(tls=true).
+ - See also I(validate_certs).
+ - B(Note:) this forces the use of anonymous Diffie-Hellman (ADH) ciphers. The protocol is susceptible
+ to Man-in-the-Middle attacks, because the keys used in the exchange are not authenticated.
+ Instead of simply connecting without a certificate to "make things work" have a look at
+ I(validate_certs) and I(ca_path).
+ type: bool
+ default: false
+ version_added: 2.4.0
+ validate_certs:
+ description:
+ - Set to C(false) to skip validation of TLS certificates.
+ - See also I(validate_cert_hostname). Only used when I(tls=true).
+ - B(Note:) instead of simply deactivating certificate validations to "make things work",
+ please consider creating your own CA certificate and using it to sign certificates used
+ for your router. You can tell the module about your CA certificate with the I(ca_path)
+ option.
+ type: bool
+ default: true
+ version_added: 1.2.0
+ validate_cert_hostname:
+ description:
+ - Set to C(true) to validate hostnames in certificates.
+ - See also I(validate_certs). Only used when I(tls=true) and I(validate_certs=true).
+ type: bool
+ default: false
+ version_added: 1.2.0
+ ca_path:
+ description:
+ - PEM formatted file that contains a CA certificate to be used for certificate validation.
+ - See also I(validate_cert_hostname). Only used when I(tls=true) and I(validate_certs=true).
+ type: path
+ version_added: 1.2.0
+ encoding:
+ description:
+ - Use the specified encoding when communicating with the RouterOS device.
+ - Default is C(ASCII). Note that C(UTF-8) requires librouteros 3.2.1 or newer.
+ type: str
+ default: ASCII
+ version_added: 2.1.0
+requirements:
+ - librouteros
+ - Python >= 3.6 (for librouteros)
+seealso:
+ - ref: ansible_collections.community.routeros.docsite.api-guide
+ description: How to connect to RouterOS devices with the RouterOS API
+'''
diff --git a/ansible_collections/community/routeros/plugins/doc_fragments/attributes.py b/ansible_collections/community/routeros/plugins/doc_fragments/attributes.py
new file mode 100644
index 000000000..e18a48ff2
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/doc_fragments/attributes.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+
+ # Standard documentation fragment
+ DOCUMENTATION = r'''
+options: {}
+attributes:
+ check_mode:
+ description: Can run in C(check_mode) and return changed status prediction without modifying target.
+ diff_mode:
+ description: Will return details on what has changed (or possibly needs changing in C(check_mode)), when in diff mode.
+ platform:
+ description: Target OS/families that can be operated against.
+ support: N/A
+'''
+
+ # Should be used together with the standard fragment
+ INFO_MODULE = r'''
+options: {}
+attributes:
+ check_mode:
+ support: full
+ details:
+ - This action does not modify state.
+ diff_mode:
+ support: N/A
+ details:
+ - This action does not modify state.
+'''
+
+ ACTIONGROUP_API = r'''
+options: {}
+attributes:
+ action_group:
+ description: Use C(group/community.routeros.api) in C(module_defaults) to set defaults for this module.
+ support: full
+ membership:
+ - community.routeros.api
+'''
+
+ CONN = r'''
+options: {}
+attributes:
+ become:
+ description: Is usable alongside C(become) keywords.
+ connection:
+ description: Uses the target's configured connection information to execute code on it.
+ delegation:
+ description: Can be used in conjunction with C(delegate_to) and related keywords.
+'''
+
+ FACTS = r'''
+options: {}
+attributes:
+ facts:
+ description: Action returns an C(ansible_facts) dictionary that will update existing host facts.
+'''
+
+ # Should be used together with the standard fragment and the FACTS fragment
+ FACTS_MODULE = r'''
+options: {}
+attributes:
+ check_mode:
+ support: full
+ details:
+ - This action does not modify state.
+ diff_mode:
+ support: N/A
+ details:
+ - This action does not modify state.
+ facts:
+ support: full
+'''
+
+ FILES = r'''
+options: {}
+attributes:
+ safe_file_operations:
+ description: Uses Ansible's strict file operation functions to ensure proper permissions and avoid data corruption.
+'''
+
+ FLOW = r'''
+options: {}
+attributes:
+ action:
+ description: Indicates this has a corresponding action plugin so some parts of the options can be executed on the controller.
+ async:
+ description: Supports being used with the C(async) keyword.
+'''
diff --git a/ansible_collections/community/routeros/plugins/filter/join.yml b/ansible_collections/community/routeros/plugins/filter/join.yml
new file mode 100644
index 000000000..9ff8a50f1
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/filter/join.yml
@@ -0,0 +1,31 @@
+---
+# 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
+
+DOCUMENTATION:
+ name: join
+ short_description: Join a list of arguments to a command
+ version_added: 2.0.0
+ description:
+ - Join and quotes a list of arguments to a command.
+ options:
+ _input:
+ description:
+ - A list of arguments to quote and join.
+ type: list
+ elements: string
+ required: true
+ author:
+ - Felix Fontein (@felixfontein)
+
+EXAMPLES: |
+ - name: Join arguments for a RouterOS CLI command
+ ansible.builtin.set_fact:
+ arguments: "{{ ['foo=bar', 'comment=foo is bar'] | community.routeros.join }}"
+ # Should result in 'foo=bar comment="foo is bar"'
+
+RETURN:
+ _value:
+ description: The joined and quoted result.
+ type: string
diff --git a/ansible_collections/community/routeros/plugins/filter/list_to_dict.yml b/ansible_collections/community/routeros/plugins/filter/list_to_dict.yml
new file mode 100644
index 000000000..920414ced
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/filter/list_to_dict.yml
@@ -0,0 +1,41 @@
+---
+# 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
+
+DOCUMENTATION:
+ name: list_to_dict
+ short_description: Convert a list of arguments to a dictionary
+ version_added: 2.0.0
+ description:
+ - Convert a list of arguments to a dictionary.
+ options:
+ _input:
+ description:
+ - A list of assignments. Can be the result of the C(community.routeros.split) filter.
+ type: list
+ elements: string
+ required: true
+ require_assignment:
+ description:
+ - Allows to accept arguments without values when set to C(false).
+ type: boolean
+ default: true
+ skip_empty_values:
+ description:
+ - Allows to skip arguments whose value is empty when set to C(true).
+ type: boolean
+ default: false
+ author:
+ - Felix Fontein (@felixfontein)
+
+EXAMPLES: |
+ - name: Convert a list to a dictionary
+ ansible.builtin.set_fact:
+ dictionary: "{{ ['foo=bar', 'comment=foo is bar'] | community.routeros.list_to_dict }}"
+ # dictionary == {'foo': 'bar', 'comment': 'foo is bar'}
+
+RETURN:
+ _value:
+ description: A dictionary representation of the input data.
+ type: dictionary
diff --git a/ansible_collections/community/routeros/plugins/filter/quote_argument.yml b/ansible_collections/community/routeros/plugins/filter/quote_argument.yml
new file mode 100644
index 000000000..26a1f0401
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/filter/quote_argument.yml
@@ -0,0 +1,30 @@
+---
+# 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
+
+DOCUMENTATION:
+ name: quote_argument
+ short_description: Quote an argument
+ version_added: 2.0.0
+ description:
+ - Quote an argument.
+ options:
+ _input:
+ description:
+ - An argument to quote.
+ type: string
+ required: true
+ author:
+ - Felix Fontein (@felixfontein)
+
+EXAMPLES: |
+ - name: Quote a RouterOS CLI command argument
+ ansible.builtin.set_fact:
+ quoted: "{{ 'comment=this is a "comment"' | community.routeros.quote_argument }}"
+ # Should result in 'comment="this is a \"comment\""'
+
+RETURN:
+ _value:
+ description: The quoted argument.
+ type: string
diff --git a/ansible_collections/community/routeros/plugins/filter/quote_argument_value.yml b/ansible_collections/community/routeros/plugins/filter/quote_argument_value.yml
new file mode 100644
index 000000000..839895bc9
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/filter/quote_argument_value.yml
@@ -0,0 +1,30 @@
+---
+# 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
+
+DOCUMENTATION:
+ name: quote_argument_value
+ short_description: Quote an argument value
+ version_added: 2.0.0
+ description:
+ - Quote an argument value.
+ options:
+ _input:
+ description:
+ - An argument value to quote.
+ type: string
+ required: true
+ author:
+ - Felix Fontein (@felixfontein)
+
+EXAMPLES: |
+ - name: Quote a RouterOS CLI command argument's value
+ ansible.builtin.set_fact:
+ quoted: "{{ 'this is a "comment"' | community.routeros.quote_argument_value }}"
+ # Should result in '"this is a \"comment\""'
+
+RETURN:
+ _value:
+ description: The quoted argument value.
+ type: string
diff --git a/ansible_collections/community/routeros/plugins/filter/quoting.py b/ansible_collections/community/routeros/plugins/filter/quoting.py
new file mode 100644
index 000000000..3985d5581
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/filter/quoting.py
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021, 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
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.errors import AnsibleFilterError
+from ansible.module_utils.common.text.converters import to_text
+
+from ansible_collections.community.routeros.plugins.module_utils.quoting import (
+ ParseError,
+ convert_list_to_dictionary,
+ join_routeros_command,
+ quote_routeros_argument,
+ quote_routeros_argument_value,
+ split_routeros_command,
+)
+
+
+def wrap_exception(fn, *args, **kwargs):
+ try:
+ return fn(*args, **kwargs)
+ except ParseError as e:
+ raise AnsibleFilterError(to_text(e))
+
+
+def split(line):
+ '''
+ Split a command into arguments.
+
+ Example:
+ 'add name=wrap comment="with space"'
+ is converted to:
+ ['add', 'name=wrap', 'comment=with space']
+ '''
+ return wrap_exception(split_routeros_command, line)
+
+
+def quote_argument_value(argument):
+ '''
+ Quote an argument value.
+
+ Example:
+ 'with "space"'
+ is converted to:
+ r'"with \"space\""'
+ '''
+ return wrap_exception(quote_routeros_argument_value, argument)
+
+
+def quote_argument(argument):
+ '''
+ Quote an argument.
+
+ Example:
+ 'comment=with "space"'
+ is converted to:
+ r'comment="with \"space\""'
+ '''
+ return wrap_exception(quote_routeros_argument, argument)
+
+
+def join(arguments):
+ '''
+ Join a list of arguments to a command.
+
+ Example:
+ ['add', 'name=wrap', 'comment=with space']
+ is converted to:
+ 'add name=wrap comment="with space"'
+ '''
+ return wrap_exception(join_routeros_command, arguments)
+
+
+def list_to_dict(string_list, require_assignment=True, skip_empty_values=False):
+ '''
+ Convert a list of arguments to a list of dictionary.
+
+ Example:
+ ['foo=bar', 'comment=with space', 'additional=']
+ is converted to:
+ {'foo': 'bar', 'comment': 'with space', 'additional': ''}
+
+ If require_assignment is True (default), arguments without assignments are
+ rejected. (Example: in ['add', 'name=foo'], 'add' is an argument without
+ assignment.) If it is False, these are given value None.
+
+ If skip_empty_values is True, arguments with empty value are removed from
+ the result. (Example: in ['name='], 'name' has an empty value.)
+ If it is False (default), these are kept.
+
+ '''
+ return wrap_exception(
+ convert_list_to_dictionary,
+ string_list,
+ require_assignment=require_assignment,
+ skip_empty_values=skip_empty_values,
+ )
+
+
+class FilterModule(object):
+ '''Ansible jinja2 filters for RouterOS command quoting and unquoting'''
+
+ def filters(self):
+ return {
+ 'split': split,
+ 'quote_argument': quote_argument,
+ 'quote_argument_value': quote_argument_value,
+ 'join': join,
+ 'list_to_dict': list_to_dict,
+ }
diff --git a/ansible_collections/community/routeros/plugins/filter/split.yml b/ansible_collections/community/routeros/plugins/filter/split.yml
new file mode 100644
index 000000000..5fc4b30c7
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/filter/split.yml
@@ -0,0 +1,31 @@
+---
+# 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
+
+DOCUMENTATION:
+ name: split
+ short_description: Split a command into arguments
+ version_added: 2.0.0
+ description:
+ - Split a command into arguments.
+ options:
+ _input:
+ description:
+ - A command.
+ type: string
+ required: true
+ author:
+ - Felix Fontein (@felixfontein)
+
+EXAMPLES: |
+ - name: Split command into list of arguments
+ ansible.builtin.set_fact:
+ argument_list: "{{ 'foo=bar comment="foo is bar" baz' | community.routeros.split }}"
+ # Should result in ['foo=bar', 'comment=foo is bar', 'baz']
+
+RETURN:
+ _value:
+ description: The list of arguments.
+ type: list
+ elements: string
diff --git a/ansible_collections/community/routeros/plugins/module_utils/_api_data.py b/ansible_collections/community/routeros/plugins/module_utils/_api_data.py
new file mode 100644
index 000000000..59e5b5c52
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/module_utils/_api_data.py
@@ -0,0 +1,2860 @@
+# -*- coding: utf-8 -*-
+# 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
+
+# The data inside here is private to this collection. If you use this from outside the collection,
+# you are on your own. There can be random changes to its format even in bugfix releases!
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+class APIData(object):
+ def __init__(self, primary_keys=None,
+ stratify_keys=None,
+ required_one_of=None,
+ mutually_exclusive=None,
+ has_identifier=False,
+ single_value=False,
+ unknown_mechanism=False,
+ fully_understood=False,
+ fixed_entries=False,
+ fields=None):
+ if sum([primary_keys is not None, stratify_keys is not None, has_identifier, single_value, unknown_mechanism]) > 1:
+ raise ValueError('primary_keys, stratify_keys, has_identifier, single_value, and unknown_mechanism are mutually exclusive')
+ if unknown_mechanism and fully_understood:
+ raise ValueError('unknown_mechanism and fully_understood cannot be combined')
+ self.primary_keys = primary_keys
+ self.stratify_keys = stratify_keys
+ self.required_one_of = required_one_of or []
+ self.mutually_exclusive = mutually_exclusive or []
+ self.has_identifier = has_identifier
+ self.single_value = single_value
+ self.unknown_mechanism = unknown_mechanism
+ self.fully_understood = fully_understood
+ self.fixed_entries = fixed_entries
+ if fixed_entries and primary_keys is None:
+ raise ValueError('fixed_entries can only be used with primary_keys')
+ if fields is None:
+ raise ValueError('fields must be provided')
+ self.fields = fields
+ if primary_keys:
+ for pk in primary_keys:
+ if pk not in fields:
+ raise ValueError('Primary key {pk} must be in fields!'.format(pk=pk))
+ if stratify_keys:
+ for sk in stratify_keys:
+ if sk not in fields:
+ raise ValueError('Stratify key {sk} must be in fields!'.format(sk=sk))
+ if required_one_of:
+ for index, require_list in enumerate(required_one_of):
+ if not isinstance(require_list, list):
+ raise ValueError('Require one of element at index #{index} must be a list!'.format(index=index + 1))
+ for rk in require_list:
+ if rk not in fields:
+ raise ValueError('Require one of key {rk} must be in fields!'.format(rk=rk))
+ if mutually_exclusive:
+ for index, exclusive_list in enumerate(mutually_exclusive):
+ if not isinstance(exclusive_list, list):
+ raise ValueError('Mutually exclusive element at index #{index} must be a list!'.format(index=index + 1))
+ for ek in exclusive_list:
+ if ek not in fields:
+ raise ValueError('Mutually exclusive key {ek} must be in fields!'.format(ek=ek))
+
+
+class KeyInfo(object):
+ def __init__(self, _dummy=None, can_disable=False, remove_value=None, absent_value=None, default=None, required=False, automatically_computed_from=None):
+ if _dummy is not None:
+ raise ValueError('KeyInfo() does not have positional arguments')
+ if sum([required, default is not None or can_disable, automatically_computed_from is not None]) > 1:
+ raise ValueError(
+ 'required, default, automatically_computed_from, and can_disable are mutually exclusive ' +
+ 'besides default and can_disable which can be set together')
+ if not can_disable and remove_value is not None:
+ raise ValueError('remove_value can only be specified if can_disable=True')
+ if absent_value is not None and any([default is not None, automatically_computed_from is not None, can_disable]):
+ raise ValueError('absent_value can not be combined with default, automatically_computed_from, can_disable=True, or absent_value')
+ self.can_disable = can_disable
+ self.remove_value = remove_value
+ self.automatically_computed_from = automatically_computed_from
+ self.default = default
+ self.required = required
+ self.absent_value = absent_value
+
+
+def split_path(path):
+ return path.split()
+
+
+def join_path(path):
+ return ' '.join(path)
+
+
+# How to obtain this information:
+# 1. Run `/export verbose` in the CLI;
+# 2. All attributes listed there go into the `fields` list;
+# attributes which can have a `!` ahead should have `canDisable=True`
+# 3. All bold attributes go into the `primary_keys` list -- this is not always true!
+
+PATHS = {
+ ('interface', 'bonding'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'arp': KeyInfo(default='enabled'),
+ 'arp-interval': KeyInfo(default='100ms'),
+ 'arp-ip-targets': KeyInfo(default=''),
+ 'arp-timeout': KeyInfo(default='auto'),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'down-delay': KeyInfo(default='0ms'),
+ 'forced-mac-address': KeyInfo(can_disable=True),
+ 'lacp-rate': KeyInfo(default='30secs'),
+ 'lacp-user-key': KeyInfo(can_disable=True, remove_value=0),
+ 'link-monitoring': KeyInfo(default='mii'),
+ 'mii-interval': KeyInfo(default='100ms'),
+ 'min-links': KeyInfo(default=0),
+ 'mlag-id': KeyInfo(can_disable=True, remove_value=0),
+ 'mode': KeyInfo(default='balance-rr'),
+ 'mtu': KeyInfo(default=1500),
+ 'name': KeyInfo(),
+ 'primary': KeyInfo(default='none'),
+ 'slaves': KeyInfo(required=True),
+ 'transmit-hash-policy': KeyInfo(default='layer-2'),
+ 'up-delay': KeyInfo(default='0ms'),
+ }
+ ),
+ ('interface', 'bridge'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'admin-mac': KeyInfo(default=''),
+ 'ageing-time': KeyInfo(default='5m'),
+ 'arp': KeyInfo(default='enabled'),
+ 'arp-timeout': KeyInfo(default='auto'),
+ 'auto-mac': KeyInfo(default=True),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'dhcp-snooping': KeyInfo(default=False),
+ 'disabled': KeyInfo(default=False),
+ 'ether-type': KeyInfo(default='0x8100'),
+ 'fast-forward': KeyInfo(default=True),
+ 'frame-types': KeyInfo(default='admit-all'),
+ 'forward-delay': KeyInfo(default='15s'),
+ 'igmp-snooping': KeyInfo(default=False),
+ 'ingress-filtering': KeyInfo(default=True),
+ 'max-message-age': KeyInfo(default='20s'),
+ 'mtu': KeyInfo(default='auto'),
+ 'name': KeyInfo(),
+ 'priority': KeyInfo(default='0x8000'),
+ 'protocol-mode': KeyInfo(default='rstp'),
+ 'pvid': KeyInfo(default=1),
+ 'transmit-hold-count': KeyInfo(default=6),
+ 'vlan-filtering': KeyInfo(default=False),
+ },
+ ),
+ ('interface', 'eoip'): APIData(
+ fully_understood=True,
+ primary_keys=('name',),
+ fields={
+ 'allow-fast-path': KeyInfo(default=True),
+ 'arp': KeyInfo(default='enabled'),
+ 'arp-timeout': KeyInfo(default='auto'),
+ 'clamp-tcp-mss': KeyInfo(default=True),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'dont-fragment': KeyInfo(default=False),
+ 'dscp': KeyInfo(default='inherit'),
+ 'ipsec-secret': KeyInfo(can_disable=True),
+ 'keepalive': KeyInfo(default='10s,10', can_disable=True),
+ 'local-address': KeyInfo(default='0.0.0.0'),
+ 'loop-protect': KeyInfo(default='default'),
+ 'loop-protect-disable-time': KeyInfo(default='5m'),
+ 'loop-protect-send-interval': KeyInfo(default='5s'),
+ 'mac-address': KeyInfo(),
+ 'mtu': KeyInfo(default='auto'),
+ 'name': KeyInfo(),
+ 'remote-address': KeyInfo(required=True),
+ 'tunnel-id': KeyInfo(required=True),
+ },
+ ),
+ ('interface', 'ethernet'): APIData(
+ fixed_entries=True,
+ fully_understood=True,
+ primary_keys=('default-name', ),
+ fields={
+ 'default-name': KeyInfo(),
+ 'advertise': KeyInfo(),
+ 'arp': KeyInfo(default='enabled'),
+ 'arp-timeout': KeyInfo(default='auto'),
+ 'auto-negotiation': KeyInfo(default=True),
+ 'bandwidth': KeyInfo(default='unlimited/unlimited'),
+ 'combo-mode': KeyInfo(can_disable=True),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'fec-mode': KeyInfo(can_disable=True),
+ 'full-duplex': KeyInfo(default=True),
+ 'l2mtu': KeyInfo(default=1598),
+ 'loop-protect': KeyInfo(default='default'),
+ 'loop-protect-disable-time': KeyInfo(default='5m'),
+ 'loop-protect-send-interval': KeyInfo(default='5s'),
+ 'mac-address': KeyInfo(),
+ 'mdix-enable': KeyInfo(),
+ 'mtu': KeyInfo(default=1500),
+ 'name': KeyInfo(),
+ 'orig-mac-address': KeyInfo(),
+ 'poe-out': KeyInfo(can_disable=True),
+ 'poe-priority': KeyInfo(can_disable=True),
+ 'poe-voltage': KeyInfo(can_disable=True),
+ 'power-cycle-interval': KeyInfo(),
+ 'power-cycle-ping-address': KeyInfo(can_disable=True),
+ 'power-cycle-ping-enabled': KeyInfo(),
+ 'power-cycle-ping-timeout': KeyInfo(can_disable=True),
+ 'rx-flow-control': KeyInfo(default='off'),
+ 'sfp-rate-select': KeyInfo(default='high'),
+ 'sfp-shutdown-temperature': KeyInfo(default='95C'),
+ 'speed': KeyInfo(),
+ 'tx-flow-control': KeyInfo(default='off'),
+ },
+ ),
+ ('interface', 'ethernet', 'poe'): APIData(
+ fixed_entries=True,
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'name': KeyInfo(),
+ 'poe-out': KeyInfo(default='auto-on'),
+ 'poe-priority': KeyInfo(default=10),
+ 'poe-voltage': KeyInfo(default='auto'),
+ 'power-cycle-interval': KeyInfo(default='none'),
+ 'power-cycle-ping-address': KeyInfo(can_disable=True),
+ 'power-cycle-ping-enabled': KeyInfo(default=False),
+ 'power-cycle-ping-timeout': KeyInfo(can_disable=True),
+ }
+ ),
+ ('interface', 'gre'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'allow-fast-path': KeyInfo(default=True),
+ 'clamp-tcp-mss': KeyInfo(default=True),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'dont-fragment': KeyInfo(default=False),
+ 'dscp': KeyInfo(default='inherit'),
+ 'ipsec-secret': KeyInfo(can_disable=True),
+ 'keepalive': KeyInfo(default='10s,10', can_disable=True),
+ 'local-address': KeyInfo(default='0.0.0.0'),
+ 'mtu': KeyInfo(default='auto'),
+ 'name': KeyInfo(),
+ 'remote-address': KeyInfo(required=True),
+ },
+ ),
+ ('interface', 'gre6'): APIData(
+ fully_understood=True,
+ primary_keys=('name',),
+ fields={
+ 'clamp-tcp-mss': KeyInfo(default=True),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'dscp': KeyInfo(default='inherit'),
+ 'ipsec-secret': KeyInfo(can_disable=True),
+ 'keepalive': KeyInfo(default='10s,10', can_disable=True),
+ 'local-address': KeyInfo(default='::'),
+ 'mtu': KeyInfo(default='auto'),
+ 'name': KeyInfo(),
+ 'remote-address': KeyInfo(required=True),
+ },
+ ),
+ ('interface', 'list'): APIData(
+ primary_keys=('name', ),
+ fully_understood=True,
+ fields={
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'exclude': KeyInfo(),
+ 'include': KeyInfo(),
+ 'name': KeyInfo(),
+ },
+ ),
+ ('interface', 'list', 'member'): APIData(
+ primary_keys=('list', 'interface', ),
+ fully_understood=True,
+ fields={
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'interface': KeyInfo(),
+ 'list': KeyInfo(),
+ 'disabled': KeyInfo(default=False),
+ },
+ ),
+ ('interface', 'lte', 'apn'): APIData(
+ unknown_mechanism=True,
+ # primary_keys=('default', ),
+ fields={
+ 'default': KeyInfo(),
+ 'add-default-route': KeyInfo(),
+ 'apn': KeyInfo(),
+ 'default-route-distance': KeyInfo(),
+ 'name': KeyInfo(),
+ 'use-peer-dns': KeyInfo(),
+ },
+ ),
+ ('interface', 'pppoe-client'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'ac-name': KeyInfo(default=''),
+ 'add-default-route': KeyInfo(default=False),
+ 'allow': KeyInfo(default='pap,chap,mschap1,mschap2'),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'default-route-distance': KeyInfo(default=1),
+ 'dial-on-demand': KeyInfo(default=False),
+ 'disabled': KeyInfo(default=True),
+ 'host-uniq': KeyInfo(can_disable=True),
+ 'interface': KeyInfo(required=True),
+ 'keepalive-timeout': KeyInfo(default=10),
+ 'max-mru': KeyInfo(default='auto'),
+ 'max-mtu': KeyInfo(default='auto'),
+ 'mrru': KeyInfo(default='disabled'),
+ 'name': KeyInfo(),
+ 'password': KeyInfo(default=''),
+ 'profile': KeyInfo(default='default'),
+ 'service-name': KeyInfo(default=''),
+ 'use-peer-dns': KeyInfo(default=False),
+ 'user': KeyInfo(default=''),
+ },
+ ),
+ ('interface', 'vlan'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'arp': KeyInfo(default='enabled'),
+ 'arp-timeout': KeyInfo(default='auto'),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'interface': KeyInfo(required=True),
+ 'loop-protect': KeyInfo(default='default'),
+ 'loop-protect-disable-time': KeyInfo(default='5m'),
+ 'loop-protect-send-interval': KeyInfo(default='5s'),
+ 'mtu': KeyInfo(default=1500),
+ 'name': KeyInfo(),
+ 'use-service-tag': KeyInfo(default=False),
+ 'vlan-id': KeyInfo(required=True),
+ },
+ ),
+ ('interface', 'vrrp'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'arp': KeyInfo(default='enabled'),
+ 'arp-timeout': KeyInfo(default='auto'),
+ 'authentication': KeyInfo(default='none'),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'group-master': KeyInfo(default=''),
+ 'interface': KeyInfo(required=True),
+ 'interval': KeyInfo(default='1s'),
+ 'mtu': KeyInfo(default=1500),
+ 'name': KeyInfo(),
+ 'on-backup': KeyInfo(default=''),
+ 'on-fail': KeyInfo(default=''),
+ 'on-master': KeyInfo(default=''),
+ 'password': KeyInfo(default=''),
+ 'preemption-mode': KeyInfo(default=True),
+ 'priority': KeyInfo(default=100),
+ 'remote-address': KeyInfo(),
+ 'sync-connection-tracking': KeyInfo(default=False),
+ 'v3-protocol': KeyInfo(default='ipv4'),
+ 'version': KeyInfo(default=3),
+ 'vrid': KeyInfo(default=1),
+ },
+ ),
+ ('interface', 'wireless', 'security-profiles'): APIData(
+ unknown_mechanism=True,
+ # primary_keys=('default', ),
+ fields={
+ 'default': KeyInfo(),
+ 'authentication-types': KeyInfo(),
+ 'disable-pmkid': KeyInfo(),
+ 'eap-methods': KeyInfo(),
+ 'group-ciphers': KeyInfo(),
+ 'group-key-update': KeyInfo(),
+ 'interim-update': KeyInfo(),
+ 'management-protection': KeyInfo(),
+ 'management-protection-key': KeyInfo(),
+ 'mode': KeyInfo(),
+ 'mschapv2-password': KeyInfo(),
+ 'mschapv2-username': KeyInfo(),
+ 'name': KeyInfo(),
+ 'radius-called-format': KeyInfo(),
+ 'radius-eap-accounting': KeyInfo(),
+ 'radius-mac-accounting': KeyInfo(),
+ 'radius-mac-authentication': KeyInfo(),
+ 'radius-mac-caching': KeyInfo(),
+ 'radius-mac-format': KeyInfo(),
+ 'radius-mac-mode': KeyInfo(),
+ 'static-algo-0': KeyInfo(),
+ 'static-algo-1': KeyInfo(),
+ 'static-algo-2': KeyInfo(),
+ 'static-algo-3': KeyInfo(),
+ 'static-key-0': KeyInfo(),
+ 'static-key-1': KeyInfo(),
+ 'static-key-2': KeyInfo(),
+ 'static-key-3': KeyInfo(),
+ 'static-sta-private-algo': KeyInfo(),
+ 'static-sta-private-key': KeyInfo(),
+ 'static-transmit-key': KeyInfo(),
+ 'supplicant-identity': KeyInfo(),
+ 'tls-certificate': KeyInfo(),
+ 'tls-mode': KeyInfo(),
+ 'unicast-ciphers': KeyInfo(),
+ 'wpa-pre-shared-key': KeyInfo(),
+ 'wpa2-pre-shared-key': KeyInfo(),
+ },
+ ),
+ ('ip', 'hotspot', 'profile'): APIData(
+ unknown_mechanism=True,
+ # primary_keys=('default', ),
+ fields={
+ 'default': KeyInfo(),
+ 'dns-name': KeyInfo(),
+ 'hotspot-address': KeyInfo(),
+ 'html-directory': KeyInfo(),
+ 'html-directory-override': KeyInfo(),
+ 'http-cookie-lifetime': KeyInfo(),
+ 'http-proxy': KeyInfo(),
+ 'login-by': KeyInfo(),
+ 'name': KeyInfo(),
+ 'rate-limit': KeyInfo(),
+ 'smtp-server': KeyInfo(),
+ 'split-user-domain': KeyInfo(),
+ 'use-radius': KeyInfo(),
+ },
+ ),
+ ('ip', 'hotspot', 'user', 'profile'): APIData(
+ unknown_mechanism=True,
+ # primary_keys=('default', ),
+ fields={
+ 'default': KeyInfo(),
+ 'add-mac-cookie': KeyInfo(),
+ 'address-list': KeyInfo(),
+ 'idle-timeout': KeyInfo(),
+ 'insert-queue-before': KeyInfo(can_disable=True),
+ 'keepalive-timeout': KeyInfo(),
+ 'mac-cookie-timeout': KeyInfo(),
+ 'name': KeyInfo(),
+ 'parent-queue': KeyInfo(can_disable=True),
+ 'queue-type': KeyInfo(can_disable=True),
+ 'shared-users': KeyInfo(),
+ 'status-autorefresh': KeyInfo(),
+ 'transparent-proxy': KeyInfo(),
+ },
+ ),
+ ('ip', 'ipsec', 'identity'): APIData(
+ fully_understood=True,
+ primary_keys=('peer', ),
+ fields={
+ 'auth-method': KeyInfo(default='pre-shared-key'),
+ 'certificate': KeyInfo(),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'eap-methods': KeyInfo(default='eap-tls'),
+ 'generate-policy': KeyInfo(default=False),
+ 'key': KeyInfo(),
+ 'match-by': KeyInfo(can_disable=True, remove_value='remote-id'),
+ 'mode-config': KeyInfo(can_disable=True, remove_value='none'),
+ 'my-id': KeyInfo(can_disable=True, remove_value='auto'),
+ 'notrack-chain': KeyInfo(can_disable=True, remove_value=''),
+ 'password': KeyInfo(),
+ 'peer': KeyInfo(),
+ 'policy-template-group': KeyInfo(can_disable=True, remove_value='default'),
+ 'remote-certificate': KeyInfo(),
+ 'remote-id': KeyInfo(can_disable=True, remove_value='auto'),
+ 'remote-key': KeyInfo(),
+ 'secret': KeyInfo(default=''),
+ 'username': KeyInfo(),
+ },
+ ),
+ ('ip', 'ipsec', 'mode-config'): APIData(
+ unknown_mechanism=True,
+ # primary_keys=('default', ),
+ fields={
+ 'default': KeyInfo(),
+ 'name': KeyInfo(),
+ 'responder': KeyInfo(),
+ 'use-responder-dns': KeyInfo(),
+ },
+ ),
+ ('ip', 'ipsec', 'peer'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'address': KeyInfo(can_disable=True, remove_value=''),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'exchange-mode': KeyInfo(default='main'),
+ 'local-address': KeyInfo(can_disable=True, remove_value='0.0.0.0'),
+ 'name': KeyInfo(),
+ 'passive': KeyInfo(can_disable=True, remove_value=False),
+ 'port': KeyInfo(can_disable=True, remove_value=500),
+ 'profile': KeyInfo(default='default'),
+ 'send-initial-contact': KeyInfo(default=True),
+ },
+ ),
+ ('ip', 'ipsec', 'policy', 'group'): APIData(
+ unknown_mechanism=True,
+ # primary_keys=('default', ),
+ fields={
+ 'default': KeyInfo(),
+ 'name': KeyInfo(),
+ },
+ ),
+ ('ip', 'ipsec', 'profile'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'dh-group': KeyInfo(default='modp2048,modp1024'),
+ 'dpd-interval': KeyInfo(default='2m'),
+ 'dpd-maximum-failures': KeyInfo(default=5),
+ 'enc-algorithm': KeyInfo(default='aes-128,3des'),
+ 'hash-algorithm': KeyInfo(default='sha1'),
+ 'lifebytes': KeyInfo(can_disable=True, remove_value=0),
+ 'lifetime': KeyInfo(default='1d'),
+ 'name': KeyInfo(),
+ 'nat-traversal': KeyInfo(default=True),
+ 'prf-algorithm': KeyInfo(can_disable=True, remove_value='auto'),
+ 'proposal-check': KeyInfo(default='obey'),
+ },
+ ),
+ ('ip', 'ipsec', 'proposal'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'auth-algorithms': KeyInfo(default='sha1'),
+ 'disabled': KeyInfo(default=False),
+ 'enc-algorithms': KeyInfo(default='aes-256-cbc,aes-192-cbc,aes-128-cbc'),
+ 'lifetime': KeyInfo(default='30m'),
+ 'name': KeyInfo(),
+ 'pfs-group': KeyInfo(default='modp1024'),
+ },
+ ),
+ ('ip', 'pool'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'name': KeyInfo(),
+ 'ranges': KeyInfo(),
+ },
+ ),
+ ('ip', 'route'): APIData(
+ fully_understood=True,
+ fields={
+ 'blackhole': KeyInfo(can_disable=True),
+ 'check-gateway': KeyInfo(can_disable=True),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'distance': KeyInfo(),
+ 'dst-address': KeyInfo(),
+ 'gateway': KeyInfo(),
+ 'pref-src': KeyInfo(),
+ 'routing-table': KeyInfo(default='main'),
+ 'route-tag': KeyInfo(can_disable=True),
+ 'routing-mark': KeyInfo(can_disable=True),
+ 'scope': KeyInfo(),
+ 'suppress-hw-offload': KeyInfo(default=False),
+ 'target-scope': KeyInfo(),
+ 'type': KeyInfo(can_disable=True, remove_value='unicast'),
+ 'vrf-interface': KeyInfo(can_disable=True),
+ },
+ ),
+ ('ip', 'route', 'vrf'): APIData(
+ fully_understood=True,
+ primary_keys=('routing-mark', ),
+ fields={
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'interfaces': KeyInfo(),
+ 'routing-mark': KeyInfo(),
+ },
+ ),
+ ('ip', 'dhcp-server'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'address-pool': KeyInfo(default='static-only'),
+ 'allow-dual-stack-queue': KeyInfo(can_disable=True, remove_value=True),
+ 'always-broadcast': KeyInfo(can_disable=True, remove_value=False),
+ 'authoritative': KeyInfo(default=True),
+ 'bootp-lease-time': KeyInfo(default='forever'),
+ 'bootp-support': KeyInfo(can_disable=True, remove_value='static'),
+ 'client-mac-limit': KeyInfo(can_disable=True, remove_value='unlimited'),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'conflict-detection': KeyInfo(can_disable=True, remove_value=True),
+ 'delay-threshold': KeyInfo(can_disable=True, remove_value='none'),
+ 'dhcp-option-set': KeyInfo(can_disable=True, remove_value='none'),
+ 'disabled': KeyInfo(default=False),
+ 'insert-queue-before': KeyInfo(can_disable=True, remove_value='first'),
+ 'interface': KeyInfo(required=True),
+ 'lease-script': KeyInfo(default=''),
+ 'lease-time': KeyInfo(default='10m'),
+ 'name': KeyInfo(),
+ 'parent-queue': KeyInfo(can_disable=True, remove_value='none'),
+ 'relay': KeyInfo(can_disable=True, remove_value='0.0.0.0'),
+ 'server-address': KeyInfo(can_disable=True, remove_value='0.0.0.0'),
+ 'use-framed-as-classless': KeyInfo(can_disable=True, remove_value=True),
+ 'use-radius': KeyInfo(default=False),
+ },
+ ),
+ ('routing', 'ospf', 'instance'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'domain-id': KeyInfo(can_disable=True),
+ 'domain-tag': KeyInfo(can_disable=True),
+ 'in-filter-chain': KeyInfo(can_disable=True),
+ 'mpls-te-address': KeyInfo(can_disable=True),
+ 'mpls-te-area': KeyInfo(can_disable=True),
+ 'name': KeyInfo(),
+ 'originate-default': KeyInfo(can_disable=True),
+ 'out-filter-chain': KeyInfo(can_disable=True),
+ 'out-filter-select': KeyInfo(can_disable=True),
+ 'redistribute': KeyInfo(can_disable=True),
+ 'router-id': KeyInfo(default='main'),
+ 'routing-table': KeyInfo(can_disable=True),
+ 'use-dn': KeyInfo(can_disable=True),
+ 'version': KeyInfo(default=2),
+ 'vrf': KeyInfo(default='main'),
+ },
+ ),
+ ('routing', 'ospf', 'area'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'area-id': KeyInfo(default='0.0.0.0'),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'default-cost': KeyInfo(can_disable=True),
+ 'disabled': KeyInfo(default=False),
+ 'instance': KeyInfo(required=True),
+ 'name': KeyInfo(),
+ 'no-summaries': KeyInfo(can_disable=True),
+ 'nssa-translator': KeyInfo(can_disable=True),
+ 'type': KeyInfo(default='default'),
+ },
+ ),
+ ('routing', 'ospf', 'area', 'range'): APIData(
+ fully_understood=True,
+ primary_keys=('area', 'prefix', ),
+ fields={
+ 'advertise': KeyInfo(default=True),
+ 'area': KeyInfo(),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'cost': KeyInfo(can_disable=True),
+ 'disabled': KeyInfo(default=False),
+ 'prefix': KeyInfo(),
+ },
+ ),
+ ('routing', 'ospf', 'interface-template'): APIData(
+ fully_understood=True,
+ fields={
+ 'area': KeyInfo(required=True),
+ 'auth': KeyInfo(can_disable=True),
+ 'auth-id': KeyInfo(can_disable=True),
+ 'auth-key': KeyInfo(can_disable=True),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'cost': KeyInfo(default=1),
+ 'dead-interval': KeyInfo(default='40s'),
+ 'disabled': KeyInfo(default=False),
+ 'hello-interval': KeyInfo(default='10s'),
+ 'instance-id': KeyInfo(default=0),
+ 'interfaces': KeyInfo(can_disable=True),
+ 'networks': KeyInfo(can_disable=True),
+ 'passive': KeyInfo(can_disable=True),
+ 'prefix-list': KeyInfo(can_disable=True),
+ 'priority': KeyInfo(default=128),
+ 'retransmit-interval': KeyInfo(default='5s'),
+ 'transmit-delay': KeyInfo(default='1s'),
+ 'type': KeyInfo(default='broadcast'),
+ 'vlink-neighbor-id': KeyInfo(can_disable=True),
+ 'vlink-transit-area': KeyInfo(can_disable=True),
+ },
+ ),
+ ('routing', 'ospf-v3', 'instance'): APIData(
+ unknown_mechanism=True,
+ # primary_keys=('default', ),
+ fields={
+ 'default': KeyInfo(),
+ 'disabled': KeyInfo(),
+ 'distribute-default': KeyInfo(),
+ 'metric-bgp': KeyInfo(),
+ 'metric-connected': KeyInfo(),
+ 'metric-default': KeyInfo(),
+ 'metric-other-ospf': KeyInfo(),
+ 'metric-rip': KeyInfo(),
+ 'metric-static': KeyInfo(),
+ 'name': KeyInfo(),
+ 'redistribute-bgp': KeyInfo(),
+ 'redistribute-connected': KeyInfo(),
+ 'redistribute-other-ospf': KeyInfo(),
+ 'redistribute-rip': KeyInfo(),
+ 'redistribute-static': KeyInfo(),
+ 'router-id': KeyInfo(),
+ },
+ ),
+ ('routing', 'ospf-v3', 'area'): APIData(
+ unknown_mechanism=True,
+ # primary_keys=('default', ),
+ fields={
+ 'default': KeyInfo(),
+ 'area-id': KeyInfo(),
+ 'disabled': KeyInfo(),
+ 'instance': KeyInfo(),
+ 'name': KeyInfo(),
+ 'type': KeyInfo(),
+ },
+ ),
+ ('routing', 'pimsm', 'instance'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'afi': KeyInfo(default='ipv4'),
+ 'bsm-forward-back': KeyInfo(),
+ 'crp-advertise-contained': KeyInfo(),
+ 'disabled': KeyInfo(default=False),
+ 'name': KeyInfo(),
+ 'rp-hash-mask-length': KeyInfo(),
+ 'rp-static-override': KeyInfo(default=False),
+ 'ssm-range': KeyInfo(),
+ 'switch-to-spt': KeyInfo(default=True),
+ 'switch-to-spt-bytes': KeyInfo(default=0),
+ 'switch-to-spt-interval': KeyInfo(),
+ 'vrf': KeyInfo(default="main"),
+ },
+ ),
+ ('routing', 'pimsm', 'interface-template'): APIData(
+ fully_understood=True,
+ fields={
+ 'disabled': KeyInfo(default=False),
+ 'hello-delay': KeyInfo(default='5s'),
+ 'hello-period': KeyInfo(default='30s'),
+ 'instance': KeyInfo(required=True),
+ 'interfaces': KeyInfo(can_disable=True),
+ 'join-prune-period': KeyInfo(default='1m'),
+ 'join-tracking-support': KeyInfo(default=True),
+ 'override-interval': KeyInfo(default='2s500ms'),
+ 'priority': KeyInfo(default=1),
+ 'propagation-delay': KeyInfo(default='500ms'),
+ 'source-addresses': KeyInfo(can_disable=True),
+ },
+ ),
+ ('snmp', 'community'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'addresses': KeyInfo(default='::/0'),
+ 'authentication-password': KeyInfo(default=''),
+ 'authentication-protocol': KeyInfo(default='MD5'),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'encryption-password': KeyInfo(default=''),
+ 'encryption-protocol': KeyInfo(default='DES'),
+ 'name': KeyInfo(required=True),
+ 'read-access': KeyInfo(default=True),
+ 'security': KeyInfo(default='none'),
+ 'write-access': KeyInfo(default=False),
+ },
+ ),
+ ('caps-man', 'aaa'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'called-format': KeyInfo(default='mac:ssid'),
+ 'interim-update': KeyInfo(default='disabled'),
+ 'mac-caching': KeyInfo(default='disabled'),
+ 'mac-format': KeyInfo(default='XX:XX:XX:XX:XX:XX'),
+ 'mac-mode': KeyInfo(default='as-username'),
+ },
+ ),
+ ('caps-man', 'access-list'): APIData(
+ fully_understood=True,
+ fields={
+ 'action': KeyInfo(can_disable=True),
+ 'allow-signal-out-of-range': KeyInfo(can_disable=True),
+ 'ap-tx-limit': KeyInfo(can_disable=True),
+ 'client-to-client-forwarding': KeyInfo(can_disable=True),
+ 'client-tx-limit': KeyInfo(can_disable=True),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(),
+ 'interface': KeyInfo(can_disable=True),
+ 'mac-address': KeyInfo(can_disable=True),
+ 'mac-address-mask': KeyInfo(can_disable=True),
+ 'private-passphrase': KeyInfo(can_disable=True),
+ 'radius-accounting': KeyInfo(can_disable=True),
+ 'signal-range': KeyInfo(can_disable=True),
+ 'ssid-regexp': KeyInfo(),
+ 'time': KeyInfo(can_disable=True),
+ 'vlan-id': KeyInfo(can_disable=True),
+ 'vlan-mode': KeyInfo(can_disable=True),
+ },
+ ),
+ ('caps-man', 'configuration'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'channel': KeyInfo(can_disable=True),
+ 'channel.band': KeyInfo(can_disable=True),
+ 'channel.control-channel-width': KeyInfo(can_disable=True),
+ 'channel.extension-channel': KeyInfo(can_disable=True),
+ 'channel.frequency': KeyInfo(can_disable=True),
+ 'channel.reselect-interval': KeyInfo(can_disable=True),
+ 'channel.save-selected': KeyInfo(can_disable=True),
+ 'channel.secondary-frequency': KeyInfo(can_disable=True),
+ 'channel.skip-dfs-channels': KeyInfo(can_disable=True),
+ 'channel.tx-power': KeyInfo(can_disable=True),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'country': KeyInfo(can_disable=True),
+ 'datapath': KeyInfo(can_disable=True),
+ 'datapath.arp': KeyInfo(),
+ 'datapath.bridge': KeyInfo(can_disable=True),
+ 'datapath.bridge-cost': KeyInfo(can_disable=True),
+ 'datapath.bridge-horizon': KeyInfo(can_disable=True),
+ 'datapath.client-to-client-forwarding': KeyInfo(can_disable=True),
+ 'datapath.interface-list': KeyInfo(can_disable=True),
+ 'datapath.l2mtu': KeyInfo(),
+ 'datapath.local-forwarding': KeyInfo(can_disable=True),
+ 'datapath.mtu': KeyInfo(),
+ 'datapath.openflow-switch': KeyInfo(can_disable=True),
+ 'datapath.vlan-id': KeyInfo(can_disable=True),
+ 'datapath.vlan-mode': KeyInfo(can_disable=True),
+ 'disconnect-timeout': KeyInfo(can_disable=True),
+ 'distance': KeyInfo(can_disable=True),
+ 'frame-lifetime': KeyInfo(can_disable=True),
+ 'guard-interval': KeyInfo(can_disable=True),
+ 'hide-ssid': KeyInfo(can_disable=True),
+ 'hw-protection-mode': KeyInfo(can_disable=True),
+ 'hw-retries': KeyInfo(can_disable=True),
+ 'installation': KeyInfo(can_disable=True),
+ 'keepalive-frames': KeyInfo(can_disable=True),
+ 'load-balancing-group': KeyInfo(can_disable=True),
+ 'max-sta-count': KeyInfo(can_disable=True),
+ 'mode': KeyInfo(can_disable=True),
+ 'multicast-helper': KeyInfo(can_disable=True),
+ 'name': KeyInfo(),
+ 'rates': KeyInfo(can_disable=True),
+ 'rates.basic': KeyInfo(can_disable=True),
+ 'rates.ht-basic-mcs': KeyInfo(can_disable=True),
+ 'rates.ht-supported-mcs': KeyInfo(can_disable=True),
+ 'rates.supported': KeyInfo(can_disable=True),
+ 'rates.vht-basic-mcs': KeyInfo(can_disable=True),
+ 'rates.vht-supported-mcs': KeyInfo(can_disable=True),
+ 'rx-chains': KeyInfo(can_disable=True),
+ 'security': KeyInfo(can_disable=True),
+ 'security.authentication-types': KeyInfo(can_disable=True),
+ 'security.disable-pmkid': KeyInfo(can_disable=True),
+ 'security.eap-methods': KeyInfo(can_disable=True),
+ 'security.eap-radius-accounting': KeyInfo(can_disable=True),
+ 'security.encryption': KeyInfo(can_disable=True),
+ 'security.group-encryption': KeyInfo(can_disable=True),
+ 'security.group-key-update': KeyInfo(),
+ 'security.passphrase': KeyInfo(can_disable=True),
+ 'security.tls-certificate': KeyInfo(),
+ 'security.tls-mode': KeyInfo(),
+ 'ssid': KeyInfo(can_disable=True),
+ 'tx-chains': KeyInfo(can_disable=True),
+ },
+ ),
+ ('caps-man', 'datapath'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'arp': KeyInfo(),
+ 'bridge': KeyInfo(can_disable=True),
+ 'bridge-cost': KeyInfo(can_disable=True),
+ 'bridge-horizon': KeyInfo(can_disable=True),
+ 'client-to-client-forwarding': KeyInfo(can_disable=True),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'interface-list': KeyInfo(can_disable=True),
+ 'l2mtu': KeyInfo(),
+ 'local-forwarding': KeyInfo(can_disable=True),
+ 'mtu': KeyInfo(),
+ 'name': KeyInfo(),
+ 'openflow-switch': KeyInfo(can_disable=True),
+ 'vlan-id': KeyInfo(can_disable=True),
+ 'vlan-mode': KeyInfo(can_disable=True),
+ },
+ ),
+ ('caps-man', 'manager', 'interface'): APIData(
+ unknown_mechanism=True,
+ # primary_keys=('default', ),
+ fields={
+ 'default': KeyInfo(),
+ 'disabled': KeyInfo(),
+ 'forbid': KeyInfo(),
+ 'interface': KeyInfo(),
+ },
+ ),
+ ('caps-man', 'provisioning'): APIData(
+ fully_understood=True,
+ fields={
+ 'action': KeyInfo(default='none'),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'common-name-regexp': KeyInfo(default=''),
+ 'disabled': KeyInfo(default=False),
+ 'hw-supported-modes': KeyInfo(default=''),
+ 'identity-regexp': KeyInfo(default=''),
+ 'ip-address-ranges': KeyInfo(default=''),
+ 'master-configuration': KeyInfo(default='*FFFFFFFF'),
+ 'name-format': KeyInfo(default='cap'),
+ 'name-prefix': KeyInfo(default=''),
+ 'radio-mac': KeyInfo(default='00:00:00:00:00:00'),
+ 'slave-configurations': KeyInfo(default=''),
+ },
+ ),
+ ('caps-man', 'security'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'authentication-types': KeyInfo(can_disable=True),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disable-pmkid': KeyInfo(can_disable=True),
+ 'eap-methods': KeyInfo(can_disable=True),
+ 'eap-radius-accounting': KeyInfo(can_disable=True),
+ 'encryption': KeyInfo(can_disable=True),
+ 'group-encryption': KeyInfo(can_disable=True),
+ 'group-key-update': KeyInfo(),
+ 'name': KeyInfo(),
+ 'passphrase': KeyInfo(can_disable=True),
+ 'tls-certificate': KeyInfo(),
+ 'tls-mode': KeyInfo(),
+ }
+ ),
+ ('certificate', 'settings'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'crl-download': KeyInfo(default=False),
+ 'crl-store': KeyInfo(default='ram'),
+ 'crl-use': KeyInfo(default=False),
+ },
+ ),
+ ('interface', 'bridge', 'port'): APIData(
+ fully_understood=True,
+ primary_keys=('interface', ),
+ fields={
+ 'auto-isolate': KeyInfo(default=False),
+ 'bpdu-guard': KeyInfo(default=False),
+ 'bridge': KeyInfo(required=True),
+ 'broadcast-flood': KeyInfo(default=True),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'edge': KeyInfo(default='auto'),
+ 'fast-leave': KeyInfo(default=False),
+ 'frame-types': KeyInfo(default='admit-all'),
+ 'horizon': KeyInfo(default='none'),
+ 'hw': KeyInfo(default=True),
+ 'ingress-filtering': KeyInfo(default=True),
+ 'interface': KeyInfo(),
+ 'internal-path-cost': KeyInfo(default=10),
+ 'learn': KeyInfo(default='auto'),
+ 'multicast-router': KeyInfo(default='temporary-query'),
+ 'path-cost': KeyInfo(default=10),
+ 'point-to-point': KeyInfo(default='auto'),
+ 'priority': KeyInfo(default='0x80'),
+ 'pvid': KeyInfo(default=1),
+ 'restricted-role': KeyInfo(default=False),
+ 'restricted-tcn': KeyInfo(default=False),
+ 'tag-stacking': KeyInfo(default=False),
+ 'trusted': KeyInfo(default=False),
+ 'unknown-multicast-flood': KeyInfo(default=True),
+ 'unknown-unicast-flood': KeyInfo(default=True),
+ },
+ ),
+ ('interface', 'bridge', 'mlag'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'bridge': KeyInfo(default='none'),
+ 'peer-port': KeyInfo(default='none'),
+ }
+ ),
+ ('interface', 'bridge', 'port-controller'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'bridge': KeyInfo(default='none'),
+ 'cascade-ports': KeyInfo(default=''),
+ 'switch': KeyInfo(default='none'),
+ },
+ ),
+ ('interface', 'bridge', 'port-extender'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'control-ports': KeyInfo(default=''),
+ 'excluded-ports': KeyInfo(default=''),
+ 'switch': KeyInfo(default='none'),
+ },
+ ),
+ ('interface', 'bridge', 'settings'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'allow-fast-path': KeyInfo(default=True),
+ 'use-ip-firewall': KeyInfo(default=False),
+ 'use-ip-firewall-for-pppoe': KeyInfo(default=False),
+ 'use-ip-firewall-for-vlan': KeyInfo(default=False),
+ },
+ ),
+ ('interface', 'bridge', 'vlan'): APIData(
+ fully_understood=True,
+ primary_keys=('bridge', 'vlan-ids', ),
+ fields={
+ 'bridge': KeyInfo(),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'tagged': KeyInfo(default=''),
+ 'untagged': KeyInfo(default=''),
+ 'vlan-ids': KeyInfo(),
+ },
+ ),
+ ('ip', 'firewall', 'connection', 'tracking'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'enabled': KeyInfo(default='auto'),
+ 'generic-timeout': KeyInfo(default='10m'),
+ 'icmp-timeout': KeyInfo(default='10s'),
+ 'loose-tcp-tracking': KeyInfo(default=True),
+ 'tcp-close-timeout': KeyInfo(default='10s'),
+ 'tcp-close-wait-timeout': KeyInfo(default='10s'),
+ 'tcp-established-timeout': KeyInfo(default='1d'),
+ 'tcp-fin-wait-timeout': KeyInfo(default='10s'),
+ 'tcp-last-ack-timeout': KeyInfo(default='10s'),
+ 'tcp-max-retrans-timeout': KeyInfo(default='5m'),
+ 'tcp-syn-received-timeout': KeyInfo(default='5s'),
+ 'tcp-syn-sent-timeout': KeyInfo(default='5s'),
+ 'tcp-time-wait-timeout': KeyInfo(default='10s'),
+ 'tcp-unacked-timeout': KeyInfo(default='5m'),
+ 'udp-stream-timeout': KeyInfo(default='3m'),
+ 'udp-timeout': KeyInfo(default='10s'),
+ },
+ ),
+ ('ip', 'neighbor', 'discovery-settings'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'discover-interface-list': KeyInfo(),
+ 'lldp-med-net-policy-vlan': KeyInfo(default='disabled'),
+ 'protocol': KeyInfo(default='cdp,lldp,mndp'),
+ },
+ ),
+ ('ip', 'settings'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'accept-redirects': KeyInfo(default=False),
+ 'accept-source-route': KeyInfo(default=False),
+ 'allow-fast-path': KeyInfo(default=True),
+ 'arp-timeout': KeyInfo(default='30s'),
+ 'icmp-rate-limit': KeyInfo(default=10),
+ 'icmp-rate-mask': KeyInfo(default='0x1818'),
+ 'ip-forward': KeyInfo(default=True),
+ 'max-neighbor-entries': KeyInfo(default=8192),
+ 'route-cache': KeyInfo(default=True),
+ 'rp-filter': KeyInfo(default=False),
+ 'secure-redirects': KeyInfo(default=True),
+ 'send-redirects': KeyInfo(default=True),
+ 'tcp-syncookies': KeyInfo(default=False),
+ },
+ ),
+ ('ipv6', 'address'): APIData(
+ fully_understood=True,
+ fields={
+ 'address': KeyInfo(),
+ 'advertise': KeyInfo(default=True),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'eui-64': KeyInfo(default=False),
+ 'from-pool': KeyInfo(),
+ 'interface': KeyInfo(required=True),
+ 'no-dad': KeyInfo(default=False),
+ },
+ ),
+ ('ipv6', 'settings'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'accept-redirects': KeyInfo(default='yes-if-forwarding-disabled'),
+ 'accept-router-advertisements': KeyInfo(default='yes-if-forwarding-disabled'),
+ 'disable-ipv6': KeyInfo(default=False),
+ 'forward': KeyInfo(default=True),
+ 'max-neighbor-entries': KeyInfo(default=8192),
+ },
+ ),
+ ('interface', 'detect-internet'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'detect-interface-list': KeyInfo(default='none'),
+ 'internet-interface-list': KeyInfo(default='none'),
+ 'lan-interface-list': KeyInfo(default='none'),
+ 'wan-interface-list': KeyInfo(default='none'),
+ },
+ ),
+ ('interface', 'l2tp-server', 'server'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'allow-fast-path': KeyInfo(default=False),
+ 'authentication': KeyInfo(default='pap,chap,mschap1,mschap2'),
+ 'caller-id-type': KeyInfo(default='ip-address'),
+ 'default-profile': KeyInfo(default='default-encryption'),
+ 'enabled': KeyInfo(default=False),
+ 'ipsec-secret': KeyInfo(default=''),
+ 'keepalive-timeout': KeyInfo(default=30),
+ 'max-mru': KeyInfo(default=1450),
+ 'max-mtu': KeyInfo(default=1450),
+ 'max-sessions': KeyInfo(default='unlimited'),
+ 'mrru': KeyInfo(default='disabled'),
+ 'one-session-per-host': KeyInfo(default=False),
+ 'use-ipsec': KeyInfo(default=False),
+ },
+ ),
+ ('interface', 'ovpn-server', 'server'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'auth': KeyInfo(),
+ 'cipher': KeyInfo(),
+ 'default-profile': KeyInfo(default='default'),
+ 'enabled': KeyInfo(default=False),
+ 'keepalive-timeout': KeyInfo(default=60),
+ 'mac-address': KeyInfo(),
+ 'max-mtu': KeyInfo(default=1500),
+ 'mode': KeyInfo(default='ip'),
+ 'netmask': KeyInfo(default=24),
+ 'port': KeyInfo(default=1194),
+ 'require-client-certificate': KeyInfo(default=False),
+ },
+ ),
+ ('interface', 'pptp-server', 'server'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'authentication': KeyInfo(default='mschap1,mschap2'),
+ 'default-profile': KeyInfo(default='default-encryption'),
+ 'enabled': KeyInfo(default=False),
+ 'keepalive-timeout': KeyInfo(default=30),
+ 'max-mru': KeyInfo(default=1450),
+ 'max-mtu': KeyInfo(default=1450),
+ 'mrru': KeyInfo(default='disabled'),
+ },
+ ),
+ ('interface', 'sstp-server', 'server'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'authentication': KeyInfo(default='pap,chap,mschap1,mschap2'),
+ 'certificate': KeyInfo(default='none'),
+ 'default-profile': KeyInfo(default='default'),
+ 'enabled': KeyInfo(default=False),
+ 'force-aes': KeyInfo(default=False),
+ 'keepalive-timeout': KeyInfo(default=60),
+ 'max-mru': KeyInfo(default=1500),
+ 'max-mtu': KeyInfo(default=1500),
+ 'mrru': KeyInfo(default='disabled'),
+ 'pfs': KeyInfo(default=False),
+ 'port': KeyInfo(default=443),
+ 'tls-version': KeyInfo(default='any'),
+ 'verify-client-certificate': KeyInfo(default='no'),
+ },
+ ),
+ ('interface', 'wireguard'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'listen-port': KeyInfo(),
+ 'mtu': KeyInfo(default=1420),
+ 'name': KeyInfo(),
+ 'private-key': KeyInfo(),
+ },
+ ),
+ ('interface', 'wireguard', 'peers'): APIData(
+ fully_understood=True,
+ primary_keys=('public-key', 'interface'),
+ fields={
+ 'allowed-address': KeyInfo(required=True),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'endpoint-address': KeyInfo(default=''),
+ 'endpoint-port': KeyInfo(default=0),
+ 'interface': KeyInfo(),
+ 'persistent-keepalive': KeyInfo(can_disable=True, remove_value=0),
+ 'preshared-key': KeyInfo(can_disable=True, remove_value=''),
+ 'public-key': KeyInfo(),
+ },
+ ),
+ ('interface', 'wireless', 'align'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'active-mode': KeyInfo(default=True),
+ 'audio-max': KeyInfo(default=-20),
+ 'audio-min': KeyInfo(default=-100),
+ 'audio-monitor': KeyInfo(default='00:00:00:00:00:00'),
+ 'filter-mac': KeyInfo(default='00:00:00:00:00:00'),
+ 'frame-size': KeyInfo(default=300),
+ 'frames-per-second': KeyInfo(default=25),
+ 'receive-all': KeyInfo(default=False),
+ 'ssid-all': KeyInfo(default=False),
+ },
+ ),
+ ('interface', 'wireless', 'cap'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'bridge': KeyInfo(default='none'),
+ 'caps-man-addresses': KeyInfo(default=''),
+ 'caps-man-certificate-common-names': KeyInfo(default=''),
+ 'caps-man-names': KeyInfo(default=''),
+ 'certificate': KeyInfo(default='none'),
+ 'discovery-interfaces': KeyInfo(default=''),
+ 'enabled': KeyInfo(default=False),
+ 'interfaces': KeyInfo(default=''),
+ 'lock-to-caps-man': KeyInfo(default=False),
+ 'static-virtual': KeyInfo(default=False),
+ },
+ ),
+ ('interface', 'wireless', 'sniffer'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'channel-time': KeyInfo(default='200ms'),
+ 'file-limit': KeyInfo(default=10),
+ 'file-name': KeyInfo(default=''),
+ 'memory-limit': KeyInfo(default=10),
+ 'multiple-channels': KeyInfo(default=False),
+ 'only-headers': KeyInfo(default=False),
+ 'receive-errors': KeyInfo(default=False),
+ 'streaming-enabled': KeyInfo(default=False),
+ 'streaming-max-rate': KeyInfo(default=0),
+ 'streaming-server': KeyInfo(default='0.0.0.0'),
+ },
+ ),
+ ('interface', 'wireless', 'snooper'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'channel-time': KeyInfo(default='200ms'),
+ 'multiple-channels': KeyInfo(default=True),
+ 'receive-errors': KeyInfo(default=False),
+ },
+ ),
+ ('ip', 'accounting'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'account-local-traffic': KeyInfo(default=False),
+ 'enabled': KeyInfo(default=False),
+ 'threshold': KeyInfo(default=256),
+ },
+ ),
+ ('ip', 'accounting', 'web-access'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'accessible-via-web': KeyInfo(default=False),
+ 'address': KeyInfo(default='0.0.0.0/0'),
+ },
+ ),
+ ('ip', 'address'): APIData(
+ fully_understood=True,
+ primary_keys=('address', 'interface', ),
+ fields={
+ 'address': KeyInfo(),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'interface': KeyInfo(),
+ 'network': KeyInfo(automatically_computed_from=('address', )),
+ },
+ ),
+ ('ip', 'arp'): APIData(
+ fully_understood=True,
+ fields={
+ 'address': KeyInfo(default='0.0.0.0'),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'interface': KeyInfo(required=True),
+ 'mac-address': KeyInfo(default='00:00:00:00:00:00'),
+ 'published': KeyInfo(default=False),
+ },
+ ),
+ ('ip', 'cloud'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'ddns-enabled': KeyInfo(default=False),
+ 'ddns-update-interval': KeyInfo(default='none'),
+ 'update-time': KeyInfo(default=True),
+ },
+ ),
+ ('ip', 'cloud', 'advanced'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'use-local-address': KeyInfo(default=False),
+ },
+ ),
+ ('ip', 'dhcp-client'): APIData(
+ fully_understood=True,
+ primary_keys=('interface', ),
+ fields={
+ 'add-default-route': KeyInfo(default=True),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'default-route-distance': KeyInfo(default=1),
+ 'dhcp-options': KeyInfo(default='hostname,clientid', can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'interface': KeyInfo(),
+ 'script': KeyInfo(can_disable=True),
+ 'use-peer-dns': KeyInfo(default=True),
+ 'use-peer-ntp': KeyInfo(default=True),
+ },
+ ),
+ ('ip', 'dhcp-server', 'config'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'accounting': KeyInfo(default=True),
+ 'interim-update': KeyInfo(default='0s'),
+ 'store-leases-disk': KeyInfo(default='5m'),
+ },
+ ),
+ ('ip', 'dhcp-server', 'lease'): APIData(
+ fully_understood=True,
+ primary_keys=('server', 'address', ),
+ fields={
+ 'address': KeyInfo(),
+ 'address-lists': KeyInfo(default=''),
+ 'always-broadcast': KeyInfo(),
+ 'client-id': KeyInfo(can_disable=True, remove_value=''),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'dhcp-option': KeyInfo(default=''),
+ 'disabled': KeyInfo(default=False),
+ 'insert-queue-before': KeyInfo(can_disable=True),
+ 'mac-address': KeyInfo(can_disable=True, remove_value=''),
+ 'server': KeyInfo(absent_value='all'),
+ },
+ ),
+ ('ip', 'dhcp-server', 'network'): APIData(
+ fully_understood=True,
+ primary_keys=('address', ),
+ fields={
+ 'address': KeyInfo(),
+ 'boot-file-name': KeyInfo(default=''),
+ 'caps-manager': KeyInfo(default=''),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'dhcp-option': KeyInfo(default=''),
+ 'dhcp-option-set': KeyInfo(default=''),
+ 'dns-none': KeyInfo(default=False),
+ 'dns-server': KeyInfo(default=''),
+ 'domain': KeyInfo(default=''),
+ 'gateway': KeyInfo(default=''),
+ 'netmask': KeyInfo(can_disable=True, remove_value=0),
+ 'next-server': KeyInfo(can_disable=True),
+ 'ntp-server': KeyInfo(default=''),
+ 'wins-server': KeyInfo(default=''),
+ },
+ ),
+ ('ip', 'dns'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'allow-remote-requests': KeyInfo(),
+ 'cache-max-ttl': KeyInfo(default='1w'),
+ 'cache-size': KeyInfo(default='2048KiB'),
+ 'max-concurrent-queries': KeyInfo(default=100),
+ 'max-concurrent-tcp-sessions': KeyInfo(default=20),
+ 'max-udp-packet-size': KeyInfo(default=4096),
+ 'query-server-timeout': KeyInfo(default='2s'),
+ 'query-total-timeout': KeyInfo(default='10s'),
+ 'servers': KeyInfo(default=''),
+ 'use-doh-server': KeyInfo(default=''),
+ 'verify-doh-cert': KeyInfo(default=False),
+ },
+ ),
+ ('ip', 'dns', 'static'): APIData(
+ fully_understood=True,
+ required_one_of=[['name', 'regexp']],
+ mutually_exclusive=[['name', 'regexp']],
+ fields={
+ 'address': KeyInfo(),
+ 'cname': KeyInfo(),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'forward-to': KeyInfo(),
+ 'mx-exchange': KeyInfo(),
+ 'mx-preference': KeyInfo(),
+ 'name': KeyInfo(),
+ 'ns': KeyInfo(),
+ 'regexp': KeyInfo(),
+ 'srv-port': KeyInfo(),
+ 'srv-priority': KeyInfo(),
+ 'srv-target': KeyInfo(),
+ 'srv-weight': KeyInfo(),
+ 'text': KeyInfo(),
+ 'ttl': KeyInfo(default='1d'),
+ 'type': KeyInfo(),
+ },
+ ),
+ ('ip', 'firewall', 'address-list'): APIData(
+ fully_understood=True,
+ primary_keys=('address', 'list', ),
+ fields={
+ 'address': KeyInfo(),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'list': KeyInfo(),
+ },
+ ),
+ ('ip', 'firewall', 'filter'): APIData(
+ fully_understood=True,
+ stratify_keys=('chain', ),
+ fields={
+ 'action': KeyInfo(),
+ 'chain': KeyInfo(),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'connection-bytes': KeyInfo(can_disable=True),
+ 'connection-limit': KeyInfo(can_disable=True),
+ 'connection-mark': KeyInfo(can_disable=True),
+ 'connection-nat-state': KeyInfo(can_disable=True),
+ 'connection-rate': KeyInfo(can_disable=True),
+ 'connection-state': KeyInfo(can_disable=True),
+ 'connection-type': KeyInfo(can_disable=True),
+ 'content': KeyInfo(can_disable=True),
+ 'disabled': KeyInfo(),
+ 'dscp': KeyInfo(can_disable=True),
+ 'dst-address': KeyInfo(can_disable=True),
+ 'dst-address-list': KeyInfo(can_disable=True),
+ 'dst-address-type': KeyInfo(can_disable=True),
+ 'dst-limit': KeyInfo(can_disable=True),
+ 'dst-port': KeyInfo(can_disable=True),
+ 'fragment': KeyInfo(can_disable=True),
+ 'hotspot': KeyInfo(can_disable=True),
+ 'hw-offload': KeyInfo(can_disable=True),
+ 'icmp-options': KeyInfo(can_disable=True),
+ 'in-bridge-port': KeyInfo(can_disable=True),
+ 'in-bridge-port-list': KeyInfo(can_disable=True),
+ 'in-interface': KeyInfo(can_disable=True),
+ 'in-interface-list': KeyInfo(can_disable=True),
+ 'ingress-priority': KeyInfo(can_disable=True),
+ 'ipsec-policy': KeyInfo(can_disable=True),
+ 'ipv4-options': KeyInfo(can_disable=True),
+ 'jump-target': KeyInfo(),
+ 'layer7-protocol': KeyInfo(can_disable=True),
+ 'limit': KeyInfo(can_disable=True),
+ 'log': KeyInfo(),
+ 'log-prefix': KeyInfo(),
+ 'nth': KeyInfo(can_disable=True),
+ 'out-bridge-port': KeyInfo(can_disable=True),
+ 'out-bridge-port-list': KeyInfo(can_disable=True),
+ 'out-interface': KeyInfo(can_disable=True),
+ 'out-interface-list': KeyInfo(can_disable=True),
+ 'p2p': KeyInfo(can_disable=True),
+ 'packet-mark': KeyInfo(can_disable=True),
+ 'packet-size': KeyInfo(can_disable=True),
+ 'per-connection-classifier': KeyInfo(can_disable=True),
+ 'port': KeyInfo(can_disable=True),
+ 'priority': KeyInfo(can_disable=True),
+ 'protocol': KeyInfo(can_disable=True),
+ 'psd': KeyInfo(can_disable=True),
+ 'random': KeyInfo(can_disable=True),
+ 'reject-with': KeyInfo(),
+ 'routing-mark': KeyInfo(can_disable=True),
+ 'routing-table': KeyInfo(can_disable=True),
+ 'src-address': KeyInfo(can_disable=True),
+ 'src-address-list': KeyInfo(can_disable=True),
+ 'src-address-type': KeyInfo(can_disable=True),
+ 'src-mac-address': KeyInfo(can_disable=True),
+ 'src-port': KeyInfo(can_disable=True),
+ 'tcp-flags': KeyInfo(can_disable=True),
+ 'tcp-mss': KeyInfo(can_disable=True),
+ 'time': KeyInfo(can_disable=True),
+ 'tls-host': KeyInfo(can_disable=True),
+ 'ttl': KeyInfo(can_disable=True),
+ },
+ ),
+ ('ip', 'firewall', 'mangle'): APIData(
+ fully_understood=True,
+ stratify_keys=('chain', ),
+ fields={
+ 'action': KeyInfo(),
+ 'chain': KeyInfo(),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'connection-bytes': KeyInfo(can_disable=True),
+ 'connection-limit': KeyInfo(can_disable=True),
+ 'connection-mark': KeyInfo(can_disable=True),
+ 'connection-nat-state': KeyInfo(can_disable=True),
+ 'connection-rate': KeyInfo(can_disable=True),
+ 'connection-state': KeyInfo(can_disable=True),
+ 'connection-type': KeyInfo(can_disable=True),
+ 'content': KeyInfo(can_disable=True),
+ 'disabled': KeyInfo(),
+ 'dscp': KeyInfo(can_disable=True),
+ 'dst-address': KeyInfo(can_disable=True),
+ 'dst-address-list': KeyInfo(can_disable=True),
+ 'dst-address-type': KeyInfo(can_disable=True),
+ 'dst-limit': KeyInfo(can_disable=True),
+ 'dst-port': KeyInfo(can_disable=True),
+ 'fragment': KeyInfo(can_disable=True),
+ 'hotspot': KeyInfo(can_disable=True),
+ 'icmp-options': KeyInfo(can_disable=True),
+ 'in-bridge-port': KeyInfo(can_disable=True),
+ 'in-bridge-port-list': KeyInfo(can_disable=True),
+ 'in-interface': KeyInfo(can_disable=True),
+ 'in-interface-list': KeyInfo(can_disable=True),
+ 'ingress-priority': KeyInfo(can_disable=True),
+ 'ipsec-policy': KeyInfo(can_disable=True),
+ 'ipv4-options': KeyInfo(can_disable=True),
+ 'jump-target': KeyInfo(),
+ 'layer7-protocol': KeyInfo(can_disable=True),
+ 'limit': KeyInfo(can_disable=True),
+ 'log': KeyInfo(),
+ 'log-prefix': KeyInfo(),
+ 'new-connection-mark': KeyInfo(can_disable=True),
+ 'new-dscp': KeyInfo(can_disable=True),
+ 'new-mss': KeyInfo(can_disable=True),
+ 'new-packet-mark': KeyInfo(can_disable=True),
+ 'new-priority': KeyInfo(can_disable=True),
+ 'new-routing-mark': KeyInfo(can_disable=True),
+ 'new-ttl': KeyInfo(can_disable=True),
+ 'nth': KeyInfo(can_disable=True),
+ 'out-bridge-port': KeyInfo(can_disable=True),
+ 'out-bridge-port-list': KeyInfo(can_disable=True),
+ 'out-interface': KeyInfo(can_disable=True),
+ 'out-interface-list': KeyInfo(can_disable=True),
+ 'p2p': KeyInfo(can_disable=True),
+ 'packet-mark': KeyInfo(can_disable=True),
+ 'packet-size': KeyInfo(can_disable=True),
+ 'passthrough': KeyInfo(can_disable=True),
+ 'per-connection-classifier': KeyInfo(can_disable=True),
+ 'port': KeyInfo(can_disable=True),
+ 'priority': KeyInfo(can_disable=True),
+ 'protocol': KeyInfo(can_disable=True),
+ 'psd': KeyInfo(can_disable=True),
+ 'random': KeyInfo(can_disable=True),
+ 'route-dst': KeyInfo(can_disable=True),
+ 'routing-mark': KeyInfo(can_disable=True),
+ 'routing-table': KeyInfo(can_disable=True),
+ 'sniff-id': KeyInfo(can_disable=True),
+ 'sniff-target': KeyInfo(can_disable=True),
+ 'sniff-target-port': KeyInfo(can_disable=True),
+ 'src-address': KeyInfo(can_disable=True),
+ 'src-address-list': KeyInfo(can_disable=True),
+ 'src-address-type': KeyInfo(can_disable=True),
+ 'src-mac-address': KeyInfo(can_disable=True),
+ 'src-port': KeyInfo(can_disable=True),
+ 'tcp-flags': KeyInfo(can_disable=True),
+ 'tcp-mss': KeyInfo(can_disable=True),
+ 'time': KeyInfo(can_disable=True),
+ 'tls-host': KeyInfo(can_disable=True),
+ 'ttl': KeyInfo(can_disable=True),
+ },
+ ),
+ ('ip', 'firewall', 'nat'): APIData(
+ fully_understood=True,
+ stratify_keys=('chain', ),
+ fields={
+ 'action': KeyInfo(),
+ 'address-list': KeyInfo(),
+ 'address-list-timeout': KeyInfo(),
+ 'chain': KeyInfo(),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'connection-bytes': KeyInfo(can_disable=True),
+ 'connection-limit': KeyInfo(can_disable=True),
+ 'connection-mark': KeyInfo(can_disable=True),
+ 'connection-rate': KeyInfo(can_disable=True),
+ 'connection-type': KeyInfo(can_disable=True),
+ 'content': KeyInfo(can_disable=True),
+ 'disabled': KeyInfo(),
+ 'dscp': KeyInfo(can_disable=True),
+ 'dst-address': KeyInfo(can_disable=True),
+ 'dst-address-list': KeyInfo(can_disable=True),
+ 'dst-address-type': KeyInfo(can_disable=True),
+ 'dst-limit': KeyInfo(can_disable=True),
+ 'dst-port': KeyInfo(can_disable=True),
+ 'fragment': KeyInfo(can_disable=True),
+ 'hotspot': KeyInfo(can_disable=True),
+ 'icmp-options': KeyInfo(can_disable=True),
+ 'in-bridge-port': KeyInfo(can_disable=True),
+ 'in-bridge-port-list': KeyInfo(can_disable=True),
+ 'in-interface': KeyInfo(can_disable=True),
+ 'in-interface-list': KeyInfo(can_disable=True),
+ 'ingress-priority': KeyInfo(can_disable=True),
+ 'ipsec-policy': KeyInfo(can_disable=True),
+ 'ipv4-options': KeyInfo(can_disable=True),
+ 'jump-target': KeyInfo(),
+ 'layer7-protocol': KeyInfo(can_disable=True),
+ 'limit': KeyInfo(can_disable=True),
+ 'log': KeyInfo(),
+ 'log-prefix': KeyInfo(),
+ 'nth': KeyInfo(can_disable=True),
+ 'out-bridge-port': KeyInfo(can_disable=True),
+ 'out-bridge-port-list': KeyInfo(can_disable=True),
+ 'out-interface': KeyInfo(can_disable=True),
+ 'out-interface-list': KeyInfo(can_disable=True),
+ 'packet-mark': KeyInfo(can_disable=True),
+ 'packet-size': KeyInfo(can_disable=True),
+ 'per-connection-classifier': KeyInfo(can_disable=True),
+ 'port': KeyInfo(can_disable=True),
+ 'priority': KeyInfo(can_disable=True),
+ 'protocol': KeyInfo(can_disable=True),
+ 'psd': KeyInfo(can_disable=True),
+ 'random': KeyInfo(can_disable=True),
+ 'realm': KeyInfo(can_disable=True),
+ 'routing-mark': KeyInfo(can_disable=True),
+ 'same-not-by-dst': KeyInfo(),
+ 'src-address': KeyInfo(can_disable=True),
+ 'src-address-list': KeyInfo(can_disable=True),
+ 'src-address-type': KeyInfo(can_disable=True),
+ 'src-mac-address': KeyInfo(can_disable=True),
+ 'src-port': KeyInfo(can_disable=True),
+ 'tcp-mss': KeyInfo(can_disable=True),
+ 'time': KeyInfo(can_disable=True),
+ 'tls-host': KeyInfo(can_disable=True),
+ 'to-addresses': KeyInfo(can_disable=True),
+ 'to-ports': KeyInfo(can_disable=True),
+ 'ttl': KeyInfo(can_disable=True),
+ },
+ ),
+ ('ip', 'firewall', 'raw'): APIData(
+ fully_understood=True,
+ stratify_keys=('chain',),
+ fields={
+ 'action': KeyInfo(),
+ 'address-list': KeyInfo(),
+ 'address-list-timeout': KeyInfo(),
+ 'chain': KeyInfo(),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'content': KeyInfo(can_disable=True),
+ 'disabled': KeyInfo(),
+ 'dscp': KeyInfo(can_disable=True),
+ 'dst-address': KeyInfo(can_disable=True),
+ 'dst-address-list': KeyInfo(can_disable=True),
+ 'dst-address-type': KeyInfo(can_disable=True),
+ 'dst-limit': KeyInfo(can_disable=True),
+ 'dst-port': KeyInfo(can_disable=True),
+ 'fragment': KeyInfo(can_disable=True),
+ 'hotspot': KeyInfo(can_disable=True),
+ 'icmp-options': KeyInfo(can_disable=True),
+ 'in-bridge-port': KeyInfo(can_disable=True),
+ 'in-bridge-port-list': KeyInfo(can_disable=True),
+ 'in-interface': KeyInfo(can_disable=True),
+ 'in-interface-list': KeyInfo(can_disable=True),
+ 'ingress-priority': KeyInfo(can_disable=True),
+ 'ipsec-policy': KeyInfo(can_disable=True),
+ 'ipv4-options': KeyInfo(can_disable=True),
+ 'jump-target': KeyInfo(),
+ 'limit': KeyInfo(can_disable=True),
+ 'log': KeyInfo(),
+ 'log-prefix': KeyInfo(),
+ 'nth': KeyInfo(can_disable=True),
+ 'out-bridge-port': KeyInfo(can_disable=True),
+ 'out-bridge-port-list': KeyInfo(can_disable=True),
+ 'out-interface': KeyInfo(can_disable=True),
+ 'out-interface-list': KeyInfo(can_disable=True),
+ 'packet-mark': KeyInfo(can_disable=True),
+ 'packet-size': KeyInfo(can_disable=True),
+ 'per-connection-classifier': KeyInfo(can_disable=True),
+ 'port': KeyInfo(can_disable=True),
+ 'priority': KeyInfo(can_disable=True),
+ 'protocol': KeyInfo(can_disable=True),
+ 'psd': KeyInfo(can_disable=True),
+ 'random': KeyInfo(can_disable=True),
+ 'src-address': KeyInfo(can_disable=True),
+ 'src-address-list': KeyInfo(can_disable=True),
+ 'src-address-type': KeyInfo(can_disable=True),
+ 'src-mac-address': KeyInfo(can_disable=True),
+ 'src-port': KeyInfo(can_disable=True),
+ 'tcp-flags': KeyInfo(can_disable=True),
+ 'tcp-mss': KeyInfo(can_disable=True),
+ 'time': KeyInfo(can_disable=True),
+ 'tls-host': KeyInfo(can_disable=True),
+ 'ttl': KeyInfo(can_disable=True),
+ },
+ ),
+ ('ip', 'hotspot', 'user'): APIData(
+ unknown_mechanism=True,
+ # primary_keys=('default', ),
+ fields={
+ 'default': KeyInfo(),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(),
+ 'name': KeyInfo(),
+ },
+ ),
+ ('ip', 'ipsec', 'settings'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'accounting': KeyInfo(default=True),
+ 'interim-update': KeyInfo(default='0s'),
+ 'xauth-use-radius': KeyInfo(default=False),
+ },
+ ),
+ ('ip', 'proxy'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'always-from-cache': KeyInfo(default=False),
+ 'anonymous': KeyInfo(default=False),
+ 'cache-administrator': KeyInfo(default='webmaster'),
+ 'cache-hit-dscp': KeyInfo(default=4),
+ 'cache-on-disk': KeyInfo(default=False),
+ 'cache-path': KeyInfo(default='web-proxy'),
+ 'enabled': KeyInfo(default=False),
+ 'max-cache-object-size': KeyInfo(default='2048KiB'),
+ 'max-cache-size': KeyInfo(default='unlimited'),
+ 'max-client-connections': KeyInfo(default=600),
+ 'max-fresh-time': KeyInfo(default='3d'),
+ 'max-server-connections': KeyInfo(default=600),
+ 'parent-proxy': KeyInfo(default='::'),
+ 'parent-proxy-port': KeyInfo(default=0),
+ 'port': KeyInfo(default=8080),
+ 'serialize-connections': KeyInfo(default=False),
+ 'src-address': KeyInfo(default='::'),
+ },
+ ),
+ ('ip', 'smb'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'allow-guests': KeyInfo(default=True),
+ 'comment': KeyInfo(default='MikrotikSMB'),
+ 'domain': KeyInfo(default='MSHOME'),
+ 'enabled': KeyInfo(default=False),
+ 'interfaces': KeyInfo(default='all'),
+ },
+ ),
+ ('ip', 'smb', 'shares'): APIData(
+ unknown_mechanism=True,
+ # primary_keys=('default', ),
+ fields={
+ 'default': KeyInfo(),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'directory': KeyInfo(),
+ 'disabled': KeyInfo(),
+ 'max-sessions': KeyInfo(),
+ 'name': KeyInfo(),
+ },
+ ),
+ ('ip', 'smb', 'users'): APIData(
+ unknown_mechanism=True,
+ # primary_keys=('default', ),
+ fields={
+ 'default': KeyInfo(),
+ 'disabled': KeyInfo(),
+ 'name': KeyInfo(),
+ 'password': KeyInfo(),
+ 'read-only': KeyInfo(),
+ },
+ ),
+ ('ip', 'socks'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'auth-method': KeyInfo(default='none'),
+ 'connection-idle-timeout': KeyInfo(default='2m'),
+ 'enabled': KeyInfo(default=False),
+ 'max-connections': KeyInfo(default=200),
+ 'port': KeyInfo(default=1080),
+ 'version': KeyInfo(default=4),
+ },
+ ),
+ ('ip', 'ssh'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'allow-none-crypto': KeyInfo(default=False),
+ 'always-allow-password-login': KeyInfo(default=False),
+ 'forwarding-enabled': KeyInfo(default=False),
+ 'host-key-size': KeyInfo(default=2048),
+ 'strong-crypto': KeyInfo(default=False),
+ },
+ ),
+ ('ip', 'tftp', 'settings'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'max-block-size': KeyInfo(default=4096),
+ },
+ ),
+ ('ip', 'traffic-flow'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'active-flow-timeout': KeyInfo(default='30m'),
+ 'cache-entries': KeyInfo(default='32k'),
+ 'enabled': KeyInfo(default=False),
+ 'inactive-flow-timeout': KeyInfo(default='15s'),
+ 'interfaces': KeyInfo(default='all'),
+ 'packet-sampling': KeyInfo(default=False),
+ 'sampling-interval': KeyInfo(default=0),
+ 'sampling-space': KeyInfo(default=0),
+ },
+ ),
+ ('ip', 'traffic-flow', 'ipfix'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'bytes': KeyInfo(default=True),
+ 'dst-address': KeyInfo(default=True),
+ 'dst-address-mask': KeyInfo(default=True),
+ 'dst-mac-address': KeyInfo(default=True),
+ 'dst-port': KeyInfo(default=True),
+ 'first-forwarded': KeyInfo(default=True),
+ 'gateway': KeyInfo(default=True),
+ 'icmp-code': KeyInfo(default=True),
+ 'icmp-type': KeyInfo(default=True),
+ 'igmp-type': KeyInfo(default=True),
+ 'in-interface': KeyInfo(default=True),
+ 'ip-header-length': KeyInfo(default=True),
+ 'ip-total-length': KeyInfo(default=True),
+ 'ipv6-flow-label': KeyInfo(default=True),
+ 'is-multicast': KeyInfo(default=True),
+ 'last-forwarded': KeyInfo(default=True),
+ 'nat-dst-address': KeyInfo(default=True),
+ 'nat-dst-port': KeyInfo(default=True),
+ 'nat-events': KeyInfo(default=False),
+ 'nat-src-address': KeyInfo(default=True),
+ 'nat-src-port': KeyInfo(default=True),
+ 'out-interface': KeyInfo(default=True),
+ 'packets': KeyInfo(default=True),
+ 'protocol': KeyInfo(default=True),
+ 'src-address': KeyInfo(default=True),
+ 'src-address-mask': KeyInfo(default=True),
+ 'src-mac-address': KeyInfo(default=True),
+ 'src-port': KeyInfo(default=True),
+ 'sys-init-time': KeyInfo(default=True),
+ 'tcp-ack-num': KeyInfo(default=True),
+ 'tcp-flags': KeyInfo(default=True),
+ 'tcp-seq-num': KeyInfo(default=True),
+ 'tcp-window-size': KeyInfo(default=True),
+ 'tos': KeyInfo(default=True),
+ 'ttl': KeyInfo(default=True),
+ 'udp-length': KeyInfo(default=True),
+ },
+ ),
+ ('ip', 'upnp'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'allow-disable-external-interface': KeyInfo(default=False),
+ 'enabled': KeyInfo(default=False),
+ 'show-dummy-rule': KeyInfo(default=True),
+ },
+ ),
+ ('ipv6', 'dhcp-client'): APIData(
+ fully_understood=True,
+ primary_keys=('interface', 'request'),
+ fields={
+ 'add-default-route': KeyInfo(default=False),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'default-route-distance': KeyInfo(default=1),
+ 'dhcp-options': KeyInfo(default=''),
+ 'disabled': KeyInfo(default=False),
+ 'interface': KeyInfo(),
+ 'pool-name': KeyInfo(required=True),
+ 'pool-prefix-length': KeyInfo(default=64),
+ 'prefix-hint': KeyInfo(default='::/0'),
+ 'request': KeyInfo(),
+ 'use-peer-dns': KeyInfo(default=True),
+ },
+ ),
+ ('ipv6', 'dhcp-server'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'address-pool': KeyInfo(required=True),
+ 'allow-dual-stack-queue': KeyInfo(can_disable=True, remove_value=True),
+ 'binding-script': KeyInfo(can_disable=True, remove_value=''),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'dhcp-option': KeyInfo(default=''),
+ 'disabled': KeyInfo(default=False),
+ 'insert-queue-before': KeyInfo(can_disable=True, remove_value='first'),
+ 'interface': KeyInfo(required=True),
+ 'lease-time': KeyInfo(default='3d'),
+ 'name': KeyInfo(),
+ 'parent-queue': KeyInfo(can_disable=True, remove_value='none'),
+ 'preference': KeyInfo(default=255),
+ 'rapid-commit': KeyInfo(default=True),
+ 'route-distance': KeyInfo(default=1),
+ 'use-radius': KeyInfo(default=False),
+ },
+ ),
+ ('ipv6', 'dhcp-server', 'option'): APIData(
+ fully_understood=True,
+ primary_keys=('name',),
+ fields={
+ 'code': KeyInfo(required=True),
+ 'name': KeyInfo(),
+ 'value': KeyInfo(default=''),
+ },
+ ),
+ ('ipv6', 'firewall', 'address-list'): APIData(
+ fully_understood=True,
+ primary_keys=('address', 'list', ),
+ fields={
+ 'address': KeyInfo(),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'list': KeyInfo(),
+ },
+ ),
+ ('ipv6', 'firewall', 'filter'): APIData(
+ fully_understood=True,
+ stratify_keys=('chain', ),
+ fields={
+ 'action': KeyInfo(),
+ 'chain': KeyInfo(),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'connection-bytes': KeyInfo(can_disable=True),
+ 'connection-limit': KeyInfo(can_disable=True),
+ 'connection-mark': KeyInfo(can_disable=True),
+ 'connection-rate': KeyInfo(can_disable=True),
+ 'connection-state': KeyInfo(can_disable=True),
+ 'connection-type': KeyInfo(can_disable=True),
+ 'content': KeyInfo(can_disable=True),
+ 'disabled': KeyInfo(),
+ 'dscp': KeyInfo(can_disable=True),
+ 'dst-address': KeyInfo(can_disable=True),
+ 'dst-address-list': KeyInfo(can_disable=True),
+ 'dst-address-type': KeyInfo(can_disable=True),
+ 'dst-limit': KeyInfo(can_disable=True),
+ 'dst-port': KeyInfo(can_disable=True),
+ 'headers': KeyInfo(can_disable=True),
+ 'hop-limit': KeyInfo(can_disable=True),
+ 'icmp-options': KeyInfo(can_disable=True),
+ 'in-bridge-port': KeyInfo(can_disable=True),
+ 'in-bridge-port-list': KeyInfo(can_disable=True),
+ 'in-interface': KeyInfo(can_disable=True),
+ 'in-interface-list': KeyInfo(can_disable=True),
+ 'ingress-priority': KeyInfo(can_disable=True),
+ 'ipsec-policy': KeyInfo(can_disable=True),
+ 'jump-target': KeyInfo(),
+ 'limit': KeyInfo(can_disable=True),
+ 'log': KeyInfo(),
+ 'log-prefix': KeyInfo(),
+ 'nth': KeyInfo(can_disable=True),
+ 'out-bridge-port': KeyInfo(can_disable=True),
+ 'out-bridge-port-list': KeyInfo(can_disable=True),
+ 'out-interface': KeyInfo(can_disable=True),
+ 'out-interface-list': KeyInfo(can_disable=True),
+ 'packet-mark': KeyInfo(can_disable=True),
+ 'packet-size': KeyInfo(can_disable=True),
+ 'per-connection-classifier': KeyInfo(can_disable=True),
+ 'port': KeyInfo(can_disable=True),
+ 'priority': KeyInfo(can_disable=True),
+ 'protocol': KeyInfo(can_disable=True),
+ 'random': KeyInfo(can_disable=True),
+ 'reject-with': KeyInfo(),
+ 'src-address': KeyInfo(can_disable=True),
+ 'src-address-list': KeyInfo(can_disable=True),
+ 'src-address-type': KeyInfo(can_disable=True),
+ 'src-mac-address': KeyInfo(can_disable=True),
+ 'src-port': KeyInfo(can_disable=True),
+ 'tcp-flags': KeyInfo(can_disable=True),
+ 'tcp-mss': KeyInfo(can_disable=True),
+ 'time': KeyInfo(can_disable=True),
+ },
+ ),
+ ('ipv6', 'firewall', 'mangle'): APIData(
+ fully_understood=True,
+ stratify_keys=('chain', ),
+ fields={
+ 'action': KeyInfo(),
+ 'address-list': KeyInfo(),
+ 'address-list-timeout': KeyInfo(),
+ 'chain': KeyInfo(),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'connection-bytes': KeyInfo(can_disable=True),
+ 'connection-limit': KeyInfo(can_disable=True),
+ 'connection-mark': KeyInfo(can_disable=True),
+ 'connection-rate': KeyInfo(can_disable=True),
+ 'connection-state': KeyInfo(can_disable=True),
+ 'connection-type': KeyInfo(can_disable=True),
+ 'content': KeyInfo(can_disable=True),
+ 'disabled': KeyInfo(),
+ 'dscp': KeyInfo(can_disable=True),
+ 'dst-address': KeyInfo(can_disable=True),
+ 'dst-address-list': KeyInfo(can_disable=True),
+ 'dst-address-type': KeyInfo(can_disable=True),
+ 'dst-limit': KeyInfo(can_disable=True),
+ 'dst-port': KeyInfo(can_disable=True),
+ 'dst-prefix': KeyInfo(),
+ 'headers': KeyInfo(can_disable=True),
+ 'hop-limit': KeyInfo(can_disable=True),
+ 'icmp-options': KeyInfo(can_disable=True),
+ 'in-bridge-port': KeyInfo(can_disable=True),
+ 'in-bridge-port-list': KeyInfo(can_disable=True),
+ 'in-interface': KeyInfo(can_disable=True),
+ 'in-interface-list': KeyInfo(can_disable=True),
+ 'ingress-priority': KeyInfo(can_disable=True),
+ 'ipsec-policy': KeyInfo(can_disable=True),
+ 'jump-target': KeyInfo(),
+ 'limit': KeyInfo(can_disable=True),
+ 'log': KeyInfo(),
+ 'log-prefix': KeyInfo(),
+ 'new-connection-mark': KeyInfo(),
+ 'new-dscp': KeyInfo(),
+ 'new-hop-limit': KeyInfo(),
+ 'new-mss': KeyInfo(),
+ 'new-packet-mark': KeyInfo(),
+ 'new-routing-mark': KeyInfo(),
+ 'nth': KeyInfo(can_disable=True),
+ 'out-bridge-port': KeyInfo(can_disable=True),
+ 'out-bridge-port-list': KeyInfo(can_disable=True),
+ 'out-interface': KeyInfo(can_disable=True),
+ 'out-interface-list': KeyInfo(can_disable=True),
+ 'packet-mark': KeyInfo(can_disable=True),
+ 'packet-size': KeyInfo(can_disable=True),
+ 'passthrough': KeyInfo(),
+ 'per-connection-classifier': KeyInfo(can_disable=True),
+ 'port': KeyInfo(can_disable=True),
+ 'priority': KeyInfo(can_disable=True),
+ 'protocol': KeyInfo(can_disable=True),
+ 'random': KeyInfo(can_disable=True),
+ 'routing-mark': KeyInfo(can_disable=True),
+ 'sniff-id': KeyInfo(),
+ 'sniff-target': KeyInfo(),
+ 'sniff-target-port': KeyInfo(),
+ 'src-address': KeyInfo(can_disable=True),
+ 'src-address-list': KeyInfo(can_disable=True),
+ 'src-address-type': KeyInfo(can_disable=True),
+ 'src-mac-address': KeyInfo(can_disable=True),
+ 'src-port': KeyInfo(can_disable=True),
+ 'src-prefix': KeyInfo(),
+ 'tcp-flags': KeyInfo(can_disable=True),
+ 'tcp-mss': KeyInfo(can_disable=True),
+ 'time': KeyInfo(can_disable=True),
+ 'tls-host': KeyInfo(can_disable=True),
+ }
+ ),
+ ('ipv6', 'firewall', 'raw'): APIData(
+ fully_understood=True,
+ stratify_keys=('chain',),
+ fields={
+ 'action': KeyInfo(),
+ 'address-list': KeyInfo(),
+ 'address-list-timeout': KeyInfo(),
+ 'chain': KeyInfo(),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'content': KeyInfo(can_disable=True),
+ 'disabled': KeyInfo(),
+ 'dscp': KeyInfo(can_disable=True),
+ 'dst-address': KeyInfo(can_disable=True),
+ 'dst-address-list': KeyInfo(can_disable=True),
+ 'dst-address-type': KeyInfo(can_disable=True),
+ 'dst-limit': KeyInfo(can_disable=True),
+ 'dst-port': KeyInfo(can_disable=True),
+ 'headers': KeyInfo(can_disable=True),
+ 'hop-limit': KeyInfo(can_disable=True),
+ 'icmp-options': KeyInfo(can_disable=True),
+ 'in-bridge-port': KeyInfo(can_disable=True),
+ 'in-bridge-port-list': KeyInfo(can_disable=True),
+ 'in-interface': KeyInfo(can_disable=True),
+ 'in-interface-list': KeyInfo(can_disable=True),
+ 'ingress-priority': KeyInfo(can_disable=True),
+ 'ipsec-policy': KeyInfo(can_disable=True),
+ 'jump-target': KeyInfo(),
+ 'limit': KeyInfo(can_disable=True),
+ 'log': KeyInfo(),
+ 'log-prefix': KeyInfo(),
+ 'nth': KeyInfo(can_disable=True),
+ 'out-bridge-port': KeyInfo(can_disable=True),
+ 'out-bridge-port-list': KeyInfo(can_disable=True),
+ 'out-interface': KeyInfo(can_disable=True),
+ 'out-interface-list': KeyInfo(can_disable=True),
+ 'packet-mark': KeyInfo(can_disable=True),
+ 'packet-size': KeyInfo(can_disable=True),
+ 'per-connection-classifier': KeyInfo(can_disable=True),
+ 'port': KeyInfo(can_disable=True),
+ 'priority': KeyInfo(can_disable=True),
+ 'protocol': KeyInfo(can_disable=True),
+ 'random': KeyInfo(can_disable=True),
+ 'src-address': KeyInfo(can_disable=True),
+ 'src-address-list': KeyInfo(can_disable=True),
+ 'src-address-type': KeyInfo(can_disable=True),
+ 'src-mac-address': KeyInfo(can_disable=True),
+ 'src-port': KeyInfo(can_disable=True),
+ 'tcp-flags': KeyInfo(can_disable=True),
+ 'tcp-mss': KeyInfo(can_disable=True),
+ 'time': KeyInfo(can_disable=True),
+ 'tls-host': KeyInfo(can_disable=True),
+ }
+ ),
+ ('ipv6', 'nd'): APIData(
+ fully_understood=True,
+ primary_keys=('interface', ),
+ fields={
+ 'advertise-dns': KeyInfo(default=True),
+ 'advertise-mac-address': KeyInfo(default=True),
+ 'disabled': KeyInfo(default=False),
+ 'dns': KeyInfo(default=''),
+ 'hop-limit': KeyInfo(default='unspecified'),
+ 'interface': KeyInfo(),
+ 'managed-address-configuration': KeyInfo(default=False),
+ 'mtu': KeyInfo(default='unspecified'),
+ 'other-configuration': KeyInfo(default=False),
+ 'ra-delay': KeyInfo(default='3s'),
+ 'ra-interval': KeyInfo(default='3m20s-10m'),
+ 'ra-lifetime': KeyInfo(default='30m'),
+ 'ra-preference': KeyInfo(default='medium'),
+ 'reachable-time': KeyInfo(default='unspecified'),
+ 'retransmit-interval': KeyInfo(default='unspecified'),
+ },
+ ),
+ ('ipv6', 'nd', 'prefix', 'default'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'autonomous': KeyInfo(default=True),
+ 'preferred-lifetime': KeyInfo(default='1w'),
+ 'valid-lifetime': KeyInfo(default='4w2d'),
+ },
+ ),
+ ('ipv6', 'route'): APIData(
+ fully_understood=True,
+ fields={
+ 'bgp-as-path': KeyInfo(can_disable=True),
+ 'bgp-atomic-aggregate': KeyInfo(can_disable=True),
+ 'bgp-communities': KeyInfo(can_disable=True),
+ 'bgp-local-pref': KeyInfo(can_disable=True),
+ 'bgp-med': KeyInfo(can_disable=True),
+ 'bgp-origin': KeyInfo(can_disable=True),
+ 'bgp-prepend': KeyInfo(can_disable=True),
+ 'type': KeyInfo(can_disable=True, remove_value='unicast'),
+ 'blackhole': KeyInfo(can_disable=True),
+ 'check-gateway': KeyInfo(can_disable=True),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(),
+ 'distance': KeyInfo(default=1),
+ 'dst-address': KeyInfo(),
+ 'gateway': KeyInfo(),
+ 'route-tag': KeyInfo(can_disable=True),
+ 'routing-table': KeyInfo(default='main'),
+ 'scope': KeyInfo(default=30),
+ 'target-scope': KeyInfo(default=10),
+ 'vrf-interface': KeyInfo(can_disable=True),
+ },
+ ),
+ ('mpls', ): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'allow-fast-path': KeyInfo(default=True),
+ 'dynamic-label-range': KeyInfo(default='16-1048575'),
+ 'propagate-ttl': KeyInfo(default=True),
+ },
+ ),
+ ('mpls', 'interface'): APIData(
+ unknown_mechanism=True,
+ # primary_keys=('default', ),
+ fields={
+ 'default': KeyInfo(),
+ 'disabled': KeyInfo(),
+ 'interface': KeyInfo(),
+ 'mpls-mtu': KeyInfo(),
+ },
+ ),
+ ('mpls', 'ldp'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'distribute-for-default-route': KeyInfo(default=False),
+ 'enabled': KeyInfo(default=False),
+ 'hop-limit': KeyInfo(default=255),
+ 'loop-detect': KeyInfo(default=False),
+ 'lsr-id': KeyInfo(default='0.0.0.0'),
+ 'path-vector-limit': KeyInfo(default=255),
+ 'transport-address': KeyInfo(default='0.0.0.0'),
+ 'use-explicit-null': KeyInfo(default=False),
+ },
+ ),
+ ('port', 'firmware'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'directory': KeyInfo(default='firmware'),
+ 'ignore-directip-modem': KeyInfo(default=False),
+ },
+ ),
+ ('ppp', 'aaa'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'accounting': KeyInfo(default=True),
+ 'interim-update': KeyInfo(default='0s'),
+ 'use-circuit-id-in-nas-port-id': KeyInfo(default=False),
+ 'use-radius': KeyInfo(default=False),
+ },
+ ),
+ ('radius', 'incoming'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'accept': KeyInfo(default=False),
+ 'port': KeyInfo(default=3799),
+ },
+ ),
+ ('routing', 'bfd', 'interface'): APIData(
+ unknown_mechanism=True,
+ # primary_keys=('default', ),
+ fields={
+ 'default': KeyInfo(),
+ 'disabled': KeyInfo(),
+ 'interface': KeyInfo(),
+ 'interval': KeyInfo(),
+ 'min-rx': KeyInfo(),
+ 'multiplier': KeyInfo(),
+ },
+ ),
+ ('routing', 'mme'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'bidirectional-timeout': KeyInfo(default=2),
+ 'gateway-class': KeyInfo(default='none'),
+ 'gateway-keepalive': KeyInfo(default='1m'),
+ 'gateway-selection': KeyInfo(default='no-gateway'),
+ 'origination-interval': KeyInfo(default='5s'),
+ 'preferred-gateway': KeyInfo(default='0.0.0.0'),
+ 'timeout': KeyInfo(default='1m'),
+ 'ttl': KeyInfo(default=50),
+ },
+ ),
+ ('routing', 'rip'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'distribute-default': KeyInfo(default='never'),
+ 'garbage-timer': KeyInfo(default='2m'),
+ 'metric-bgp': KeyInfo(default=1),
+ 'metric-connected': KeyInfo(default=1),
+ 'metric-default': KeyInfo(default=1),
+ 'metric-ospf': KeyInfo(default=1),
+ 'metric-static': KeyInfo(default=1),
+ 'redistribute-bgp': KeyInfo(default=False),
+ 'redistribute-connected': KeyInfo(default=False),
+ 'redistribute-ospf': KeyInfo(default=False),
+ 'redistribute-static': KeyInfo(default=False),
+ 'routing-table': KeyInfo(default='main'),
+ 'timeout-timer': KeyInfo(default='3m'),
+ 'update-timer': KeyInfo(default='30s'),
+ },
+ ),
+ ('routing', 'ripng'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'distribute-default': KeyInfo(default='never'),
+ 'garbage-timer': KeyInfo(default='2m'),
+ 'metric-bgp': KeyInfo(default=1),
+ 'metric-connected': KeyInfo(default=1),
+ 'metric-default': KeyInfo(default=1),
+ 'metric-ospf': KeyInfo(default=1),
+ 'metric-static': KeyInfo(default=1),
+ 'redistribute-bgp': KeyInfo(default=False),
+ 'redistribute-connected': KeyInfo(default=False),
+ 'redistribute-ospf': KeyInfo(default=False),
+ 'redistribute-static': KeyInfo(default=False),
+ 'timeout-timer': KeyInfo(default='3m'),
+ 'update-timer': KeyInfo(default='30s'),
+ },
+ ),
+ ('snmp', ): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'contact': KeyInfo(default=''),
+ 'enabled': KeyInfo(default=False),
+ 'engine-id': KeyInfo(default=''),
+ 'location': KeyInfo(default=''),
+ 'src-address': KeyInfo(default='::'),
+ 'trap-community': KeyInfo(default='public'),
+ 'trap-generators': KeyInfo(default='temp-exception'),
+ 'trap-target': KeyInfo(default=''),
+ 'trap-version': KeyInfo(default=1),
+ 'trap-interfaces': KeyInfo(default=''),
+ },
+ ),
+ ('system', 'clock'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'time-zone-autodetect': KeyInfo(default=True),
+ 'time-zone-name': KeyInfo(default='manual'),
+ },
+ ),
+ ('system', 'clock', 'manual'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'dst-delta': KeyInfo(default='00:00'),
+ 'dst-end': KeyInfo(default='jan/01/1970 00:00:00'),
+ 'dst-start': KeyInfo(default='jan/01/1970 00:00:00'),
+ 'time-zone': KeyInfo(default='+00:00'),
+ },
+ ),
+ ('system', 'identity'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'name': KeyInfo(default='Mikrotik'),
+ },
+ ),
+ ('system', 'leds', 'settings'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'all-leds-off': KeyInfo(default='never'),
+ },
+ ),
+ ('system', 'note'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'note': KeyInfo(default=''),
+ 'show-at-login': KeyInfo(default=True),
+ },
+ ),
+ ('system', 'ntp', 'client'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'enabled': KeyInfo(default=False),
+ 'primary-ntp': KeyInfo(default='0.0.0.0'),
+ 'secondary-ntp': KeyInfo(default='0.0.0.0'),
+ 'server-dns-names': KeyInfo(default=''),
+ 'servers': KeyInfo(default=''),
+ 'mode': KeyInfo(default='unicast'),
+ 'vrf': KeyInfo(default='main'),
+ },
+ ),
+ ('system', 'ntp', 'client', 'servers'): APIData(
+ primary_keys=('address', ),
+ fully_understood=True,
+ fields={
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'address': KeyInfo(),
+ 'auth-key': KeyInfo(default='none'),
+ 'iburst': KeyInfo(default=True),
+ 'max-poll': KeyInfo(default=10),
+ 'min-poll': KeyInfo(default=6),
+ },
+ ),
+ ('system', 'ntp', 'server'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'auth-key': KeyInfo(default='none'),
+ 'broadcast': KeyInfo(default=False),
+ 'broadcast-addresses': KeyInfo(default=''),
+ 'enabled': KeyInfo(default=False),
+ 'local-clock-stratum': KeyInfo(default=5),
+ 'manycast': KeyInfo(default=False),
+ 'multicast': KeyInfo(default=False),
+ 'use-local-clock': KeyInfo(default=False),
+ 'vrf': KeyInfo(default='main'),
+ },
+ ),
+ ('system', 'package', 'update'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'channel': KeyInfo(default='stable'),
+ },
+ ),
+ ('system', 'routerboard', 'settings'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'auto-upgrade': KeyInfo(default=False),
+ 'baud-rate': KeyInfo(default=115200),
+ 'boot-delay': KeyInfo(default='2s'),
+ 'boot-device': KeyInfo(default='nand-if-fail-then-ethernet'),
+ 'boot-protocol': KeyInfo(default='bootp'),
+ 'enable-jumper-reset': KeyInfo(default=True),
+ 'enter-setup-on': KeyInfo(default='any-key'),
+ 'force-backup-booter': KeyInfo(default=False),
+ 'protected-routerboot': KeyInfo(default='disabled'),
+ 'reformat-hold-button': KeyInfo(default='20s'),
+ 'reformat-hold-button-max': KeyInfo(default='10m'),
+ 'silent-boot': KeyInfo(default=False),
+ },
+ ),
+ ('system', 'upgrade', 'mirror'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'check-interval': KeyInfo(default='1d'),
+ 'enabled': KeyInfo(default=False),
+ 'primary-server': KeyInfo(default='0.0.0.0'),
+ 'secondary-server': KeyInfo(default='0.0.0.0'),
+ 'user': KeyInfo(default=''),
+ },
+ ),
+ ('system', 'ups'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'alarm-setting': KeyInfo(default='immediate'),
+ 'check-capabilities': KeyInfo(can_disable=True, remove_value=True),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=True),
+ 'min-runtime': KeyInfo(default='never'),
+ 'name': KeyInfo(),
+ 'offline-time': KeyInfo(default='0s'),
+ 'port': KeyInfo(required=True),
+ },
+ ),
+ ('system', 'watchdog'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'auto-send-supout': KeyInfo(default=False),
+ 'automatic-supout': KeyInfo(default=True),
+ 'ping-start-after-boot': KeyInfo(default='5m'),
+ 'ping-timeout': KeyInfo(default='1m'),
+ 'watch-address': KeyInfo(default='none'),
+ 'watchdog-timer': KeyInfo(default=True),
+ },
+ ),
+ ('tool', 'bandwidth-server'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'allocate-udp-ports-from': KeyInfo(default=2000),
+ 'authenticate': KeyInfo(default=True),
+ 'enabled': KeyInfo(default=True),
+ 'max-sessions': KeyInfo(default=100),
+ },
+ ),
+ ('tool', 'e-mail'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'address': KeyInfo(default='0.0.0.0'),
+ 'from': KeyInfo(default='<>'),
+ 'password': KeyInfo(default=''),
+ 'port': KeyInfo(default=25),
+ 'start-tls': KeyInfo(default=False),
+ 'tls': KeyInfo(default=False),
+ 'user': KeyInfo(default=''),
+ },
+ ),
+ ('tool', 'graphing'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'page-refresh': KeyInfo(default=300),
+ 'store-every': KeyInfo(default='5min'),
+ },
+ ),
+ ('tool', 'mac-server'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'allowed-interface-list': KeyInfo(),
+ },
+ ),
+ ('tool', 'mac-server', 'mac-winbox'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'allowed-interface-list': KeyInfo(),
+ },
+ ),
+ ('tool', 'mac-server', 'ping'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'enabled': KeyInfo(default=True),
+ },
+ ),
+ ('tool', 'romon'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'enabled': KeyInfo(default=False),
+ 'id': KeyInfo(default='00:00:00:00:00:00'),
+ 'secrets': KeyInfo(default=''),
+ },
+ ),
+ ('tool', 'romon', 'port'): APIData(
+ fields={
+ 'cost': KeyInfo(),
+ 'disabled': KeyInfo(),
+ 'forbid': KeyInfo(),
+ 'interface': KeyInfo(),
+ 'secrets': KeyInfo(),
+ },
+ ),
+ ('tool', 'sms'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'allowed-number': KeyInfo(default=''),
+ 'auto-erase': KeyInfo(default=False),
+ 'channel': KeyInfo(default=0),
+ 'port': KeyInfo(default='none'),
+ 'receive-enabled': KeyInfo(default=False),
+ 'secret': KeyInfo(default=''),
+ 'sim-pin': KeyInfo(default=''),
+ },
+ ),
+ ('tool', 'sniffer'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'file-limit': KeyInfo(default='1000KiB'),
+ 'file-name': KeyInfo(default=''),
+ 'filter-cpu': KeyInfo(default=''),
+ 'filter-direction': KeyInfo(default='any'),
+ 'filter-interface': KeyInfo(default=''),
+ 'filter-ip-address': KeyInfo(default=''),
+ 'filter-ip-protocol': KeyInfo(default=''),
+ 'filter-ipv6-address': KeyInfo(default=''),
+ 'filter-mac-address': KeyInfo(default=''),
+ 'filter-mac-protocol': KeyInfo(default=''),
+ 'filter-operator-between-entries': KeyInfo(default='or'),
+ 'filter-port': KeyInfo(default=''),
+ 'filter-size': KeyInfo(default=''),
+ 'filter-stream': KeyInfo(default=False),
+ 'memory-limit': KeyInfo(default='100KiB'),
+ 'memory-scroll': KeyInfo(default=True),
+ 'only-headers': KeyInfo(default=False),
+ 'streaming-enabled': KeyInfo(default=False),
+ 'streaming-server': KeyInfo(default='0.0.0.0:37008'),
+ },
+ ),
+ ('tool', 'traffic-generator'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'latency-distribution-max': KeyInfo(default='100us'),
+ 'measure-out-of-order': KeyInfo(default=True),
+ 'stats-samples-to-keep': KeyInfo(default=100),
+ 'test-id': KeyInfo(default=0),
+ },
+ ),
+ ('user', 'aaa'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'accounting': KeyInfo(default=True),
+ 'default-group': KeyInfo(default='read'),
+ 'exclude-groups': KeyInfo(default=''),
+ 'interim-update': KeyInfo(default='0s'),
+ 'use-radius': KeyInfo(default=False),
+ },
+ ),
+ ('queue', 'interface'): APIData(
+ primary_keys=('interface', ),
+ fully_understood=True,
+ fixed_entries=True,
+ fields={
+ 'interface': KeyInfo(required=True),
+ 'queue': KeyInfo(required=True),
+ },
+ ),
+ ('queue', 'tree'): APIData(
+ primary_keys=('name', ),
+ fully_understood=True,
+ fields={
+ 'bucket-size': KeyInfo(default='0.1'),
+ 'burst-limit': KeyInfo(default=0),
+ 'burst-threshold': KeyInfo(default=0),
+ 'burst-time': KeyInfo(default='0s'),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'limit-at': KeyInfo(default=0),
+ 'max-limit': KeyInfo(default=0),
+ 'name': KeyInfo(),
+ 'packet-mark': KeyInfo(default=''),
+ 'parent': KeyInfo(required=True),
+ 'priority': KeyInfo(default=8),
+ 'queue': KeyInfo(default='default-small'),
+ },
+ ),
+ ('interface', 'ethernet', 'switch'): APIData(
+ fixed_entries=True,
+ primary_keys=('name', ),
+ fully_understood=True,
+ fields={
+ 'cpu-flow-control': KeyInfo(default=True),
+ 'mirror-source': KeyInfo(default='none'),
+ 'mirror-target': KeyInfo(default='none'),
+ 'name': KeyInfo(),
+ },
+ ),
+ ('interface', 'ethernet', 'switch', 'port'): APIData(
+ fixed_entries=True,
+ primary_keys=('name', ),
+ fully_understood=True,
+ fields={
+ 'default-vlan-id': KeyInfo(),
+ 'name': KeyInfo(),
+ 'vlan-header': KeyInfo(default='leave-as-is'),
+ 'vlan-mode': KeyInfo(default='disabled'),
+ },
+ ),
+ ('ip', 'dhcp-client', 'option'): APIData(
+ fixed_entries=True,
+ primary_keys=('name', ),
+ fully_understood=True,
+ fields={
+ 'code': KeyInfo(),
+ 'name': KeyInfo(),
+ 'value': KeyInfo(),
+ },
+ ),
+ ('ppp', 'profile'): APIData(
+ has_identifier=True,
+ fields={
+ 'address-list': KeyInfo(),
+ 'bridge': KeyInfo(can_disable=True),
+ 'bridge-horizon': KeyInfo(can_disable=True),
+ 'bridge-learning': KeyInfo(),
+ 'bridge-path-cost': KeyInfo(can_disable=True),
+ 'bridge-port-priority': KeyInfo(can_disable=True),
+ 'change-tcp-mss': KeyInfo(),
+ 'dns-server': KeyInfo(can_disable=True),
+ 'idle-timeout': KeyInfo(can_disable=True),
+ 'incoming-filter': KeyInfo(can_disable=True),
+ 'insert-queue-before': KeyInfo(can_disable=True),
+ 'interface-list': KeyInfo(can_disable=True),
+ 'local-address': KeyInfo(can_disable=True),
+ 'name': KeyInfo(),
+ 'on-down': KeyInfo(),
+ 'on-up': KeyInfo(),
+ 'only-one': KeyInfo(),
+ 'outgoing-filter': KeyInfo(can_disable=True),
+ 'parent-queue': KeyInfo(can_disable=True),
+ 'queue-type': KeyInfo(can_disable=True),
+ 'rate-limit': KeyInfo(can_disable=True),
+ 'remote-address': KeyInfo(can_disable=True),
+ 'session-timeout': KeyInfo(can_disable=True),
+ 'use-compression': KeyInfo(),
+ 'use-encryption': KeyInfo(),
+ 'use-ipv6': KeyInfo(),
+ 'use-mpls': KeyInfo(),
+ 'use-upnp': KeyInfo(),
+ 'wins-server': KeyInfo(can_disable=True),
+ },
+ ),
+ ('queue', 'type'): APIData(
+ has_identifier=True,
+ fields={
+ 'kind': KeyInfo(),
+ 'mq-pfifo-limit': KeyInfo(),
+ 'name': KeyInfo(),
+ 'pcq-burst-rate': KeyInfo(),
+ 'pcq-burst-threshold': KeyInfo(),
+ 'pcq-burst-time': KeyInfo(),
+ 'pcq-classifier': KeyInfo(),
+ 'pcq-dst-address-mask': KeyInfo(),
+ 'pcq-dst-address6-mask': KeyInfo(),
+ 'pcq-limit': KeyInfo(),
+ 'pcq-rate': KeyInfo(),
+ 'pcq-src-address-mask': KeyInfo(),
+ 'pcq-src-address6-mask': KeyInfo(),
+ 'pcq-total-limit': KeyInfo(),
+ 'pfifo-limit': KeyInfo(),
+ 'red-avg-packet': KeyInfo(),
+ 'red-burst': KeyInfo(),
+ 'red-limit': KeyInfo(),
+ 'red-max-threshold': KeyInfo(),
+ 'red-min-threshold': KeyInfo(),
+ 'sfq-allot': KeyInfo(),
+ 'sfq-perturb': KeyInfo(),
+ },
+ ),
+ ('routing', 'bgp', 'instance'): APIData(
+ fixed_entries=True,
+ primary_keys=('name', ),
+ fully_understood=True,
+ fields={
+ 'as': KeyInfo(),
+ 'client-to-client-reflection': KeyInfo(),
+ 'cluster-id': KeyInfo(can_disable=True),
+ 'confederation': KeyInfo(can_disable=True),
+ 'disabled': KeyInfo(),
+ 'ignore-as-path-len': KeyInfo(),
+ 'name': KeyInfo(),
+ 'out-filter': KeyInfo(),
+ 'redistribute-connected': KeyInfo(),
+ 'redistribute-ospf': KeyInfo(),
+ 'redistribute-other-bgp': KeyInfo(),
+ 'redistribute-rip': KeyInfo(),
+ 'redistribute-static': KeyInfo(),
+ 'router-id': KeyInfo(),
+ 'routing-table': KeyInfo(),
+ },
+ ),
+ ('system', 'logging', 'action'): APIData(
+ fully_understood=True,
+ primary_keys=('name',),
+ fields={
+ 'bsd-syslog': KeyInfo(default=False),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disk-file-count': KeyInfo(default=2),
+ 'disk-file-name': KeyInfo(default='log'),
+ 'disk-lines-per-file': KeyInfo(default=1000),
+ 'disk-stop-on-full': KeyInfo(default=False),
+ 'email-start-tls': KeyInfo(default=False),
+ 'email-to': KeyInfo(default=''),
+ 'memory-lines': KeyInfo(default=1000),
+ 'memory-stop-on-full': KeyInfo(default=False),
+ 'name': KeyInfo(),
+ 'remember': KeyInfo(default=True),
+ 'remote': KeyInfo(default='0.0.0.0'),
+ 'remote-port': KeyInfo(default=514),
+ 'src-address': KeyInfo(default='0.0.0.0'),
+ 'syslog-facility': KeyInfo(default='daemon'),
+ 'syslog-severity': KeyInfo(default='auto'),
+ 'syslog-time-format': KeyInfo(default='bsd-syslog'),
+ 'target': KeyInfo(required=True),
+ },
+ ),
+ ('user', 'group'): APIData(
+ fixed_entries=True,
+ primary_keys=('name', ),
+ fully_understood=True,
+ fields={
+ 'name': KeyInfo(),
+ 'policy': KeyInfo(),
+ 'skin': KeyInfo(default='default'),
+ },
+ ),
+ ('caps-man', 'manager'): APIData(
+ single_value=True,
+ fully_understood=True,
+ fields={
+ 'ca-certificate': KeyInfo(default='none'),
+ 'certificate': KeyInfo(default='none'),
+ 'enabled': KeyInfo(default=False),
+ 'package-path': KeyInfo(default=''),
+ 'require-peer-certificate': KeyInfo(default=False),
+ 'upgrade-policy': KeyInfo(default='none'),
+ },
+ ),
+ ('ip', 'firewall', 'service-port'): APIData(
+ primary_keys=('name', ),
+ fully_understood=True,
+ fields={
+ 'disabled': KeyInfo(default=False),
+ 'name': KeyInfo(),
+ 'ports': KeyInfo(),
+ 'sip-direct-media': KeyInfo(),
+ 'sip-timeout': KeyInfo(),
+ },
+ ),
+ ('ip', 'firewall', 'layer7-protocol'): APIData(
+ primary_keys=('name', ),
+ fully_understood=True,
+ fields={
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'name': KeyInfo(),
+ 'regexp': KeyInfo(),
+ },
+ ),
+ ('ip', 'hotspot', 'service-port'): APIData(
+ fixed_entries=True,
+ primary_keys=('name', ),
+ fully_understood=True,
+ fields={
+ 'disabled': KeyInfo(default=False),
+ 'name': KeyInfo(),
+ 'ports': KeyInfo(),
+ },
+ ),
+ ('ip', 'ipsec', 'policy'): APIData(
+ fully_understood=True,
+ fields={
+ 'action': KeyInfo(default='encrypt'),
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'dst-address': KeyInfo(),
+ 'dst-port': KeyInfo(default='any'),
+ 'group': KeyInfo(can_disable=True, remove_value='default'),
+ 'ipsec-protocols': KeyInfo(default='esp'),
+ 'level': KeyInfo(default='require'),
+ 'peer': KeyInfo(),
+ 'proposal': KeyInfo(default='default'),
+ 'protocol': KeyInfo(default='all'),
+ 'src-address': KeyInfo(),
+ 'src-port': KeyInfo(default='any'),
+ 'template': KeyInfo(can_disable=True, remove_value=False),
+ # the tepmlate field can't really be changed once the item is created. This config captures the behavior best as it can
+ # i.e. tepmplate=yes is shown, tepmlate=no is hidden
+ 'tunnel': KeyInfo(default=False),
+ },
+ ),
+ ('ip', 'service'): APIData(
+ fixed_entries=True,
+ primary_keys=('name', ),
+ fully_understood=True,
+ fields={
+ 'address': KeyInfo(),
+ 'certificate': KeyInfo(),
+ 'disabled': KeyInfo(default=False),
+ 'name': KeyInfo(),
+ 'port': KeyInfo(),
+ 'tls-version': KeyInfo(),
+ },
+ ),
+ ('system', 'logging'): APIData(
+ fully_understood=True,
+ fields={
+ 'action': KeyInfo(default='memory'),
+ 'disabled': KeyInfo(default=False),
+ 'prefix': KeyInfo(default=''),
+ 'topics': KeyInfo(default=''),
+ },
+ ),
+ ('system', 'resource', 'irq'): APIData(
+ has_identifier=True,
+ fields={
+ 'cpu': KeyInfo(),
+ },
+ ),
+ ('system', 'scheduler'): APIData(
+ fully_understood=True,
+ primary_keys=('name', ),
+ fields={
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'disabled': KeyInfo(default=False),
+ 'interval': KeyInfo(default='0s'),
+ 'name': KeyInfo(),
+ 'on-event': KeyInfo(default=''),
+ 'policy': KeyInfo(default='ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon'),
+ 'start-date': KeyInfo(),
+ 'start-time': KeyInfo(),
+ },
+ ),
+ ('system', 'script'): APIData(
+ fully_understood=True,
+ primary_keys=('name',),
+ fields={
+ 'comment': KeyInfo(can_disable=True, remove_value=''),
+ 'dont-require-permissions': KeyInfo(default=False),
+ 'name': KeyInfo(),
+ 'owner': KeyInfo(),
+ 'policy': KeyInfo(default='ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon'),
+ 'source': KeyInfo(default=''),
+ },
+ ),
+}
diff --git a/ansible_collections/community/routeros/plugins/module_utils/_version.py b/ansible_collections/community/routeros/plugins/module_utils/_version.py
new file mode 100644
index 000000000..f7954074e
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/module_utils/_version.py
@@ -0,0 +1,345 @@
+# Vendored copy of distutils/version.py from CPython 3.9.5
+#
+# Implements multiple version numbering conventions for the
+# Python Module Distribution Utilities.
+#
+# Copyright (c) 2001-2022 Python Software Foundation. All rights reserved.
+# PSF License (see LICENSES/PSF-2.0.txt or https://opensource.org/licenses/Python-2.0)
+# SPDX-License-Identifier: PSF-2.0
+#
+
+"""Provides classes to represent module version numbers (one class for
+each style of version numbering). There are currently two such classes
+implemented: StrictVersion and LooseVersion.
+
+Every version number class implements the following interface:
+ * the 'parse' method takes a string and parses it to some internal
+ representation; if the string is an invalid version number,
+ 'parse' raises a ValueError exception
+ * the class constructor takes an optional string argument which,
+ if supplied, is passed to 'parse'
+ * __str__ reconstructs the string that was passed to 'parse' (or
+ an equivalent string -- ie. one that will generate an equivalent
+ version number instance)
+ * __repr__ generates Python code to recreate the version number instance
+ * _cmp compares the current instance with either another instance
+ of the same class or a string (which will be parsed to an instance
+ of the same class, thus must follow the same rules)
+"""
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import re
+
+try:
+ RE_FLAGS = re.VERBOSE | re.ASCII
+except AttributeError:
+ RE_FLAGS = re.VERBOSE
+
+
+class Version:
+ """Abstract base class for version numbering classes. Just provides
+ constructor (__init__) and reproducer (__repr__), because those
+ seem to be the same for all version numbering classes; and route
+ rich comparisons to _cmp.
+ """
+
+ def __init__(self, vstring=None):
+ if vstring:
+ self.parse(vstring)
+
+ def __repr__(self):
+ return "%s ('%s')" % (self.__class__.__name__, str(self))
+
+ def __eq__(self, other):
+ c = self._cmp(other)
+ if c is NotImplemented:
+ return c
+ return c == 0
+
+ def __lt__(self, other):
+ c = self._cmp(other)
+ if c is NotImplemented:
+ return c
+ return c < 0
+
+ def __le__(self, other):
+ c = self._cmp(other)
+ if c is NotImplemented:
+ return c
+ return c <= 0
+
+ def __gt__(self, other):
+ c = self._cmp(other)
+ if c is NotImplemented:
+ return c
+ return c > 0
+
+ def __ge__(self, other):
+ c = self._cmp(other)
+ if c is NotImplemented:
+ return c
+ return c >= 0
+
+
+# Interface for version-number classes -- must be implemented
+# by the following classes (the concrete ones -- Version should
+# be treated as an abstract class).
+# __init__ (string) - create and take same action as 'parse'
+# (string parameter is optional)
+# parse (string) - convert a string representation to whatever
+# internal representation is appropriate for
+# this style of version numbering
+# __str__ (self) - convert back to a string; should be very similar
+# (if not identical to) the string supplied to parse
+# __repr__ (self) - generate Python code to recreate
+# the instance
+# _cmp (self, other) - compare two version numbers ('other' may
+# be an unparsed version string, or another
+# instance of your version class)
+
+
+class StrictVersion(Version):
+ """Version numbering for anal retentives and software idealists.
+ Implements the standard interface for version number classes as
+ described above. A version number consists of two or three
+ dot-separated numeric components, with an optional "pre-release" tag
+ on the end. The pre-release tag consists of the letter 'a' or 'b'
+ followed by a number. If the numeric components of two version
+ numbers are equal, then one with a pre-release tag will always
+ be deemed earlier (lesser) than one without.
+
+ The following are valid version numbers (shown in the order that
+ would be obtained by sorting according to the supplied cmp function):
+
+ 0.4 0.4.0 (these two are equivalent)
+ 0.4.1
+ 0.5a1
+ 0.5b3
+ 0.5
+ 0.9.6
+ 1.0
+ 1.0.4a3
+ 1.0.4b1
+ 1.0.4
+
+ The following are examples of invalid version numbers:
+
+ 1
+ 2.7.2.2
+ 1.3.a4
+ 1.3pl1
+ 1.3c4
+
+ The rationale for this version numbering system will be explained
+ in the distutils documentation.
+ """
+
+ version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$',
+ RE_FLAGS)
+
+ def parse(self, vstring):
+ match = self.version_re.match(vstring)
+ if not match:
+ raise ValueError("invalid version number '%s'" % vstring)
+
+ (major, minor, patch, prerelease, prerelease_num) = \
+ match.group(1, 2, 4, 5, 6)
+
+ if patch:
+ self.version = tuple(map(int, [major, minor, patch]))
+ else:
+ self.version = tuple(map(int, [major, minor])) + (0,)
+
+ if prerelease:
+ self.prerelease = (prerelease[0], int(prerelease_num))
+ else:
+ self.prerelease = None
+
+ def __str__(self):
+ if self.version[2] == 0:
+ vstring = '.'.join(map(str, self.version[0:2]))
+ else:
+ vstring = '.'.join(map(str, self.version))
+
+ if self.prerelease:
+ vstring = vstring + self.prerelease[0] + str(self.prerelease[1])
+
+ return vstring
+
+ def _cmp(self, other):
+ if isinstance(other, str):
+ other = StrictVersion(other)
+ elif not isinstance(other, StrictVersion):
+ return NotImplemented
+
+ if self.version != other.version:
+ # numeric versions don't match
+ # prerelease stuff doesn't matter
+ if self.version < other.version:
+ return -1
+ else:
+ return 1
+
+ # have to compare prerelease
+ # case 1: neither has prerelease; they're equal
+ # case 2: self has prerelease, other doesn't; other is greater
+ # case 3: self doesn't have prerelease, other does: self is greater
+ # case 4: both have prerelease: must compare them!
+
+ if (not self.prerelease and not other.prerelease):
+ return 0
+ elif (self.prerelease and not other.prerelease):
+ return -1
+ elif (not self.prerelease and other.prerelease):
+ return 1
+ elif (self.prerelease and other.prerelease):
+ if self.prerelease == other.prerelease:
+ return 0
+ elif self.prerelease < other.prerelease:
+ return -1
+ else:
+ return 1
+ else:
+ raise AssertionError("never get here")
+
+# end class StrictVersion
+
+# The rules according to Greg Stein:
+# 1) a version number has 1 or more numbers separated by a period or by
+# sequences of letters. If only periods, then these are compared
+# left-to-right to determine an ordering.
+# 2) sequences of letters are part of the tuple for comparison and are
+# compared lexicographically
+# 3) recognize the numeric components may have leading zeroes
+#
+# The LooseVersion class below implements these rules: a version number
+# string is split up into a tuple of integer and string components, and
+# comparison is a simple tuple comparison. This means that version
+# numbers behave in a predictable and obvious way, but a way that might
+# not necessarily be how people *want* version numbers to behave. There
+# wouldn't be a problem if people could stick to purely numeric version
+# numbers: just split on period and compare the numbers as tuples.
+# However, people insist on putting letters into their version numbers;
+# the most common purpose seems to be:
+# - indicating a "pre-release" version
+# ('alpha', 'beta', 'a', 'b', 'pre', 'p')
+# - indicating a post-release patch ('p', 'pl', 'patch')
+# but of course this can't cover all version number schemes, and there's
+# no way to know what a programmer means without asking him.
+#
+# The problem is what to do with letters (and other non-numeric
+# characters) in a version number. The current implementation does the
+# obvious and predictable thing: keep them as strings and compare
+# lexically within a tuple comparison. This has the desired effect if
+# an appended letter sequence implies something "post-release":
+# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002".
+#
+# However, if letters in a version number imply a pre-release version,
+# the "obvious" thing isn't correct. Eg. you would expect that
+# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison
+# implemented here, this just isn't so.
+#
+# Two possible solutions come to mind. The first is to tie the
+# comparison algorithm to a particular set of semantic rules, as has
+# been done in the StrictVersion class above. This works great as long
+# as everyone can go along with bondage and discipline. Hopefully a
+# (large) subset of Python module programmers will agree that the
+# particular flavour of bondage and discipline provided by StrictVersion
+# provides enough benefit to be worth using, and will submit their
+# version numbering scheme to its domination. The free-thinking
+# anarchists in the lot will never give in, though, and something needs
+# to be done to accommodate them.
+#
+# Perhaps a "moderately strict" version class could be implemented that
+# lets almost anything slide (syntactically), and makes some heuristic
+# assumptions about non-digits in version number strings. This could
+# sink into special-case-hell, though; if I was as talented and
+# idiosyncratic as Larry Wall, I'd go ahead and implement a class that
+# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is
+# just as happy dealing with things like "2g6" and "1.13++". I don't
+# think I'm smart enough to do it right though.
+#
+# In any case, I've coded the test suite for this module (see
+# ../test/test_version.py) specifically to fail on things like comparing
+# "1.2a2" and "1.2". That's not because the *code* is doing anything
+# wrong, it's because the simple, obvious design doesn't match my
+# complicated, hairy expectations for real-world version numbers. It
+# would be a snap to fix the test suite to say, "Yep, LooseVersion does
+# the Right Thing" (ie. the code matches the conception). But I'd rather
+# have a conception that matches common notions about version numbers.
+
+
+class LooseVersion(Version):
+ """Version numbering for anarchists and software realists.
+ Implements the standard interface for version number classes as
+ described above. A version number consists of a series of numbers,
+ separated by either periods or strings of letters. When comparing
+ version numbers, the numeric components will be compared
+ numerically, and the alphabetic components lexically. The following
+ are all valid version numbers, in no particular order:
+
+ 1.5.1
+ 1.5.2b2
+ 161
+ 3.10a
+ 8.02
+ 3.4j
+ 1996.07.12
+ 3.2.pl0
+ 3.1.1.6
+ 2g6
+ 11g
+ 0.960923
+ 2.2beta29
+ 1.13++
+ 5.5.kw
+ 2.0b1pl0
+
+ In fact, there is no such thing as an invalid version number under
+ this scheme; the rules for comparison are simple and predictable,
+ but may not always give the results you want (for some definition
+ of "want").
+ """
+
+ component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE)
+
+ def __init__(self, vstring=None):
+ if vstring:
+ self.parse(vstring)
+
+ def parse(self, vstring):
+ # I've given up on thinking I can reconstruct the version string
+ # from the parsed tuple -- so I just store the string here for
+ # use by __str__
+ self.vstring = vstring
+ components = [x for x in self.component_re.split(vstring) if x and x != '.']
+ for i, obj in enumerate(components):
+ try:
+ components[i] = int(obj)
+ except ValueError:
+ pass
+
+ self.version = components
+
+ def __str__(self):
+ return self.vstring
+
+ def __repr__(self):
+ return "LooseVersion ('%s')" % str(self)
+
+ def _cmp(self, other):
+ if isinstance(other, str):
+ other = LooseVersion(other)
+ elif not isinstance(other, LooseVersion):
+ return NotImplemented
+
+ if self.version == other.version:
+ return 0
+ if self.version < other.version:
+ return -1
+ if self.version > other.version:
+ return 1
+
+# end class LooseVersion
diff --git a/ansible_collections/community/routeros/plugins/module_utils/api.py b/ansible_collections/community/routeros/plugins/module_utils/api.py
new file mode 100644
index 000000000..5c598f3eb
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/module_utils/api.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022, Felix Fontein (@felixfontein) <felix@fontein.de>
+# Copyright (c) 2020, Nikolay Dachev <nikolay@dachev.info>
+# 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
+
+
+from ansible.module_utils.basic import missing_required_lib
+from ansible.module_utils.common.text.converters import to_native
+
+import ssl
+import traceback
+
+LIB_IMP_ERR = None
+try:
+ from librouteros import connect
+ from librouteros.exceptions import LibRouterosError # noqa: F401, pylint: disable=unused-import
+ HAS_LIB = True
+except Exception as e:
+ HAS_LIB = False
+ LIB_IMP_ERR = traceback.format_exc()
+
+
+def check_has_library(module):
+ if not HAS_LIB:
+ module.fail_json(
+ msg=missing_required_lib('librouteros'),
+ exception=LIB_IMP_ERR,
+ )
+
+
+def api_argument_spec():
+ return dict(
+ username=dict(type='str', required=True),
+ password=dict(type='str', required=True, no_log=True),
+ hostname=dict(type='str', required=True),
+ port=dict(type='int'),
+ tls=dict(type='bool', default=False, aliases=['ssl']),
+ force_no_cert=dict(type='bool', default=False),
+ validate_certs=dict(type='bool', default=True),
+ validate_cert_hostname=dict(type='bool', default=False),
+ ca_path=dict(type='path'),
+ encoding=dict(type='str', default='ASCII'),
+ timeout=dict(type='int', default=10),
+ )
+
+
+def _ros_api_connect(module, username, password, host, port, use_tls, force_no_cert, validate_certs, validate_cert_hostname, ca_path, encoding, timeout):
+ '''Connect to RouterOS API.'''
+ if not port:
+ if use_tls:
+ port = 8729
+ else:
+ port = 8728
+ try:
+ params = dict(
+ username=username,
+ password=password,
+ host=host,
+ port=port,
+ encoding=encoding,
+ timeout=timeout,
+ )
+ if use_tls:
+ ctx = ssl.create_default_context(cafile=ca_path)
+ wrap_context = ctx.wrap_socket
+ if force_no_cert:
+ ctx.check_hostname = False
+ ctx.set_ciphers("ADH:@SECLEVEL=0")
+ elif not validate_certs:
+ ctx.check_hostname = False
+ ctx.verify_mode = ssl.CERT_NONE
+ elif not validate_cert_hostname:
+ ctx.check_hostname = False
+ else:
+ # Since librouteros doesn't pass server_hostname,
+ # we have to do this ourselves:
+ def wrap_context(*args, **kwargs):
+ kwargs.pop('server_hostname', None)
+ return ctx.wrap_socket(*args, server_hostname=host, **kwargs)
+ params['ssl_wrapper'] = wrap_context
+ api = connect(**params)
+ except Exception as e:
+ connection = {
+ 'username': username,
+ 'hostname': host,
+ 'port': port,
+ 'ssl': use_tls,
+ 'status': 'Error while connecting: %s' % to_native(e),
+ }
+ module.fail_json(msg=connection['status'], connection=connection)
+ return api
+
+
+def create_api(module):
+ return _ros_api_connect(
+ module,
+ module.params['username'],
+ module.params['password'],
+ module.params['hostname'],
+ module.params['port'],
+ module.params['tls'],
+ module.params['force_no_cert'],
+ module.params['validate_certs'],
+ module.params['validate_cert_hostname'],
+ module.params['ca_path'],
+ module.params['encoding'],
+ module.params['timeout'],
+ )
diff --git a/ansible_collections/community/routeros/plugins/module_utils/quoting.py b/ansible_collections/community/routeros/plugins/module_utils/quoting.py
new file mode 100644
index 000000000..4b7098971
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/module_utils/quoting.py
@@ -0,0 +1,207 @@
+# -*- 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 sys
+
+from ansible.module_utils.common.text.converters import to_native, to_bytes
+
+
+class ParseError(Exception):
+ pass
+
+
+ESCAPE_SEQUENCES = {
+ b'"': b'"',
+ b'\\': b'\\',
+ b'?': b'?',
+ b'$': b'$',
+ b'_': b' ',
+ b'a': b'\a',
+ b'b': b'\b',
+ b'f': b'\xFF',
+ b'n': b'\n',
+ b'r': b'\r',
+ b't': b'\t',
+ b'v': b'\v',
+}
+
+ESCAPE_SEQUENCE_REVERSED = dict([(v, k) for k, v in ESCAPE_SEQUENCES.items()])
+
+ESCAPE_DIGITS = b'0123456789ABCDEF'
+
+
+if sys.version_info[0] < 3:
+ _int_to_byte = chr
+else:
+ def _int_to_byte(value):
+ return bytes((value, ))
+
+
+def parse_argument_value(line, start_index=0, must_match_everything=True):
+ '''
+ Parse an argument value (quoted or not quoted) from ``line``.
+
+ Will start at offset ``start_index``. Returns pair ``(parsed_value,
+ end_index)``, where ``end_index`` is the first character after the
+ attribute.
+
+ If ``must_match_everything`` is ``True`` (default), will fail if
+ ``end_index < len(line)``.
+ '''
+ line = to_bytes(line)
+ length = len(line)
+ index = start_index
+ if index == length:
+ raise ParseError('Expected value, but found end of string')
+ quoted = False
+ if line[index:index + 1] == b'"':
+ quoted = True
+ index += 1
+ current = []
+ while index < length:
+ ch = line[index:index + 1]
+ index += 1
+ if not quoted and ch == b' ':
+ index -= 1
+ break
+ elif ch == b'"':
+ if quoted:
+ quoted = False
+ if line[index:index + 1] not in (b'', b' '):
+ raise ParseError('Ending \'"\' must be followed by space or end of string')
+ break
+ raise ParseError('\'"\' must not appear in an unquoted value')
+ elif ch == b'\\':
+ if not quoted:
+ raise ParseError('Escape sequences can only be used inside double quotes')
+ if index == length:
+ raise ParseError('\'\\\' must not be at the end of the line')
+ ch = line[index:index + 1]
+ index += 1
+ if ch in ESCAPE_SEQUENCES:
+ current.append(ESCAPE_SEQUENCES[ch])
+ else:
+ d1 = ESCAPE_DIGITS.find(ch)
+ if d1 < 0:
+ raise ParseError('Invalid escape sequence \'\\{0}\''.format(to_native(ch)))
+ if index == length:
+ raise ParseError('Hex escape sequence cut off at end of line')
+ ch2 = line[index:index + 1]
+ d2 = ESCAPE_DIGITS.find(ch2)
+ index += 1
+ if d2 < 0:
+ raise ParseError('Invalid hex escape sequence \'\\{0}\''.format(to_native(ch + ch2)))
+ current.append(_int_to_byte(d1 * 16 + d2))
+ else:
+ if not quoted and ch in (b"'", b'=', b'(', b')', b'$', b'[', b'{', b'`'):
+ raise ParseError('"{0}" can only be used inside double quotes'.format(to_native(ch)))
+ if ch == b'?':
+ raise ParseError('"{0}" can only be used in escaped form'.format(to_native(ch)))
+ current.append(ch)
+ if quoted:
+ raise ParseError('Unexpected end of string during escaped parameter')
+ if must_match_everything and index < length:
+ raise ParseError('Unexpected data at end of value')
+ return to_native(b''.join(current)), index
+
+
+def split_routeros_command(line):
+ line = to_bytes(line)
+ result = []
+ current = []
+ index = 0
+ length = len(line)
+ parsing_attribute_name = False
+ while index < length:
+ ch = line[index:index + 1]
+ index += 1
+ if ch == b' ':
+ if parsing_attribute_name:
+ parsing_attribute_name = False
+ result.append(b''.join(current))
+ current = []
+ elif ch == b'=' and parsing_attribute_name:
+ current.append(ch)
+ value, index = parse_argument_value(line, start_index=index, must_match_everything=False)
+ current.append(to_bytes(value))
+ parsing_attribute_name = False
+ result.append(b''.join(current))
+ current = []
+ elif ch in (b'"', b'\\', b"'", b'=', b'(', b')', b'$', b'[', b'{', b'`', b'?'):
+ raise ParseError('Found unexpected "{0}"'.format(to_native(ch)))
+ else:
+ current.append(ch)
+ parsing_attribute_name = True
+ if parsing_attribute_name and current:
+ result.append(b''.join(current))
+ return [to_native(part) for part in result]
+
+
+def quote_routeros_argument_value(argument):
+ argument = to_bytes(argument)
+ result = []
+ quote = False
+ length = len(argument)
+ index = 0
+ while index < length:
+ letter = argument[index:index + 1]
+ index += 1
+ if letter in ESCAPE_SEQUENCE_REVERSED:
+ result.append(b'\\%s' % ESCAPE_SEQUENCE_REVERSED[letter])
+ quote = True
+ continue
+ elif ord(letter) < 32:
+ v = ord(letter)
+ v1 = v % 16
+ v2 = v // 16
+ result.append(b'\\%s%s' % (ESCAPE_DIGITS[v2:v2 + 1], ESCAPE_DIGITS[v1:v1 + 1]))
+ quote = True
+ continue
+ elif letter in (b' ', b'=', b';', b"'"):
+ quote = True
+ result.append(letter)
+ argument = to_native(b''.join(result))
+ if quote or not argument:
+ argument = '"%s"' % argument
+ return argument
+
+
+def quote_routeros_argument(argument):
+ def check_attribute(attribute):
+ if ' ' in attribute:
+ raise ParseError('Attribute names must not contain spaces')
+ return attribute
+
+ if '=' not in argument:
+ check_attribute(argument)
+ return argument
+
+ attribute, value = argument.split('=', 1)
+ check_attribute(attribute)
+ value = quote_routeros_argument_value(value)
+ return '%s=%s' % (attribute, value)
+
+
+def join_routeros_command(arguments):
+ return ' '.join([quote_routeros_argument(argument) for argument in arguments])
+
+
+def convert_list_to_dictionary(string_list, require_assignment=True, skip_empty_values=False):
+ dictionary = {}
+ for p in string_list:
+ if '=' not in p:
+ if require_assignment:
+ raise ParseError("missing '=' after '%s'" % p)
+ dictionary[p] = None
+ continue
+ p = p.split('=', 1)
+ if not skip_empty_values or p[1]:
+ dictionary[p[0]] = p[1]
+ return dictionary
diff --git a/ansible_collections/community/routeros/plugins/module_utils/routeros.py b/ansible_collections/community/routeros/plugins/module_utils/routeros.py
new file mode 100644
index 000000000..c2bd09c75
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/module_utils/routeros.py
@@ -0,0 +1,153 @@
+# Copyright (c) 2016 Red Hat Inc.
+# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause)
+# SPDX-License-Identifier: BSD-2-Clause
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import json
+from ansible.module_utils.common.text.converters import to_native
+from ansible.module_utils.basic import env_fallback
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list, ComplexList
+from ansible_collections.community.routeros.plugins.module_utils.version import LooseVersion
+from ansible.module_utils.connection import Connection, ConnectionError
+
+_DEVICE_CONFIGS = {}
+
+routeros_provider_spec = {
+ 'host': dict(),
+ 'port': dict(type='int'),
+ 'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
+ 'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), no_log=True),
+ 'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
+ 'timeout': dict(type='int')
+}
+routeros_argument_spec = {}
+
+
+def get_provider_argspec():
+ return routeros_provider_spec
+
+
+def get_connection(module):
+ if hasattr(module, '_routeros_connection'):
+ return module._routeros_connection
+
+ capabilities = get_capabilities(module)
+ network_api = capabilities.get('network_api')
+ if network_api == 'cliconf':
+ module._routeros_connection = Connection(module._socket_path)
+ else:
+ module.fail_json(msg='Invalid connection type %s' % network_api)
+
+ return module._routeros_connection
+
+
+def get_capabilities(module):
+ if hasattr(module, '_routeros_capabilities'):
+ return module._routeros_capabilities
+
+ try:
+ capabilities = Connection(module._socket_path).get_capabilities()
+ module._routeros_capabilities = json.loads(capabilities)
+ return module._routeros_capabilities
+ except ConnectionError as exc:
+ module.fail_json(msg=to_native(exc, errors='surrogate_then_replace'))
+
+
+def get_defaults_flag(module):
+ connection = get_connection(module)
+
+ try:
+ out = connection.get('/system default-configuration print')
+ except ConnectionError as exc:
+ module.fail_json(msg=to_native(exc, errors='surrogate_then_replace'))
+
+ out = to_native(out, errors='surrogate_then_replace')
+
+ commands = set()
+ for line in out.splitlines():
+ if line.strip():
+ commands.add(line.strip().split()[0])
+
+ if 'all' in commands:
+ return ['all']
+ else:
+ return ['full']
+
+
+def get_config(module, flags=None):
+ flag_str = ' '.join(to_list(flags))
+
+ try:
+ return _DEVICE_CONFIGS[flag_str]
+ except KeyError:
+ connection = get_connection(module)
+
+ try:
+ out = connection.get_config(flags=flags)
+ except ConnectionError as exc:
+ module.fail_json(msg=to_native(exc, errors='surrogate_then_replace'))
+
+ cfg = to_native(out, errors='surrogate_then_replace').strip()
+ _DEVICE_CONFIGS[flag_str] = cfg
+ return cfg
+
+
+def to_commands(module, commands):
+ spec = {
+ 'command': dict(key=True),
+ 'prompt': dict(),
+ 'answer': dict()
+ }
+ transform = ComplexList(spec, module)
+ return transform(commands)
+
+
+def should_add_leading_space(module):
+ """Determines whether adding a leading space to the command is needed
+ to workaround prompt bug in 6.49 <= ROS < 7.2"""
+ capabilities = get_capabilities(module)
+ network_os_version = capabilities.get('device_info', {}).get('network_os_version')
+ if network_os_version is None:
+ return False
+ return LooseVersion('6.49') <= LooseVersion(network_os_version) < LooseVersion('7.2')
+
+
+def run_commands(module, commands, check_rc=True):
+ responses = list()
+ connection = get_connection(module)
+
+ for cmd in to_list(commands):
+ if isinstance(cmd, dict):
+ command = cmd['command']
+ prompt = cmd['prompt']
+ answer = cmd['answer']
+ else:
+ command = cmd
+ prompt = None
+ answer = None
+
+ if should_add_leading_space(module):
+ command = " " + command
+
+ try:
+ out = connection.get(command, prompt, answer)
+ except ConnectionError as exc:
+ module.fail_json(msg=to_native(exc, errors='surrogate_then_replace'))
+
+ try:
+ out = to_native(out, errors='surrogate_or_strict')
+ except UnicodeError:
+ module.fail_json(
+ msg=u'Failed to decode output from %s: %s' % (cmd, to_native(out)))
+
+ responses.append(out)
+
+ return responses
+
+
+def load_config(module, commands):
+ connection = get_connection(module)
+
+ out = connection.edit_config(commands)
diff --git a/ansible_collections/community/routeros/plugins/module_utils/version.py b/ansible_collections/community/routeros/plugins/module_utils/version.py
new file mode 100644
index 000000000..dc01ffe8f
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/module_utils/version.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021, 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
+
+"""Provide version object to compare version numbers."""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+# Once we drop support for Ansible 2.9, ansible-base 2.10, and ansible-core 2.11, we can
+# remove the _version.py file, and replace the following import by
+#
+# from ansible.module_utils.compat.version import LooseVersion
+
+from ._version import LooseVersion # noqa: F401, pylint: disable=unused-import
diff --git a/ansible_collections/community/routeros/plugins/modules/api.py b/ansible_collections/community/routeros/plugins/modules/api.py
new file mode 100644
index 000000000..f9c619fc1
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/modules/api.py
@@ -0,0 +1,577 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020, Nikolay Dachev <nikolay@dachev.info>
+# GNU General Public License v3.0+ 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
+
+DOCUMENTATION = '''
+---
+module: api
+author: "Nikolay Dachev (@NikolayDachev)"
+short_description: Ansible module for RouterOS API
+description:
+ - Ansible module for RouterOS API with the Python C(librouteros) library.
+ - This module can add, remove, update, query and execute arbitrary command in RouterOS via API.
+notes:
+ - I(add), I(remove), I(update), I(cmd) and I(query) are mutually exclusive.
+ - Use the M(community.routeros.api_modify) and M(community.routeros.api_find_and_modify) modules
+ for more specific modifications, and the M(community.routeros.api_info) module for a more controlled
+ way of returning all entries for a path.
+extends_documentation_fragment:
+ - community.routeros.api
+ - community.routeros.attributes
+ - community.routeros.attributes.actiongroup_api
+attributes:
+ check_mode:
+ support: none
+ diff_mode:
+ support: none
+ platform:
+ support: full
+ platforms: RouterOS
+ action_group:
+ version_added: 2.1.0
+options:
+ path:
+ description:
+ - Main path for all other arguments.
+ - If other arguments are not set, api will return all items in selected path.
+ - Example C(ip address). Equivalent of RouterOS CLI C(/ip address print).
+ required: true
+ type: str
+ add:
+ description:
+ - Will add selected arguments in selected path to RouterOS config.
+ - Example C(address=1.1.1.1/32 interface=ether1).
+ - Equivalent in RouterOS CLI C(/ip address add address=1.1.1.1/32 interface=ether1).
+ type: str
+ remove:
+ description:
+ - Remove config/value from RouterOS by '.id'.
+ - Example C(*03) will remove config/value with C(id=*03) in selected path.
+ - Equivalent in RouterOS CLI C(/ip address remove numbers=1).
+ - Note C(number) in RouterOS CLI is different from C(.id).
+ type: str
+ update:
+ description:
+ - Update config/value in RouterOS by '.id' in selected path.
+ - Example C(.id=*03 address=1.1.1.3/32) and path C(ip address) will replace existing ip address with C(.id=*03).
+ - Equivalent in RouterOS CLI C(/ip address set address=1.1.1.3/32 numbers=1).
+ - Note C(number) in RouterOS CLI is different from C(.id).
+ type: str
+ query:
+ description:
+ - Query given path for selected query attributes from RouterOS aip.
+ - WHERE is key word which extend query. WHERE format is key operator value - with spaces.
+ - WHERE valid operators are C(==) or C(eq), C(!=) or C(not), C(>) or C(more), C(<) or C(less).
+ - Example path C(ip address) and query C(.id address) will return only C(.id) and C(address) for all items in C(ip address) path.
+ - Example path C(ip address) and query C(.id address WHERE address == 1.1.1.3/32).
+ will return only C(.id) and C(address) for items in C(ip address) path, where address is eq to 1.1.1.3/32.
+ - Example path C(interface) and query C(mtu name WHERE mut > 1400) will
+ return only interfaces C(mtu,name) where mtu is bigger than 1400.
+ - Equivalent in RouterOS CLI C(/interface print where mtu > 1400).
+ type: str
+ extended_query:
+ description:
+ - Extended query given path for selected query attributes from RouterOS API.
+ - Extended query allow conjunctive input. If there is no matching entry, an empty list will be returned.
+ type: dict
+ suboptions:
+ attributes:
+ description:
+ - The list of attributes to return.
+ - Every attribute used in a I(where) clause need to be listed here.
+ type: list
+ elements: str
+ required: true
+ where:
+ description:
+ - Allows to restrict the objects returned.
+ - The conditions here must all match. An I(or) condition needs at least one of its conditions to match.
+ type: list
+ elements: dict
+ suboptions:
+ attribute:
+ description:
+ - The attribute to match. Must be part of I(attributes).
+ - Either I(or) or all of I(attribute), I(is), and I(value) have to be specified.
+ type: str
+ is:
+ description:
+ - The operator to use for matching.
+ - For equality use C(==) or C(eq). For less use C(<) or C(less). For more use C(>) or C(more).
+ - Use C(in) to check whether the value is part of a list. In that case, I(value) must be a list.
+ - Either I(or) or all of I(attribute), I(is), and I(value) have to be specified.
+ type: str
+ choices: ["==", "!=", ">", "<", "in", "eq", "not", "more", "less"]
+ value:
+ description:
+ - The value to compare to. Must be a list for I(is=in).
+ - Either I(or) or all of I(attribute), I(is), and I(value) have to be specified.
+ type: raw
+ or:
+ description:
+ - A list of conditions so that at least one of them has to match.
+ - Either I(or) or all of I(attribute), I(is), and I(value) have to be specified.
+ type: list
+ elements: dict
+ suboptions:
+ attribute:
+ description:
+ - The attribute to match. Must be part of I(attributes).
+ type: str
+ required: true
+ is:
+ description:
+ - The operator to use for matching.
+ - For equality use C(==) or C(eq). For less use C(<) or C(less). For more use C(>) or C(more).
+ - Use C(in) to check whether the value is part of a list. In that case, I(value) must be a list.
+ type: str
+ choices: ["==", "!=", ">", "<", "in", "eq", "not", "more", "less"]
+ required: true
+ value:
+ description:
+ - The value to compare to. Must be a list for I(is=in).
+ type: raw
+ required: true
+ cmd:
+ description:
+ - Execute any/arbitrary command in selected path, after the command we can add C(.id).
+ - Example path C(system script) and cmd C(run .id=*03) is equivalent in RouterOS CLI C(/system script run number=0).
+ - Example path C(ip address) and cmd C(print) is equivalent in RouterOS CLI C(/ip address print).
+ type: str
+seealso:
+ - ref: ansible_collections.community.routeros.docsite.quoting
+ description: How to quote and unquote commands and arguments
+ - module: community.routeros.api_facts
+ - module: community.routeros.api_find_and_modify
+ - module: community.routeros.api_info
+ - module: community.routeros.api_modify
+'''
+
+EXAMPLES = '''
+- name: Get example - ip address print
+ community.routeros.api:
+ hostname: "{{ hostname }}"
+ password: "{{ password }}"
+ username: "{{ username }}"
+ path: "ip address"
+ register: ipaddrd_printout
+
+- name: Dump "Get example" output
+ ansible.builtin.debug:
+ msg: '{{ ipaddrd_printout }}'
+
+- name: Add example - ip address
+ community.routeros.api:
+ hostname: "{{ hostname }}"
+ password: "{{ password }}"
+ username: "{{ username }}"
+ path: "ip address"
+ add: "address=192.168.255.10/24 interface=ether2"
+
+- name: Query example - ".id, address" in "ip address WHERE address == 192.168.255.10/24"
+ community.routeros.api:
+ hostname: "{{ hostname }}"
+ password: "{{ password }}"
+ username: "{{ username }}"
+ path: "ip address"
+ query: ".id address WHERE address == {{ ip2 }}"
+ register: queryout
+
+- name: Dump "Query example" output
+ ansible.builtin.debug:
+ msg: '{{ queryout }}'
+
+- name: Extended query example - ".id,address,network" where address is not 192.168.255.10/24 or is 10.20.36.20/24
+ community.routeros.api:
+ hostname: "{{ hostname }}"
+ password: "{{ password }}"
+ username: "{{ username }}"
+ path: "ip address"
+ extended_query:
+ attributes:
+ - network
+ - address
+ - .id
+ where:
+ - attribute: "network"
+ is: "=="
+ value: "192.168.255.0"
+ - or:
+ - attribute: "address"
+ is: "!="
+ value: "192.168.255.10/24"
+ - attribute: "address"
+ is: "eq"
+ value: "10.20.36.20/24"
+ - attribute: "network"
+ is: "in"
+ value:
+ - "10.20.36.0"
+ - "192.168.255.0"
+ register: extended_queryout
+
+- name: Dump "Extended query example" output
+ ansible.builtin.debug:
+ msg: '{{ extended_queryout }}'
+
+- name: Update example - ether2 ip addres with ".id = *14"
+ community.routeros.api:
+ hostname: "{{ hostname }}"
+ password: "{{ password }}"
+ username: "{{ username }}"
+ path: "ip address"
+ update: >-
+ .id=*14
+ address=192.168.255.20/24
+ comment={{ 'Update 192.168.255.10/24 to 192.168.255.20/24 on ether2' | community.routeros.quote_argument_value }}
+
+- name: Remove example - ether2 ip 192.168.255.20/24 with ".id = *14"
+ community.routeros.api:
+ hostname: "{{ hostname }}"
+ password: "{{ password }}"
+ username: "{{ username }}"
+ path: "ip address"
+ remove: "*14"
+
+- name: Arbitrary command example "/system identity print"
+ community.routeros.api:
+ hostname: "{{ hostname }}"
+ password: "{{ password }}"
+ username: "{{ username }}"
+ path: "system identity"
+ cmd: "print"
+ register: arbitraryout
+
+- name: Dump "Arbitrary command example" output
+ ansible.builtin.debug:
+ msg: '{{ arbitraryout }}'
+'''
+
+RETURN = '''
+---
+message:
+ description: All outputs are in list with dictionary elements returned from RouterOS api.
+ sample:
+ - address: 1.2.3.4
+ - address: 2.3.4.5
+ type: list
+ returned: always
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+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,
+ parse_argument_value,
+ split_routeros_command,
+)
+
+from ansible_collections.community.routeros.plugins.module_utils.api import (
+ api_argument_spec,
+ check_has_library,
+ create_api,
+)
+
+import re
+
+try:
+ from librouteros.exceptions import LibRouterosError
+ from librouteros.query import Key, Or
+except Exception:
+ # Handled in api module_utils
+ pass
+
+
+class ROS_api_module:
+ def __init__(self):
+ module_args = dict(
+ path=dict(type='str', required=True),
+ add=dict(type='str'),
+ remove=dict(type='str'),
+ update=dict(type='str'),
+ cmd=dict(type='str'),
+ query=dict(type='str'),
+ extended_query=dict(type='dict', options=dict(
+ attributes=dict(type='list', elements='str', required=True),
+ where=dict(
+ type='list',
+ elements='dict',
+ options={
+ 'attribute': dict(type='str'),
+ 'is': dict(type='str', choices=["==", "!=", ">", "<", "in", "eq", "not", "more", "less"]),
+ 'value': dict(type='raw'),
+ 'or': dict(type='list', elements='dict', options={
+ 'attribute': dict(type='str', required=True),
+ 'is': dict(type='str', choices=["==", "!=", ">", "<", "in", "eq", "not", "more", "less"], required=True),
+ 'value': dict(type='raw', required=True),
+ }),
+ },
+ required_together=[('attribute', 'is', 'value')],
+ mutually_exclusive=[('attribute', 'or')],
+ required_one_of=[('attribute', 'or')],
+ ),
+ )),
+ )
+ module_args.update(api_argument_spec())
+
+ self.module = AnsibleModule(argument_spec=module_args,
+ supports_check_mode=False,
+ mutually_exclusive=(('add', 'remove', 'update',
+ 'cmd', 'query', 'extended_query'),),)
+
+ check_has_library(self.module)
+
+ self.api = create_api(self.module)
+
+ self.path = self.module.params['path'].split()
+ self.add = self.module.params['add']
+ self.remove = self.module.params['remove']
+ self.update = self.module.params['update']
+ self.arbitrary = self.module.params['cmd']
+
+ self.where = None
+ self.query = self.module.params['query']
+ self.extended_query = self.module.params['extended_query']
+
+ self.result = dict(
+ message=[])
+
+ # create api base path
+ self.api_path = self.api_add_path(self.api, self.path)
+
+ # api calls
+ try:
+ if self.add:
+ self.api_add()
+ elif self.remove:
+ self.api_remove()
+ elif self.update:
+ self.api_update()
+ elif self.query:
+ self.check_query()
+ self.api_query()
+ elif self.extended_query:
+ self.check_extended_query()
+ self.api_extended_query()
+ elif self.arbitrary:
+ self.api_arbitrary()
+ else:
+ self.api_get_all()
+ except UnicodeEncodeError as exc:
+ self.module.fail_json(msg='Error while encoding text: {error}'.format(error=exc))
+
+ def check_query(self):
+ where_index = self.query.find(' WHERE ')
+ if where_index < 0:
+ self.query = self.split_params(self.query)
+ else:
+ where = self.query[where_index + len(' WHERE '):]
+ self.query = self.split_params(self.query[:where_index])
+ # where must be of the format '<attribute> <operator> <value>'
+ m = re.match(r'^\s*([^ ]+)\s+([^ ]+)\s+(.*)$', where)
+ if not m:
+ self.errors("invalid syntax for 'WHERE %s'" % where)
+ try:
+ self.where = [
+ m.group(1), # attribute
+ m.group(2), # operator
+ parse_argument_value(m.group(3).rstrip())[0], # value
+ ]
+ except ParseError as exc:
+ self.errors("invalid syntax for 'WHERE %s': %s" % (where, exc))
+ try:
+ idx = self.query.index('WHERE')
+ self.where = self.query[idx + 1:]
+ self.query = self.query[:idx]
+ except ValueError:
+ # Raised when WHERE has not been found
+ pass
+
+ def check_extended_query_syntax(self, test_atr, or_msg=''):
+ if test_atr['is'] == "in" and not isinstance(test_atr['value'], list):
+ self.errors("invalid syntax 'extended_query':'where':%s%s 'value' must be a type list" % (or_msg, test_atr))
+
+ def check_extended_query(self):
+ if self.extended_query["where"]:
+ for i in self.extended_query['where']:
+ if i["or"] is not None:
+ if len(i['or']) < 2:
+ self.errors("invalid syntax 'extended_query':'where':'or':%s 'or' requires minimum two items" % i["or"])
+ for orv in i['or']:
+ self.check_extended_query_syntax(orv, ":'or':")
+ else:
+ self.check_extended_query_syntax(i)
+
+ def list_to_dic(self, ldict):
+ return convert_list_to_dictionary(ldict, skip_empty_values=True, require_assignment=True)
+
+ def split_params(self, params):
+ if not isinstance(params, str):
+ raise AssertionError('Parameters can only be a string, received %s' % type(params))
+ try:
+ return split_routeros_command(params)
+ except ParseError as e:
+ self.module.fail_json(msg=to_native(e))
+
+ def api_add_path(self, api, path):
+ api_path = api.path()
+ for p in path:
+ api_path = api_path.join(p)
+ return api_path
+
+ def api_get_all(self):
+ try:
+ for i in self.api_path:
+ self.result['message'].append(i)
+ self.return_result(False, True)
+ except LibRouterosError as e:
+ self.errors(e)
+
+ def api_add(self):
+ param = self.list_to_dic(self.split_params(self.add))
+ try:
+ self.result['message'].append("added: .id= %s"
+ % self.api_path.add(**param))
+ self.return_result(True)
+ except LibRouterosError as e:
+ self.errors(e)
+
+ def api_remove(self):
+ try:
+ self.api_path.remove(self.remove)
+ self.result['message'].append("removed: .id= %s" % self.remove)
+ self.return_result(True)
+ except LibRouterosError as e:
+ self.errors(e)
+
+ def api_update(self):
+ param = self.list_to_dic(self.split_params(self.update))
+ if '.id' not in param.keys():
+ self.errors("missing '.id' for %s" % param)
+ try:
+ self.api_path.update(**param)
+ self.result['message'].append("updated: %s" % param)
+ self.return_result(True)
+ except LibRouterosError as e:
+ self.errors(e)
+
+ def api_query(self):
+ keys = {}
+ for k in self.query:
+ if 'id' in k and k != ".id":
+ self.errors("'%s' must be '.id'" % k)
+ keys[k] = Key(k)
+ try:
+ if self.where:
+ if self.where[1] in ('==', 'eq'):
+ select = self.api_path.select(*keys).where(keys[self.where[0]] == self.where[2])
+ elif self.where[1] in ('!=', 'not'):
+ select = self.api_path.select(*keys).where(keys[self.where[0]] != self.where[2])
+ elif self.where[1] in ('>', 'more'):
+ select = self.api_path.select(*keys).where(keys[self.where[0]] > self.where[2])
+ elif self.where[1] in ('<', 'less'):
+ select = self.api_path.select(*keys).where(keys[self.where[0]] < self.where[2])
+ else:
+ self.errors("'%s' is not operator for 'where'"
+ % self.where[1])
+ else:
+ select = self.api_path.select(*keys)
+ for row in select:
+ self.result['message'].append(row)
+ if len(self.result['message']) < 1:
+ msg = "no results for '%s 'query' %s" % (' '.join(self.path),
+ ' '.join(self.query))
+ if self.where:
+ msg = msg + ' WHERE %s' % ' '.join(self.where)
+ self.result['message'].append(msg)
+ self.return_result(False)
+ except LibRouterosError as e:
+ self.errors(e)
+
+ def build_api_extended_query(self, item):
+ if item['attribute'] not in self.extended_query['attributes']:
+ self.errors("'%s' attribute is not in attributes: %s"
+ % (item, self.extended_query['attributes']))
+ if item['is'] in ('eq', '=='):
+ return self.query_keys[item['attribute']] == item['value']
+ elif item['is'] in ('not', '!='):
+ return self.query_keys[item['attribute']] != item['value']
+ elif item['is'] in ('less', '<'):
+ return self.query_keys[item['attribute']] < item['value']
+ elif item['is'] in ('more', '>'):
+ return self.query_keys[item['attribute']] > item['value']
+ elif item['is'] == 'in':
+ return self.query_keys[item['attribute']].In(*item['value'])
+ else:
+ self.errors("'%s' is not operator for 'is'" % item['is'])
+
+ def api_extended_query(self):
+ self.query_keys = {}
+ for k in self.extended_query['attributes']:
+ if k == 'id':
+ self.errors("'extended_query':'attributes':'%s' must be '.id'" % k)
+ self.query_keys[k] = Key(k)
+ try:
+ if self.extended_query['where']:
+ where_args = []
+ for i in self.extended_query['where']:
+ if i['or']:
+ where_or_args = []
+ for ior in i['or']:
+ where_or_args.append(self.build_api_extended_query(ior))
+ where_args.append(Or(*where_or_args))
+ else:
+ where_args.append(self.build_api_extended_query(i))
+ select = self.api_path.select(*self.query_keys).where(*where_args)
+ else:
+ select = self.api_path.select(*self.extended_query['attributes'])
+ for row in select:
+ self.result['message'].append(row)
+ self.return_result(False)
+ except LibRouterosError as e:
+ self.errors(e)
+
+ def api_arbitrary(self):
+ param = {}
+ self.arbitrary = self.split_params(self.arbitrary)
+ arb_cmd = self.arbitrary[0]
+ if len(self.arbitrary) > 1:
+ param = self.list_to_dic(self.arbitrary[1:])
+ try:
+ arbitrary_result = self.api_path(arb_cmd, **param)
+ for i in arbitrary_result:
+ self.result['message'].append(i)
+ self.return_result(False)
+ except LibRouterosError as e:
+ self.errors(e)
+
+ def return_result(self, ch_status=False, status=True):
+ if not status:
+ self.module.fail_json(msg=self.result['message'])
+ else:
+ self.module.exit_json(changed=ch_status,
+ msg=self.result['message'])
+
+ def errors(self, e):
+ if e.__class__.__name__ == 'TrapError':
+ self.result['message'].append("%s" % e)
+ self.return_result(False, False)
+ self.result['message'].append("%s" % e)
+ self.return_result(False, False)
+
+
+def main():
+
+ ROS_api_module()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/routeros/plugins/modules/api_facts.py b/ansible_collections/community/routeros/plugins/modules/api_facts.py
new file mode 100644
index 000000000..f29723667
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/modules/api_facts.py
@@ -0,0 +1,495 @@
+#!/usr/bin/python
+
+# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
+# Copyright (c) 2020, Nikolay Dachev <nikolay@dachev.info>
+# Copyright (c) 2018, Egor Zaitsev (@heuels)
+# 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
+
+DOCUMENTATION = '''
+---
+module: api_facts
+author:
+ - "Egor Zaitsev (@heuels)"
+ - "Nikolay Dachev (@NikolayDachev)"
+ - "Felix Fontein (@felixfontein)"
+version_added: 2.1.0
+short_description: Collect facts from remote devices running MikroTik RouterOS using the API
+description:
+ - Collects a base set of device facts from a remote device that
+ is running RouterOS. This module prepends all of the
+ base network fact keys with C(ansible_net_<fact>). The facts
+ module will always collect a base set of facts from the device
+ and can enable or disable collection of additional facts.
+ - As opposed to the M(community.routeros.facts) module, it uses the
+ RouterOS API, similar to the M(community.routeros.api) module.
+extends_documentation_fragment:
+ - community.routeros.api
+ - community.routeros.attributes
+ - community.routeros.attributes.actiongroup_api
+ - community.routeros.attributes.facts
+ - community.routeros.attributes.facts_module
+attributes:
+ platform:
+ support: full
+ platforms: RouterOS
+options:
+ gather_subset:
+ description:
+ - When supplied, this argument will restrict the facts collected
+ to a given subset. Possible values for this argument include
+ C(all), C(hardware), C(interfaces), and C(routing).
+ - Can specify a list of values to include a larger subset.
+ Values can also be used with an initial C(!) to specify that a
+ specific subset should not be collected.
+ required: false
+ default:
+ - all
+ type: list
+ elements: str
+seealso:
+ - module: community.routeros.facts
+ - module: community.routeros.api
+ - module: community.routeros.api_find_and_modify
+ - module: community.routeros.api_info
+ - module: community.routeros.api_modify
+'''
+
+EXAMPLES = """
+- name: Collect all facts from the device
+ community.routeros.api_facts:
+ hostname: 192.168.88.1
+ username: admin
+ password: password
+ gather_subset: all
+
+- name: Do not collect hardware facts
+ community.routeros.api_facts:
+ hostname: 192.168.88.1
+ username: admin
+ password: password
+ gather_subset:
+ - "!hardware"
+"""
+
+RETURN = """
+ansible_facts:
+ description: "Dictionary of IP geolocation facts for a host's IP address."
+ returned: always
+ type: dict
+ contains:
+ ansible_net_gather_subset:
+ description: The list of fact subsets collected from the device.
+ returned: always
+ type: list
+
+ # default
+ ansible_net_model:
+ description: The model name returned from the device.
+ returned: I(gather_subset) contains C(default)
+ type: str
+ ansible_net_serialnum:
+ description: The serial number of the remote device.
+ returned: I(gather_subset) contains C(default)
+ type: str
+ ansible_net_version:
+ description: The operating system version running on the remote device.
+ returned: I(gather_subset) contains C(default)
+ type: str
+ ansible_net_hostname:
+ description: The configured hostname of the device.
+ returned: I(gather_subset) contains C(default)
+ type: str
+ ansible_net_arch:
+ description: The CPU architecture of the device.
+ returned: I(gather_subset) contains C(default)
+ type: str
+ ansible_net_uptime:
+ description: The uptime of the device.
+ returned: I(gather_subset) contains C(default)
+ type: str
+ ansible_net_cpu_load:
+ description: Current CPU load.
+ returned: I(gather_subset) contains C(default)
+ type: str
+
+ # hardware
+ ansible_net_spacefree_mb:
+ description: The available disk space on the remote device in MiB.
+ returned: I(gather_subset) contains C(hardware)
+ type: dict
+ ansible_net_spacetotal_mb:
+ description: The total disk space on the remote device in MiB.
+ returned: I(gather_subset) contains C(hardware)
+ type: dict
+ ansible_net_memfree_mb:
+ description: The available free memory on the remote device in MiB.
+ returned: I(gather_subset) contains C(hardware)
+ type: int
+ ansible_net_memtotal_mb:
+ description: The total memory on the remote device in MiB.
+ returned: I(gather_subset) contains C(hardware)
+ type: int
+
+ # interfaces
+ ansible_net_all_ipv4_addresses:
+ description: All IPv4 addresses configured on the device.
+ returned: I(gather_subset) contains C(interfaces)
+ type: list
+ ansible_net_all_ipv6_addresses:
+ description: All IPv6 addresses configured on the device.
+ returned: I(gather_subset) contains C(interfaces)
+ type: list
+ ansible_net_interfaces:
+ description: A hash of all interfaces running on the system.
+ returned: I(gather_subset) contains C(interfaces)
+ type: dict
+ ansible_net_neighbors:
+ description: The list of neighbors from the remote device.
+ returned: I(gather_subset) contains C(interfaces)
+ type: dict
+
+ # routing
+ ansible_net_bgp_peer:
+ description: A dictionary with BGP peer information.
+ returned: I(gather_subset) contains C(routing)
+ type: dict
+ ansible_net_bgp_vpnv4_route:
+ description: A dictionary with BGP vpnv4 route information.
+ returned: I(gather_subset) contains C(routing)
+ type: dict
+ ansible_net_bgp_instance:
+ description: A dictionary with BGP instance information.
+ returned: I(gather_subset) contains C(routing)
+ type: dict
+ ansible_net_route:
+ description: A dictionary for routes in all routing tables.
+ returned: I(gather_subset) contains C(routing)
+ type: dict
+ ansible_net_ospf_instance:
+ description: A dictionary with OSPF instances.
+ returned: I(gather_subset) contains C(routing)
+ type: dict
+ ansible_net_ospf_neighbor:
+ description: A dictionary with OSPF neighbors.
+ returned: I(gather_subset) contains C(routing)
+ type: dict
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.six import iteritems
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.routeros.plugins.module_utils.api import (
+ api_argument_spec,
+ check_has_library,
+ create_api,
+)
+
+try:
+ from librouteros.exceptions import LibRouterosError
+except Exception:
+ # Handled in api module_utils
+ pass
+
+
+class FactsBase(object):
+
+ COMMANDS = []
+
+ def __init__(self, module, api):
+ self.module = module
+ self.api = api
+ self.facts = {}
+ self.responses = None
+
+ def populate(self):
+ self.responses = []
+ for path in self.COMMANDS:
+ self.responses.append(self.query_path(path))
+
+ def query_path(self, path):
+ api_path = self.api.path()
+ for part in path:
+ api_path = api_path.join(part)
+ try:
+ return list(api_path)
+ except LibRouterosError as e:
+ self.module.warn('Error while querying path {path}: {error}'.format(
+ path=' '.join(path),
+ error=to_native(e),
+ ))
+ return []
+
+
+class Default(FactsBase):
+
+ COMMANDS = [
+ ['system', 'identity'],
+ ['system', 'resource'],
+ ['system', 'routerboard'],
+ ]
+
+ def populate(self):
+ super(Default, self).populate()
+ data = self.responses[0]
+ if data:
+ self.facts['hostname'] = data[0].get('name')
+ data = self.responses[1]
+ if data:
+ self.facts['version'] = data[0].get('version')
+ self.facts['arch'] = data[0].get('architecture-name')
+ self.facts['uptime'] = data[0].get('uptime')
+ self.facts['cpu_load'] = data[0].get('cpu-load')
+ data = self.responses[2]
+ if data:
+ self.facts['model'] = data[0].get('model')
+ self.facts['serialnum'] = data[0].get('serial-number')
+
+
+class Hardware(FactsBase):
+
+ COMMANDS = [
+ ['system', 'resource'],
+ ]
+
+ def populate(self):
+ super(Hardware, self).populate()
+ data = self.responses[0]
+ if data:
+ self.parse_filesystem_info(data[0])
+ self.parse_memory_info(data[0])
+
+ def parse_filesystem_info(self, data):
+ self.facts['spacefree_mb'] = self.to_megabytes(data.get('free-hdd-space'))
+ self.facts['spacetotal_mb'] = self.to_megabytes(data.get('total-hdd-space'))
+
+ def parse_memory_info(self, data):
+ self.facts['memfree_mb'] = self.to_megabytes(data.get('free-memory'))
+ self.facts['memtotal_mb'] = self.to_megabytes(data.get('total-memory'))
+
+ def to_megabytes(self, value):
+ if value is None:
+ return None
+ return float(value) / 1024 / 1024
+
+
+class Interfaces(FactsBase):
+
+ COMMANDS = [
+ ['interface'],
+ ['ip', 'address'],
+ ['ipv6', 'address'],
+ ['ip', 'neighbor'],
+ ]
+
+ def populate(self):
+ super(Interfaces, self).populate()
+
+ self.facts['interfaces'] = {}
+ self.facts['all_ipv4_addresses'] = []
+ self.facts['all_ipv6_addresses'] = []
+ self.facts['neighbors'] = []
+
+ data = self.responses[0]
+ if data:
+ interfaces = self.parse_interfaces(data)
+ self.populate_interfaces(interfaces)
+
+ data = self.responses[1]
+ if data:
+ data = self.parse_detail(data)
+ self.populate_addresses(data, 'ipv4')
+
+ data = self.responses[2]
+ if data:
+ data = self.parse_detail(data)
+ self.populate_addresses(data, 'ipv6')
+
+ data = self.responses[3]
+ if data:
+ self.facts['neighbors'] = list(self.parse_detail(data))
+
+ def populate_interfaces(self, data):
+ for key, value in iteritems(data):
+ self.facts['interfaces'][key] = value
+
+ def populate_addresses(self, data, family):
+ for value in data:
+ key = value['interface']
+ if family not in self.facts['interfaces'][key]:
+ self.facts['interfaces'][key][family] = []
+ addr, subnet = value['address'].split('/')
+ subnet = subnet.strip()
+ # Try to convert subnet to an integer
+ try:
+ subnet = int(subnet)
+ except Exception:
+ pass
+ ip = dict(address=addr.strip(), subnet=subnet)
+ self.add_ip_address(addr.strip(), family)
+ self.facts['interfaces'][key][family].append(ip)
+
+ def add_ip_address(self, address, family):
+ if family == 'ipv4':
+ self.facts['all_ipv4_addresses'].append(address)
+ else:
+ self.facts['all_ipv6_addresses'].append(address)
+
+ def parse_interfaces(self, data):
+ facts = {}
+ for entry in data:
+ if 'name' not in entry:
+ continue
+ entry.pop('.id', None)
+ facts[entry['name']] = entry
+ return facts
+
+ def parse_detail(self, data):
+ for entry in data:
+ if 'interface' not in entry:
+ continue
+ entry.pop('.id', None)
+ yield entry
+
+
+class Routing(FactsBase):
+
+ COMMANDS = [
+ ['routing', 'bgp', 'peer'],
+ ['routing', 'bgp', 'vpnv4-route'],
+ ['routing', 'bgp', 'instance'],
+ ['ip', 'route'],
+ ['routing', 'ospf', 'instance'],
+ ['routing', 'ospf', 'neighbor'],
+ ]
+
+ def populate(self):
+ super(Routing, self).populate()
+ self.facts['bgp_peer'] = {}
+ self.facts['bgp_vpnv4_route'] = {}
+ self.facts['bgp_instance'] = {}
+ self.facts['route'] = {}
+ self.facts['ospf_instance'] = {}
+ self.facts['ospf_neighbor'] = {}
+ data = self.responses[0]
+ if data:
+ peer = self.parse(data, 'name')
+ self.populate_result('bgp_peer', peer)
+ data = self.responses[1]
+ if data:
+ vpnv4 = self.parse(data, 'interface')
+ self.populate_result('bgp_vpnv4_route', vpnv4)
+ data = self.responses[2]
+ if data:
+ instance = self.parse(data, 'name')
+ self.populate_result('bgp_instance', instance)
+ data = self.responses[3]
+ if data:
+ route = self.parse(data, 'routing-mark', fallback='main')
+ self.populate_result('route', route)
+ data = self.responses[4]
+ if data:
+ instance = self.parse(data, 'name')
+ self.populate_result('ospf_instance', instance)
+ data = self.responses[5]
+ if data:
+ instance = self.parse(data, 'instance')
+ self.populate_result('ospf_neighbor', instance)
+
+ def parse(self, data, key, fallback=None):
+ facts = {}
+ for line in data:
+ name = line.get(key) or fallback
+ line.pop('.id', None)
+ facts[name] = line
+ return facts
+
+ def populate_result(self, name, data):
+ for key, value in iteritems(data):
+ self.facts[name][key] = value
+
+
+FACT_SUBSETS = dict(
+ default=Default,
+ hardware=Hardware,
+ interfaces=Interfaces,
+ routing=Routing,
+)
+
+VALID_SUBSETS = frozenset(FACT_SUBSETS.keys())
+
+warnings = []
+
+
+def main():
+ argument_spec = dict(
+ gather_subset=dict(
+ default=['all'],
+ type='list',
+ elements='str',
+ )
+ )
+ argument_spec.update(api_argument_spec())
+
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ check_has_library(module)
+ api = create_api(module)
+
+ gather_subset = module.params['gather_subset']
+
+ runable_subsets = set()
+ exclude_subsets = set()
+
+ for subset in gather_subset:
+ if subset == 'all':
+ runable_subsets.update(VALID_SUBSETS)
+ continue
+
+ if subset.startswith('!'):
+ subset = subset[1:]
+ if subset == 'all':
+ exclude_subsets.update(VALID_SUBSETS)
+ continue
+ exclude = True
+ else:
+ exclude = False
+
+ if subset not in VALID_SUBSETS:
+ module.fail_json(msg='Bad subset: %s' % subset)
+
+ if exclude:
+ exclude_subsets.add(subset)
+ else:
+ runable_subsets.add(subset)
+
+ if not runable_subsets:
+ runable_subsets.update(VALID_SUBSETS)
+
+ runable_subsets.difference_update(exclude_subsets)
+ runable_subsets.add('default')
+
+ facts = {}
+ facts['gather_subset'] = sorted(runable_subsets)
+
+ instances = []
+ for key in runable_subsets:
+ instances.append(FACT_SUBSETS[key](module, api))
+
+ for inst in instances:
+ inst.populate()
+ facts.update(inst.facts)
+
+ ansible_facts = {}
+ for key, value in iteritems(facts):
+ key = 'ansible_net_%s' % key
+ ansible_facts[key] = value
+
+ module.exit_json(ansible_facts=ansible_facts, warnings=warnings)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/routeros/plugins/modules/api_find_and_modify.py b/ansible_collections/community/routeros/plugins/modules/api_find_and_modify.py
new file mode 100644
index 000000000..0be3f7039
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/modules/api_find_and_modify.py
@@ -0,0 +1,327 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ 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
+
+DOCUMENTATION = '''
+---
+module: api_find_and_modify
+author:
+ - "Felix Fontein (@felixfontein)"
+short_description: Find and modify information using the API
+version_added: 2.1.0
+description:
+ - Allows to find entries for a path by conditions and modify the values of these entries.
+ - Use the M(community.routeros.api_find_and_modify) module to set all entries of a path to specific values,
+ or change multiple entries in different ways in one step.
+notes:
+ - "If you want to change values based on their old values (like change all comments 'foo' to 'bar') and make sure that
+ there are at least N such values, you can use I(require_matches_min=N) together with I(allow_no_matches=true).
+ This will make the module fail if there are less than N such entries, but not if there is no match. The latter case
+ is needed for idempotency of the task: once the values have been changed, there should be no further match."
+extends_documentation_fragment:
+ - community.routeros.api
+ - community.routeros.attributes
+ - community.routeros.attributes.actiongroup_api
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: full
+ platform:
+ support: full
+ platforms: RouterOS
+options:
+ path:
+ description:
+ - Path to query.
+ - An example value is C(ip address). This is equivalent to running C(/ip address) in the RouterOS CLI.
+ required: true
+ type: str
+ find:
+ description:
+ - Fields to search for.
+ - The module will only consider entries in the given I(path) that match all fields provided here.
+ - Use YAML C(~), or prepend keys with C(!), to specify an unset value.
+ - Note that if the dictionary specified here is empty, every entry in the path will be matched.
+ required: true
+ type: dict
+ values:
+ description:
+ - On all entries matching the conditions in I(find), set the keys of this option to the values specified here.
+ - Use YAML C(~), or prepend keys with C(!), to specify to unset a value.
+ required: true
+ type: dict
+ require_matches_min:
+ description:
+ - Make sure that there are no less matches than this number.
+ - If there are less matches, fail instead of modifying anything.
+ type: int
+ default: 0
+ require_matches_max:
+ description:
+ - Make sure that there are no more matches than this number.
+ - If there are more matches, fail instead of modifying anything.
+ - If not specified, there is no upper limit.
+ type: int
+ allow_no_matches:
+ description:
+ - Whether to allow that no match is found.
+ - If not specified, this value is induced from whether I(require_matches_min) is 0 or larger.
+ type: bool
+seealso:
+ - module: community.routeros.api
+ - module: community.routeros.api_facts
+ - module: community.routeros.api_modify
+ - module: community.routeros.api_info
+'''
+
+EXAMPLES = '''
+---
+- name: Rename bridge from 'bridge' to 'my-bridge'
+ community.routeros.api_find_and_modify:
+ hostname: "{{ hostname }}"
+ password: "{{ password }}"
+ username: "{{ username }}"
+ path: interface bridge
+ find:
+ name: bridge
+ values:
+ name: my-bridge
+
+- name: Change IP address to 192.168.1.1 for interface bridge - assuming there is only one
+ community.routeros.api_find_and_modify:
+ hostname: "{{ hostname }}"
+ password: "{{ password }}"
+ username: "{{ username }}"
+ path: ip address
+ find:
+ interface: bridge
+ values:
+ address: "192.168.1.1/24"
+ # If there are zero entries, or more than one: fail! We expected that
+ # exactly one is configured.
+ require_matches_min: 1
+ require_matches_max: 1
+'''
+
+RETURN = '''
+---
+old_data:
+ description:
+ - A list of all elements for the current path before a change was made.
+ sample:
+ - '.id': '*1'
+ actual-interface: bridge
+ address: "192.168.88.1/24"
+ comment: defconf
+ disabled: false
+ dynamic: false
+ interface: bridge
+ invalid: false
+ network: 192.168.88.0
+ type: list
+ elements: dict
+ returned: success
+new_data:
+ description:
+ - A list of all elements for the current path after a change was made.
+ sample:
+ - '.id': '*1'
+ actual-interface: bridge
+ address: "192.168.1.1/24"
+ comment: awesome
+ disabled: false
+ dynamic: false
+ interface: bridge
+ invalid: false
+ network: 192.168.1.0
+ type: list
+ elements: dict
+ returned: success
+match_count:
+ description:
+ - The number of entries that matched the criteria in I(find).
+ sample: 1
+ type: int
+ returned: success
+modify__count:
+ description:
+ - The number of entries that were modified.
+ sample: 1
+ type: int
+ returned: success
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.routeros.plugins.module_utils.api import (
+ api_argument_spec,
+ check_has_library,
+ create_api,
+)
+
+from ansible_collections.community.routeros.plugins.module_utils._api_data import (
+ split_path,
+)
+
+try:
+ from librouteros.exceptions import LibRouterosError
+except Exception:
+ # Handled in api module_utils
+ pass
+
+
+def compose_api_path(api, path):
+ api_path = api.path()
+ for p in path:
+ api_path = api_path.join(p)
+ return api_path
+
+
+DISABLED_MEANS_EMPTY_STRING = ('comment', )
+
+
+def main():
+ module_args = dict(
+ path=dict(type='str', required=True),
+ find=dict(type='dict', required=True),
+ values=dict(type='dict', required=True),
+ require_matches_min=dict(type='int', default=0),
+ require_matches_max=dict(type='int'),
+ allow_no_matches=dict(type='bool'),
+ )
+ module_args.update(api_argument_spec())
+
+ module = AnsibleModule(
+ argument_spec=module_args,
+ supports_check_mode=True,
+ )
+ if module.params['allow_no_matches'] is None:
+ module.params['allow_no_matches'] = module.params['require_matches_min'] <= 0
+
+ find = module.params['find']
+ for key, value in sorted(find.items()):
+ if key.startswith('!'):
+ key = key[1:]
+ if value not in (None, ''):
+ module.fail_json(msg='The value for "!{key}" in `find` must not be non-trivial!'.format(key=key))
+ if key in find:
+ module.fail_json(msg='`find` must not contain both "{key}" and "!{key}"!'.format(key=key))
+ values = module.params['values']
+ for key, value in sorted(values.items()):
+ if key.startswith('!'):
+ key = key[1:]
+ if value not in (None, ''):
+ module.fail_json(msg='The value for "!{key}" in `values` must not be non-trivial!'.format(key=key))
+ if key in values:
+ module.fail_json(msg='`values` must not contain both "{key}" and "!{key}"!'.format(key=key))
+
+ check_has_library(module)
+ api = create_api(module)
+
+ path = split_path(module.params['path'])
+
+ api_path = compose_api_path(api, path)
+
+ old_data = list(api_path)
+ new_data = [entry.copy() for entry in old_data]
+
+ # Find matching entries
+ matching_entries = []
+ for index, entry in enumerate(new_data):
+ matches = True
+ for key, value in find.items():
+ if key.startswith('!'):
+ # Allow to specify keys that should not be present by prepending '!'
+ key = key[1:]
+ value = None
+ current_value = entry.get(key)
+ if key in DISABLED_MEANS_EMPTY_STRING and value == '' and current_value is None:
+ current_value = value
+ if current_value != value:
+ matches = False
+ break
+ if matches:
+ matching_entries.append((index, entry))
+
+ # Check whether the correct amount of entries was found
+ if matching_entries:
+ if len(matching_entries) < module.params['require_matches_min']:
+ module.fail_json(msg='Found %d entries, but expected at least %d' % (len(matching_entries), module.params['require_matches_min']))
+ if module.params['require_matches_max'] is not None and len(matching_entries) > module.params['require_matches_max']:
+ module.fail_json(msg='Found %d entries, but expected at most %d' % (len(matching_entries), module.params['require_matches_max']))
+ elif not module.params['allow_no_matches']:
+ module.fail_json(msg='Found no entries, but allow_no_matches=false')
+
+ # Identify entries to update
+ modifications = []
+ for index, entry in matching_entries:
+ modification = {}
+ for key, value in values.items():
+ if key.startswith('!'):
+ # Allow to specify keys to remove by prepending '!'
+ key = key[1:]
+ value = None
+ current_value = entry.get(key)
+ if key in DISABLED_MEANS_EMPTY_STRING and value == '' and current_value is None:
+ current_value = value
+ if current_value != value:
+ if value is None:
+ disable_key = '!%s' % key
+ if key in DISABLED_MEANS_EMPTY_STRING:
+ disable_key = key
+ modification[disable_key] = ''
+ entry.pop(key, None)
+ else:
+ modification[key] = value
+ entry[key] = value
+ if modification:
+ if '.id' in entry:
+ modification['.id'] = entry['.id']
+ modifications.append(modification)
+
+ # Apply changes
+ if not module.check_mode and modifications:
+ for modification in modifications:
+ try:
+ api_path.update(**modification)
+ except (LibRouterosError, UnicodeEncodeError) as e:
+ module.fail_json(
+ msg='Error while modifying for .id={id}: {error}'.format(
+ id=modification['.id'],
+ error=to_native(e),
+ )
+ )
+ new_data = list(api_path)
+
+ # Produce return value
+ more = {}
+ if module._diff:
+ # Only include the matching values
+ more['diff'] = {
+ 'before': {
+ 'values': [old_data[index] for index, entry in matching_entries],
+ },
+ 'after': {
+ 'values': [entry for index, entry in matching_entries],
+ },
+ }
+ module.exit_json(
+ changed=bool(modifications),
+ old_data=old_data,
+ new_data=new_data,
+ match_count=len(matching_entries),
+ modify_count=len(modifications),
+ **more
+ )
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/routeros/plugins/modules/api_info.py b/ansible_collections/community/routeros/plugins/modules/api_info.py
new file mode 100644
index 000000000..50228c063
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/modules/api_info.py
@@ -0,0 +1,366 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# 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
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: api_info
+author:
+ - "Felix Fontein (@felixfontein)"
+short_description: Retrieve information from API
+version_added: 2.2.0
+description:
+ - Allows to retrieve information for a path using the API.
+ - This can be used to backup a path to restore it with the M(community.routeros.api_modify) module.
+ - Entries are normalized, dynamic and builtin entries are not returned. Use the I(handle_disabled) and
+ I(hide_defaults) options to control normalization, the I(include_dynamic) and I(include_builtin) options to also return
+ dynamic resp. builtin entries, and use I(unfiltered) to return all fields including counters.
+ - B(Note) that this module is still heavily in development, and only supports B(some) paths.
+ If you want to support new paths, or think you found problems with existing paths, please first
+ L(create an issue in the community.routeros Issue Tracker,https://github.com/ansible-collections/community.routeros/issues/).
+extends_documentation_fragment:
+ - community.routeros.api
+ - community.routeros.attributes
+ - community.routeros.attributes.actiongroup_api
+ - community.routeros.attributes.info_module
+attributes:
+ platform:
+ support: full
+ platforms: RouterOS
+options:
+ path:
+ description:
+ - Path to query.
+ - An example value is C(ip address). This is equivalent to running C(/ip address print) in the RouterOS CLI.
+ required: true
+ type: str
+ choices:
+ # BEGIN PATH LIST
+ - caps-man aaa
+ - caps-man access-list
+ - caps-man configuration
+ - caps-man datapath
+ - caps-man manager
+ - caps-man provisioning
+ - caps-man security
+ - certificate settings
+ - interface bonding
+ - interface bridge
+ - interface bridge mlag
+ - interface bridge port
+ - interface bridge port-controller
+ - interface bridge port-extender
+ - interface bridge settings
+ - interface bridge vlan
+ - interface detect-internet
+ - interface eoip
+ - interface ethernet
+ - interface ethernet poe
+ - interface ethernet switch
+ - interface ethernet switch port
+ - interface gre
+ - interface gre6
+ - interface l2tp-server server
+ - interface list
+ - interface list member
+ - interface ovpn-server server
+ - interface pppoe-client
+ - interface pptp-server server
+ - interface sstp-server server
+ - interface vlan
+ - interface vrrp
+ - interface wireguard
+ - interface wireguard peers
+ - interface wireless align
+ - interface wireless cap
+ - interface wireless sniffer
+ - interface wireless snooper
+ - ip accounting
+ - ip accounting web-access
+ - ip address
+ - ip arp
+ - ip cloud
+ - ip cloud advanced
+ - ip dhcp-client
+ - ip dhcp-client option
+ - ip dhcp-server
+ - ip dhcp-server config
+ - ip dhcp-server lease
+ - ip dhcp-server network
+ - ip dns
+ - ip dns static
+ - ip firewall address-list
+ - ip firewall connection tracking
+ - ip firewall filter
+ - ip firewall layer7-protocol
+ - ip firewall mangle
+ - ip firewall nat
+ - ip firewall raw
+ - ip firewall service-port
+ - ip hotspot service-port
+ - ip ipsec identity
+ - ip ipsec peer
+ - ip ipsec policy
+ - ip ipsec profile
+ - ip ipsec proposal
+ - ip ipsec settings
+ - ip neighbor discovery-settings
+ - ip pool
+ - ip proxy
+ - ip route
+ - ip route vrf
+ - ip service
+ - ip settings
+ - ip smb
+ - ip socks
+ - ip ssh
+ - ip tftp settings
+ - ip traffic-flow
+ - ip traffic-flow ipfix
+ - ip upnp
+ - ipv6 address
+ - ipv6 dhcp-client
+ - ipv6 dhcp-server
+ - ipv6 dhcp-server option
+ - ipv6 firewall address-list
+ - ipv6 firewall filter
+ - ipv6 firewall mangle
+ - ipv6 firewall raw
+ - ipv6 nd
+ - ipv6 nd prefix default
+ - ipv6 route
+ - ipv6 settings
+ - mpls
+ - mpls ldp
+ - port firmware
+ - ppp aaa
+ - queue interface
+ - queue tree
+ - radius incoming
+ - routing bgp instance
+ - routing mme
+ - routing ospf area
+ - routing ospf area range
+ - routing ospf instance
+ - routing ospf interface-template
+ - routing pimsm instance
+ - routing pimsm interface-template
+ - routing rip
+ - routing ripng
+ - snmp
+ - snmp community
+ - system clock
+ - system clock manual
+ - system identity
+ - system leds settings
+ - system logging
+ - system logging action
+ - system note
+ - system ntp client
+ - system ntp client servers
+ - system ntp server
+ - system package update
+ - system routerboard settings
+ - system scheduler
+ - system script
+ - system upgrade mirror
+ - system ups
+ - system watchdog
+ - tool bandwidth-server
+ - tool e-mail
+ - tool graphing
+ - tool mac-server
+ - tool mac-server mac-winbox
+ - tool mac-server ping
+ - tool romon
+ - tool sms
+ - tool sniffer
+ - tool traffic-generator
+ - user aaa
+ - user group
+ # END PATH LIST
+ unfiltered:
+ description:
+ - Whether to output all fields, and not just the ones supported as input for M(community.routeros.api_modify).
+ - Unfiltered output can contain counters and other state information.
+ type: bool
+ default: false
+ handle_disabled:
+ description:
+ - How to handle unset values.
+ - C(exclamation) prepends the keys with C(!) in the output with value C(null).
+ - C(null-value) uses the regular key with value C(null).
+ - C(omit) omits these values from the result.
+ type: str
+ choices:
+ - exclamation
+ - null-value
+ - omit
+ default: exclamation
+ hide_defaults:
+ description:
+ - Whether to hide default values.
+ type: bool
+ default: true
+ include_dynamic:
+ description:
+ - Whether to include dynamic values.
+ - By default, they are not returned, and the C(dynamic) keys are omitted.
+ - If set to C(true), they are returned as well, and the C(dynamic) keys are returned as well.
+ type: bool
+ default: false
+ include_builtin:
+ description:
+ - Whether to include builtin values.
+ - By default, they are not returned, and the C(builtin) keys are omitted.
+ - If set to C(true), they are returned as well, and the C(builtin) keys are returned as well.
+ type: bool
+ default: false
+ version_added: 2.4.0
+seealso:
+ - module: community.routeros.api
+ - module: community.routeros.api_facts
+ - module: community.routeros.api_find_and_modify
+ - module: community.routeros.api_modify
+'''
+
+EXAMPLES = '''
+---
+- name: Get IP addresses
+ community.routeros.api_info:
+ hostname: "{{ hostname }}"
+ password: "{{ password }}"
+ username: "{{ username }}"
+ path: ip address
+ register: ip_addresses
+
+- name: Print data for IP addresses
+ ansible.builtin.debug:
+ var: ip_addresses.result
+'''
+
+RETURN = '''
+---
+result:
+ description: A list of all elements for the current path.
+ sample:
+ - '.id': '*1'
+ actual-interface: bridge
+ address: "192.168.88.1/24"
+ comment: defconf
+ disabled: false
+ dynamic: false
+ interface: bridge
+ invalid: false
+ network: 192.168.88.0
+ type: list
+ elements: dict
+ returned: always
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.routeros.plugins.module_utils.api import (
+ api_argument_spec,
+ check_has_library,
+ create_api,
+)
+
+from ansible_collections.community.routeros.plugins.module_utils._api_data import (
+ PATHS,
+ join_path,
+ split_path,
+)
+
+try:
+ from librouteros.exceptions import LibRouterosError
+except Exception:
+ # Handled in api module_utils
+ pass
+
+
+def compose_api_path(api, path):
+ api_path = api.path()
+ for p in path:
+ api_path = api_path.join(p)
+ return api_path
+
+
+def main():
+ module_args = dict(
+ path=dict(type='str', required=True, choices=sorted([join_path(path) for path in PATHS if PATHS[path].fully_understood])),
+ unfiltered=dict(type='bool', default=False),
+ handle_disabled=dict(type='str', choices=['exclamation', 'null-value', 'omit'], default='exclamation'),
+ hide_defaults=dict(type='bool', default=True),
+ include_dynamic=dict(type='bool', default=False),
+ include_builtin=dict(type='bool', default=False),
+ )
+ module_args.update(api_argument_spec())
+
+ module = AnsibleModule(
+ argument_spec=module_args,
+ supports_check_mode=True,
+ )
+
+ check_has_library(module)
+ api = create_api(module)
+
+ path = split_path(module.params['path'])
+ path_info = PATHS.get(tuple(path))
+ if path_info is None:
+ module.fail_json(msg='Path /{path} is not yet supported'.format(path='/'.join(path)))
+
+ handle_disabled = module.params['handle_disabled']
+ hide_defaults = module.params['hide_defaults']
+ include_dynamic = module.params['include_dynamic']
+ include_builtin = module.params['include_builtin']
+ try:
+ api_path = compose_api_path(api, path)
+
+ result = []
+ unfiltered = module.params['unfiltered']
+ for entry in api_path:
+ if not include_dynamic:
+ if entry.get('dynamic', False):
+ continue
+ if not include_builtin:
+ if entry.get('builtin', False):
+ continue
+ if not unfiltered:
+ for k in list(entry):
+ if k == '.id':
+ continue
+ if k == 'dynamic' and include_dynamic:
+ continue
+ if k == 'builtin' and include_builtin:
+ continue
+ if k not in path_info.fields:
+ entry.pop(k)
+ if handle_disabled != 'omit':
+ for k in path_info.fields:
+ if k not in entry:
+ if handle_disabled == 'exclamation':
+ k = '!%s' % k
+ entry[k] = None
+ for k, field_info in path_info.fields.items():
+ if hide_defaults:
+ if field_info.default is not None and entry.get(k) == field_info.default:
+ entry.pop(k)
+ if field_info.absent_value and k not in entry:
+ entry[k] = field_info.absent_value
+ result.append(entry)
+
+ module.exit_json(result=result)
+ except (LibRouterosError, UnicodeEncodeError) as e:
+ module.fail_json(msg=to_native(e))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/routeros/plugins/modules/api_modify.py b/ansible_collections/community/routeros/plugins/modules/api_modify.py
new file mode 100644
index 000000000..5d410e9fb
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/modules/api_modify.py
@@ -0,0 +1,1030 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# 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
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: api_modify
+author:
+ - "Felix Fontein (@felixfontein)"
+short_description: Modify data at paths with API
+version_added: 2.2.0
+description:
+ - Allows to modify information for a path using the API.
+ - Use the M(community.routeros.api_find_and_modify) module to modify one or multiple entries in a controlled way
+ depending on some search conditions.
+ - To make a backup of a path that can be restored with this module, use the M(community.routeros.api_info) module.
+ - The module ignores dynamic and builtin entries.
+ - B(Note) that this module is still heavily in development, and only supports B(some) paths.
+ If you want to support new paths, or think you found problems with existing paths, please first
+ L(create an issue in the community.routeros Issue Tracker,https://github.com/ansible-collections/community.routeros/issues/).
+requirements:
+ - Needs L(ordereddict,https://pypi.org/project/ordereddict) for Python 2.6
+extends_documentation_fragment:
+ - community.routeros.api
+ - community.routeros.attributes
+ - community.routeros.attributes.actiongroup_api
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: full
+ platform:
+ support: full
+ platforms: RouterOS
+options:
+ path:
+ description:
+ - Path to query.
+ - An example value is C(ip address). This is equivalent to running modification commands in C(/ip address) in the RouterOS CLI.
+ required: true
+ type: str
+ choices:
+ # BEGIN PATH LIST
+ - caps-man aaa
+ - caps-man access-list
+ - caps-man configuration
+ - caps-man datapath
+ - caps-man manager
+ - caps-man provisioning
+ - caps-man security
+ - certificate settings
+ - interface bonding
+ - interface bridge
+ - interface bridge mlag
+ - interface bridge port
+ - interface bridge port-controller
+ - interface bridge port-extender
+ - interface bridge settings
+ - interface bridge vlan
+ - interface detect-internet
+ - interface eoip
+ - interface ethernet
+ - interface ethernet poe
+ - interface ethernet switch
+ - interface ethernet switch port
+ - interface gre
+ - interface gre6
+ - interface l2tp-server server
+ - interface list
+ - interface list member
+ - interface ovpn-server server
+ - interface pppoe-client
+ - interface pptp-server server
+ - interface sstp-server server
+ - interface vlan
+ - interface vrrp
+ - interface wireguard
+ - interface wireguard peers
+ - interface wireless align
+ - interface wireless cap
+ - interface wireless sniffer
+ - interface wireless snooper
+ - ip accounting
+ - ip accounting web-access
+ - ip address
+ - ip arp
+ - ip cloud
+ - ip cloud advanced
+ - ip dhcp-client
+ - ip dhcp-client option
+ - ip dhcp-server
+ - ip dhcp-server config
+ - ip dhcp-server lease
+ - ip dhcp-server network
+ - ip dns
+ - ip dns static
+ - ip firewall address-list
+ - ip firewall connection tracking
+ - ip firewall filter
+ - ip firewall layer7-protocol
+ - ip firewall mangle
+ - ip firewall nat
+ - ip firewall raw
+ - ip firewall service-port
+ - ip hotspot service-port
+ - ip ipsec identity
+ - ip ipsec peer
+ - ip ipsec policy
+ - ip ipsec profile
+ - ip ipsec proposal
+ - ip ipsec settings
+ - ip neighbor discovery-settings
+ - ip pool
+ - ip proxy
+ - ip route
+ - ip route vrf
+ - ip service
+ - ip settings
+ - ip smb
+ - ip socks
+ - ip ssh
+ - ip tftp settings
+ - ip traffic-flow
+ - ip traffic-flow ipfix
+ - ip upnp
+ - ipv6 address
+ - ipv6 dhcp-client
+ - ipv6 dhcp-server
+ - ipv6 dhcp-server option
+ - ipv6 firewall address-list
+ - ipv6 firewall filter
+ - ipv6 firewall mangle
+ - ipv6 firewall raw
+ - ipv6 nd
+ - ipv6 nd prefix default
+ - ipv6 route
+ - ipv6 settings
+ - mpls
+ - mpls ldp
+ - port firmware
+ - ppp aaa
+ - queue interface
+ - queue tree
+ - radius incoming
+ - routing bgp instance
+ - routing mme
+ - routing ospf area
+ - routing ospf area range
+ - routing ospf instance
+ - routing ospf interface-template
+ - routing pimsm instance
+ - routing pimsm interface-template
+ - routing rip
+ - routing ripng
+ - snmp
+ - snmp community
+ - system clock
+ - system clock manual
+ - system identity
+ - system leds settings
+ - system logging
+ - system logging action
+ - system note
+ - system ntp client
+ - system ntp client servers
+ - system ntp server
+ - system package update
+ - system routerboard settings
+ - system scheduler
+ - system script
+ - system upgrade mirror
+ - system ups
+ - system watchdog
+ - tool bandwidth-server
+ - tool e-mail
+ - tool graphing
+ - tool mac-server
+ - tool mac-server mac-winbox
+ - tool mac-server ping
+ - tool romon
+ - tool sms
+ - tool sniffer
+ - tool traffic-generator
+ - user aaa
+ - user group
+ # END PATH LIST
+ data:
+ description:
+ - Data to ensure that is present for this path.
+ - Fields not provided will not be modified.
+ - If C(.id) appears in an entry, it will be ignored.
+ required: true
+ type: list
+ elements: dict
+ ensure_order:
+ description:
+ - Whether to ensure the same order of the config as present in I(data).
+ - Requires I(handle_absent_entries=remove).
+ type: bool
+ default: false
+ handle_absent_entries:
+ description:
+ - How to handle entries that are present in the current config, but not in I(data).
+ - C(ignore) ignores them.
+ - C(remove) removes them.
+ type: str
+ choices:
+ - ignore
+ - remove
+ default: ignore
+ handle_entries_content:
+ description:
+ - For a single entry in I(data), this describes how to handle fields that are not mentioned
+ in that entry, but appear in the actual config.
+ - If C(ignore), they are not modified.
+ - If C(remove), they are removed. If at least one cannot be removed, the module will fail.
+ - If C(remove_as_much_as_possible), all that can be removed will be removed. The ones that
+ cannot be removed will be kept.
+ type: str
+ choices:
+ - ignore
+ - remove
+ - remove_as_much_as_possible
+ default: ignore
+seealso:
+ - module: community.routeros.api
+ - module: community.routeros.api_facts
+ - module: community.routeros.api_find_and_modify
+ - module: community.routeros.api_info
+'''
+
+EXAMPLES = '''
+---
+- name: Setup DHCP server networks
+ # Ensures that we have exactly two DHCP server networks (in the specified order)
+ community.routeros.api_modify:
+ path: ip dhcp-server network
+ handle_absent_entries: remove
+ handle_entries_content: remove_as_much_as_possible
+ ensure_order: true
+ data:
+ - address: 192.168.88.0/24
+ comment: admin network
+ dns-server: 192.168.88.1
+ gateway: 192.168.88.1
+ - address: 192.168.1.0/24
+ comment: customer network 1
+ dns-server: 192.168.1.1
+ gateway: 192.168.1.1
+ netmask: 24
+
+- name: Adjust NAT
+ community.routeros.api_modify:
+ hostname: "{{ hostname }}"
+ password: "{{ password }}"
+ username: "{{ username }}"
+ path: ip firewall nat
+ data:
+ - action: masquerade
+ chain: srcnat
+ comment: NAT to WAN
+ out-interface-list: WAN
+ # Three ways to unset values:
+ # - nothing after `:`
+ # - "empty" value (null/~/None)
+ # - prepend '!'
+ out-interface:
+ to-addresses: ~
+ '!to-ports':
+'''
+
+RETURN = '''
+---
+old_data:
+ description:
+ - A list of all elements for the current path before a change was made.
+ sample:
+ - '.id': '*1'
+ actual-interface: bridge
+ address: "192.168.88.1/24"
+ comment: defconf
+ disabled: false
+ dynamic: false
+ interface: bridge
+ invalid: false
+ network: 192.168.88.0
+ type: list
+ elements: dict
+ returned: always
+new_data:
+ description:
+ - A list of all elements for the current path after a change was made.
+ sample:
+ - '.id': '*1'
+ actual-interface: bridge
+ address: "192.168.1.1/24"
+ comment: awesome
+ disabled: false
+ dynamic: false
+ interface: bridge
+ invalid: false
+ network: 192.168.1.0
+ type: list
+ elements: dict
+ returned: always
+'''
+
+from collections import defaultdict
+
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.routeros.plugins.module_utils.api import (
+ api_argument_spec,
+ check_has_library,
+ create_api,
+)
+
+from ansible_collections.community.routeros.plugins.module_utils._api_data import (
+ PATHS,
+ join_path,
+ split_path,
+)
+
+HAS_ORDEREDDICT = True
+try:
+ from collections import OrderedDict
+except ImportError:
+ try:
+ from ordereddict import OrderedDict
+ except ImportError:
+ HAS_ORDEREDDICT = False
+ OrderedDict = dict
+
+try:
+ from librouteros.exceptions import LibRouterosError
+except Exception:
+ # Handled in api module_utils
+ pass
+
+
+def compose_api_path(api, path):
+ api_path = api.path()
+ for p in path:
+ api_path = api_path.join(p)
+ return api_path
+
+
+def find_modifications(old_entry, new_entry, path_info, module, for_text='', return_none_instead_of_fail=False):
+ modifications = OrderedDict()
+ updated_entry = old_entry.copy()
+ for k, v in new_entry.items():
+ if k == '.id':
+ continue
+ disabled_k = None
+ if k.startswith('!'):
+ disabled_k = k[1:]
+ elif v is None or v == path_info.fields[k].remove_value:
+ disabled_k = k
+ if disabled_k is not None:
+ if disabled_k in old_entry:
+ if path_info.fields[disabled_k].remove_value is not None:
+ modifications[disabled_k] = path_info.fields[disabled_k].remove_value
+ else:
+ modifications['!%s' % disabled_k] = ''
+ del updated_entry[disabled_k]
+ continue
+ if k not in old_entry and path_info.fields[k].default == v and not path_info.fields[k].can_disable:
+ continue
+ if k not in old_entry or old_entry[k] != v:
+ modifications[k] = v
+ updated_entry[k] = v
+ handle_entries_content = module.params['handle_entries_content']
+ if handle_entries_content != 'ignore':
+ for k in old_entry:
+ if k == '.id' or k in new_entry or ('!%s' % k) in new_entry or k not in path_info.fields:
+ continue
+ field_info = path_info.fields[k]
+ if field_info.default is not None and field_info.default == old_entry[k]:
+ continue
+ if field_info.remove_value is not None and field_info.remove_value == old_entry[k]:
+ continue
+ if field_info.can_disable:
+ if field_info.default is not None:
+ modifications[k] = field_info.default
+ elif field_info.remove_value is not None:
+ modifications[k] = field_info.remove_value
+ else:
+ modifications['!%s' % k] = ''
+ del updated_entry[k]
+ elif field_info.default is not None:
+ modifications[k] = field_info.default
+ updated_entry[k] = field_info.default
+ elif handle_entries_content == 'remove':
+ if return_none_instead_of_fail:
+ return None, None
+ module.fail_json(msg='Key "{key}" cannot be removed{for_text}.'.format(key=k, for_text=for_text))
+ for k in path_info.fields:
+ field_info = path_info.fields[k]
+ if k not in old_entry and k not in new_entry and field_info.can_disable and field_info.default is not None:
+ modifications[k] = field_info.default
+ updated_entry[k] = field_info.default
+ return modifications, updated_entry
+
+
+def essentially_same_weight(old_entry, new_entry, path_info, module):
+ for k, v in new_entry.items():
+ if k == '.id':
+ continue
+ disabled_k = None
+ if k.startswith('!'):
+ disabled_k = k[1:]
+ elif v is None or v == path_info.fields[k].remove_value:
+ disabled_k = k
+ if disabled_k is not None:
+ if disabled_k in old_entry:
+ return None
+ continue
+ if k not in old_entry and path_info.fields[k].default == v:
+ continue
+ if k not in old_entry or old_entry[k] != v:
+ return None
+ handle_entries_content = module.params['handle_entries_content']
+ weight = 0
+ for k in old_entry:
+ if k == '.id' or k in new_entry or ('!%s' % k) in new_entry or k not in path_info.fields:
+ continue
+ field_info = path_info.fields[k]
+ if field_info.default is not None and field_info.default == old_entry[k]:
+ continue
+ if handle_entries_content != 'ignore':
+ return None
+ else:
+ weight += 1
+ return weight
+
+
+def format_pk(primary_keys, values):
+ return ', '.join('{pk}="{value}"'.format(pk=pk, value=value) for pk, value in zip(primary_keys, values))
+
+
+def polish_entry(entry, path_info, module, for_text):
+ if '.id' in entry:
+ entry.pop('.id')
+ for key, value in entry.items():
+ real_key = key
+ disabled_key = False
+ if key.startswith('!'):
+ disabled_key = True
+ key = key[1:]
+ if key in entry:
+ module.fail_json(msg='Not both "{key}" and "!{key}" must appear{for_text}.'.format(key=key, for_text=for_text))
+ key_info = path_info.fields.get(key)
+ if key_info is None:
+ module.fail_json(msg='Unknown key "{key}"{for_text}.'.format(key=real_key, for_text=for_text))
+ if disabled_key:
+ if not key_info.can_disable:
+ module.fail_json(msg='Key "!{key}" must not be disabled (leading "!"){for_text}.'.format(key=key, for_text=for_text))
+ if value not in (None, '', key_info.remove_value):
+ module.fail_json(msg='Disabled key "!{key}" must not have a value{for_text}.'.format(key=key, for_text=for_text))
+ elif value is None:
+ if not key_info.can_disable:
+ module.fail_json(msg='Key "{key}" must not be disabled (value null/~/None){for_text}.'.format(key=key, for_text=for_text))
+ for key, field_info in path_info.fields.items():
+ if field_info.required and key not in entry:
+ module.fail_json(msg='Key "{key}" must be present{for_text}.'.format(key=key, for_text=for_text))
+ for require_list in path_info.required_one_of:
+ found_req_keys = [rk for rk in require_list if rk in entry]
+ if len(require_list) > 0 and not found_req_keys:
+ module.fail_json(
+ msg='Every element in data must contain one of {required_keys}. For example, the element{for_text} does not provide it.'.format(
+ required_keys=', '.join(['"{k}"'.format(k=k) for k in require_list]),
+ for_text=for_text,
+ )
+ )
+ for exclusive_list in path_info.mutually_exclusive:
+ found_ex_keys = [ek for ek in exclusive_list if ek in entry]
+ if len(found_ex_keys) > 1:
+ module.fail_json(
+ msg='Keys {exclusive_keys} cannot be used at the same time{for_text}.'.format(
+ exclusive_keys=', '.join(['"{k}"'.format(k=k) for k in found_ex_keys]),
+ for_text=for_text,
+ )
+ )
+
+
+def remove_irrelevant_data(entry, path_info):
+ for k, v in list(entry.items()):
+ if k == '.id':
+ continue
+ if k not in path_info.fields or v is None:
+ del entry[k]
+
+
+def match_entries(new_entries, old_entries, path_info, module):
+ matching_old_entries = [None for entry in new_entries]
+ old_entries = list(old_entries)
+ matches = []
+ handle_absent_entries = module.params['handle_absent_entries']
+ if handle_absent_entries == 'remove':
+ for new_index, (unused, new_entry) in enumerate(new_entries):
+ for old_index, (unused, old_entry) in enumerate(old_entries):
+ modifications, unused = find_modifications(old_entry, new_entry, path_info, module, return_none_instead_of_fail=True)
+ if modifications is not None:
+ matches.append((new_index, old_index, len(modifications)))
+ else:
+ for new_index, (unused, new_entry) in enumerate(new_entries):
+ for old_index, (unused, old_entry) in enumerate(old_entries):
+ weight = essentially_same_weight(old_entry, new_entry, path_info, module)
+ if weight is not None:
+ matches.append((new_index, old_index, weight))
+ matches.sort(key=lambda entry: entry[2])
+ for new_index, old_index, rating in matches:
+ if matching_old_entries[new_index] is not None or old_entries[old_index] is None:
+ continue
+ matching_old_entries[new_index], old_entries[old_index] = old_entries[old_index], None
+ unmatched_old_entries = [index_entry for index_entry in old_entries if index_entry is not None]
+ return matching_old_entries, unmatched_old_entries
+
+
+def remove_dynamic(entries):
+ result = []
+ for entry in entries:
+ if entry.get('dynamic', False) or entry.get('builtin', False):
+ continue
+ result.append(entry)
+ return result
+
+
+def get_api_data(api_path, path_info):
+ entries = list(api_path)
+ for entry in entries:
+ for k, field_info in path_info.fields.items():
+ if field_info.absent_value is not None and k not in entry:
+ entry[k] = field_info.absent_value
+ return entries
+
+
+def prepare_for_add(entry, path_info):
+ new_entry = {}
+ for k, v in entry.items():
+ if k.startswith('!'):
+ real_k = k[1:]
+ remove_value = path_info.fields[real_k].remove_value
+ if remove_value is not None:
+ k = real_k
+ v = remove_value
+ else:
+ if v is None:
+ v = path_info.fields[k].remove_value
+ new_entry[k] = v
+ return new_entry
+
+
+def sync_list(module, api, path, path_info):
+ handle_absent_entries = module.params['handle_absent_entries']
+ handle_entries_content = module.params['handle_entries_content']
+ if handle_absent_entries == 'remove':
+ if handle_entries_content == 'ignore':
+ module.fail_json('For this path, handle_absent_entries=remove cannot be combined with handle_entries_content=ignore')
+
+ stratify_keys = path_info.stratify_keys or ()
+
+ data = module.params['data']
+ stratified_data = defaultdict(list)
+ for index, entry in enumerate(data):
+ for stratify_key in stratify_keys:
+ if stratify_key not in entry:
+ module.fail_json(
+ msg='Every element in data must contain "{stratify_key}". For example, the element at index #{index} does not provide it.'.format(
+ stratify_key=stratify_key,
+ index=index + 1,
+ )
+ )
+ sks = tuple(entry[stratify_key] for stratify_key in stratify_keys)
+ polish_entry(
+ entry, path_info, module,
+ ' at index {index}'.format(index=index + 1),
+ )
+ stratified_data[sks].append((index, entry))
+ stratified_data = dict(stratified_data)
+
+ api_path = compose_api_path(api, path)
+
+ old_data = get_api_data(api_path, path_info)
+ old_data = remove_dynamic(old_data)
+ stratified_old_data = defaultdict(list)
+ for index, entry in enumerate(old_data):
+ sks = tuple(entry[stratify_key] for stratify_key in stratify_keys)
+ stratified_old_data[sks].append((index, entry))
+ stratified_old_data = dict(stratified_old_data)
+
+ create_list = []
+ modify_list = []
+ remove_list = []
+
+ new_data = []
+ for key, indexed_entries in stratified_data.items():
+ old_entries = stratified_old_data.pop(key, [])
+
+ # Try to match indexed_entries with old_entries
+ matching_old_entries, unmatched_old_entries = match_entries(indexed_entries, old_entries, path_info, module)
+
+ # Update existing entries
+ for (index, new_entry), potential_old_entry in zip(indexed_entries, matching_old_entries):
+ if potential_old_entry is not None:
+ old_index, old_entry = potential_old_entry
+ modifications, updated_entry = find_modifications(
+ old_entry, new_entry, path_info, module,
+ ' at index {index}'.format(index=index + 1),
+ )
+ # Add to modification list if there are changes
+ if modifications:
+ modifications['.id'] = old_entry['.id']
+ modify_list.append(modifications)
+ new_data.append((old_index, updated_entry))
+ new_entry['.id'] = old_entry['.id']
+ else:
+ create_list.append(new_entry)
+
+ if handle_absent_entries == 'remove':
+ remove_list.extend(entry['.id'] for index, entry in unmatched_old_entries)
+ else:
+ new_data.extend(unmatched_old_entries)
+
+ for key, entries in stratified_old_data.items():
+ if handle_absent_entries == 'remove':
+ remove_list.extend(entry['.id'] for index, entry in entries)
+ else:
+ new_data.extend(entries)
+
+ new_data = [entry for index, entry in sorted(new_data, key=lambda entry: entry[0])]
+ new_data.extend(create_list)
+
+ reorder_list = []
+ if module.params['ensure_order']:
+ for index, entry in enumerate(data):
+ if '.id' in entry:
+ def match(current_entry):
+ return current_entry['.id'] == entry['.id']
+
+ else:
+ def match(current_entry):
+ return current_entry is entry
+
+ current_index = next(current_index + index for current_index, current_entry in enumerate(new_data[index:]) if match(current_entry))
+ if current_index != index:
+ reorder_list.append((index, new_data[current_index], new_data[index]))
+ new_data.insert(index, new_data.pop(current_index))
+
+ if not module.check_mode:
+ if remove_list:
+ try:
+ api_path.remove(*remove_list)
+ except (LibRouterosError, UnicodeEncodeError) as e:
+ module.fail_json(
+ msg='Error while removing {remove_list}: {error}'.format(
+ remove_list=', '.join(['ID {id}'.format(id=id) for id in remove_list]),
+ error=to_native(e),
+ )
+ )
+ for modifications in modify_list:
+ try:
+ api_path.update(**modifications)
+ except (LibRouterosError, UnicodeEncodeError) as e:
+ module.fail_json(
+ msg='Error while modifying for ID {id}: {error}'.format(
+ id=modifications['.id'],
+ error=to_native(e),
+ )
+ )
+ for entry in create_list:
+ try:
+ entry['.id'] = api_path.add(**prepare_for_add(entry, path_info))
+ except (LibRouterosError, UnicodeEncodeError) as e:
+ module.fail_json(
+ msg='Error while creating entry: {error}'.format(
+ error=to_native(e),
+ )
+ )
+ for new_index, new_entry, old_entry in reorder_list:
+ try:
+ for res in api_path('move', numbers=new_entry['.id'], destination=old_entry['.id']):
+ pass
+ except (LibRouterosError, UnicodeEncodeError) as e:
+ module.fail_json(
+ msg='Error while moving entry ID {element_id} to position #{new_index} ID ({new_id}): {error}'.format(
+ element_id=new_entry['.id'],
+ new_index=new_index,
+ new_id=old_entry['.id'],
+ error=to_native(e),
+ )
+ )
+
+ # For sake of completeness, retrieve the full new data:
+ if modify_list or create_list or reorder_list:
+ new_data = remove_dynamic(get_api_data(api_path, path_info))
+
+ # Remove 'irrelevant' data
+ for entry in old_data:
+ remove_irrelevant_data(entry, path_info)
+ for entry in new_data:
+ remove_irrelevant_data(entry, path_info)
+
+ # Produce return value
+ more = {}
+ if module._diff:
+ more['diff'] = {
+ 'before': {
+ 'data': old_data,
+ },
+ 'after': {
+ 'data': new_data,
+ },
+ }
+ module.exit_json(
+ changed=bool(create_list or modify_list or remove_list or reorder_list),
+ old_data=old_data,
+ new_data=new_data,
+ **more
+ )
+
+
+def sync_with_primary_keys(module, api, path, path_info):
+ primary_keys = path_info.primary_keys
+
+ if path_info.fixed_entries:
+ if module.params['ensure_order']:
+ module.fail_json(msg='ensure_order=true cannot be used with this path')
+ if module.params['handle_absent_entries'] == 'remove':
+ module.fail_json(msg='handle_absent_entries=remove cannot be used with this path')
+
+ data = module.params['data']
+ new_data_by_key = OrderedDict()
+ for index, entry in enumerate(data):
+ for primary_key in primary_keys:
+ if primary_key not in entry:
+ module.fail_json(
+ msg='Every element in data must contain "{primary_key}". For example, the element at index #{index} does not provide it.'.format(
+ primary_key=primary_key,
+ index=index + 1,
+ )
+ )
+ pks = tuple(entry[primary_key] for primary_key in primary_keys)
+ if pks in new_data_by_key:
+ module.fail_json(
+ msg='Every element in data must contain a unique value for {primary_keys}. The value {value} appears at least twice.'.format(
+ primary_keys=','.join(primary_keys),
+ value=','.join(['"{0}"'.format(pk) for pk in pks]),
+ )
+ )
+ polish_entry(
+ entry, path_info, module,
+ ' for {values}'.format(
+ values=', '.join([
+ '{primary_key}="{value}"'.format(primary_key=primary_key, value=value)
+ for primary_key, value in zip(primary_keys, pks)
+ ])
+ ),
+ )
+ new_data_by_key[pks] = entry
+
+ api_path = compose_api_path(api, path)
+
+ old_data = get_api_data(api_path, path_info)
+ old_data = remove_dynamic(old_data)
+ old_data_by_key = OrderedDict()
+ id_by_key = {}
+ for entry in old_data:
+ pks = tuple(entry[primary_key] for primary_key in primary_keys)
+ old_data_by_key[pks] = entry
+ id_by_key[pks] = entry['.id']
+ new_data = []
+
+ create_list = []
+ modify_list = []
+ remove_list = []
+ remove_keys = []
+ handle_absent_entries = module.params['handle_absent_entries']
+ for key, old_entry in old_data_by_key.items():
+ new_entry = new_data_by_key.pop(key, None)
+ if new_entry is None:
+ if handle_absent_entries == 'remove':
+ remove_list.append(old_entry['.id'])
+ remove_keys.append(key)
+ else:
+ new_data.append(old_entry)
+ else:
+ modifications, updated_entry = find_modifications(
+ old_entry, new_entry, path_info, module,
+ ' for {values}'.format(
+ values=', '.join([
+ '{primary_key}="{value}"'.format(primary_key=primary_key, value=value)
+ for primary_key, value in zip(primary_keys, key)
+ ])
+ )
+ )
+ new_data.append(updated_entry)
+ # Add to modification list if there are changes
+ if modifications:
+ modifications['.id'] = old_entry['.id']
+ modify_list.append((key, modifications))
+ for new_entry in new_data_by_key.values():
+ if path_info.fixed_entries:
+ module.fail_json(msg='Cannot add new entry {values} to this path'.format(
+ values=', '.join([
+ '{primary_key}="{value}"'.format(primary_key=primary_key, value=new_entry[primary_key])
+ for primary_key in primary_keys
+ ]),
+ ))
+ create_list.append(new_entry)
+ new_entry = new_entry.copy()
+ for key in list(new_entry):
+ if key.startswith('!'):
+ new_entry.pop(key)
+ new_data.append(new_entry)
+
+ reorder_list = []
+ if module.params['ensure_order']:
+ index_by_key = dict()
+ for index, entry in enumerate(new_data):
+ index_by_key[tuple(entry[primary_key] for primary_key in primary_keys)] = index
+ for index, source_entry in enumerate(data):
+ source_pks = tuple(source_entry[primary_key] for primary_key in primary_keys)
+ source_index = index_by_key.pop(source_pks)
+ if index == source_index:
+ continue
+ entry = new_data[index]
+ pks = tuple(entry[primary_key] for primary_key in primary_keys)
+ reorder_list.append((source_pks, index, pks))
+ for k, v in index_by_key.items():
+ if v >= index and v < source_index:
+ index_by_key[k] = v + 1
+ new_data.insert(index, new_data.pop(source_index))
+
+ if not module.check_mode:
+ if remove_list:
+ try:
+ api_path.remove(*remove_list)
+ except (LibRouterosError, UnicodeEncodeError) as e:
+ module.fail_json(
+ msg='Error while removing {remove_list}: {error}'.format(
+ remove_list=', '.join([
+ '{identifier} (ID {id})'.format(identifier=format_pk(primary_keys, key), id=id)
+ for id, key in zip(remove_list, remove_keys)
+ ]),
+ error=to_native(e),
+ )
+ )
+ for key, modifications in modify_list:
+ try:
+ api_path.update(**modifications)
+ except (LibRouterosError, UnicodeEncodeError) as e:
+ module.fail_json(
+ msg='Error while modifying for {identifier} (ID {id}): {error}'.format(
+ identifier=format_pk(primary_keys, key),
+ id=modifications['.id'],
+ error=to_native(e),
+ )
+ )
+ for entry in create_list:
+ try:
+ entry['.id'] = api_path.add(**prepare_for_add(entry, path_info))
+ # Store ID for primary keys
+ pks = tuple(entry[primary_key] for primary_key in primary_keys)
+ id_by_key[pks] = entry['.id']
+ except (LibRouterosError, UnicodeEncodeError) as e:
+ module.fail_json(
+ msg='Error while creating entry for {identifier}: {error}'.format(
+ identifier=format_pk(primary_keys, [entry[pk] for pk in primary_keys]),
+ error=to_native(e),
+ )
+ )
+ for element_pks, new_index, new_pks in reorder_list:
+ try:
+ element_id = id_by_key[element_pks]
+ new_id = id_by_key[new_pks]
+ for res in api_path('move', numbers=element_id, destination=new_id):
+ pass
+ except (LibRouterosError, UnicodeEncodeError) as e:
+ module.fail_json(
+ msg='Error while moving entry ID {element_id} to position of ID {new_id}: {error}'.format(
+ element_id=element_id,
+ new_id=new_id,
+ error=to_native(e),
+ )
+ )
+
+ # For sake of completeness, retrieve the full new data:
+ if modify_list or create_list or reorder_list:
+ new_data = remove_dynamic(get_api_data(api_path, path_info))
+
+ # Remove 'irrelevant' data
+ for entry in old_data:
+ remove_irrelevant_data(entry, path_info)
+ for entry in new_data:
+ remove_irrelevant_data(entry, path_info)
+
+ # Produce return value
+ more = {}
+ if module._diff:
+ more['diff'] = {
+ 'before': {
+ 'data': old_data,
+ },
+ 'after': {
+ 'data': new_data,
+ },
+ }
+ module.exit_json(
+ changed=bool(create_list or modify_list or remove_list or reorder_list),
+ old_data=old_data,
+ new_data=new_data,
+ **more
+ )
+
+
+def sync_single_value(module, api, path, path_info):
+ data = module.params['data']
+ if len(data) != 1:
+ module.fail_json(msg='Data must be a list with exactly one element.')
+ new_entry = data[0]
+ polish_entry(new_entry, path_info, module, '')
+
+ api_path = compose_api_path(api, path)
+
+ old_data = get_api_data(api_path, path_info)
+ if len(old_data) != 1:
+ module.fail_json(
+ msg='Internal error: retrieving /{path} resulted in {count} elements. Expected exactly 1.'.format(
+ path=join_path(path),
+ count=len(old_data)
+ )
+ )
+ old_entry = old_data[0]
+
+ # Determine modifications
+ modifications, updated_entry = find_modifications(old_entry, new_entry, path_info, module, '')
+ # Do modifications
+ if modifications:
+ if not module.check_mode:
+ # Actually do modification
+ try:
+ api_path.update(**modifications)
+ except (LibRouterosError, UnicodeEncodeError) as e:
+ module.fail_json(msg='Error while modifying: {error}'.format(error=to_native(e)))
+ # Retrieve latest version
+ new_data = get_api_data(api_path, path_info)
+ if len(new_data) == 1:
+ updated_entry = new_data[0]
+
+ # Remove 'irrelevant' data
+ remove_irrelevant_data(old_entry, path_info)
+ remove_irrelevant_data(updated_entry, path_info)
+
+ # Produce return value
+ more = {}
+ if module._diff:
+ more['diff'] = {
+ 'before': old_entry,
+ 'after': updated_entry,
+ }
+ module.exit_json(
+ changed=bool(modifications),
+ old_data=[old_entry],
+ new_data=[updated_entry],
+ **more
+ )
+
+
+def get_backend(path_info):
+ if path_info is None:
+ return None
+ if not path_info.fully_understood:
+ return None
+
+ if path_info.primary_keys:
+ return sync_with_primary_keys
+
+ if path_info.single_value:
+ return sync_single_value
+
+ if not path_info.has_identifier:
+ return sync_list
+
+ return None
+
+
+def main():
+ path_choices = sorted([join_path(path) for path, path_info in PATHS.items() if get_backend(path_info) is not None])
+ module_args = dict(
+ path=dict(type='str', required=True, choices=path_choices),
+ data=dict(type='list', elements='dict', required=True),
+ handle_absent_entries=dict(type='str', choices=['ignore', 'remove'], default='ignore'),
+ handle_entries_content=dict(type='str', choices=['ignore', 'remove', 'remove_as_much_as_possible'], default='ignore'),
+ ensure_order=dict(type='bool', default=False),
+ )
+ module_args.update(api_argument_spec())
+
+ module = AnsibleModule(
+ argument_spec=module_args,
+ supports_check_mode=True,
+ )
+ if module.params['ensure_order'] and module.params['handle_absent_entries'] == 'ignore':
+ module.fail_json(msg='ensure_order=true requires handle_absent_entries=remove')
+
+ if not HAS_ORDEREDDICT:
+ # This should never happen for Python 2.7+
+ module.fail_json(msg=missing_required_lib('ordereddict'))
+
+ check_has_library(module)
+ api = create_api(module)
+
+ path = split_path(module.params['path'])
+ path_info = PATHS.get(tuple(path))
+ backend = get_backend(path_info)
+ if path_info is None or backend is None:
+ module.fail_json(msg='Path /{path} is not yet supported'.format(path='/'.join(path)))
+
+ backend(module, api, path, path_info)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/routeros/plugins/modules/command.py b/ansible_collections/community/routeros/plugins/modules/command.py
new file mode 100644
index 000000000..84426025c
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/modules/command.py
@@ -0,0 +1,210 @@
+#!/usr/bin/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
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: command
+author: "Egor Zaitsev (@heuels)"
+short_description: Run commands on remote devices running MikroTik RouterOS
+description:
+ - Sends arbitrary commands to an RouterOS node and returns the results
+ read from the device. This module includes an
+ argument that will cause the module to wait for a specific condition
+ before returning or timing out if the condition is not met.
+ - The module always indicates a (changed) status. You can use
+ R(the changed_when task property,override_the_changed_result) to determine
+ whether a command task actually resulted in a change or not.
+notes:
+ - The module declares that it B(supports check mode). This is a bug and will
+ be changed in community.routeros 3.0.0.
+extends_documentation_fragment:
+ - community.routeros.attributes
+attributes:
+ check_mode:
+ support: partial
+ details:
+ - The module claims to support check mode, but it simply always executes the command.
+ diff_mode:
+ support: none
+ platform:
+ support: full
+ platforms: RouterOS
+options:
+ commands:
+ description:
+ - List of commands to send to the remote RouterOS device over the
+ configured provider. The resulting output from the command
+ is returned. If the I(wait_for) argument is provided, the
+ module is not returned until the condition is satisfied or
+ the number of retries has expired.
+ required: true
+ type: list
+ elements: str
+ wait_for:
+ description:
+ - List of conditions to evaluate against the output of the
+ command. The task will wait for each condition to be true
+ before moving forward. If the conditional is not true
+ within the configured number of retries, the task fails.
+ See examples.
+ type: list
+ elements: str
+ match:
+ description:
+ - The I(match) argument is used in conjunction with the
+ I(wait_for) argument to specify the match policy. Valid
+ values are C(all) or C(any). If the value is set to C(all)
+ then all conditionals in the wait_for must be satisfied. If
+ the value is set to C(any) then only one of the values must be
+ satisfied.
+ default: all
+ choices: ['any', 'all']
+ type: str
+ retries:
+ description:
+ - Specifies the number of retries a command should by tried
+ before it is considered failed. The command is run on the
+ target device every retry and evaluated against the
+ I(wait_for) conditions.
+ default: 10
+ type: int
+ interval:
+ description:
+ - Configures the interval in seconds to wait between retries
+ of the command. If the command does not pass the specified
+ conditions, the interval indicates how long to wait before
+ trying the command again.
+ default: 1
+ type: int
+seealso:
+ - ref: ansible_collections.community.routeros.docsite.ssh-guide
+ description: How to connect to RouterOS devices with SSH
+ - ref: ansible_collections.community.routeros.docsite.quoting
+ description: How to quote and unquote commands and arguments
+'''
+
+EXAMPLES = """
+- name: Run command on remote devices
+ community.routeros.command:
+ commands: /system routerboard print
+
+- name: Run command and check to see if output contains routeros
+ community.routeros.command:
+ commands: /system resource print
+ wait_for: result[0] contains MikroTik
+
+- name: Run multiple commands on remote nodes
+ community.routeros.command:
+ commands:
+ - /system routerboard print
+ - /system identity print
+
+- name: Run multiple commands and evaluate the output
+ community.routeros.command:
+ commands:
+ - /system routerboard print
+ - /interface ethernet print
+ wait_for:
+ - result[0] contains x86
+ - result[1] contains ether1
+"""
+
+RETURN = """
+stdout:
+ description: The set of responses from the commands
+ returned: always apart from low level errors (such as action plugin)
+ type: list
+ sample: ['...', '...']
+stdout_lines:
+ description: The value of stdout split into a list
+ returned: always apart from low level errors (such as action plugin)
+ type: list
+ sample: [['...', '...'], ['...'], ['...']]
+failed_conditions:
+ description: The list of conditionals that have failed
+ returned: failed
+ type: list
+ sample: ['...', '...']
+"""
+
+import time
+
+from ansible_collections.community.routeros.plugins.module_utils.routeros import run_commands
+from ansible_collections.community.routeros.plugins.module_utils.routeros import routeros_argument_spec
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.parsing import Conditional
+from ansible.module_utils.six import string_types
+
+
+def to_lines(stdout):
+ for item in stdout:
+ if isinstance(item, string_types):
+ item = str(item).split('\n')
+ yield item
+
+
+def main():
+ """main entry point for module execution
+ """
+ argument_spec = dict(
+ commands=dict(type='list', elements='str', required=True),
+
+ wait_for=dict(type='list', elements='str'),
+ match=dict(type='str', default='all', choices=['all', 'any']),
+
+ retries=dict(default=10, type='int'),
+ interval=dict(default=1, type='int')
+ )
+
+ argument_spec.update(routeros_argument_spec)
+
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True)
+
+ result = {'changed': False}
+
+ wait_for = module.params['wait_for'] or list()
+ conditionals = [Conditional(c) for c in wait_for]
+
+ retries = module.params['retries']
+ interval = module.params['interval']
+ match = module.params['match']
+
+ while retries > 0:
+ responses = run_commands(module, module.params['commands'])
+
+ for item in list(conditionals):
+ if item(responses):
+ if match == 'any':
+ conditionals = list()
+ break
+ conditionals.remove(item)
+
+ if not conditionals:
+ break
+
+ time.sleep(interval)
+ retries -= 1
+
+ if conditionals:
+ failed_conditions = [item.raw for item in conditionals]
+ msg = 'One or more conditional statements have not been satisfied'
+ module.fail_json(msg=msg, failed_conditions=failed_conditions)
+
+ result.update({
+ 'changed': True,
+ 'stdout': responses,
+ 'stdout_lines': list(to_lines(responses))
+ })
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/routeros/plugins/modules/facts.py b/ansible_collections/community/routeros/plugins/modules/facts.py
new file mode 100644
index 000000000..85c0b37c4
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/modules/facts.py
@@ -0,0 +1,663 @@
+#!/usr/bin/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
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: facts
+author: "Egor Zaitsev (@heuels)"
+short_description: Collect facts from remote devices running MikroTik RouterOS
+description:
+ - Collects a base set of device facts from a remote device that
+ is running RouterOS. This module prepends all of the
+ base network fact keys with C(ansible_net_<fact>). The facts
+ module will always collect a base set of facts from the device
+ and can enable or disable collection of additional facts.
+extends_documentation_fragment:
+ - community.routeros.attributes
+ - community.routeros.attributes.facts
+ - community.routeros.attributes.facts_module
+attributes:
+ platform:
+ support: full
+ platforms: RouterOS
+options:
+ gather_subset:
+ description:
+ - When supplied, this argument will restrict the facts collected
+ to a given subset. Possible values for this argument include
+ C(all), C(hardware), C(config), C(interfaces), and C(routing).
+ - Can specify a list of values to include a larger subset.
+ Values can also be used with an initial C(!) to specify that a
+ specific subset should not be collected.
+ required: false
+ default:
+ - '!config'
+ type: list
+ elements: str
+seealso:
+ - ref: ansible_collections.community.routeros.docsite.ssh-guide
+ description: How to connect to RouterOS devices with SSH
+'''
+
+EXAMPLES = """
+- name: Collect all facts from the device
+ community.routeros.facts:
+ gather_subset: all
+
+- name: Collect only the config and default facts
+ community.routeros.facts:
+ gather_subset:
+ - config
+
+- name: Do not collect hardware facts
+ community.routeros.facts:
+ gather_subset:
+ - "!hardware"
+"""
+
+RETURN = """
+ansible_facts:
+ description: "Dictionary of IP geolocation facts for a host's IP address."
+ returned: always
+ type: dict
+ contains:
+ ansible_net_gather_subset:
+ description: The list of fact subsets collected from the device.
+ returned: always
+ type: list
+
+ # default
+ ansible_net_model:
+ description: The model name returned from the device.
+ returned: I(gather_subset) contains C(default)
+ type: str
+ ansible_net_serialnum:
+ description: The serial number of the remote device.
+ returned: I(gather_subset) contains C(default)
+ type: str
+ ansible_net_version:
+ description: The operating system version running on the remote device.
+ returned: I(gather_subset) contains C(default)
+ type: str
+ ansible_net_hostname:
+ description: The configured hostname of the device.
+ returned: I(gather_subset) contains C(default)
+ type: str
+ ansible_net_arch:
+ description: The CPU architecture of the device.
+ returned: I(gather_subset) contains C(default)
+ type: str
+ ansible_net_uptime:
+ description: The uptime of the device.
+ returned: I(gather_subset) contains C(default)
+ type: str
+ ansible_net_cpu_load:
+ description: Current CPU load.
+ returned: I(gather_subset) contains C(default)
+ type: str
+
+ # hardware
+ ansible_net_spacefree_mb:
+ description: The available disk space on the remote device in MiB.
+ returned: I(gather_subset) contains C(hardware)
+ type: dict
+ ansible_net_spacetotal_mb:
+ description: The total disk space on the remote device in MiB.
+ returned: I(gather_subset) contains C(hardware)
+ type: dict
+ ansible_net_memfree_mb:
+ description: The available free memory on the remote device in MiB.
+ returned: I(gather_subset) contains C(hardware)
+ type: int
+ ansible_net_memtotal_mb:
+ description: The total memory on the remote device in MiB.
+ returned: I(gather_subset) contains C(hardware)
+ type: int
+
+ # config
+ ansible_net_config:
+ description: The current active config from the device.
+ returned: I(gather_subset) contains C(config)
+ type: str
+
+ ansible_net_config_nonverbose:
+ description:
+ - The current active config from the device in minimal form.
+ - This value is idempotent in the sense that if the facts module is run twice and the device's config
+ was not changed between the runs, the value is identical. This is achieved by running C(/export)
+ and stripping the timestamp from the comment in the first line.
+ returned: I(gather_subset) contains C(config)
+ type: str
+ version_added: 1.2.0
+
+ # interfaces
+ ansible_net_all_ipv4_addresses:
+ description: All IPv4 addresses configured on the device.
+ returned: I(gather_subset) contains C(interfaces)
+ type: list
+ ansible_net_all_ipv6_addresses:
+ description: All IPv6 addresses configured on the device.
+ returned: I(gather_subset) contains C(interfaces)
+ type: list
+ ansible_net_interfaces:
+ description: A hash of all interfaces running on the system.
+ returned: I(gather_subset) contains C(interfaces)
+ type: dict
+ ansible_net_neighbors:
+ description: The list of neighbors from the remote device.
+ returned: I(gather_subset) contains C(interfaces)
+ type: dict
+
+ # routing
+ ansible_net_bgp_peer:
+ description: A dictionary with BGP peer information.
+ returned: I(gather_subset) contains C(routing)
+ type: dict
+ ansible_net_bgp_vpnv4_route:
+ description: A dictionary with BGP vpnv4 route information.
+ returned: I(gather_subset) contains C(routing)
+ type: dict
+ ansible_net_bgp_instance:
+ description: A dictionary with BGP instance information.
+ returned: I(gather_subset) contains C(routing)
+ type: dict
+ ansible_net_route:
+ description: A dictionary for routes in all routing tables.
+ returned: I(gather_subset) contains C(routing)
+ type: dict
+ ansible_net_ospf_instance:
+ description: A dictionary with OSPF instances.
+ returned: I(gather_subset) contains C(routing)
+ type: dict
+ ansible_net_ospf_neighbor:
+ description: A dictionary with OSPF neighbors.
+ returned: I(gather_subset) contains C(routing)
+ type: dict
+"""
+import re
+
+from ansible_collections.community.routeros.plugins.module_utils.routeros import run_commands
+from ansible_collections.community.routeros.plugins.module_utils.routeros import routeros_argument_spec
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.six import iteritems
+
+
+class FactsBase(object):
+
+ COMMANDS = list()
+
+ def __init__(self, module):
+ self.module = module
+ self.facts = dict()
+ self.responses = None
+
+ def populate(self):
+ self.responses = run_commands(self.module, commands=self.COMMANDS, check_rc=False)
+
+ def run(self, cmd):
+ return run_commands(self.module, commands=cmd, check_rc=False)
+
+
+class Default(FactsBase):
+
+ COMMANDS = [
+ '/system identity print without-paging',
+ '/system resource print without-paging',
+ '/system routerboard print without-paging'
+ ]
+
+ def populate(self):
+ super(Default, self).populate()
+ data = self.responses[0]
+ if data:
+ self.facts['hostname'] = self.parse_hostname(data)
+ data = self.responses[1]
+ if data:
+ self.facts['version'] = self.parse_version(data)
+ self.facts['arch'] = self.parse_arch(data)
+ self.facts['uptime'] = self.parse_uptime(data)
+ self.facts['cpu_load'] = self.parse_cpu_load(data)
+ data = self.responses[2]
+ if data:
+ self.facts['model'] = self.parse_model(data)
+ self.facts['serialnum'] = self.parse_serialnum(data)
+
+ def parse_hostname(self, data):
+ match = re.search(r'name:\s(.*)\s*$', data, re.M)
+ if match:
+ return match.group(1)
+
+ def parse_version(self, data):
+ match = re.search(r'version:\s(.*)\s*$', data, re.M)
+ if match:
+ return match.group(1)
+
+ def parse_model(self, data):
+ match = re.search(r'model:\s(.*)\s*$', data, re.M)
+ if match:
+ return match.group(1)
+
+ def parse_arch(self, data):
+ match = re.search(r'architecture-name:\s(.*)\s*$', data, re.M)
+ if match:
+ return match.group(1)
+
+ def parse_uptime(self, data):
+ match = re.search(r'uptime:\s(.*)\s*$', data, re.M)
+ if match:
+ return match.group(1)
+
+ def parse_cpu_load(self, data):
+ match = re.search(r'cpu-load:\s(.*)\s*$', data, re.M)
+ if match:
+ return match.group(1)
+
+ def parse_serialnum(self, data):
+ match = re.search(r'serial-number:\s(.*)\s*$', data, re.M)
+ if match:
+ return match.group(1)
+
+
+class Hardware(FactsBase):
+
+ COMMANDS = [
+ '/system resource print without-paging'
+ ]
+
+ def populate(self):
+ super(Hardware, self).populate()
+ data = self.responses[0]
+ if data:
+ self.parse_filesystem_info(data)
+ self.parse_memory_info(data)
+
+ def parse_filesystem_info(self, data):
+ match = re.search(r'free-hdd-space:\s(.*)([KMG]iB)', data, re.M)
+ if match:
+ self.facts['spacefree_mb'] = self.to_megabytes(match)
+ match = re.search(r'total-hdd-space:\s(.*)([KMG]iB)', data, re.M)
+ if match:
+ self.facts['spacetotal_mb'] = self.to_megabytes(match)
+
+ def parse_memory_info(self, data):
+ match = re.search(r'free-memory:\s(\d+\.?\d*)([KMG]iB)', data, re.M)
+ if match:
+ self.facts['memfree_mb'] = self.to_megabytes(match)
+ match = re.search(r'total-memory:\s(\d+\.?\d*)([KMG]iB)', data, re.M)
+ if match:
+ self.facts['memtotal_mb'] = self.to_megabytes(match)
+
+ def to_megabytes(self, data):
+ if data.group(2) == 'KiB':
+ return float(data.group(1)) / 1024
+ elif data.group(2) == 'MiB':
+ return float(data.group(1))
+ elif data.group(2) == 'GiB':
+ return float(data.group(1)) * 1024
+ else:
+ return None
+
+
+class Config(FactsBase):
+
+ COMMANDS = [
+ '/export verbose',
+ '/export',
+ ]
+
+ RM_DATE_RE = re.compile(r'^# [a-z0-9/][a-z0-9/]* [0-9:]* by RouterOS')
+
+ def populate(self):
+ super(Config, self).populate()
+
+ data = self.responses[0]
+ if data:
+ self.facts['config'] = data
+
+ data = self.responses[1]
+ if data:
+ # remove datetime
+ data = re.sub(self.RM_DATE_RE, r'# RouterOS', data)
+ self.facts['config_nonverbose'] = data
+
+
+class Interfaces(FactsBase):
+
+ COMMANDS = [
+ '/interface print detail without-paging',
+ '/ip address print detail without-paging',
+ '/ipv6 address print detail without-paging',
+ '/ip neighbor print detail without-paging'
+ ]
+
+ DETAIL_RE = re.compile(r'([\w\d\-]+)=\"?(\w{3}/\d{2}/\d{4}\s\d{2}:\d{2}:\d{2}|[\w\d\-\.:/]+)')
+ WRAPPED_LINE_RE = re.compile(r'^\s+(?!\d)')
+
+ def populate(self):
+ super(Interfaces, self).populate()
+
+ self.facts['interfaces'] = dict()
+ self.facts['all_ipv4_addresses'] = list()
+ self.facts['all_ipv6_addresses'] = list()
+ self.facts['neighbors'] = list()
+
+ data = self.responses[0]
+ if data:
+ interfaces = self.parse_interfaces(data)
+ self.populate_interfaces(interfaces)
+
+ data = self.responses[1]
+ if data:
+ data = self.parse_detail(data)
+ self.populate_addresses(data, 'ipv4')
+
+ data = self.responses[2]
+ if data:
+ data = self.parse_detail(data)
+ self.populate_addresses(data, 'ipv6')
+
+ data = self.responses[3]
+ if data:
+ self.facts['neighbors'] = list(self.parse_detail(data))
+
+ def populate_interfaces(self, data):
+ for key, value in iteritems(data):
+ self.facts['interfaces'][key] = value
+
+ def populate_addresses(self, data, family):
+ for value in data:
+ key = value['interface']
+ if family not in self.facts['interfaces'][key]:
+ self.facts['interfaces'][key][family] = list()
+ addr, subnet = value['address'].split("/")
+ ip = dict(address=addr.strip(), subnet=subnet.strip())
+ self.add_ip_address(addr.strip(), family)
+ self.facts['interfaces'][key][family].append(ip)
+
+ def add_ip_address(self, address, family):
+ if family == 'ipv4':
+ self.facts['all_ipv4_addresses'].append(address)
+ else:
+ self.facts['all_ipv6_addresses'].append(address)
+
+ def preprocess(self, data):
+ preprocessed = list()
+ for line in data.split('\n'):
+ if len(line) == 0 or line[:5] == 'Flags':
+ continue
+ elif not preprocessed or not re.match(self.WRAPPED_LINE_RE, line):
+ preprocessed.append(line)
+ else:
+ preprocessed[-1] += line
+ return preprocessed
+
+ def parse_interfaces(self, data):
+ facts = dict()
+ data = self.preprocess(data)
+ for line in data:
+ parsed = dict(re.findall(self.DETAIL_RE, line))
+ if "name" not in parsed:
+ continue
+ facts[parsed["name"]] = dict(re.findall(self.DETAIL_RE, line))
+ return facts
+
+ def parse_detail(self, data):
+ data = self.preprocess(data)
+ for line in data:
+ parsed = dict(re.findall(self.DETAIL_RE, line))
+ if "interface" not in parsed:
+ continue
+ yield parsed
+
+
+class Routing(FactsBase):
+
+ COMMANDS = [
+ '/routing bgp peer print detail without-paging',
+ '/routing bgp vpnv4-route print detail without-paging',
+ '/routing bgp instance print detail without-paging',
+ '/ip route print detail without-paging',
+ '/routing ospf instance print detail without-paging',
+ '/routing ospf neighbor print detail without-paging'
+ ]
+
+ DETAIL_RE = re.compile(r'([\w\d\-]+)=\"?(\w{3}/\d{2}/\d{4}\s\d{2}:\d{2}:\d{2}|[\w\d\-\.:/]+)')
+ WRAPPED_LINE_RE = re.compile(r'^\s+(?!\d)')
+
+ def populate(self):
+ super(Routing, self).populate()
+ self.facts['bgp_peer'] = dict()
+ self.facts['bgp_vpnv4_route'] = dict()
+ self.facts['bgp_instance'] = dict()
+ self.facts['route'] = dict()
+ self.facts['ospf_instance'] = dict()
+ self.facts['ospf_neighbor'] = dict()
+ data = self.responses[0]
+ if data:
+ peer = self.parse_bgp_peer(data)
+ self.populate_bgp_peer(peer)
+ data = self.responses[1]
+ if data:
+ vpnv4 = self.parse_vpnv4_route(data)
+ self.populate_vpnv4_route(vpnv4)
+ data = self.responses[2]
+ if data:
+ instance = self.parse_instance(data)
+ self.populate_bgp_instance(instance)
+ data = self.responses[3]
+ if data:
+ route = self.parse_route(data)
+ self.populate_route(route)
+ data = self.responses[4]
+ if data:
+ instance = self.parse_instance(data)
+ self.populate_ospf_instance(instance)
+ data = self.responses[5]
+ if data:
+ instance = self.parse_ospf_neighbor(data)
+ self.populate_ospf_neighbor(instance)
+
+ def preprocess(self, data):
+ preprocessed = list()
+ for line in data.split('\n'):
+ if len(line) == 0 or line[:5] == 'Flags':
+ continue
+ elif not preprocessed or not re.match(self.WRAPPED_LINE_RE, line):
+ preprocessed.append(line)
+ else:
+ preprocessed[-1] += line
+ return preprocessed
+
+ def parse_name(self, data):
+ match = re.search(r'name=.(\S+\b)', data, re.M)
+ if match:
+ return match.group(1)
+
+ def parse_interface(self, data):
+ match = re.search(r'interface=([\w\d\-]+)', data, re.M)
+ if match:
+ return match.group(1)
+
+ def parse_instance_name(self, data):
+ match = re.search(r'instance=([\w\d\-]+)', data, re.M)
+ if match:
+ return match.group(1)
+
+ def parse_routing_mark(self, data):
+ match = re.search(r'routing-mark=([\w\d\-]+)', data, re.M)
+ if match:
+ return match.group(1)
+ else:
+ match = 'main'
+ return match
+
+ def parse_bgp_peer(self, data):
+ facts = dict()
+ data = self.preprocess(data)
+ for line in data:
+ name = self.parse_name(line)
+ facts[name] = dict()
+ for (key, value) in re.findall(self.DETAIL_RE, line):
+ facts[name][key] = value
+ return facts
+
+ def parse_instance(self, data):
+ facts = dict()
+ data = self.preprocess(data)
+ for line in data:
+ name = self.parse_name(line)
+ facts[name] = dict()
+ for (key, value) in re.findall(self.DETAIL_RE, line):
+ facts[name][key] = value
+ return facts
+
+ def parse_vpnv4_route(self, data):
+ facts = dict()
+ data = self.preprocess(data)
+ for line in data:
+ name = self.parse_interface(line)
+ facts[name] = dict()
+ for (key, value) in re.findall(self.DETAIL_RE, line):
+ facts[name][key] = value
+ return facts
+
+ def parse_route(self, data):
+ facts = dict()
+ data = self.preprocess(data)
+ for line in data:
+ name = self.parse_routing_mark(line)
+ facts[name] = dict()
+ for (key, value) in re.findall(self.DETAIL_RE, line):
+ facts[name][key] = value
+ return facts
+
+ def parse_ospf_instance(self, data):
+ facts = dict()
+ data = self.preprocess(data)
+ for line in data:
+ name = self.parse_name(line)
+ facts[name] = dict()
+ for (key, value) in re.findall(self.DETAIL_RE, line):
+ facts[name][key] = value
+ return facts
+
+ def parse_ospf_neighbor(self, data):
+ facts = dict()
+ data = self.preprocess(data)
+ for line in data:
+ name = self.parse_instance_name(line)
+ facts[name] = dict()
+ for (key, value) in re.findall(self.DETAIL_RE, line):
+ facts[name][key] = value
+ return facts
+
+ def populate_bgp_peer(self, data):
+ for key, value in iteritems(data):
+ self.facts['bgp_peer'][key] = value
+
+ def populate_vpnv4_route(self, data):
+ for key, value in iteritems(data):
+ self.facts['bgp_vpnv4_route'][key] = value
+
+ def populate_bgp_instance(self, data):
+ for key, value in iteritems(data):
+ self.facts['bgp_instance'][key] = value
+
+ def populate_route(self, data):
+ for key, value in iteritems(data):
+ self.facts['route'][key] = value
+
+ def populate_ospf_instance(self, data):
+ for key, value in iteritems(data):
+ self.facts['ospf_instance'][key] = value
+
+ def populate_ospf_neighbor(self, data):
+ for key, value in iteritems(data):
+ self.facts['ospf_neighbor'][key] = value
+
+
+FACT_SUBSETS = dict(
+ default=Default,
+ hardware=Hardware,
+ interfaces=Interfaces,
+ config=Config,
+ routing=Routing,
+)
+
+VALID_SUBSETS = frozenset(FACT_SUBSETS.keys())
+
+warnings = list()
+
+
+def main():
+ """main entry point for module execution
+ """
+ argument_spec = dict(
+ gather_subset=dict(default=['!config'], type='list', elements='str')
+ )
+
+ argument_spec.update(routeros_argument_spec)
+
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True)
+
+ gather_subset = module.params['gather_subset']
+
+ runable_subsets = set()
+ exclude_subsets = set()
+
+ for subset in gather_subset:
+ if subset == 'all':
+ runable_subsets.update(VALID_SUBSETS)
+ continue
+
+ if subset.startswith('!'):
+ subset = subset[1:]
+ if subset == 'all':
+ exclude_subsets.update(VALID_SUBSETS)
+ continue
+ exclude = True
+ else:
+ exclude = False
+
+ if subset not in VALID_SUBSETS:
+ module.fail_json(msg='Bad subset: %s' % subset)
+
+ if exclude:
+ exclude_subsets.add(subset)
+ else:
+ runable_subsets.add(subset)
+
+ if not runable_subsets:
+ runable_subsets.update(VALID_SUBSETS)
+
+ runable_subsets.difference_update(exclude_subsets)
+ runable_subsets.add('default')
+
+ facts = dict()
+ facts['gather_subset'] = list(runable_subsets)
+
+ instances = list()
+ for key in runable_subsets:
+ instances.append(FACT_SUBSETS[key](module))
+
+ for inst in instances:
+ inst.populate()
+ facts.update(inst.facts)
+
+ ansible_facts = dict()
+ for key, value in iteritems(facts):
+ key = 'ansible_net_%s' % key
+ ansible_facts[key] = value
+
+ module.exit_json(ansible_facts=ansible_facts, warnings=warnings)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/routeros/plugins/terminal/routeros.py b/ansible_collections/community/routeros/plugins/terminal/routeros.py
new file mode 100644
index 000000000..9d50fa25f
--- /dev/null
+++ b/ansible_collections/community/routeros/plugins/terminal/routeros.py
@@ -0,0 +1,53 @@
+# 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
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import re
+
+from ansible.errors import AnsibleConnectionFailure
+from ansible.plugins.terminal import TerminalBase
+from ansible.utils.display import Display
+
+display = Display()
+
+
+class TerminalModule(TerminalBase):
+
+ ansi_re = [
+ # check ECMA-48 Section 5.4 (Control Sequences)
+ re.compile(br'(\x1b\[\?1h\x1b=)'),
+ re.compile(br'((?:\x9b|\x1b\x5b)[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e])'),
+ re.compile(br'\x08.')
+ ]
+
+ terminal_initial_prompt = [
+ br'\x1bZ',
+ ]
+
+ terminal_initial_answer = b'\x1b/Z'
+
+ terminal_stdout_re = [
+ re.compile(br"\x1b<"),
+ re.compile(br"\[[\w\-\.]+\@[\w\s\-\.\/]+\] ?(<SAFE)?> ?$"),
+ re.compile(br"Please press \"Enter\" to continue!"),
+ re.compile(br"Do you want to see the software license\? \[Y\/n\]: ?"),
+ ]
+
+ terminal_stderr_re = [
+ re.compile(br"\nbad command name"),
+ re.compile(br"\nno such item"),
+ re.compile(br"\ninvalid value for"),
+ ]
+
+ def on_open_shell(self):
+ prompt = self._get_prompt()
+ try:
+ if prompt.strip().endswith(b':'):
+ self._exec_cli_command(b' ')
+ if prompt.strip().endswith(b'!'):
+ self._exec_cli_command(b'\n')
+ except AnsibleConnectionFailure:
+ raise AnsibleConnectionFailure('unable to bypass license prompt')