From a453ac31f3428614cceb99027f8efbdb9258a40b Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Tue, 14 May 2024 22:03:01 +0200 Subject: Adding upstream version 2.10.7+merged+base+2.10.8+dfsg. Signed-off-by: Daniel Baumann --- .../community/routeros/plugins/cliconf/routeros.py | 79 +++ .../routeros/plugins/module_utils/__init__.py | 0 .../routeros/plugins/module_utils/routeros.py | 163 ++++++ .../community/routeros/plugins/modules/api.py | 480 ++++++++++++++++ .../community/routeros/plugins/modules/command.py | 180 ++++++ .../community/routeros/plugins/modules/facts.py | 625 +++++++++++++++++++++ .../routeros/plugins/terminal/routeros.py | 69 +++ 7 files changed, 1596 insertions(+) create mode 100644 collections-debian-merged/ansible_collections/community/routeros/plugins/cliconf/routeros.py create mode 100644 collections-debian-merged/ansible_collections/community/routeros/plugins/module_utils/__init__.py create mode 100644 collections-debian-merged/ansible_collections/community/routeros/plugins/module_utils/routeros.py create mode 100644 collections-debian-merged/ansible_collections/community/routeros/plugins/modules/api.py create mode 100644 collections-debian-merged/ansible_collections/community/routeros/plugins/modules/command.py create mode 100644 collections-debian-merged/ansible_collections/community/routeros/plugins/modules/facts.py create mode 100644 collections-debian-merged/ansible_collections/community/routeros/plugins/terminal/routeros.py (limited to 'collections-debian-merged/ansible_collections/community/routeros/plugins') diff --git a/collections-debian-merged/ansible_collections/community/routeros/plugins/cliconf/routeros.py b/collections-debian-merged/ansible_collections/community/routeros/plugins/cliconf/routeros.py new file mode 100644 index 00000000..2e4adf7c --- /dev/null +++ b/collections-debian-merged/ansible_collections/community/routeros/plugins/cliconf/routeros.py @@ -0,0 +1,79 @@ +# +# (c) 2017 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +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 itertools import chain + +from ansible.module_utils._text import to_bytes, to_text +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list +from ansible.plugins.cliconf import CliconfBase, enable_mode + + +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', format='text', flags=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/collections-debian-merged/ansible_collections/community/routeros/plugins/module_utils/__init__.py b/collections-debian-merged/ansible_collections/community/routeros/plugins/module_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/collections-debian-merged/ansible_collections/community/routeros/plugins/module_utils/routeros.py b/collections-debian-merged/ansible_collections/community/routeros/plugins/module_utils/routeros.py new file mode 100644 index 00000000..20a8400b --- /dev/null +++ b/collections-debian-merged/ansible_collections/community/routeros/plugins/module_utils/routeros.py @@ -0,0 +1,163 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# (c) 2016 Red Hat Inc. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import json +from ansible.module_utils._text 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.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 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 + + 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/collections-debian-merged/ansible_collections/community/routeros/plugins/modules/api.py b/collections-debian-merged/ansible_collections/community/routeros/plugins/modules/api.py new file mode 100644 index 00000000..bb5d7a86 --- /dev/null +++ b/collections-debian-merged/ansible_collections/community/routeros/plugins/modules/api.py @@ -0,0 +1,480 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Nikolay Dachev +# GNU General Public License v3.0+ https://www.gnu.org/licenses/gpl-3.0.txt + +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 python librouteros. + - 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. + - I(check_mode) is not supported. +requirements: + - librouteros + - Python >= 3.6 (for librouteros) +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 + ssl: + description: + - If is set TLS will be used for RouterOS API connection. + required: false + type: bool + default: false + port: + description: + - RouterOS api port. If ssl is set, port will apply to ssl connection. + - Defaults are C(8728) for the HTTP API, and C(8729) for the HTTPS API. + type: int + 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 and return '.id'. + - WHERE is key word which extend query. WHERE format is key operator value - with spaces. + - WHERE valid operators are C(==), C(!=), C(>), C(<). + - 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 + 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 +''' + +EXAMPLES = ''' +--- +- name: Use RouterOS API + hosts: localhost + gather_facts: no + vars: + hostname: "ros_api_hostname/ip" + username: "admin" + password: "secret_password" + + path: "ip address" + + nic: "ether2" + ip1: "1.1.1.1/32" + ip2: "2.2.2.2/32" + ip3: "3.3.3.3/32" + + tasks: + - name: Get "{{ path }} print" + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "{{ path }}" + register: print_path + + - name: Dump "{{ path }} print" output + ansible.builtin.debug: + msg: '{{ print_path }}' + + - name: Add ip address "{{ ip1 }}" and "{{ ip2 }}" + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "{{ path }}" + add: "{{ item }}" + loop: + - "address={{ ip1 }} interface={{ nic }}" + - "address={{ ip2 }} interface={{ nic }}" + register: addout + + - name: Dump "Add ip address" output - ".id" for new added items + ansible.builtin.debug: + msg: '{{ addout }}' + + - name: Query for ".id" in "{{ path }} WHERE address == {{ ip2 }}" + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "{{ path }}" + query: ".id address WHERE address == {{ ip2 }}" + register: queryout + + - name: Dump "Query for" output and set fact with ".id" for "{{ ip2 }}" + ansible.builtin.debug: + msg: '{{ queryout }}' + + - name: Store query_id for later usage + ansible.builtin.set_fact: + query_id: "{{ queryout['msg'][0]['.id'] }}" + + - name: Update ".id = {{ query_id }}" taken with custom fact "fquery_id" + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "{{ path }}" + update: ".id={{ query_id }} address={{ ip3 }}" + register: updateout + + - name: Dump "Update" output + ansible.builtin.debug: + msg: '{{ updateout }}' + + - name: Remove ips - stage 1 - query ".id" for "{{ ip2 }}" and "{{ ip3 }}" + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "{{ path }}" + query: ".id address WHERE address == {{ item }}" + register: id_to_remove + loop: + - "{{ ip2 }}" + - "{{ ip3 }}" + + - name: Set fact for ".id" from "Remove ips - stage 1 - query" + ansible.builtin.set_fact: + to_be_remove: "{{ to_be_remove |default([]) + [item['msg'][0]['.id']] }}" + loop: "{{ id_to_remove.results }}" + + - name: Dump "Remove ips - stage 1 - query" output + ansible.builtin.debug: + msg: '{{ to_be_remove }}' + + # Remove "{{ rmips }}" with ".id" by "to_be_remove" from query + - name: Remove ips - stage 2 - remove "{{ ip2 }}" and "{{ ip3 }}" by '.id' + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "{{ path }}" + remove: "{{ item }}" + register: remove + loop: "{{ to_be_remove }}" + + - name: Dump "Remove ips - stage 2 - remove" output + ansible.builtin.debug: + msg: '{{ remove }}' + + - name: Arbitrary command example "/system identity print" + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "system identity" + cmd: "print" + register: cmdout + + - name: Dump "Arbitrary command example" output + ansible.builtin.debug: + msg: "{{ cmdout }}" +''' + +RETURN = ''' +--- +message: + description: All outputs are in list with dictionary elements returned from RouterOS api. + sample: C([{...},{...}]) + type: list + returned: always +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils._text import to_native + +import ssl +import traceback + +LIB_IMP_ERR = None +try: + from librouteros import connect + from librouteros.query import Key + HAS_LIB = True +except Exception as e: + HAS_LIB = False + LIB_IMP_ERR = traceback.format_exc() + + +class ROS_api_module: + def __init__(self): + module_args = (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'), + ssl=dict(type='bool', default=False), + 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'))) + + self.module = AnsibleModule(argument_spec=module_args, + supports_check_mode=False, + mutually_exclusive=(('add', 'remove', 'update', + 'cmd', 'query'),),) + + if not HAS_LIB: + self.module.fail_json(msg=missing_required_lib("librouteros"), + exception=LIB_IMP_ERR) + + self.api = self.ros_api_connect(self.module.params['username'], + self.module.params['password'], + self.module.params['hostname'], + self.module.params['port'], + self.module.params['ssl']) + + self.path = self.list_remove_empty(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'] + if self.query: + if 'WHERE' in self.query: + split = self.query.split('WHERE') + self.query = self.list_remove_empty(split[0].split(' ')) + self.where = self.list_remove_empty(split[1].split(' ')) + else: + self.query = self.list_remove_empty(self.module.params['query'].split(' ')) + + self.result = dict( + message=[]) + + # create api base path + self.api_path = self.api_add_path(self.api, self.path) + + # api call's + if self.add: + self.api_add() + elif self.remove: + self.api_remove() + elif self.update: + self.api_update() + elif self.query: + self.api_query() + elif self.arbitrary: + self.api_arbitrary() + else: + self.api_get_all() + + def list_remove_empty(self, check_list): + while("" in check_list): + check_list.remove("") + return check_list + + def list_to_dic(self, ldict): + dict = {} + for p in ldict: + if '=' not in p: + self.errors("missing '=' after '%s'" % p) + p = p.split('=') + if p[1]: + dict[p[0]] = p[1] + return dict + + 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 Exception as e: + self.errors(e) + + def api_add(self): + param = self.list_to_dic(self.add.split(' ')) + try: + self.result['message'].append("added: .id= %s" + % self.api_path.add(**param)) + self.return_result(True) + except Exception 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 Exception as e: + self.errors(e) + + def api_update(self): + param = self.list_to_dic(self.update.split(' ')) + 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 Exception 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 len(self.where) < 3: + self.errors("invalid syntax for 'WHERE %s'" + % ' '.join(self.where)) + + where = [] + if self.where[1] == '==': + select = self.api_path.select(*keys).where(keys[self.where[0]] == self.where[2]) + elif self.where[1] == '!=': + select = self.api_path.select(*keys).where(keys[self.where[0]] != self.where[2]) + elif self.where[1] == '>': + select = self.api_path.select(*keys).where(keys[self.where[0]] > self.where[2]) + elif self.where[1] == '<': + 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]) + for row in select: + self.result['message'].append(row) + else: + for row in self.api_path.select(*keys): + 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 Exception as e: + self.errors(e) + + def api_arbitrary(self): + param = {} + self.arbitrary = self.arbitrary.split(' ') + 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 Exception as e: + self.errors(e) + + def return_result(self, ch_status=False, status=True): + if status == "False": + self.module.fail_json(msg=to_native(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, True) + self.result['message'].append("%s" % e) + self.return_result(False, False) + + def ros_api_connect(self, username, password, host, port, use_ssl): + # connect to routeros api + conn_status = {"connection": {"username": username, + "hostname": host, + "port": port, + "ssl": use_ssl, + "status": "Connected"}} + try: + if use_ssl is True: + if not port: + port = 8729 + conn_status["connection"]["port"] = port + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.set_ciphers('ADH:@SECLEVEL=0') + api = connect(username=username, + password=password, + host=host, + ssl_wrapper=ctx.wrap_socket, + port=port) + else: + if not port: + port = 8728 + conn_status["connection"]["port"] = port + api = connect(username=username, + password=password, + host=host, + port=port) + except Exception as e: + conn_status["connection"]["status"] = "error: %s" % e + self.module.fail_json(msg=to_native([conn_status])) + return api + + +def main(): + + ROS_api_module() + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/community/routeros/plugins/modules/command.py b/collections-debian-merged/ansible_collections/community/routeros/plugins/modules/command.py new file mode 100644 index 00000000..82331a57 --- /dev/null +++ b/collections-debian-merged/ansible_collections/community/routeros/plugins/modules/command.py @@ -0,0 +1,180 @@ +#!/usr/bin/python + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__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. +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 + 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. + 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'] + 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 + 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 +''' + +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 re +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.utils import ComplexList +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', required=True), + + wait_for=dict(type='list'), + match=dict(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': False, + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + }) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/community/routeros/plugins/modules/facts.py b/collections-debian-merged/ansible_collections/community/routeros/plugins/modules/facts.py new file mode 100644 index 00000000..4a4db7c3 --- /dev/null +++ b/collections-debian-merged/ansible_collections/community/routeros/plugins/modules/facts.py @@ -0,0 +1,625 @@ +#!/usr/bin/python + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__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 RotuerOS. This module prepends all of the + base network fact keys with C(ansible_net_). The facts + module will always collect a base set of facts from the device + and can enable or disable collection of additional facts. +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), and C(interfaces). 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' +''' + +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: always + type: str + ansible_net_serialnum: + description: The serial number of the remote device + returned: always + type: str + ansible_net_version: + description: The operating system version running on the remote device + returned: always + type: str + ansible_net_hostname: + description: The configured hostname of the device + returned: always + type: str + ansible_net_arch: + description: The CPU architecture of the device + returned: always + type: str + ansible_net_uptime: + description: The uptime of the device + returned: always + type: str + ansible_net_cpu_load: + description: Current CPU load + returned: always + type: str + + # hardware + ansible_net_spacefree_mb: + description: The available disk space on the remote device in MiB + returned: when hardware is configured + type: dict + ansible_net_spacetotal_mb: + description: The total disk space on the remote device in MiB + returned: when hardware is configured + type: dict + ansible_net_memfree_mb: + description: The available free memory on the remote device in MiB + returned: when hardware is configured + type: int + ansible_net_memtotal_mb: + description: The total memory on the remote device in MiB + returned: when hardware is configured + type: int + + # config + ansible_net_config: + description: The current active config from the device + returned: when config is configured + type: str + + # interfaces + ansible_net_all_ipv4_addresses: + description: All IPv4 addresses configured on the device + returned: when interfaces is configured + type: list + ansible_net_all_ipv6_addresses: + description: All IPv6 addresses configured on the device + returned: when interfaces is configured + type: list + ansible_net_interfaces: + description: A hash of all interfaces running on the system + returned: when interfaces is configured + type: dict + ansible_net_neighbors: + description: The list of neighbors from the remote device + returned: when interfaces is configured + type: dict + + # routing + ansible_net_bgp_peer: + description: The dict bgp peer + returned: peer information + type: dict + ansible_net_bgp_vpnv4_route: + description: The dict bgp vpnv4 route + returned: vpnv4 route information + type: dict + ansible_net_bgp_instance: + description: The dict bgp instance + returned: bgp instance information + type: dict + ansible_net_route: + description: The dict routes in all routing table + returned: routes information in all routing table + type: dict + ansible_net_ospf_instance: + description: The dict ospf instance + returned: ospf instance information + type: dict + ansible_net_ospf_neighbor: + description: The dict ospf neighbor + returned: ospf neighbor information + 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'] + + def populate(self): + super(Config, self).populate() + data = self.responses[0] + if data: + self.facts['config'] = 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 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 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') + ) + + 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/collections-debian-merged/ansible_collections/community/routeros/plugins/terminal/routeros.py b/collections-debian-merged/ansible_collections/community/routeros/plugins/terminal/routeros.py new file mode 100644 index 00000000..2c29d1db --- /dev/null +++ b/collections-debian-merged/ansible_collections/community/routeros/plugins/terminal/routeros.py @@ -0,0 +1,69 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json +import re + +from ansible.errors import AnsibleConnectionFailure +from ansible.module_utils._text import to_text, to_bytes +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\-\.\/]+\] ?> ?$"), + 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') -- cgit v1.2.3