diff --git a/lib/ansible/module_utils/enos.py b/lib/ansible/module_utils/enos.py new file mode 100644 index 00000000000..d2db1e8435c --- /dev/null +++ b/lib/ansible/module_utils/enos.py @@ -0,0 +1,162 @@ +# 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. +# +# Copyright (C) 2017 Lenovo, Inc. +# All rights reserved. +# +# 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. +# +# Contains utility methods +# Lenovo Networking + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import env_fallback, return_values +from ansible.module_utils.network_common import to_list, EntityCollection +from ansible.module_utils.connection import Connection, exec_command + +_DEVICE_CONFIGS = {} +_CONNECTION = None + +enos_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'), + 'authorize': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTHORIZE']), type='bool'), + 'auth_pass': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTH_PASS']), no_log=True), + 'timeout': dict(type='int'), + 'context': dict(), + 'passwords': dict() +} + +enos_argument_spec = { + 'provider': dict(type='dict', options=enos_provider_spec), +} + +command_spec = { + 'command': dict(key=True), + 'prompt': dict(), + 'answer': dict() +} + + +def get_provider_argspec(): + return enos_provider_spec + + +def check_args(module, warnings): + pass + + +def get_connection(module): + global _CONNECTION + if _CONNECTION: + return _CONNECTION + _CONNECTION = Connection(module) + + context = None + try: + context = module.params['context'] + except KeyError: + context = None + + if context: + if context == 'system': + command = 'changeto system' + else: + command = 'changeto context %s' % context + _CONNECTION.get(command) + + return _CONNECTION + + +def get_config(module, flags=None): + flags = [] if flags is None else flags + + passwords = module.params['passwords'] + if passwords: + cmd = 'more system:running-config' + else: + cmd = 'show running-config ' + cmd += ' '.join(flags) + cmd = cmd.strip() + + try: + return _DEVICE_CONFIGS[cmd] + except KeyError: + conn = get_connection(module) + out = conn.get(cmd) + cfg = to_text(out, errors='surrogate_then_replace').strip() + _DEVICE_CONFIGS[cmd] = cfg + return cfg + + +def to_commands(module, commands): + assert isinstance(commands, list), 'argument must be of type ' + + transform = EntityCollection(module, command_spec) + commands = transform(commands) + + for index, item in enumerate(commands): + if module.check_mode and not item['command'].startswith('show'): + module.warn('only show commands are supported when using check ' + 'mode, not executing `%s`' % item['command']) + + return commands + + +def run_commands(module, commands, check_rc=True): + connection = get_connection(module) + + commands = to_commands(module, to_list(commands)) + + responses = list() + + for cmd in commands: + out = connection.get(**cmd) + responses.append(to_text(out, errors='surrogate_then_replace')) + + return responses + + +def load_config(module, config): + conn = get_connection(module) + conn.edit_config(config) + + +def get_defaults_flag(module): + rc, out, err = exec_command(module, 'show running-config ?') + out = to_text(out, errors='surrogate_then_replace') + + commands = set() + for line in out.splitlines(): + if line: + commands.add(line.strip().split()[0]) + + if 'all' in commands: + return 'all' + else: + return 'full' diff --git a/lib/ansible/modules/network/enos/enos_facts.py b/lib/ansible/modules/network/enos/enos_facts.py new file mode 100644 index 00000000000..bac97f52b99 --- /dev/null +++ b/lib/ansible/modules/network/enos/enos_facts.py @@ -0,0 +1,488 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function +__metaclass__ = type +# +# Copyright (C) 2017 Lenovo, 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 . +# +# Module to Collect facts from Lenovo Switches running Lenovo ENOS commands +# Lenovo Networking +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: enos_facts +version_added: "2.5" +author: "Anil Kumar Muraleedharan (@amuraleedhar)" +short_description: Collect facts from remote devices running Lenovo ENOS +description: + - Collects a base set of device facts from a remote Lenovo device + running on ENOS. 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. +extends_documentation_fragment: enos +notes: + - Tested against ENOS 8.4.1.68 +options: + gather_subset: + description: + - When supplied, this argument will restrict the facts collected + to a given subset. Possible values for this argument include + all, hardware, config, and interfaces. Can specify a list of + values to include a larger subset. Values can also be used + with an initial C(M(!)) to specify that a specific subset should + not be collected. + required: false + default: '!config' +''' +EXAMPLES = ''' +Tasks: The following are examples of using the module enos_facts. +--- +- name: Test Enos Facts + enos_facts: + provider={{ cli }} + + vars: + cli: + host: "{{ inventory_hostname }}" + port: 22 + username: admin + password: admin + transport: cli + timeout: 30 + authorize: True + auth_pass: + +--- +# Collect all facts from the device +- enos_facts: + gather_subset: all + provider: "{{ cli }}" + +# Collect only the config and default facts +- enos_facts: + gather_subset: + - config + provider: "{{ cli }}" + +# Do not collect hardware facts +- enos_facts: + gather_subset: + - "!hardware" + provider: "{{ cli }}" + +''' +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 Lenovo ENOS device + returned: always + type: str + ansible_net_serialnum: + description: The serial number of the Lenovo ENOS device + returned: always + type: str + ansible_net_version: + description: The ENOS 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: string + ansible_net_image: + description: Indicates the active image for the device + returned: always + type: string +# hardware + ansible_net_memfree_mb: + description: The available free memory on the remote device in MB + 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. + This gives information on description, mac address, mtu, speed, + duplex and operstatus + returned: when interfaces is configured + type: dict + ansible_net_neighbors: + description: The list of LLDP neighbors from the remote device + returned: when interfaces is configured + type: dict +''' + +import re + +from ansible.module_utils.enos import run_commands, enos_argument_spec, check_args +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems +from ansible.module_utils.six.moves import zip + + +class FactsBase(object): + + COMMANDS = list() + + def __init__(self, module): + self.module = module + self.facts = dict() + self.responses = None + self.PERSISTENT_COMMAND_TIMEOUT = 60 + + def populate(self): + self.responses = run_commands(self.module, self.COMMANDS, + check_rc=False) + + def run(self, cmd): + return run_commands(self.module, cmd, check_rc=False) + + +class Default(FactsBase): + + COMMANDS = ['show version', 'show run'] + + def populate(self): + super(Default, self).populate() + data = self.responses[0] + data_run = self.responses[1] + if data: + self.facts['version'] = self.parse_version(data) + self.facts['serialnum'] = self.parse_serialnum(data) + self.facts['model'] = self.parse_model(data) + self.facts['image'] = self.parse_image(data) + if data_run: + self.facts['hostname'] = self.parse_hostname(data_run) + + def parse_version(self, data): + match = re.search(r'^Software Version (.*?) ', data, re.M | re.I) + if match: + return match.group(1) + + def parse_hostname(self, data_run): + for line in data_run.split('\n'): + line = line.strip() + match = re.match(r'hostname (.*?)', line, re.M | re.I) + if match: + hosts = line.split() + hostname = hosts[1].strip('\"') + return hostname + return "NA" + + def parse_model(self, data): + # match = re.search(r'^Cisco (.+) \(revision', data, re.M) + match = re.search(r'^Lenovo RackSwitch (\S+)', data, re.M | re.I) + if match: + return match.group(1) + + def parse_image(self, data): + match = re.search(r'(.*) image1(.*)', data, re.M | re.I) + if match: + return "Image1" + else: + return "Image2" + + def parse_serialnum(self, data): + # match = re.search(r'board ID (\S+)', data) + match = re.search(r'^Switch Serial No: (\S+)', data, re.M | re.I) + if match: + return match.group(1) + + +class Hardware(FactsBase): + + COMMANDS = [ + 'show system memory' + ] + + def populate(self): + super(Hardware, self).populate() + data = self.responses[0] + if data: + self.facts['memtotal_mb'] = self.parse_memtotal(data) + self.facts['memfree_mb'] = self.parse_memfree(data) + + def parse_memtotal(self, data): + match = re.search(r'^MemTotal:\s*(.*) kB', data, re.M | re.I) + if match: + return int(match.group(1)) / 1024 + + def parse_memfree(self, data): + match = re.search(r'^MemFree:\s*(.*) kB', data, re.M | re.I) + if match: + return int(match.group(1)) / 1024 + + +class Config(FactsBase): + + COMMANDS = ['show running-config'] + + def populate(self): + super(Config, self).populate() + data = self.responses[0] + if data: + self.facts['config'] = data + + +class Interfaces(FactsBase): + + COMMANDS = ['show interface status'] + + def populate(self): + super(Interfaces, self).populate() + + self.facts['all_ipv4_addresses'] = list() + self.facts['all_ipv6_addresses'] = list() + + data1 = self.run(['show interface status']) + data1 = to_text(data1, errors='surrogate_or_strict').strip() + data1 = data1.replace(r"\n", "\n") + data2 = self.run(['show lldp port']) + data2 = to_text(data2, errors='surrogate_or_strict').strip() + data2 = data2.replace(r"\n", "\n") + lines1 = None + lines2 = None + if data1: + lines1 = self.parse_interfaces(data1) + if data2: + lines2 = self.parse_interfaces(data2) + if lines1 is not None and lines2 is not None: + self.facts['interfaces'] = self.populate_interfaces(lines1, lines2) + data3 = self.run(['show lldp remote-device port']) + data3 = to_text(data3, errors='surrogate_or_strict').strip() + data3 = data3.replace(r"\n", "\n") + + lines3 = None + if data3: + lines3 = self.parse_neighbors(data3) + if lines3 is not None: + self.facts['neighbors'] = self.populate_neighbors(lines3) + + data4 = self.run(['show interface ip']) + data4 = data4[0].split('\n') + lines4 = None + if data4: + lines4 = self.parse_ipaddresses(data4) + ipv4_interfaces = self.set_ipv4_interfaces(lines4) + self.facts['all_ipv4_addresses'] = ipv4_interfaces + ipv6_interfaces = self.set_ipv6_interfaces(lines4) + self.facts['all_ipv6_addresses'] = ipv6_interfaces + + def parse_ipaddresses(self, data4): + parsed = list() + for line in data4: + if len(line) == 0: + continue + else: + line = line.strip() + if len(line) == 0: + continue + match = re.search(r'IP4', line, re.M | re.I) + if match: + key = match.group() + parsed.append(line) + match = re.search(r'IP6', line, re.M | re.I) + if match: + key = match.group() + parsed.append(line) + return parsed + + def set_ipv4_interfaces(self, line4): + ipv4_addresses = list() + for line in line4: + ipv4Split = line.split() + if ipv4Split[1] == "IP4": + ipv4_addresses.append(ipv4Split[2]) + return ipv4_addresses + + def set_ipv6_interfaces(self, line4): + ipv6_addresses = list() + for line in line4: + ipv6Split = line.split() + if ipv6Split[1] == "IP6": + ipv6_addresses.append(ipv6Split[2]) + return ipv6_addresses + + def populate_neighbors(self, lines3): + neighbors = dict() + for line in lines3: + neighborSplit = line.split("|") + innerData = dict() + innerData['Remote Chassis ID'] = neighborSplit[2].strip() + innerData['Remote Port'] = neighborSplit[3].strip() + sysName = neighborSplit[4].strip() + if sysName is not None: + innerData['Remote System Name'] = neighborSplit[4].strip() + else: + innerData['Remote System Name'] = "NA" + neighbors[neighborSplit[0].strip()] = innerData + return neighbors + + def populate_interfaces(self, lines1, lines2): + interfaces = dict() + for line1, line2 in zip(lines1, lines2): + line = line1 + " " + line2 + intfSplit = line.split() + innerData = dict() + innerData['description'] = intfSplit[6].strip() + innerData['macaddress'] = intfSplit[8].strip() + innerData['mtu'] = intfSplit[9].strip() + innerData['speed'] = intfSplit[1].strip() + innerData['duplex'] = intfSplit[2].strip() + innerData['operstatus'] = intfSplit[5].strip() + interfaces[intfSplit[0].strip()] = innerData + return interfaces + + def parse_neighbors(self, neighbors): + parsed = list() + for line in neighbors.split('\n'): + if len(line) == 0: + continue + else: + line = line.strip() + match = re.match(r'^([0-9]+)', line) + if match: + key = match.group(1) + parsed.append(line) + match = re.match(r'^(MGT+)', line) + if match: + key = match.group(1) + parsed.append(line) + return parsed + + def parse_interfaces(self, data): + parsed = list() + for line in data.split('\n'): + if len(line) == 0: + continue + else: + line = line.strip() + match = re.match(r'^([0-9]+)', line) + if match: + key = match.group(1) + parsed.append(line) + match = re.match(r'^(MGT+)', line) + if match: + key = match.group(1) + parsed.append(line) + return parsed + +FACT_SUBSETS = dict( + default=Default, + hardware=Hardware, + interfaces=Interfaces, + config=Config, +) + +VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) + +PERSISTENT_COMMAND_TIMEOUT = 60 + + +def main(): + """main entry point for module execution + """ + argument_spec = dict( + gather_subset=dict(default=['!config'], type='list') + ) + + argument_spec.update(enos_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') + + 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 + + warnings = list() + check_args(module, warnings) + + module.exit_json(ansible_facts=ansible_facts, warnings=warnings) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/plugins/action/enos.py b/lib/ansible/plugins/action/enos.py index 2e1366321fb..35f28b90391 100644 --- a/lib/ansible/plugins/action/enos.py +++ b/lib/ansible/plugins/action/enos.py @@ -4,7 +4,7 @@ # Ansible still belong to the author of the module, and may assign their own # license to the complete work. # -# Copyright (C) 2018 Lenovo, Inc. +# Copyright (C) 2017 Lenovo, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/lib/ansible/plugins/cliconf/enos.py b/lib/ansible/plugins/cliconf/enos.py index f0a6b0203d5..1092fea5c25 100644 --- a/lib/ansible/plugins/cliconf/enos.py +++ b/lib/ansible/plugins/cliconf/enos.py @@ -4,7 +4,7 @@ # Ansible still belong to the author of the module, and may assign their own # license to the complete work. # -# Copyright (C) 2018 Lenovo, Inc. +# Copyright (C) 2017 Lenovo, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/lib/ansible/plugins/terminal/enos.py b/lib/ansible/plugins/terminal/enos.py index cb08f72270b..c6a89e05268 100644 --- a/lib/ansible/plugins/terminal/enos.py +++ b/lib/ansible/plugins/terminal/enos.py @@ -4,7 +4,7 @@ # Ansible still belong to the author of the module, and may assign their own # license to the complete work. # -# Copyright (C) 2018 Lenovo, Inc. +# Copyright (C) 2017 Lenovo, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/units/modules/network/enos/__init__.py b/test/units/modules/network/enos/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/units/modules/network/enos/enos_module.py b/test/units/modules/network/enos/enos_module.py new file mode 100644 index 00000000000..d9395f5745f --- /dev/null +++ b/test/units/modules/network/enos/enos_module.py @@ -0,0 +1,114 @@ +# (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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import json + +from ansible.compat.tests import unittest +from ansible.compat.tests.mock import patch +from ansible.module_utils import basic +from ansible.module_utils._text import to_bytes + + +def set_module_args(args): + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) + +fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') +fixture_data = {} + + +def load_fixture(name): + path = os.path.join(fixture_path, name) + + if path in fixture_data: + return fixture_data[path] + + with open(path) as f: + data = f.read() + + try: + data = json.loads(data) + except: + pass + + fixture_data[path] = data + return data + + +class AnsibleExitJson(Exception): + pass + + +class AnsibleFailJson(Exception): + pass + + +class TestEnosModule(unittest.TestCase): + + def execute_module(self, failed=False, changed=False, commands=None, sort=True, defaults=False): + + self.load_fixtures(commands) + + if failed: + result = self.failed() + self.assertTrue(result['failed'], result) + else: + result = self.changed(changed) + self.assertEqual(result['changed'], changed, result) + + if commands is not None: + if sort: + self.assertEqual(sorted(commands), sorted(result['commands']), result['commands']) + else: + self.assertEqual(commands, result['commands'], result['commands']) + + return result + + def failed(self): + def fail_json(*args, **kwargs): + kwargs['failed'] = True + raise AnsibleFailJson(kwargs) + + with patch.object(basic.AnsibleModule, 'fail_json', fail_json): + with self.assertRaises(AnsibleFailJson) as exc: + self.module.main() + + result = exc.exception.args[0] + self.assertTrue(result['failed'], result) + return result + + def changed(self, changed=False): + def exit_json(*args, **kwargs): + if 'changed' not in kwargs: + kwargs['changed'] = False + raise AnsibleExitJson(kwargs) + + with patch.object(basic.AnsibleModule, 'exit_json', exit_json): + with self.assertRaises(AnsibleExitJson) as exc: + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], changed, result) + return result + + def load_fixtures(self, commands=None): + pass diff --git a/test/units/modules/network/enos/fixtures/show_interface_ip b/test/units/modules/network/enos/fixtures/show_interface_ip new file mode 100644 index 00000000000..263a861f57c --- /dev/null +++ b/test/units/modules/network/enos/fixtures/show_interface_ip @@ -0,0 +1,9 @@ +Interface information: +1: IP4 192.168.49.50 255.255.255.0 192.168.49.255, vlan 1, up +128: IP4 10.241.105.24 255.255.255.0 10.241.105.255, vlan 4095, up + +Routed Port Interface Information: + +Loopback interface information: + + diff --git a/test/units/modules/network/enos/fixtures/show_interface_status b/test/units/modules/network/enos/fixtures/show_interface_status new file mode 100644 index 00000000000..52b37eb184e --- /dev/null +++ b/test/units/modules/network/enos/fixtures/show_interface_status @@ -0,0 +1,59 @@ +----------------------------------------------------------------------- +Port Speed Duplex Flow Ctrl Link Description +------- ------ -------- --TX-----RX-- ------ ------------- +1 1G/10G full no no down 1 +2 1G/10G full no no down 2 +3 1G/10G full no no down 3 +4 1G/10G full no no down 4 +5 1G/10G full no no down 5 +6 1G/10G full no no down 6 +7 1G/10G full no no down 7 +8 1G/10G full no no down 8 +9 1G/10G full no no down 9 +10 1G/10G full no no down 10 +11 1G/10G full no no down 11 +12 1G/10G full no no down 12 +13 1G/10G full no no down 13 +14 1G/10G full no no down 14 +15 1G/10G full no no down 15 +16 1G/10G full no no down 16 +17 1G/10G full no no down 17 +18 1G/10G full no no down 18 +19 1G/10G full no no down 19 +20 1G/10G full no no down 20 +21 1G/10G full no no down 21 +22 1G/10G full no no down 22 +23 1G/10G full no no down 23 +24 1G/10G full no no down 24 +25 1G/10G full no no down 25 +26 1G/10G full no no down 26 +27 1G/10G full no no down 27 +28 1G/10G full no no down 28 +29 1G/10G full no no down 29 +30 1G/10G full no no down 30 +31 1G/10G full no no down 31 +32 1G/10G full no no down 32 +33 1G/10G full no no down 33 +34 1G/10G full no no down 34 +35 1G/10G full no no down 35 +36 1G/10G full no no down 36 +37 1G/10G full no no down 37 +38 1000 full no no up 38 +39 1G/10G full no no down 39 +40 1G/10G full no no down 40 +41 1G/10G full no no down 41 +42 1G/10G full no no down 42 +43 1G/10G full no no down 43 +44 1G/10G full no no down 44 +45 1G/10G full no no down 45 +46 1G/10G full no no down 46 +47 1G/10G full no no down 47 +48 10000 full no no down 48 +49 40000 full no no down 49 +50 40000 full no no down 50 +51 40000 full no no down 51 +52 40000 full no no down 52 +53 40000 full no no down 53 +54 40000 full no no down 54 +MGT 1000 full yes yes up MGT + diff --git a/test/units/modules/network/enos/fixtures/show_lldp_port b/test/units/modules/network/enos/fixtures/show_lldp_port new file mode 100644 index 00000000000..fcefdf03bdb --- /dev/null +++ b/test/units/modules/network/enos/fixtures/show_lldp_port @@ -0,0 +1,60 @@ +LLDP Port Info +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Port MAC address MTU PortEnabled AdminStatus RxChange TrapNotify +======= ================= ==== =========== =========== ======== ========== +1 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +2 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +3 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +4 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +5 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +6 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +7 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +8 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +9 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +10 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +11 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +12 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +13 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +14 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +15 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +16 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +17 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +18 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +19 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +20 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +21 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +22 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +23 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +24 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +25 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +26 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +27 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +28 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +29 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +30 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +31 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +32 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +33 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +34 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +35 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +36 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +37 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +38 a8:97:dc:dd:e2:00 9216 enabled tx_rx no disabled +39 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +40 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +41 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +42 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +43 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +44 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +45 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +46 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +47 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +48 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +49 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +50 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +51 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +52 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +53 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +54 a8:97:dc:dd:e2:00 9216 disabled tx_rx no disabled +MGT a8:97:dc:dd:e2:fe 9216 enabled tx_rx no disabled + diff --git a/test/units/modules/network/enos/fixtures/show_lldp_remote-device_port b/test/units/modules/network/enos/fixtures/show_lldp_remote-device_port new file mode 100644 index 00000000000..1381c099dc4 --- /dev/null +++ b/test/units/modules/network/enos/fixtures/show_lldp_remote-device_port @@ -0,0 +1,12 @@ +LLDP Remote Devices Information +Legend(possible values in DMAC column) : +NB - Nearest Bridge - 01-80-C2-00-00-0E +NnTB - Nearest non-TPMR Bridge - 01-80-C2-00-00-03 +NCB - Nearest Customer Bridge - 01-80-C2-00-00-00 +Total number of current entries: 1 + +LocalPort | Index | Remote Chassis ID | Remote Port | Remote System Name | DMAC +----------|-------|---------------------------|----------------------|-------------------------------|--------- +MGT | 1 | 74 26 ac 3d 3c c0 | Gi3/18 | INDIA-LAB-1-C4506E-A.labs.l...| NB + + diff --git a/test/units/modules/network/enos/fixtures/show_run b/test/units/modules/network/enos/fixtures/show_run new file mode 100644 index 00000000000..4d918dfe002 --- /dev/null +++ b/test/units/modules/network/enos/fixtures/show_run @@ -0,0 +1,59 @@ +Current configuration: +! +version "8.4.3.12" +switch-type "Lenovo RackSwitch G8272" +iscli-new +! +! +access https enable + +snmp-server location "Location:,Room:,Rack:Rack 3,LRU:40" +snmp-server read-community "public" +snmp-server trap-source 128 +! +! +! +no system dhcp +no system default-ip mgt +hostname "test1" +! +! +! +! +! +! +! +! +! +! +! +! +! +! +! +! +! +! +!interface ip 1 +! addr +! enable +! +interface ip 128 + ip address 10.241.105.24 255.255.255.0 + enable + exit +! +ip gateway 4 address 10.241.105.1 +ip gateway 4 enable +! +! +! +! +! +! +router bgp + as 100 +! +! +end + diff --git a/test/units/modules/network/enos/fixtures/show_running-config b/test/units/modules/network/enos/fixtures/show_running-config new file mode 100644 index 00000000000..4d918dfe002 --- /dev/null +++ b/test/units/modules/network/enos/fixtures/show_running-config @@ -0,0 +1,59 @@ +Current configuration: +! +version "8.4.3.12" +switch-type "Lenovo RackSwitch G8272" +iscli-new +! +! +access https enable + +snmp-server location "Location:,Room:,Rack:Rack 3,LRU:40" +snmp-server read-community "public" +snmp-server trap-source 128 +! +! +! +no system dhcp +no system default-ip mgt +hostname "test1" +! +! +! +! +! +! +! +! +! +! +! +! +! +! +! +! +! +! +!interface ip 1 +! addr +! enable +! +interface ip 128 + ip address 10.241.105.24 255.255.255.0 + enable + exit +! +ip gateway 4 address 10.241.105.1 +ip gateway 4 enable +! +! +! +! +! +! +router bgp + as 100 +! +! +end + diff --git a/test/units/modules/network/enos/fixtures/show_system_memory b/test/units/modules/network/enos/fixtures/show_system_memory new file mode 100644 index 00000000000..0a0625eb16b --- /dev/null +++ b/test/units/modules/network/enos/fixtures/show_system_memory @@ -0,0 +1,166 @@ +------------------------------------------------------------------ + +Memory utilization: +MemTotal: 4088580 kB +MemFree: 3464304 kB +MemAvailable: 3586864 kB +Buffers: 2016 kB +Cached: 173236 kB +SwapCached: 0 kB +Active: 504316 kB +Inactive: 38056 kB +Active(anon): 376332 kB +Inactive(anon): 27460 kB +Active(file): 127984 kB +Inactive(file): 10596 kB +Unevictable: 0 kB +Mlocked: 0 kB +HighTotal: 3407860 kB +HighFree: 2952904 kB +LowTotal: 680720 kB +LowFree: 511400 kB +SwapTotal: 0 kB +SwapFree: 0 kB +Dirty: 0 kB +Writeback: 0 kB +AnonPages: 367120 kB +Mapped: 20664 kB +Shmem: 36672 kB +Slab: 8240 kB +SReclaimable: 3492 kB +SUnreclaim: 4748 kB +KernelStack: 760 kB +PageTables: 1592 kB +NFS_Unstable: 0 kB +Bounce: 0 kB +WritebackTmp: 0 kB +CommitLimit: 2044288 kB +Committed_AS: 1128364 kB +VmallocTotal: 241652 kB +VmallocUsed: 17116 kB +VmallocChunk: 223920 kB +HugePages_Total: 0 +HugePages_Free: 0 +HugePagesPercentage used 15 + +Memory tracing: enabled +Extended Memory tracing: disabled +High-water monitoring: enabled + +Memory high-water: 20 percent (at 1818 seconds from boot) + +Memory stats: + allocs: 16484474 + frees: 16481108 + current: 3378 + alloc fails: 0 + +STEM thread memory stats: + thid name allocs frees diff current * largest + 0 INIT 2655 933 1722 69381079 31982881 + 1 STEM 0 0 0 0 0 + 2 STP 13 6 7 41165721 16673868 + 3 MFDB 1 0 1 6 6 + 4 TND 41745 42134 -389 847441 336 + 5 CONS 3867 3866 1 26622356 6291456 + 6 TNET 3806775 3809157 -2382 1032303745 12582912 + 7 TNET 126519 127060 -541 269598653 12582912 + 8 TNET 1 0 1 6131 6131 + 9 TNET 1 0 1 6131 6131 + 10 TNET 1 0 1 6131 6131 + 11 TNET 1 0 1 6131 6131 + 12 LOG 441 441 0 61437 272 + 13 TRAP 16911 16911 0 1416745 544 + 14 NTP 0 0 0 0 0 + 15 RMON 0 0 0 0 0 + 18 IP 40 7 33 26152 4248 + 19 RIP 0 0 0 0 0 + 20 AGR 24643 23177 1466 8949189 6131 + 21 EPI 0 0 0 0 0 + 22 PORT 56 0 56 60032 16384 + 23 BGP 0 10 -10 0 0 + 27 MGMT 335 162 173 48883648 524436 + 28 SCAN 0 0 0 0 0 + 29 OSPF 0 20 -20 0 0 + 30 VRRP 1 0 1 16 16 + 31 SNMP 4670978 4670164 814 2315549793 12582912 + 32 SNMP 1108 1068 40 208175203 12582912 + 34 SSHD 800286 796910 3376 271976834 2017 + 36 DT1X 0 0 0 0 0 + 37 NCFD 1 0 1 6131 6131 + 38 NCFD 1 0 1 6131 6131 + 39 NCFD 1 0 1 6131 6131 + 40 NCFD 1 0 1 6131 6131 + 41 SWR 0 0 0 0 0 + 42 SWRH 0 0 0 0 0 + 43 OBS 0 0 0 0 0 + 44 TEAM 0 0 0 0 0 + 45 I2C 0 0 0 0 0 + 46 LACP 72 0 72 1152 16 + 47 SFP 0 0 0 0 0 + 48 SWKY 0 0 0 0 0 + 49 HLNK 0 0 0 0 0 + 50 LLDP 5794454 5794373 81 598072737 14336 + 51 IPV6 0 0 0 0 0 + 52 RTM6 0 0 0 0 0 + 53 PNG6 0 0 0 0 0 + 55 OSP3 0 0 0 0 0 + 56 VMAC 0 0 0 0 0 + 57 MEMM 0 0 0 0 0 + 58 UDLD 0 0 0 0 0 + 59 FCOE 0 0 0 0 0 + 60 SFLO 0 0 0 0 0 + 61 PROX 0 0 0 0 0 + 62 OAM 0 0 0 0 0 + 63 PIM 0 0 0 0 0 + 64 DCBX 1 1 0 126 126 + 65 NBOO 1 0 1 6131 6131 + 66 VLAG 0 0 0 0 0 + 67 MLD6 0 0 0 0 0 + 68 DHCP 0 0 0 0 0 + 69 ETMR 0 0 0 0 0 + 70 IKE2 0 0 0 0 0 + 71 ACLG 1 0 1 5120 5120 + 72 HWRT 0 0 0 0 0 + 73 OFLO 17 0 17 32244 8048 + 74 SFM 0 0 0 0 0 + 75 UPTM 0 0 0 0 0 + 76 VSDB 0 2 -2 0 0 + 77 ECPT 3 0 3 168532 168000 + 78 ECPR 0 0 0 0 0 + 79 VDPT 5 0 5 5260 1460 + 80 VFDB 0 1 -1 0 0 + 81 PTP 0 0 0 0 0 + 82 PBR 0 0 0 0 0 + 83 HIST 0 0 0 0 0 + 84 SLP 699757 701435 -1678 254297215 262140 + 85 UFP 217 73 144 12908 132 + 86 CDCP 0 0 0 0 0 + 87 IGMP 0 0 0 0 0 + 88 ICMP 0 0 0 0 0 + 89 HCM 0 0 0 0 0 + 90 CFCF 0 0 0 0 0 + 91 FDF@ 0 0 0 0 0 + 92 NAT 0 0 0 0 0 + 93 OCM1 11 0 11 44 4 + 94 OCM2 0 0 0 0 0 + 95 OFDT 0 0 0 0 0 + 96 OSFM 5 0 5 2636 1024 + 97 OBSC 0 0 0 0 0 + 98 STPM 0 0 0 0 0 + 99 ARP 0 0 0 0 0 + 100 VXLN 0 0 0 0 0 + 101 OVSD 0 0 0 0 0 + 102 OVSC 0 0 0 0 0 + 103 VTEP 0 0 0 0 0 + 104 BFD 18 0 18 440 44 + 105 STPR 0 0 0 0 0 + 106 VMFD 0 0 0 0 0 + 107 NORM 0 0 0 0 0 + 108 DONE 494136 493788 348 280129530 6291456 + Total 16485149 16481768 3381 1132837544 + + Non-STEM allocs 0 + Non-STEM frees 2 + Overhead 1780 + diff --git a/test/units/modules/network/enos/fixtures/show_version b/test/units/modules/network/enos/fixtures/show_version new file mode 100644 index 00000000000..a0f52b5731e --- /dev/null +++ b/test/units/modules/network/enos/fixtures/show_version @@ -0,0 +1,60 @@ +System Information at 11:37:06 Fri Oct 27, 2017 +Time zone: No timezone configured +Daylight Savings Time Status: Disabled + +Lenovo RackSwitch G8272 + +Switch has been up for 30 days, 10 hours, 43 minutes and 14 seconds. +Last boot: 00:53:32 Wed Sep 27, 2017 (power cycle) + +MAC address: a8:97:dc:dd:e2:00 IP (If 1) address: 192.168.49.50 +Management Port MAC Address: a8:97:dc:dd:e2:fe +Management Port IP Address (if 128): 10.241.105.24 +Hardware Revision: 0 +Board Revision: +Hardware Part No: 00CJ066 +Old Hardware Part No: 2MV4CR01W +Switch Serial No: Y052MV4CR01W +Manufacturing date: 14/51 + +MTM Value: 7159-HCV +Old MTM Value: +ESN: MM01086 + + +WARNING: This is UNRELEASED SOFTWARE for LAB TESTING ONLY. + DO NOT USE IN A PRODUCTION NETWORK. + + +Software Version 8.4.3.12 (FLASH image1), active configuration. +Boot kernel version 8.4.3.12 + +USB Boot: disabled + + + +Temperature CPU Local : 31 C +Temperature Ambient : 32 C +Temperature Hot Spot : 44 C +Temperature Asic Max : 63 C + +System Warning at 85 C / Shutdown at 95 C / Set Point is 70 C + +Fan 1 Module 1: 4054rpm 60pwm(23%) Front-To-Back +Fan 2 Module 1: 4404rpm 60pwm(23%) Front-To-Back +Fan 3 Module 2: 4112rpm 60pwm(23%) Front-To-Back +Fan 4 Module 2: 4372rpm 60pwm(23%) Front-To-Back +Fan 5 Module 3: 4072rpm 60pwm(23%) Front-To-Back +Fan 6 Module 3: 4306rpm 60pwm(23%) Front-To-Back +Fan 7 Module 4: 4134rpm 60pwm(23%) Front-To-Back +Fan 8 Module 4: 4326rpm 60pwm(23%) Front-To-Back + +System Fan Airflow: Front-To-Back + +Power Supply 1: Front-To-Back [DPS-460KB C] +Power Supply 2: Front-To-Back [DPS-460KB C] + + Power Faults: PS1-Pwr + Fan Faults: None +Service Faults: Too-Few-PS + diff --git a/test/units/modules/network/enos/test_enos_facts.py b/test/units/modules/network/enos/test_enos_facts.py new file mode 100644 index 00000000000..4724ab19a60 --- /dev/null +++ b/test/units/modules/network/enos/test_enos_facts.py @@ -0,0 +1,79 @@ +# (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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json + +from ansible.compat.tests.mock import patch +from .enos_module import TestEnosModule, load_fixture, set_module_args +from ansible.modules.network.enos import enos_facts + + +class TestEnosFacts(TestEnosModule): + + module = enos_facts + + def setUp(self): + self.mock_run_commands = patch( + 'ansible.modules.network.enos.enos_facts.run_commands') + self.run_commands = self.mock_run_commands.start() + + def tearDown(self): + self.mock_run_commands.stop() + + def load_fixtures(self, commands=None): + + def load_from_file(*args, **kwargs): + module, commands = args + output = list() + + for item in commands: + try: + obj = json.loads(item) + command = obj['command'] + except ValueError: + command = item + filename = str(command).replace(' ', '_') + filename = filename.replace('/', '7') + output.append(load_fixture(filename)) + return output + + self.run_commands.side_effect = load_from_file + + def test_enos_facts_gather_subset_default(self): + set_module_args(dict()) + result = self.execute_module() + ansible_facts = result['ansible_facts'] + self.assertIn('hardware', ansible_facts['ansible_net_gather_subset']) + self.assertIn('default', ansible_facts['ansible_net_gather_subset']) + self.assertIn('interfaces', ansible_facts['ansible_net_gather_subset']) + self.assertEquals('test1', ansible_facts['ansible_net_hostname']) + self.assertIn('MGT', ansible_facts['ansible_net_interfaces'].keys()) + self.assertEquals(3992.75390625, ansible_facts['ansible_net_memtotal_mb']) + self.assertEquals(3383.109375, ansible_facts['ansible_net_memfree_mb']) + + def test_enos_facts_gather_subset_config(self): + set_module_args({'gather_subset': 'config'}) + result = self.execute_module() + ansible_facts = result['ansible_facts'] + self.assertIn('default', ansible_facts['ansible_net_gather_subset']) + self.assertIn('config', ansible_facts['ansible_net_gather_subset']) + self.assertEquals('test1', ansible_facts['ansible_net_hostname']) + self.assertIn('ansible_net_config', ansible_facts)