diff options
Diffstat (limited to 'ansible_collections/community/routeros/plugins')
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') |