diff options
Diffstat (limited to 'ansible_collections/community/ciscosmb/plugins')
8 files changed, 1615 insertions, 0 deletions
diff --git a/ansible_collections/community/ciscosmb/plugins/cliconf/ciscosmb.py b/ansible_collections/community/ciscosmb/plugins/cliconf/ciscosmb.py new file mode 100644 index 000000000..922edeb69 --- /dev/null +++ b/ansible_collections/community/ciscosmb/plugins/cliconf/ciscosmb.py @@ -0,0 +1,77 @@ +# +# (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 <http://www.gnu.org/licenses/>. +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' +--- +author: Egor Zaitsev (@heuels) +name: ciscosmb +short_description: Use ciscosmb cliconf to run command on Cisco SMB network devices +description: + - This ciscosmb plugin provides low level abstraction apis for + sending and receiving CLI commands from Cisco SMB network devices. +''' + +import json + +from ansible.plugins.cliconf import CliconfBase, enable_mode + + +class Cliconf(CliconfBase): + + def get_device_info(self): + device_info = {} + device_info['network_os'] = 'ciscosmb' + + return device_info + + @enable_mode + def get_config(self, source='running', flags=None, format=None): + if source not in ("running", "startup"): + raise ValueError( + "fetching configuration from %s is not supported" % source + ) + + if format: + raise ValueError( + "'format' value %s is not supported for get_config" % format + ) + + if flags: + raise ValueError( + "'flags' value %s is not supported for get_config" % flags + ) + + if source == "running": + cmd = "show running-config " + else: + cmd = "show startup-config " + + return self.send_command(cmd) + + 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().get_capabilities() + return json.dumps(result) diff --git a/ansible_collections/community/ciscosmb/plugins/module_utils/__init__.py b/ansible_collections/community/ciscosmb/plugins/module_utils/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/ciscosmb/plugins/module_utils/__init__.py diff --git a/ansible_collections/community/ciscosmb/plugins/module_utils/ciscosmb.py b/ansible_collections/community/ciscosmb/plugins/module_utils/ciscosmb.py new file mode 100644 index 000000000..db19edec6 --- /dev/null +++ b/ansible_collections/community/ciscosmb/plugins/module_utils/ciscosmb.py @@ -0,0 +1,302 @@ +# 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 +import re + +from ansible.module_utils._text import to_text, 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 + +# copy of https://github.com/napalm-automation/napalm/blob/develop/napalm/base/canonical_map.py +from ansible_collections.community.ciscosmb.plugins.module_utils.ciscosmb_canonical_map import base_interfaces + +_DEVICE_CONFIGS = {} + +ciscosmb_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') +} +ciscosmb_argument_spec = {} + + +def ciscosmb_split_to_tables(data): + TABLE_HEADER = re.compile(r"^---+ +-+.*$") + EMPTY_LINE = re.compile(r"^ *$") + + tables = dict() + tableno = -1 + lineno = 0 + tabledataget = False + + for line in data.splitlines(): + if re.match(EMPTY_LINE, line): + tabledataget = False + continue + + if re.match(TABLE_HEADER, line): + tableno += 1 + tabledataget = True + lineno = 0 + tables[tableno] = dict() + tables[tableno]["header"] = line + tables[tableno]["data"] = dict() + continue + + if tabledataget: + tables[tableno]["data"][lineno] = line + lineno += 1 + continue + + return tables + + +def ciscosmb_parse_table(table, allow_overflow=True, allow_empty_fields=None): + + if allow_empty_fields is None: + allow_empty_fields = list() + + fields_end = __get_table_columns_end(table["header"]) + data = __get_table_data( + table["data"], fields_end, allow_overflow, allow_empty_fields + ) + + return data + + +def __get_table_columns_end(headerline): + """ fields length are diferent device to device, detect them on horizontal lin """ + fields_end = [m.start() for m in re.finditer(" *", headerline.strip())] + # fields_position.insert(0,0) + # fields_end.append(len(headerline)) + fields_end.append(10000) # allow "long" last field + + return fields_end + + +def __line_to_fields(line, fields_end): + """ dynamic fields lenghts """ + line_elems = {} + index = 0 + f_start = 0 + for f_end in fields_end: + line_elems[index] = line[f_start:f_end].strip() + index += 1 + f_start = f_end + + return line_elems + + +def __get_table_data( + tabledata, fields_end, allow_overflow=True, allow_empty_fields=None +): + + if allow_empty_fields is None: + allow_empty_fields = list() + data = dict() + + dataindex = 0 + for lineno in tabledata: + owerflownfields = list() + owerflow = False + + line = tabledata[lineno] + line_elems = __line_to_fields(line, fields_end) + + if allow_overflow: + # search for overflown fields + for elemno in line_elems: + if elemno not in allow_empty_fields and line_elems[elemno] == "": + owerflow = True + else: + owerflownfields.append(elemno) + + if owerflow: + # concat owerflown elements to previous data + for fieldno in owerflownfields: + data[dataindex - 1][fieldno] += line_elems[fieldno] + + else: + data[dataindex] = line_elems + dataindex += 1 + else: + data[dataindex] = line_elems + dataindex += 1 + + return data + + +def ciscosmb_merge_dicts(a, b, path=None): + "merges b into a" + if path is None: + path = [] + + # is b empty? + if not bool(b): + return a + + for key in b: + if key in a: + if isinstance(a[key], dict) and isinstance(b[key], dict): + ciscosmb_merge_dicts(a[key], b[key], path + [str(key)]) + elif a[key] == b[key]: + pass # same leaf value + else: + raise Exception("Conflict at %s" % ".".join(path + [str(key)])) + else: + a[key] = b[key] + return a + + +def interface_canonical_name(interface): + iftype = interface.rstrip(r"/\0123456789. ") + ifno = interface[len(iftype):].lstrip() + + if iftype in base_interfaces: + iftype = base_interfaces[iftype] + + interface = iftype + str(ifno) + + return interface + + +def get_provider_argspec(): + return ciscosmb_provider_spec + + +def get_connection(module): + if hasattr(module, '_ciscosmb_connection'): + return module._ciscosmb_connection + + capabilities = get_capabilities(module) + network_api = capabilities.get('network_api') + if network_api == 'cliconf': + module._ciscosmb_connection = Connection(module._socket_path) + else: + module.fail_json(msg='Invalid connection type %s' % network_api) + + return module._ciscosmb_connection + + +def get_capabilities(module): + if hasattr(module, '_ciscosmb_capabilities'): + return module._ciscosmb_capabilities + + try: + capabilities = Connection(module._socket_path).get_capabilities() + module._ciscosmb_capabilities = json.loads(capabilities) + return module._ciscosmb_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_text(exc, errors='surrogate_then_replace')) + + out = to_text(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_text(exc, errors='surrogate_then_replace')) + + cfg = to_text(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_text(exc, errors='surrogate_then_replace')) + + try: + out = to_text(out, errors='surrogate_or_strict') + except UnicodeError: + module.fail_json( + msg=u'Failed to decode output from %s: %s' % (cmd, to_text(out))) + + responses.append(out) + + return responses diff --git a/ansible_collections/community/ciscosmb/plugins/module_utils/ciscosmb_canonical_map.py b/ansible_collections/community/ciscosmb/plugins/module_utils/ciscosmb_canonical_map.py new file mode 100644 index 000000000..1ca8a665f --- /dev/null +++ b/ansible_collections/community/ciscosmb/plugins/module_utils/ciscosmb_canonical_map.py @@ -0,0 +1,165 @@ +# The contents of this file are licensed under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +base_interfaces = { + "ATM": "ATM", + "AT": "ATM", + "B": "Bdi", + "Bd": "Bdi", + "Bdi": "Bdi", + "EOBC": "EOBC", + "EO": "EOBC", + "Ethernet": "Ethernet", + "Eth": "Ethernet", + "eth": "Ethernet", + "Et": "Ethernet", + "et": "Ethernet", + "FastEthernet": "FastEthernet", + "FastEth": "FastEthernet", + "FastE": "FastEthernet", + "Fast": "FastEthernet", + "Fas": "FastEthernet", + "FE": "FastEthernet", + "Fa": "FastEthernet", + "fa": "FastEthernet", + "Fddi": "Fddi", + "FD": "Fddi", + "FortyGigabitEthernet": "FortyGigabitEthernet", + "FortyGigEthernet": "FortyGigabitEthernet", + "FortyGigEth": "FortyGigabitEthernet", + "FortyGigE": "FortyGigabitEthernet", + "FortyGig": "FortyGigabitEthernet", + "FGE": "FortyGigabitEthernet", + "FO": "FortyGigabitEthernet", + "Fo": "FortyGigabitEthernet", + "FiftyGigabitEthernet": "FiftyGigabitEthernet", + "FiftyGigEthernet": "FiftyGigabitEthernet", + "FiftyGigEth": "FiftyGigabitEthernet", + "FiftyGigE": "FiftyGigabitEthernet", + "FI": "FiftyGigabitEthernet", + "Fi": "FiftyGigabitEthernet", + "fi": "FiftyGigabitEthernet", + "GigabitEthernet": "GigabitEthernet", + "GigEthernet": "GigabitEthernet", + "GigEth": "GigabitEthernet", + "GigE": "GigabitEthernet", + "Gig": "GigabitEthernet", + "GE": "GigabitEthernet", + "Ge": "GigabitEthernet", + "ge": "GigabitEthernet", + "Gi": "GigabitEthernet", + "gi": "GigabitEthernet", + "HundredGigabitEthernet": "HundredGigabitEthernet", + "HundredGigEthernet": "HundredGigabitEthernet", + "HundredGigEth": "HundredGigabitEthernet", + "HundredGigE": "HundredGigabitEthernet", + "HundredGig": "HundredGigabitEthernet", + "Hu": "HundredGigabitEthernet", + "TwentyFiveGigabitEthernet": "TwentyFiveGigabitEthernet", + "TwentyFiveGigEthernet": "TwentyFiveGigabitEthernet", + "TwentyFiveGigEth": "TwentyFiveGigabitEthernet", + "TwentyFiveGigE": "TwentyFiveGigabitEthernet", + "TwentyFiveGig": "TwentyFiveGigabitEthernet", + "TF": "TwentyFiveGigabitEthernet", + "Tf": "TwentyFiveGigabitEthernet", + "tf": "TwentyFiveGigabitEthernet", + "TwoHundredGigabitEthernet": "TwoHundredGigabitEthernet", + "TwoHundredGigEthernet": "TwoHundredGigabitEthernet", + "TwoHundredGigEth": "TwoHundredGigabitEthernet", + "TwoHundredGigE": "TwoHundredGigabitEthernet", + "TwoHundredGig": "TwoHundredGigabitEthernet", + "TH": "TwoHundredGigabitEthernet", + "Th": "TwoHundredGigabitEthernet", + "th": "TwoHundredGigabitEthernet", + "FourHundredGigabitEthernet": "FourHundredGigabitEthernet", + "FourHundredGigEthernet": "FourHundredGigabitEthernet", + "FourHundredGigEth": "FourHundredGigabitEthernet", + "FourHundredGigE": "FourHundredGigabitEthernet", + "FourHundredGig": "FourHundredGigabitEthernet", + "F": "FourHundredGigabitEthernet", + "f": "FourHundredGigabitEthernet", + "Loopback": "Loopback", + "loopback": "Loopback", + "Lo": "Loopback", + "lo": "Loopback", + "Management": "Management", + "Mgmt": "Management", + "mgmt": "Management", + "Ma": "Management", + "Management_short": "Ma", + "MFR": "MFR", + "Multilink": "Multilink", + "Mu": "Multilink", + "n": "nve", + "nv": "nve", + "nve": "nve", + "PortChannel": "Port-channel", + "Port-channel": "Port-channel", + "Port-Channel": "Port-channel", + "port-channel": "Port-channel", + "po": "Port-channel", + "Po": "Port-channel", + "POS": "POS", + "PO": "POS", + "Serial": "Serial", + "Se": "Serial", + "S": "Serial", + "TenGigabitEthernet": "TenGigabitEthernet", + "TenGigEthernet": "TenGigabitEthernet", + "TenGigEth": "TenGigabitEthernet", + "TenGig": "TenGigabitEthernet", + "TeGig": "TenGigabitEthernet", + "Ten": "TenGigabitEthernet", + "T": "TenGigabitEthernet", + "Te": "TenGigabitEthernet", + "te": "TenGigabitEthernet", + "Tunnel": "Tunnel", + "Tun": "Tunnel", + "Tu": "Tunnel", + "Twe": "TwentyFiveGigE", + "Tw": "TwoGigabitEthernet", + "Two": "TwoGigabitEthernet", + "Virtual-Access": "Virtual-Access", + "Vi": "Virtual-Access", + "Virtual-Template": "Virtual-Template", + "Vt": "Virtual-Template", + "VLAN": "VLAN", + "V": "VLAN", + "Vl": "VLAN", + "Wlan-GigabitEthernet": "Wlan-GigabitEthernet", +} + +reverse_mapping = { + "ATM": "At", + "EOBC": "EO", + "Ethernet": "Et", + "FastEthernet": "Fa", + "Fddi": "FD", + "FortyGigabitEthernet": "Fo", + "GigabitEthernet": "Gi", + "HundredGigabitEthernet": "Hu", + "Loopback": "Lo", + "Management": "Ma", + "MFR": "MFR", + "Multilink": "Mu", + "Port-channel": "Po", + "POS": "PO", + "Serial": "Se", + "TenGigabitEthernet": "Te", + "Tunnel": "Tu", + "TwoGigabitEthernet": "Two", + "TwentyFiveGigE": "Twe", + "Virtual-Access": "Vi", + "Virtual-Template": "Vt", + "VLAN": "Vl", + "Wlan-GigabitEthernet": "Wl-Gi", +} diff --git a/ansible_collections/community/ciscosmb/plugins/modules/__init__.py b/ansible_collections/community/ciscosmb/plugins/modules/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/ciscosmb/plugins/modules/__init__.py diff --git a/ansible_collections/community/ciscosmb/plugins/modules/command.py b/ansible_collections/community/ciscosmb/plugins/modules/command.py new file mode 100644 index 000000000..fa0290279 --- /dev/null +++ b/ansible_collections/community/ciscosmb/plugins/modules/command.py @@ -0,0 +1,185 @@ +#!/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: "Petr Klima (@qaxi)" +short_description: Run commands on remote Cisco SMB devices +description: + - Sends arbitrary commands to an Cisco SMB 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 Cisco SMB 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 + type: str + 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 + 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 +''' + +EXAMPLES = """ +- name: Run command on remote devices + community.ciscosmb.command: + commands: show version + +- name: Run command and check to see if output contains PID + community.ciscosmb.command: + commands: show inventory + wait_for: result[0] contains PID + +- name: Run multiple commands on remote nodes + community.ciscosmb.command: + commands: + - show version + - show system + +- name: Run multiple commands and evaluate the output + community.ciscosmb.command: + commands: + - show version + - show system + wait_for: + - result[0] contains Active-image + - result[1] contains "System Up Time" +""" + +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.ansible.netcommon.plugins.module_utils.network.common.parsing import Conditional +from ansible_collections.community.ciscosmb.plugins.module_utils.ciscosmb import run_commands +from ansible_collections.community.ciscosmb.plugins.module_utils.ciscosmb import ciscosmb_argument_spec +from ansible.module_utils.basic import AnsibleModule +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(default='all', choices=['all', 'any']), + + retries=dict(default=10, type='int'), + interval=dict(default=1, type='int') + ) + + argument_spec.update(ciscosmb_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=False) + + 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/ansible_collections/community/ciscosmb/plugins/modules/facts.py b/ansible_collections/community/ciscosmb/plugins/modules/facts.py new file mode 100644 index 000000000..23cd9820a --- /dev/null +++ b/ansible_collections/community/ciscosmb/plugins/modules/facts.py @@ -0,0 +1,763 @@ +#!/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: "Petr Klima (@qaxi)" +short_description: Collect facts from remote devices running Cisco SMB +description: + - Collects a base set of device facts from a remote device that + is running Cisco SMB. 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. +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 + type: list + elements: str + choices: [ 'default', 'all', 'hardware', 'config', 'interfaces', '!hardware', '!config', '!interfaces' ] + default: '!config' +notes: + - Supports C(check_mode). +""" + +EXAMPLES = """ +- name: Collect all facts from the device + community.ciscosmb.facts: + gather_subset: all + +- name: Collect only the config and default facts + community.ciscosmb.facts: + gather_subset: + - config + +- name: Do not collect hardware facts + community.ciscosmb.facts: + gather_subset: + - "!hardware" +""" + +RETURN = """ +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_uptime: + description: The uptime of the device. + returned: always + type: str +ansible_net_cpu_load: + description: Current CPU load. + returned: always + type: str +ansible_net_stacked_models: + description: The model names of each device in the stack. + returned: when multiple devices are configured in a stack + type: list +ansible_net_stacked_serialnums: + description: The serial numbers of each device in the stack. + returned: when multiple devices are configured in a stack + type: list + +# 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 + +""" +import re + +from ansible_collections.community.ciscosmb.plugins.module_utils.ciscosmb import ( + run_commands, + ciscosmb_argument_spec, + interface_canonical_name, + ciscosmb_split_to_tables, + ciscosmb_parse_table, + ciscosmb_merge_dicts, +) +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 = [ + "show version", + "show system", + "show cpu utilization", + "show inventory", + ] + + def populate(self): + super(Default, self).populate() + + data = self.responses[0] + if data: + self.facts["version"] = self.parse_version(data) + self.facts["boot_version"] = self.parse_boot_version(data) + + data = self.responses[1] + if data: + self.facts["uptime"] = self.parse_uptime(data) + self.facts["hostname"] = self.parse_hostname(data) + + data = self.responses[2] + if data: + self.facts["cpu_load"] = self.parse_cpu_load(data) + + data = self.responses[3] + if data: + modules = self.parse_inventory(data) + stacked_models = self.parse_stacked_models(modules) + if len(stacked_models) >= 2: + stacked_serialnums = self.parse_stacked_serialnums(modules) + self.facts["stacked_models"] = stacked_models + self.facts["stacked_serialnums"] = stacked_serialnums + self.facts["model"] = self.parse_model(modules) + self.facts["serialnum"] = self.parse_serialnum(modules) + self.facts["hw_version"] = self.parse_hw_version(modules) + self.facts["hw_modules"] = modules + + # show version + def parse_version(self, data): + # Cisco SMB 300 and 500 - fw 1.x.x.x + match = re.search(r"^SW version\s*(\S+)\s*.*$", data, re.M) + if match: + return match.group(1) + # Cisco SMB 350 and 550 - fw 2.x.x.x + match = re.search(r"^ Version:\s*(\S+)\s*.*$", data, re.M) + if match: + return match.group(1) + + def parse_boot_version(self, data): + match = re.search(r"Boot version\s*(\S+)\s*.*$", data, re.M) + if match: + return match.group(1) + + # show system + def parse_uptime(self, data): + match = re.search(r"^System Up Time \S+:\s+(\S+)\s*$", data, re.M) + if match: + (dayhour, mins, sec) = match.group(1).split(":") + (day, hour) = dayhour.split(",") + # output in seconds + return (int(day) * 86400) + (int(hour) * 3600) + (int(mins) * 60) + int(sec) + + def parse_hostname(self, data): + match = re.search(r"^System Name:\s*(\S+)\s*$", data, re.M) + if match: + return match.group(1) + + # show cpu utilization + def parse_cpu_load(self, data): + match = re.search(r"one minute:\s+(\d+)%;\s*", data, re.M) + if match: + return match.group(1) + + # show inventory + def parse_inventory(self, data): + # make 1 module 1 line + data = re.sub(r"\nPID", " PID", data) + # delete empty lines + data = re.sub(r"^\n", "", data) + data = re.sub(r"\n\n", "", data) + data = re.sub(r"\n\s*\n", r"\n", data) + + lines = data.splitlines() + + modules = {} + for line in lines: + # remove extra chars + line = re.sub(r'"', r"", line) + line = re.sub(r"\s+", r" ", line) + # normalize lines + line = re.sub(r":\s", r'"', line) + line = re.sub(r'\s+DESCR"', r'"DESCR"', line) + line = re.sub(r'\s+PID"', r'"PID"', line) + line = re.sub(r'\s+VID"', r'"VID"', line) + line = re.sub(r'\s+SN"', r'"SN"', line) + line = re.sub(r"\s*$", r"", line) + + match = re.search( + r'^NAME"(?P<name>[^"]+)"DESCR"(?P<descr>[^"]+)"PID"(?P<pid>[^"]+)"VID"(?P<vid>[^"]+)"SN"(?P<sn>\S+)\s*', + line, + ) + + modul = match.groupdict() + modules[modul["name"]] = modul + + if modules: + return modules + + def parse_stacked_models(self, data): + # every inventory has module with NAME: "1" + # stacks have modules 2 3 ... 8 + models = [] + for n in range(1, 9): + # index is string + if str(n) in data: + models.append(data[str(n)]["pid"]) + return models + + def parse_stacked_serialnums(self, data): + # every inventory has module with NAME: "1" + # stacks have modules 2 3 ... 8 + sn = [] + for n in range(1, 9): + # index is string + if str(n) in data: + sn.append(data[str(n)]["sn"]) + return sn + + def parse_model(self, data): + # every inventory has module with NAME: "1" + model = data["1"]["pid"] + if "stacked_models" in self.facts: + model = re.sub(r"-.*$", "", model) + model = "Stack " + model + return model + + def parse_serialnum(self, data): + # every inventory has module with NAME: "1" + sn = data["1"]["sn"] + return sn + + def parse_hw_version(self, data): + # every inventory has module with NAME: "1" + sn = data["1"]["vid"] + return sn + + +class Hardware(FactsBase): + + COMMANDS = [ + "dir", + ] + + def populate(self): + super(Hardware, self).populate() + data = self.responses[0] + if data: + self.parse_filesystem_info(data) + + def parse_filesystem_info(self, data): + match = re.search(r"Total size of (\S+): (\d+) bytes", data, re.M) + + if match: # fw 1.x + self.facts["spacetotal_mb"] = round(int(match.group(2)) / 1024 / 1024, 1) + match = re.search(r"Free size of (\S+): (\d+) bytes", data, re.M) + self.facts["spacefree_mb"] = round(int(match.group(2)) / 1024 / 1024, 1) + + else: + match = re.search(r"(\d+)K of (\d+)K are free", data, re.M) + if match: # fw 2.x, 3.x + self.facts["spacetotal_mb"] = round(int(match.group(2)) / 1024, 1) + self.facts["spacefree_mb"] = round(int(match.group(1)) / 1024, 1) + + +class Config(FactsBase): + + COMMANDS = ["show running-config detailed"] + + def populate(self): + super(Config, self).populate() + data = self.responses[0] + if data: + self.facts["config"] = data + + +class Interfaces(FactsBase): + + COMMANDS = [ + "show ports jumbo-frame", + "show ip interface", + "show ipv6 interface brief", + "show interfaces status", + "show interfaces configuration", + "show interfaces description", + "show lldp neighbors", + ] + + 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: + self.populate_interfaces_mtu(data) + + data = self.responses[1] + if data: + self.populate_addresses_ipv4(data) + + data = self.responses[2] + if data: + self.populate_addresses_ipv6(data) + + data = self.responses[3] + if data: + self.populate_interfaces_status(data) + + data = self.responses[4] + if data: + self.populate_interfaces_configuration(data) + + data = self.responses[5] + if data: + self.populate_interfaces_description(data) + + data = self.responses[6] + if data: + self.populate_neighbors(data) + + def _populate_interfaces_status_interface(self, interface_table): + interfaces = dict() + + for key in interface_table: + + i = interface_table[key] + interface = dict() + interface["state"] = i[6].lower() + interface["type"] = i[1] + interface["mtu"] = self._mtu + interface["duplex"] = i[2].lower() + interface["negotiation"] = i[4].lower() + interface["control"] = i[5].lower() + interface["presure"] = i[7].lower() + interface["mode"] = i[8].lower() + + if i[6] == "Up": + interface["bandwith"] = int(i[3]) * 1000 # to get speed in kb + else: + interface["bandwith"] = None + + for key in interface: + if interface[key] == "--": + interface[key] = None + + interfaces[interface_canonical_name(i[0])] = interface + return interfaces + + def _populate_interfaces_status_portchanel(self, interface_table): + interfaces = dict() + + for key in interface_table: + + interface = dict() + i = interface_table[key] + interface["state"] = i[6].lower() + interface["type"] = i[1] + interface["mtu"] = self._mtu + interface["duplex"] = i[2].lower() + interface["negotiation"] = i[4].lower() + interface["control"] = i[5].lower() + + if i[6] == "Up": + interface["bandwith"] = int(i[3]) * 1000 # to get speed in kb + else: + interface["bandwith"] = None + + for key in interface: + if interface[key] == "--": + interface[key] = None + + interfaces[interface_canonical_name(i[0])] = interface + + return interfaces + + def populate_interfaces_status(self, data): + tables = ciscosmb_split_to_tables(data) + + interface_table = ciscosmb_parse_table(tables[0]) + portchanel_table = ciscosmb_parse_table(tables[1]) + + interfaces = self._populate_interfaces_status_interface(interface_table) + self.facts["interfaces"] = ciscosmb_merge_dicts( + self.facts["interfaces"], interfaces + ) + interfaces = self._populate_interfaces_status_portchanel(portchanel_table) + self.facts["interfaces"] = ciscosmb_merge_dicts( + self.facts["interfaces"], interfaces + ) + + def _populate_interfaces_configuration_interface(self, interface_table): + interfaces = dict() + + for key in interface_table: + + i = interface_table[key] + interface = dict() + interface["admin_state"] = i[6].lower() + interface["mdix"] = i[8].lower() + + interfaces[interface_canonical_name(i[0])] = interface + return interfaces + + def _populate_interfaces_configuration_portchanel(self, interface_table): + interfaces = dict() + + for key in interface_table: + + interface = dict() + i = interface_table[key] + + interface["admin_state"] = i[5].lower() + + interfaces[interface_canonical_name(i[0])] = interface + + return interfaces + + def populate_interfaces_configuration(self, data): + tables = ciscosmb_split_to_tables(data) + + interface_table = ciscosmb_parse_table(tables[0]) + portchanel_table = ciscosmb_parse_table(tables[1]) + + interfaces = self._populate_interfaces_configuration_interface(interface_table) + self.facts["interfaces"] = ciscosmb_merge_dicts( + self.facts["interfaces"], interfaces + ) + interfaces = self._populate_interfaces_configuration_portchanel( + portchanel_table + ) + self.facts["interfaces"] = ciscosmb_merge_dicts( + self.facts["interfaces"], interfaces + ) + + def _populate_interfaces_description_interface(self, interface_table): + interfaces = dict() + + for key in interface_table: + + i = interface_table[key] + interface = dict() + interface["description"] = i[1] + + if interface["description"] == "": + interface["description"] = None + + interfaces[interface_canonical_name(i[0])] = interface + return interfaces + + def _populate_interfaces_description_portchanel(self, interface_table): + interfaces = dict() + + for key in interface_table: + + interface = dict() + i = interface_table[key] + + interface["description"] = i[1] + + if interface["description"] == "": + interface["description"] = None + + interfaces[interface_canonical_name(i[0])] = interface + + return interfaces + + def populate_interfaces_description(self, data): + tables = ciscosmb_split_to_tables(data) + + interface_table = ciscosmb_parse_table(tables[0], False) + portchanel_table = ciscosmb_parse_table(tables[1], False) + + interfaces = self._populate_interfaces_description_interface(interface_table) + self.facts["interfaces"] = ciscosmb_merge_dicts( + self.facts["interfaces"], interfaces + ) + interfaces = self._populate_interfaces_description_portchanel(portchanel_table) + self.facts["interfaces"] = ciscosmb_merge_dicts( + self.facts["interfaces"], interfaces + ) + + def _populate_address_ipv4(self, ip_table): + ips = list() + + for key in ip_table: + cidr = ip_table[key][0] + + interface = interface_canonical_name(ip_table[key][1]) + ip, mask = cidr.split("/") + + ips.append(ip) + + # add ips to interface + self._new_interface(interface) + if "ipv4" not in self.facts["interfaces"][interface]: + self.facts["interfaces"][interface]["ipv4"] = list() + + self.facts["interfaces"][interface]["ipv4"].append( + dict(address=ip, subnet=mask) + ) + + return ips + + def populate_addresses_ipv4(self, data): + tables = ciscosmb_split_to_tables(data) + ip_table = ciscosmb_parse_table(tables[0]) + + ips = self._populate_address_ipv4(ip_table) + self.facts["all_ipv4_addresses"] = ips + + def _populate_address_ipv6(self, ip_table): + ips = list() + + for key in ip_table: + ip = ip_table[key][3] + interface = interface_canonical_name(ip_table[key][0]) + + ips.append(ip) + + # add ips to interface + self._new_interface(interface) + if "ipv6" not in self.facts["interfaces"][interface]: + self.facts["interfaces"][interface]["ipv6"] = list() + + self.facts["interfaces"][interface]["ipv6"].append(dict(address=ip)) + + return ips + + def _new_interface(self, interface): + + if interface in self.facts["interfaces"]: + return + else: + self.facts["interfaces"][interface] = dict() + self.facts["interfaces"][interface]["mtu"] = self._mtu + self.facts["interfaces"][interface]["admin_state"] = "up" + self.facts["interfaces"][interface]["description"] = None + self.facts["interfaces"][interface]["state"] = "up" + self.facts["interfaces"][interface]["bandwith"] = None + self.facts["interfaces"][interface]["duplex"] = None + self.facts["interfaces"][interface]["negotiation"] = None + self.facts["interfaces"][interface]["control"] = None + return + + def populate_addresses_ipv6(self, data): + tables = ciscosmb_split_to_tables(data) + + ip_table = ciscosmb_parse_table(tables[0]) + ips = self._populate_address_ipv6(ip_table) + self.facts["all_ipv6_addresses"] = ips + + def populate_interfaces_mtu(self, data): + # by documentation SG350 + match = re.search(r"Jumbo frames are enabled", data, re.M) + if match: + mtu = 9000 + else: + mtu = 1518 + + self._mtu = mtu + + def populate_neighbors(self, data): + tables = ciscosmb_split_to_tables(data) + + neighbor_table = ciscosmb_parse_table(tables[0], allow_empty_fields=[3]) + + neighbors = dict() + for key in neighbor_table: + neighbor = neighbor_table[key] + + ifcname = interface_canonical_name(neighbor[0]) + + host = neighbor[3] + port = neighbor[2] + + hostport = {"host": host, "port": port} + + if ifcname not in neighbors: + neighbors[ifcname] = list() + + neighbors[ifcname].append(hostport) + + self.facts["neighbors"] = neighbors + + +FACT_SUBSETS = dict( + default=Default, + hardware=Hardware, + interfaces=Interfaces, + config=Config, +) + +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", + choices=[ + "all", + "default", + "hardware", + "interfaces", + "config", + "!hardware", + "!interfaces", + "!config", + ], + ) + ) + + argument_spec.update(ciscosmb_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/ciscosmb/plugins/terminal/ciscosmb.py b/ansible_collections/community/ciscosmb/plugins/terminal/ciscosmb.py new file mode 100644 index 000000000..e8aa46d9b --- /dev/null +++ b/ansible_collections/community/ciscosmb/plugins/terminal/ciscosmb.py @@ -0,0 +1,123 @@ +# +# (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 <http://www.gnu.org/licenses/>. +# + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +# Py 2.7 compat. +from ansible.module_utils.six import raise_from + +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): + + # https://docs.ansible.com/ansible/latest/collections/ansible/netcommon/network_cli_connection.html + + terminal_stdout_re = [ + re.compile(br"[\r\n]?[\w\+\-\.:\/\[\]]+(?:\([^\)]+\)){0,3}(?:[>#]) ?$") + ] + + terminal_stderr_re = [ + re.compile(br"% ?Error"), + re.compile(br"^% \w+", re.M), + re.compile(br"% ?Bad secret"), + re.compile(br"[\r\n%] Bad passwords"), + re.compile(br"invalid input", re.I), + re.compile(br"(?:incomplete|ambiguous) command", re.I), + re.compile(br"connection timed out", re.I), + re.compile(br"[^\r\n]+ not found"), + re.compile(br"'[^']' +returned error code: ?\d+"), + re.compile(br"Bad mask", re.I), + re.compile(br"% ?(\S+) ?overlaps with ?(\S+)", re.I), + re.compile(br"[%\S] ?Error: ?[\s]+", re.I), + re.compile(br"[%\S] ?Informational: ?[\s]+", re.I), + re.compile(br"Command authorization failed"), + ] + + def on_open_shell(self): + try: + self._exec_cli_command(b"terminal datadump") + except AnsibleConnectionFailure as e: + raise_from(AnsibleConnectionFailure("unable to set terminal parameters"), e) + + try: + self._exec_cli_command(b"terminal width 0") + except AnsibleConnectionFailure: + display.display( + "WARNING: Unable to set terminal width, command responses may be truncated" + ) + + try: + self._exec_cli_command(b"terminal no prompt") + except AnsibleConnectionFailure: + display.display( + "WARNING: Unable disable prompt, command responses may fail" + ) + + def on_become(self, passwd=None): + if self._get_prompt().endswith(b"#"): + return + + cmd = {u"command": u"enable"} + if passwd: + # Note: python-3.5 cannot combine u"" and r"" together. Thus make + # an r string and use to_text to ensure it's text on both py2 and py3. + cmd[u"prompt"] = to_text( + r"[\r\n]?(?:.*)?[Pp]assword: ?$", errors="surrogate_or_strict" + ) + cmd[u"answer"] = passwd + cmd[u"prompt_retry_check"] = True + try: + self._exec_cli_command( + to_bytes(json.dumps(cmd), errors="surrogate_or_strict") + ) + prompt = self._get_prompt() + if prompt is None or not prompt.endswith(b"#"): + raise AnsibleConnectionFailure( + "failed to elevate privilege to enable mode still at prompt [%s]" + % prompt + ) + except AnsibleConnectionFailure as e: + prompt = self._get_prompt() + raise_from(AnsibleConnectionFailure( + "unable to elevate privilege to enable mode, at prompt [%s] with error: %s" + % (prompt, e.message) + ), e) + + def on_unbecome(self): + prompt = self._get_prompt() + if prompt is None: + # if prompt is None most likely the terminal is hung up at a prompt + return + + if b"(config" in prompt: + self._exec_cli_command(b"end") + self._exec_cli_command(b"disable") + + elif prompt.endswith(b"#"): + self._exec_cli_command(b"disable") |