diff --git a/lib/ansible/module_utils/network/exos/argspec/__init__.py b/lib/ansible/module_utils/network/exos/argspec/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/exos/argspec/facts/__init__.py b/lib/ansible/module_utils/network/exos/argspec/facts/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/exos/argspec/facts/facts.py b/lib/ansible/module_utils/network/exos/argspec/facts/facts.py new file mode 100644 index 00000000000..684c093097d --- /dev/null +++ b/lib/ansible/module_utils/network/exos/argspec/facts/facts.py @@ -0,0 +1,31 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The arg spec for the exos facts module. +""" +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class FactsArgs(object): # pylint: disable=R0903 + """ The arg spec for the exos facts module + """ + + def __init__(self, **kwargs): + pass + + choices = [ + 'all', + '!all', + 'lldp_global', + '!lldp_global' + ] + + argument_spec = { + 'gather_subset': dict(default=['!config'], type='list'), + 'gather_network_resources': dict(choices=choices, + type='list'), + } diff --git a/lib/ansible/module_utils/network/exos/argspec/lldp_global/__init__.py b/lib/ansible/module_utils/network/exos/argspec/lldp_global/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/exos/argspec/lldp_global/lldp_global.py b/lib/ansible/module_utils/network/exos/argspec/lldp_global/lldp_global.py new file mode 100644 index 00000000000..4106c534284 --- /dev/null +++ b/lib/ansible/module_utils/network/exos/argspec/lldp_global/lldp_global.py @@ -0,0 +1,57 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The arg spec for the exos_lldp_global module +""" +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Lldp_globalArgs(object): # pylint: disable=R0903 + """The arg spec for the exos_lldp_global module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'options': { + 'interval': {'default': 30, 'type': 'int'}, + 'tlv_select': { + 'options': { + 'management_address': {'type': 'bool'}, + 'port_description': {'type': 'bool'}, + 'system_capabilities': {'type': 'bool'}, + 'system_description': { + 'default': True, + 'type': 'bool'}, + 'system_name': {'default': True, 'type': 'bool'}}, + 'type': 'dict'}}, + 'type': 'dict'}, + 'state': { + 'choices': ['merged', 'replaced', 'deleted'], + 'default': 'merged', + 'type': 'str'}} # pylint: disable=C0301 diff --git a/lib/ansible/module_utils/network/exos/config/__init__.py b/lib/ansible/module_utils/network/exos/config/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/exos/config/lldp_global/__init__.py b/lib/ansible/module_utils/network/exos/config/lldp_global/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/exos/config/lldp_global/lldp_global.py b/lib/ansible/module_utils/network/exos/config/lldp_global/lldp_global.py new file mode 100644 index 00000000000..b466ccb125f --- /dev/null +++ b/lib/ansible/module_utils/network/exos/config/lldp_global/lldp_global.py @@ -0,0 +1,199 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The exos_lldp_global class +It is in this file where the current configuration (as dict) +is compared to the provided configuration (as dict) and the command set +necessary to bring the current configuration to it's desired end-state is +created +""" +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.network.common.cfg.base import ConfigBase +from ansible.module_utils.network.common.utils import to_list +from ansible.module_utils.network.exos.facts.facts import Facts +from ansible.module_utils.network.exos.exos import send_requests + +import json +from copy import deepcopy + + +class Lldp_global(ConfigBase): + """ + The exos_lldp_global class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'lldp_global', + ] + + LLDP_DEFAULT_INTERVAL = 30 + LLDP_DEFAULT_TLV = { + 'system_name': True, + 'system_description': True, + 'system_capabilities': False, + 'port_description': False, + 'management_address': False + } + LLDP_REQUEST = { + "data": {"openconfig-lldp:config": {}}, + "method": "PUT", + "path": "/rest/restconf/data/openconfig-lldp:lldp/config" + } + + def __init__(self, module): + super(Lldp_global, self).__init__(module) + + def get_lldp_global_facts(self): + """ Get the 'facts' (the current configuration) + + :rtype: A dictionary + :returns: The current configuration as a dictionary + """ + facts, _warnings = Facts(self._module).get_facts( + self.gather_subset, self.gather_network_resources) + lldp_global_facts = facts['ansible_network_resources'].get('lldp_global') + if not lldp_global_facts: + return {} + return lldp_global_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + requests = list() + + existing_lldp_global_facts = self.get_lldp_global_facts() + requests.extend(self.set_config(existing_lldp_global_facts)) + if requests: + if not self._module.check_mode: + send_requests(self._module, requests) + result['changed'] = True + result['requests'] = requests + + changed_lldp_global_facts = self.get_lldp_global_facts() + + result['before'] = existing_lldp_global_facts + if result['changed']: + result['after'] = changed_lldp_global_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_lldp_global_facts): + """ Collect the configuration from the args passed to the module, + collect the current configuration (as a dict from facts) + + :rtype: A list + :returns: the requests necessary to migrate the current configuration + to the desired configuration + """ + want = self._module.params['config'] + have = existing_lldp_global_facts + resp = self.set_state(want, have) + return to_list(resp) + + def set_state(self, want, have): + """ Select the appropriate function based on the state provided + + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :rtype: A list + :returns: the requests necessary to migrate the current configuration + to the desired configuration + """ + state = self._module.params['state'] + + if state == 'deleted': + requests = self._state_deleted(want, have) + elif state == 'merged': + requests = self._state_merged(want, have) + elif state == 'replaced': + requests = self._state_replaced(want, have) + + return requests + + def _state_replaced(self, want, have): + """ The request generator when state is replaced + + :rtype: A list + :returns: the requests necessary to migrate the current configuration + to the desired configuration + """ + requests = [] + requests.extend(self._state_deleted(want, have)) + requests.extend(self._state_merged(want, have)) + return requests + + def _state_merged(self, want, have): + """ The request generator when state is merged + + :rtype: A list + :returns: the requests necessary to merge the provided into + the current configuration + """ + requests = [] + + request = deepcopy(self.LLDP_REQUEST) + self._update_lldp_config_body_if_diff(want, have, request) + + if len(request["data"]["openconfig-lldp:config"]): + request["data"] = json.dumps(request["data"]) + requests.append(request) + + return requests + + def _state_deleted(self, want, have): + """ The request generator when state is deleted + + :rtype: A list + :returns: the requests necessary to remove the current configuration + of the provided objects + """ + requests = [] + + request = deepcopy(self.LLDP_REQUEST) + if want: + self._update_lldp_config_body_if_diff(want, have, request) + else: + if self.LLDP_DEFAULT_INTERVAL != have['interval']: + request["data"]["openconfig-lldp:config"].update( + {"hello-timer": self.LLDP_DEFAULT_INTERVAL}) + + if have['tlv_select'] != self.LLDP_DEFAULT_TLV: + request["data"]["openconfig-lldp:config"].update( + {"suppress-tlv-advertisement": [key.upper() for key, value in self.LLDP_DEFAULT_TLV.items() if not value]}) + request["data"]["openconfig-lldp:config"]["suppress-tlv-advertisement"].sort() + if len(request["data"]["openconfig-lldp:config"]): + request["data"] = json.dumps(request["data"]) + requests.append(request) + + return requests + + def _update_lldp_config_body_if_diff(self, want, have, request): + if want.get('interval'): + if want['interval'] != have['interval']: + request["data"]["openconfig-lldp:config"].update( + {"hello-timer": want['interval']}) + if want.get('tlv_select'): + # Create list of TLVs to be suppressed which aren't already + want_suppress = [key.upper() for key, value in want["tlv_select"].items() if have["tlv_select"][key] != value and value is False] + if want_suppress: + # Add previously suppressed TLVs to the list as we are doing a PUT op + want_suppress.extend([key.upper() for key, value in have["tlv_select"].items() if value is False]) + request["data"]["openconfig-lldp:config"].update( + {"suppress-tlv-advertisement": want_suppress}) + request["data"]["openconfig-lldp:config"]["suppress-tlv-advertisement"].sort() diff --git a/lib/ansible/module_utils/network/exos/exos.py b/lib/ansible/module_utils/network/exos/exos.py index d8f6f959cb1..7c5c8a8b186 100644 --- a/lib/ansible/module_utils/network/exos/exos.py +++ b/lib/ansible/module_utils/network/exos/exos.py @@ -209,7 +209,7 @@ def to_request(module, requests): transform = ComplexList(dict( path=dict(key=True), method=dict(), - data=dict(), + data=dict(type='dict'), ), module) return transform(to_list(requests)) diff --git a/lib/ansible/module_utils/network/exos/facts/__init__.py b/lib/ansible/module_utils/network/exos/facts/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/exos/facts/facts.py b/lib/ansible/module_utils/network/exos/facts/facts.py new file mode 100644 index 00000000000..d9a8d4fdba6 --- /dev/null +++ b/lib/ansible/module_utils/network/exos/facts/facts.py @@ -0,0 +1,56 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The facts class for exos +this file validates each subset of facts and selectively +calls the appropriate facts gathering function +""" +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.network.exos.argspec.facts.facts import FactsArgs +from ansible.module_utils.network.common.facts.facts import FactsBase +from ansible.module_utils.network.exos.facts.lldp_global.lldp_global import Lldp_globalFacts +from ansible.module_utils.network.exos.facts.legacy.base import Default, Hardware, Interfaces, Config + +FACT_LEGACY_SUBSETS = dict( + default=Default, + hardware=Hardware, + interfaces=Interfaces, + config=Config) + +FACT_RESOURCE_SUBSETS = dict( + lldp_global=Lldp_globalFacts, +) + + +class Facts(FactsBase): + """ The fact class for exos + """ + + VALID_LEGACY_GATHER_SUBSETS = frozenset(FACT_LEGACY_SUBSETS.keys()) + VALID_RESOURCE_SUBSETS = frozenset(FACT_RESOURCE_SUBSETS.keys()) + + def __init__(self, module): + super(Facts, self).__init__(module) + + def get_facts(self, legacy_facts_type=None, resource_facts_type=None, data=None): + """ Collect the facts for exos + + :param legacy_facts_type: List of legacy facts types + :param resource_facts_type: List of resource fact types + :param data: previously collected conf + :rtype: dict + :return: the facts gathered + """ + netres_choices = FactsArgs.argument_spec['gather_network_resources'].get('choices', []) + if self.VALID_RESOURCE_SUBSETS: + self.get_network_resources_facts(netres_choices, FACT_RESOURCE_SUBSETS, resource_facts_type, data) + + if self.VALID_LEGACY_GATHER_SUBSETS: + self.get_network_legacy_facts(FACT_LEGACY_SUBSETS, legacy_facts_type) + + return self.ansible_facts, self._warnings diff --git a/lib/ansible/module_utils/network/exos/facts/legacy/__init__.py b/lib/ansible/module_utils/network/exos/facts/legacy/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/exos/facts/legacy/base.py b/lib/ansible/module_utils/network/exos/facts/legacy/base.py new file mode 100644 index 00000000000..886fc863144 --- /dev/null +++ b/lib/ansible/module_utils/network/exos/facts/legacy/base.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The exos legacy fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import re +import json + +from ansible.module_utils.network.exos.exos import run_commands +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.warnings = list() + self.responses = None + + def populate(self): + self.responses = run_commands(self.module, self.COMMANDS) + + def run(self, cmd): + return run_commands(self.module, cmd) + + +class Default(FactsBase): + + COMMANDS = [ + 'show version', + 'show switch' + ] + + def populate(self): + super(Default, self).populate() + data = self.responses[0] + if data: + self.facts['version'] = self.parse_version(data) + self.facts['serialnum'] = self.parse_serialnum(data) + + data = self.responses[1] + if data: + self.facts['model'] = self.parse_model(data) + self.facts['hostname'] = self.parse_hostname(data) + + def parse_version(self, data): + match = re.search(r'Image\s+: ExtremeXOS version (\S+)', data) + if match: + return match.group(1) + + def parse_model(self, data): + match = re.search(r'System Type:\s+(.*$)', data, re.M) + if match: + return match.group(1) + + def parse_hostname(self, data): + match = re.search(r'SysName:\s+(\S+)', data, re.M) + if match: + return match.group(1) + + def parse_serialnum(self, data): + match = re.search(r'Switch\s+: \S+ (\S+)', data, re.M) + if match: + return match.group(1) + # For stack, return serial number of the first switch in the stack. + match = re.search(r'Slot-\d+\s+: \S+ (\S+)', data, re.M) + if match: + return match.group(1) + # Handle unique formatting for VM + match = re.search(r'Switch\s+: PN:\S+\s+SN:(\S+)', data, re.M) + if match: + return match.group(1) + + +class Hardware(FactsBase): + + COMMANDS = [ + 'show memory' + ] + + def populate(self): + super(Hardware, self).populate() + data = self.responses[0] + if data: + self.facts['memtotal_mb'] = int(round(int(self.parse_memtotal(data)) / 1024, 0)) + self.facts['memfree_mb'] = int(round(int(self.parse_memfree(data)) / 1024, 0)) + + def parse_memtotal(self, data): + match = re.search(r' Total DRAM \(KB\): (\d+)', data, re.M) + if match: + return match.group(1) + # Handle unique formatting for VM + match = re.search(r' Total \s+\(KB\): (\d+)', data, re.M) + if match: + return match.group(1) + + def parse_memfree(self, data): + match = re.search(r' Free\s+\(KB\): (\d+)', data, re.M) + if match: + return match.group(1) + + +class Config(FactsBase): + + COMMANDS = ['show configuration detail'] + + def populate(self): + super(Config, self).populate() + data = self.responses[0] + if data: + self.facts['config'] = data + + +class Interfaces(FactsBase): + + COMMANDS = [ + 'show switch', + {'command': 'show port config', 'output': 'json'}, + {'command': 'show port description', 'output': 'json'}, + {'command': 'show vlan detail', 'output': 'json'}, + {'command': 'show lldp neighbors', 'output': 'json'} + ] + + def populate(self): + super(Interfaces, self).populate() + + self.facts['all_ipv4_addresses'] = list() + self.facts['all_ipv6_addresses'] = list() + + data = self.responses[0] + if data: + sysmac = self.parse_sysmac(data) + + data = self.responses[1] + if data: + self.facts['interfaces'] = self.populate_interfaces(data, sysmac) + + data = self.responses[2] + if data: + self.populate_interface_descriptions(data) + + data = self.responses[3] + if data: + self.populate_vlan_interfaces(data, sysmac) + + data = self.responses[4] + if data: + self.facts['neighbors'] = self.parse_neighbors(data) + + def parse_sysmac(self, data): + match = re.search(r'System MAC:\s+(\S+)', data, re.M) + if match: + return match.group(1) + + def populate_interfaces(self, interfaces, sysmac): + facts = dict() + for elem in interfaces: + intf = dict() + + if 'show_ports_config' not in elem: + continue + + key = str(elem['show_ports_config']['port']) + + if elem['show_ports_config']['linkState'] == 2: + # Link state is "not present", don't include + continue + + intf['type'] = 'Ethernet' + intf['macaddress'] = sysmac + intf['bandwidth_configured'] = str(elem['show_ports_config']['speedCfg']) + intf['bandwidth'] = str(elem['show_ports_config']['speedActual']) + intf['duplex_configured'] = elem['show_ports_config']['duplexCfg'] + intf['duplex'] = elem['show_ports_config']['duplexActual'] + if elem['show_ports_config']['linkState'] == 1: + intf['lineprotocol'] = 'up' + else: + intf['lineprotocol'] = 'down' + if elem['show_ports_config']['portState'] == 1: + intf['operstatus'] = 'up' + else: + intf['operstatus'] = 'admin down' + + facts[key] = intf + return facts + + def populate_interface_descriptions(self, data): + for elem in data: + if 'show_ports_description' not in elem: + continue + key = str(elem['show_ports_description']['port']) + + if 'descriptionString' in elem['show_ports_description']: + desc = elem['show_ports_description']['descriptionString'] + self.facts['interfaces'][key]['description'] = desc + + def populate_vlan_interfaces(self, data, sysmac): + for elem in data: + if 'vlanProc' in elem: + key = elem['vlanProc']['name1'] + if key not in self.facts['interfaces']: + intf = dict() + intf['type'] = 'VLAN' + intf['macaddress'] = sysmac + self.facts['interfaces'][key] = intf + + if elem['vlanProc']['ipAddress'] != '0.0.0.0': + self.facts['interfaces'][key]['ipv4'] = list() + addr = elem['vlanProc']['ipAddress'] + subnet = elem['vlanProc']['maskForDisplay'] + ipv4 = dict(address=addr, subnet=subnet) + self.add_ip_address(addr, 'ipv4') + self.facts['interfaces'][key]['ipv4'].append(ipv4) + + if 'rtifIpv6Address' in elem: + key = elem['rtifIpv6Address']['rtifName'] + if key not in self.facts['interfaces']: + intf = dict() + intf['type'] = 'VLAN' + intf['macaddress'] = sysmac + self.facts['interfaces'][key] = intf + self.facts['interfaces'][key]['ipv6'] = list() + addr, subnet = elem['rtifIpv6Address']['ipv6_address_mask'].split('/') + ipv6 = dict(address=addr, subnet=subnet) + self.add_ip_address(addr, 'ipv6') + self.facts['interfaces'][key]['ipv6'].append(ipv6) + + def add_ip_address(self, address, family): + if family == 'ipv4': + if address not in self.facts['all_ipv4_addresses']: + self.facts['all_ipv4_addresses'].append(address) + else: + if address not in self.facts['all_ipv6_addresses']: + self.facts['all_ipv6_addresses'].append(address) + + def parse_neighbors(self, data): + facts = dict() + for elem in data: + if 'lldpPortNbrInfoShort' not in elem: + continue + intf = str(elem['lldpPortNbrInfoShort']['port']) + if intf not in facts: + facts[intf] = list() + fact = dict() + fact['host'] = elem['lldpPortNbrInfoShort']['nbrSysName'] + fact['port'] = str(elem['lldpPortNbrInfoShort']['nbrPortID']) + facts[intf].append(fact) + return facts diff --git a/lib/ansible/module_utils/network/exos/facts/lldp_global/__init__.py b/lib/ansible/module_utils/network/exos/facts/lldp_global/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/exos/facts/lldp_global/lldp_global.py b/lib/ansible/module_utils/network/exos/facts/lldp_global/lldp_global.py new file mode 100644 index 00000000000..c6dad453dd2 --- /dev/null +++ b/lib/ansible/module_utils/network/exos/facts/lldp_global/lldp_global.py @@ -0,0 +1,97 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The exos lldp_global fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import re +from copy import deepcopy + +from ansible.module_utils.network.common import utils +from ansible.module_utils.network.exos.argspec.lldp_global.lldp_global \ + import Lldp_globalArgs +from ansible.module_utils.network.exos.exos import send_requests + + +class Lldp_globalFacts(object): + """ The exos lldp_global fact class + """ + + TLV_SELECT_OPTIONS = [ + "SYSTEM_NAME", + "SYSTEM_DESCRIPTION", + "SYSTEM_CAPABILITIES", + "MANAGEMENT_ADDRESS", + "PORT_DESCRIPTION"] + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Lldp_globalArgs.argument_spec + spec = deepcopy(self.argument_spec) + if subspec: + if options: + facts_argument_spec = spec[subspec][options] + else: + facts_argument_spec = spec[subspec] + else: + facts_argument_spec = spec + + self.generated_spec = utils.generate_dict(facts_argument_spec) + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for lldp_global + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if not data: + request = { + "path": "/rest/restconf/data/openconfig-lldp:lldp/config/", + "method": "GET", + } + data = send_requests(self._module, request) + + obj = {} + if data: + lldp_obj = self.render_config(self.generated_spec, data[0]) + if lldp_obj: + obj = lldp_obj + + ansible_facts['ansible_network_resources'].pop('lldp_global', None) + facts = {} + + params = utils.validate_config(self.argument_spec, {'config': obj}) + facts['lldp_global'] = params['config'] + + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + config = deepcopy(spec) + config['interval'] = conf["openconfig-lldp:config"]["hello-timer"] + + for item in self.TLV_SELECT_OPTIONS: + config["tlv_select"][item.lower()] = ( + False if (item in conf["openconfig-lldp:config"]["suppress-tlv-advertisement"]) + else True) + + return utils.remove_empties(config) diff --git a/lib/ansible/module_utils/network/exos/utils/__init__.py b/lib/ansible/module_utils/network/exos/utils/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/exos/utils/utils.py b/lib/ansible/module_utils/network/exos/utils/utils.py new file mode 100644 index 00000000000..d3b7c17de01 --- /dev/null +++ b/lib/ansible/module_utils/network/exos/utils/utils.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# utils diff --git a/lib/ansible/modules/network/exos/exos_facts.py b/lib/ansible/modules/network/exos/exos_facts.py index 7bd99eeea71..b3c713f4182 100644 --- a/lib/ansible/modules/network/exos/exos_facts.py +++ b/lib/ansible/modules/network/exos/exos_facts.py @@ -30,7 +30,9 @@ DOCUMENTATION = """ --- module: exos_facts version_added: "2.7" -author: "Lance Richardson (@hlrichardson)" +author: + - "Lance Richardson (@hlrichardson)" + - "Ujwal Koamrla (@ujwalkomarla)" short_description: Collect facts from devices running Extreme EXOS description: - Collects a base set of device facts from a remote device that @@ -50,21 +52,49 @@ options: with an initial C(M(!)) to specify that a specific subset should not be collected. required: false + type: list default: ['!config'] + gather_network_resources: + description: + - When supplied, this argument will restrict the facts collected + to a given subset. Possible values for this argument include + all and the resources like interfaces, vlans etc. + Can specify a list of values to include a larger subset. + choices: ['all', '!all', 'lldp_global', '!lldp_global'] + type: list + version_added: "2.9" """ EXAMPLES = """ - - name: collect all facts from the device + - name: Gather all legacy facts exos_facts: gather_subset: all - - name: collect only the config and default facts + - name: Gather only the config and default facts exos_facts: gather_subset: config - - name: do not collect hardware facts + - name: do not gather hardware facts exos_facts: gather_subset: "!hardware" + + - name: Gather legacy and resource facts + exos_facts: + gather_subset: all + gather_network_resources: all + + - name: Gather only the lldp global resource facts and no legacy facts + exos_facts: + gather_subset: + - '!all' + - '!min' + gather_network_resource: + - lldp_global + + - name: Gather lldp global resource and minimal legacy facts + exos_facts: + gather_subset: min + gather_network_resource: lldp_global """ RETURN = """ @@ -73,6 +103,11 @@ ansible_net_gather_subset: returned: always type: list +ansible_net_gather_network_resources: + description: The list of fact for network resource subsets collected from the device + returned: when the resource is configured + type: list + # default ansible_net_model: description: The model name returned from the device @@ -125,323 +160,27 @@ ansible_net_neighbors: returned: when interfaces is configured type: dict """ -import re -import json -from ansible.module_utils.network.exos.exos import run_commands 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, self.COMMANDS) - - def run(self, cmd): - return run_commands(self.module, cmd) - - -class Default(FactsBase): - - COMMANDS = [ - 'show version', - 'show switch' - ] - - def populate(self): - super(Default, self).populate() - data = self.responses[0] - if data: - self.facts['version'] = self.parse_version(data) - self.facts['serialnum'] = self.parse_serialnum(data) - - data = self.responses[1] - if data: - self.facts['model'] = self.parse_model(data) - self.facts['hostname'] = self.parse_hostname(data) - - def parse_version(self, data): - match = re.search(r'Image\s+: ExtremeXOS version (\S+)', data) - if match: - return match.group(1) - - def parse_model(self, data): - match = re.search(r'System Type:\s+(.*$)', data, re.M) - if match: - return match.group(1) - - def parse_hostname(self, data): - match = re.search(r'SysName:\s+(\S+)', data, re.M) - if match: - return match.group(1) - - def parse_serialnum(self, data): - match = re.search(r'Switch\s+: \S+ (\S+)', data, re.M) - if match: - return match.group(1) - # For stack, return serial number of the first switch in the stack. - match = re.search(r'Slot-\d+\s+: \S+ (\S+)', data, re.M) - if match: - return match.group(1) - # Handle unique formatting for VM - match = re.search(r'Switch\s+: PN:\S+\s+SN:(\S+)', data, re.M) - if match: - return match.group(1) - - -class Hardware(FactsBase): - - COMMANDS = [ - 'show memory' - ] - - def populate(self): - super(Hardware, self).populate() - data = self.responses[0] - if data: - self.facts['memtotal_mb'] = int(round(int(self.parse_memtotal(data)) / 1024, 0)) - self.facts['memfree_mb'] = int(round(int(self.parse_memfree(data)) / 1024, 0)) - - def parse_memtotal(self, data): - match = re.search(r' Total DRAM \(KB\): (\d+)', data, re.M) - if match: - return match.group(1) - # Handle unique formatting for VM - match = re.search(r' Total \s+\(KB\): (\d+)', data, re.M) - if match: - return match.group(1) - - def parse_memfree(self, data): - match = re.search(r' Free\s+\(KB\): (\d+)', data, re.M) - if match: - return match.group(1) - - -class Config(FactsBase): - - COMMANDS = ['show configuration detail'] - - def populate(self): - super(Config, self).populate() - data = self.responses[0] - if data: - self.facts['config'] = data - - -class Interfaces(FactsBase): - - COMMANDS = [ - 'show switch', - {'command': 'show port config', 'output': 'json'}, - {'command': 'show port description', 'output': 'json'}, - {'command': 'show vlan detail', 'output': 'json'}, - {'command': 'show lldp neighbors', 'output': 'json'} - ] - - def populate(self): - super(Interfaces, self).populate() - - self.facts['all_ipv4_addresses'] = list() - self.facts['all_ipv6_addresses'] = list() - - data = self.responses[0] - if data: - sysmac = self.parse_sysmac(data) - - data = self.responses[1] - if data: - self.facts['interfaces'] = self.populate_interfaces(data, sysmac) - - data = self.responses[2] - if data: - self.populate_interface_descriptions(data) - - data = self.responses[3] - if data: - self.populate_vlan_interfaces(data, sysmac) - - data = self.responses[4] - if data: - self.facts['neighbors'] = self.parse_neighbors(data) - - def parse_sysmac(self, data): - match = re.search(r'System MAC:\s+(\S+)', data, re.M) - if match: - return match.group(1) - - def populate_interfaces(self, interfaces, sysmac): - facts = dict() - for elem in interfaces: - intf = dict() - - if 'show_ports_config' not in elem: - continue - - key = str(elem['show_ports_config']['port']) - - if elem['show_ports_config']['linkState'] == 2: - # Link state is "not present", don't include - continue - - intf['type'] = 'Ethernet' - intf['macaddress'] = sysmac - intf['bandwidth_configured'] = str(elem['show_ports_config']['speedCfg']) - intf['bandwidth'] = str(elem['show_ports_config']['speedActual']) - intf['duplex_configured'] = elem['show_ports_config']['duplexCfg'] - intf['duplex'] = elem['show_ports_config']['duplexActual'] - if elem['show_ports_config']['linkState'] == 1: - intf['lineprotocol'] = 'up' - else: - intf['lineprotocol'] = 'down' - if elem['show_ports_config']['portState'] == 1: - intf['operstatus'] = 'up' - else: - intf['operstatus'] = 'admin down' - - facts[key] = intf - return facts - - def populate_interface_descriptions(self, data): - for elem in data: - if 'show_ports_description' not in elem: - continue - key = str(elem['show_ports_description']['port']) - - if 'descriptionString' in elem['show_ports_description']: - desc = elem['show_ports_description']['descriptionString'] - self.facts['interfaces'][key]['description'] = desc - - def populate_vlan_interfaces(self, data, sysmac): - for elem in data: - if 'vlanProc' in elem: - key = elem['vlanProc']['name1'] - if key not in self.facts['interfaces']: - intf = dict() - intf['type'] = 'VLAN' - intf['macaddress'] = sysmac - self.facts['interfaces'][key] = intf - - if elem['vlanProc']['ipAddress'] != '0.0.0.0': - self.facts['interfaces'][key]['ipv4'] = list() - addr = elem['vlanProc']['ipAddress'] - subnet = elem['vlanProc']['maskForDisplay'] - ipv4 = dict(address=addr, subnet=subnet) - self.add_ip_address(addr, 'ipv4') - self.facts['interfaces'][key]['ipv4'].append(ipv4) - - if 'rtifIpv6Address' in elem: - key = elem['rtifIpv6Address']['rtifName'] - if key not in self.facts['interfaces']: - intf = dict() - intf['type'] = 'VLAN' - intf['macaddress'] = sysmac - self.facts['interfaces'][key] = intf - self.facts['interfaces'][key]['ipv6'] = list() - addr, subnet = elem['rtifIpv6Address']['ipv6_address_mask'].split('/') - ipv6 = dict(address=addr, subnet=subnet) - self.add_ip_address(addr, 'ipv6') - self.facts['interfaces'][key]['ipv6'].append(ipv6) - - def add_ip_address(self, address, family): - if family == 'ipv4': - if address not in self.facts['all_ipv4_addresses']: - self.facts['all_ipv4_addresses'].append(address) - else: - if address not in self.facts['all_ipv6_addresses']: - self.facts['all_ipv6_addresses'].append(address) - - def parse_neighbors(self, data): - facts = dict() - for elem in data: - if 'lldpPortNbrInfoShort' not in elem: - continue - intf = str(elem['lldpPortNbrInfoShort']['port']) - if intf not in facts: - facts[intf] = list() - fact = dict() - fact['host'] = elem['lldpPortNbrInfoShort']['nbrSysName'] - fact['port'] = str(elem['lldpPortNbrInfoShort']['nbrPortID']) - facts[intf].append(fact) - return facts - - -FACT_SUBSETS = dict( - default=Default, - hardware=Hardware, - interfaces=Interfaces, - config=Config) - -VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) +from ansible.module_utils.network.exos.argspec.facts.facts import FactsArgs +from ansible.module_utils.network.exos.facts.facts import Facts def main(): - """main entry point for module execution + """Main entry point for AnsibleModule """ - argument_spec = dict( - gather_subset=dict(default=["!config"], type='list') - ) + argument_spec = FactsArgs.argument_spec module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - gather_subset = module.params['gather_subset'] + warnings = ['default value for `gather_subset` ' + 'will be changed to `min` from `!config` v2.11 onwards'] - runable_subsets = set() - exclude_subsets = set() + result = Facts(module).get_facts() - 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() + ansible_facts, additional_warnings = result + warnings.extend(additional_warnings) module.exit_json(ansible_facts=ansible_facts, warnings=warnings) diff --git a/lib/ansible/modules/network/exos/exos_lldp_global.py b/lib/ansible/modules/network/exos/exos_lldp_global.py new file mode 100644 index 00000000000..b391f1b82c7 --- /dev/null +++ b/lib/ansible/modules/network/exos/exos_lldp_global.py @@ -0,0 +1,430 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for exos_lldp_global +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = """ +--- +module: exos_lldp_global +version_added: 2.9 +short_description: Configure and manage Link Layer Discovery Protocol(LLDP) attribures on EXOS platforms. +description: This module configures and manages the Link Layer Discovery Protocol(LLDP) attributes on Extreme Networks EXOS platforms. +author: Ujwal Komarla (@ujwalkomarla) +notes: +- Tested against Extreme Networks EXOS version 30.2.1.8 on x460g2. +- This module works with connection C(httpapi). + See L(EXOS Platform Options,../network/user_guide/platform_exos.html) +options: + config: + description: A dictionary of LLDP options + type: dict + suboptions: + interval: + description: + - Frequency at which LLDP advertisements are sent (in seconds). By default - 30 seconds. + type: int + default: 30 + tlv_select: + description: + - This attribute can be used to specify the TLVs that need to be sent in the LLDP packets. By default, only system name and system description is sent + type: dict + suboptions: + management_address: + description: + - Used to specify the management address in TLV messages + type: bool + port_description: + description: + - Used to specify the port description TLV + type: bool + system_capabilities: + description: + - Used to specify the system capabilities TLV + type: bool + system_description: + description: + - Used to specify the system description TLV + type: bool + default: true + system_name: + description: + - Used to specify the system name TLV + type: bool + default: true + + state: + description: + - The state the configuration should be left in. + type: str + choices: + - merged + - replaced + - deleted + default: merged +""" +EXAMPLES = """ +# Using merged + + +# Before state: +# ------------- +# path: /rest/restconf/data/openconfig_lldp:lldp/config +# method: GET +# data: +# { +# "openconfig_lldp:config": { +# "enabled": true, +# "hello-timer": 30, +# "suppress-tlv-advertisement": [ +# "PORT_DESCRIPTION", +# "SYSTEM_CAPABILITIES", +# "MANAGEMENT_ADDRESS" +# ], +# "system-description": "ExtremeXOS (X460G2-24t-10G4) version 30.2.1.8" +# "system-name": "X460G2-24t-10G4" +# } +# } + +- name: Merge provided LLDP configuration with device configuration + exos_lldp_global: + config: + interval: 10000 + tlv_select: + system_capabilities: true + state: merged + +# Module Execution Results: +# ------------------------- +# +# "before": [ +# { +# "interval": 30, +# "tlv_select": { +# "system_name": true, +# "system_description": true +# "port_description": false, +# "management_address": false, +# "system_capabilities": false +# } +# } +# ] +# +# "requests": [ +# { +# "data": { +# "openconfig_lldp:config": { +# "hello-timer": 10000, +# "suppress-tlv-advertisement": [ +# "PORT_DESCRIPTION", +# "MANAGEMENT_ADDRESS" +# ] +# } +# }, +# "method": "PATCH", +# "path": "/rest/restconf/data/openconfig_lldp:lldp/config" +# } +# ] +# +# "after": [ +# { +# "interval": 10000, +# "tlv_select": { +# "system_name": true, +# "system_description": true, +# "port_description": false, +# "management_address": false, +# "system_capabilities": true +# } +# } +# ] + + +# After state: +# ------------- +# path: /rest/restconf/data/openconfig_lldp:lldp/config +# method: GET +# data: +# { +# "openconfig_lldp:config": { +# "enabled": true, +# "hello-timer": 10000, +# "suppress-tlv-advertisement": [ +# "PORT_DESCRIPTION", +# "MANAGEMENT_ADDRESS" +# ], +# "system-description": "ExtremeXOS (X460G2-24t-10G4) version 30.2.1.8" +# "system-name": "X460G2-24t-10G4" +# } +# } + + +# Using replaced + + +# Before state: +# ------------- +# path: /rest/restconf/data/openconfig_lldp:lldp/config +# method: GET +# data: +# { +# "openconfig_lldp:config": { +# "enabled": true, +# "hello-timer": 30, +# "suppress-tlv-advertisement": [ +# "PORT_DESCRIPTION", +# "SYSTEM_CAPABILITIES", +# "MANAGEMENT_ADDRESS" +# ], +# "system-description": "ExtremeXOS (X460G2-24t-10G4) version 30.2.1.8" +# "system-name": "X460G2-24t-10G4" +# } +# } + +- name: Replace device configuration with provided LLDP configuration + exos_lldp_global: + config: + interval: 10000 + tlv_select: + system_capabilities: true + state: replaced + +# Module Execution Results: +# ------------------------- +# +# "before": [ +# { +# "interval": 30, +# "tlv_select": { +# "system_name": true, +# "system_description": true +# "port_description": false, +# "management_address": false, +# "system_capabilities": false +# } +# } +# ] +# +# "requests": [ +# { +# "data": { +# "openconfig_lldp:config": { +# "hello-timer": 10000, +# "suppress-tlv-advertisement": [ +# "SYSTEM_NAME", +# "SYSTEM_DESCRIPTION", +# "PORT_DESCRIPTION", +# "MANAGEMENT_ADDRESS" +# ] +# } +# }, +# "method": "PATCH", +# "path": "/rest/restconf/data/openconfig_lldp:lldp/config" +# } +# ] +# +# "after": [ +# { +# "interval": 10000, +# "tlv_select": { +# "system_name": false, +# "system_description": false, +# "port_description": false, +# "management_address": false, +# "system_capabilities": true +# } +# } +# ] + + +# After state: +# ------------- +# path: /rest/restconf/data/openconfig_lldp:lldp/config +# method: GET +# data: +# { +# "openconfig_lldp:config": { +# "enabled": true, +# "hello-timer": 10000, +# "suppress-tlv-advertisement": [ +# "SYSTEM_NAME", +# "SYSTEM_DESCRIPTION", +# "PORT_DESCRIPTION", +# "MANAGEMENT_ADDRESS" +# ], +# "system-description": "ExtremeXOS (X460G2-24t-10G4) version 30.2.1.8" +# "system-name": "X460G2-24t-10G4" +# } +# } + + +# Using deleted + + +# Before state: +# ------------- +# path: /rest/restconf/data/openconfig_lldp:lldp/config +# method: GET +# data: +# { +# "openconfig_lldp:config": { +# "enabled": true, +# "hello-timer": 10000, +# "suppress-tlv-advertisement": [ +# "SYSTEM_CAPABILITIES", +# "MANAGEMENT_ADDRESS" +# ], +# "system-description": "ExtremeXOS (X460G2-24t-10G4) version 30.2.1.8" +# "system-name": "X460G2-24t-10G4" +# } +# } + +- name: Delete attributes of given LLDP service (This won't delete the LLDP service itself) + exos_lldp_global: + config: + state: deleted + +# Module Execution Results: +# ------------------------- +# +# "before": [ +# { +# "interval": 10000, +# "tlv_select": { +# "system_name": true, +# "system_description": true, +# "port_description": true, +# "management_address": false, +# "system_capabilities": false +# } +# } +# ] +# +# "requests": [ +# { +# "data": { +# "openconfig_lldp:config": { +# "hello-timer": 30, +# "suppress-tlv-advertisement": [ +# "SYSTEM_CAPABILITIES", +# "PORT_DESCRIPTION", +# "MANAGEMENT_ADDRESS" +# ] +# } +# }, +# "method": "PATCH", +# "path": "/rest/restconf/data/openconfig_lldp:lldp/config" +# } +# ] +# +# "after": [ +# { +# "interval": 30, +# "tlv_select": { +# "system_name": true, +# "system_description": true, +# "port_description": false, +# "management_address": false, +# "system_capabilities": false +# } +# } +# ] + + +# After state: +# ------------- +# path: /rest/restconf/data/openconfig_lldp:lldp/config +# method: GET +# data: +# { +# "openconfig_lldp:config": { +# "enabled": true, +# "hello-timer": 30, +# "suppress-tlv-advertisement": [ +# "SYSTEM_CAPABILITIES", +# "PORT_DESCRIPTION", +# "MANAGEMENT_ADDRESS" +# ], +# "system-description": "ExtremeXOS (X460G2-24t-10G4) version 30.2.1.8" +# "system-name": "X460G2-24t-10G4" +# } +# } + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + sample: > + The configuration returned will always be in the same format + of the parameters above. + type: list +after: + description: The resulting configuration model invocation. + returned: when changed + sample: > + The configuration returned will always be in the same format + of the parameters above. + type: list +requests: + description: The set of requests pushed to the remote device. + returned: always + type: list + sample: [{"data": "...", "method": "...", "path": "..."}, {"data": "...", "method": "...", "path": "..."}, {"data": "...", "method": "...", "path": "..."}] +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.exos.argspec.lldp_global.lldp_global import Lldp_globalArgs +from ansible.module_utils.network.exos.config.lldp_global.lldp_global import Lldp_global + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [('state', 'merged', ('config',)), + ('state', 'replaced', ('config',))] + module = AnsibleModule(argument_spec=Lldp_globalArgs.argument_spec, required_if=required_if, + supports_check_mode=True) + + result = Lldp_global(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/plugins/action/exos_lldp_global.py b/lib/ansible/plugins/action/exos_lldp_global.py new file mode 100644 index 00000000000..34a34c95d3a --- /dev/null +++ b/lib/ansible/plugins/action/exos_lldp_global.py @@ -0,0 +1,33 @@ +# +# Copyright 2015 Peter Sprygada +# +# 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 + +from ansible.plugins.action.network import ActionModule as ActionNetworkModule + + +class ActionModule(ActionNetworkModule): + + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + if self._play_context.connection != 'httpapi': + return {'failed': True, 'msg': "Connection type %s is not valid for this module" % self._play_context.connection} + + return super(ActionModule, self).run(task_vars=task_vars) diff --git a/test/integration/targets/exos_lldp_global/defaults/main.yaml b/test/integration/targets/exos_lldp_global/defaults/main.yaml new file mode 100644 index 00000000000..164afead284 --- /dev/null +++ b/test/integration/targets/exos_lldp_global/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/test/integration/targets/exos_lldp_global/meta/main.yaml b/test/integration/targets/exos_lldp_global/meta/main.yaml new file mode 100644 index 00000000000..32cf5dda7ed --- /dev/null +++ b/test/integration/targets/exos_lldp_global/meta/main.yaml @@ -0,0 +1 @@ +dependencies: [] diff --git a/test/integration/targets/exos_lldp_global/tasks/httpapi.yaml b/test/integration/targets/exos_lldp_global/tasks/httpapi.yaml new file mode 100644 index 00000000000..7c29713da64 --- /dev/null +++ b/test/integration/targets/exos_lldp_global/tasks/httpapi.yaml @@ -0,0 +1,19 @@ +--- +- name: Collect all httpapi test cases + find: + paths: "{{ role_path }}/tests/httpapi" + patterns: "{{ testcase }}.yaml" + use_regex: true + register: test_cases + delegate_to: localhost + +- name: Set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: Run test case (connection=httpapi) + include: "{{ test_case_to_run }}" + vars: + ansible_connection: httpapi + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/exos_lldp_global/tasks/main.yaml b/test/integration/targets/exos_lldp_global/tasks/main.yaml new file mode 100644 index 00000000000..22eac154642 --- /dev/null +++ b/test/integration/targets/exos_lldp_global/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: httpapi.yaml, tags: ['httpapi'] } diff --git a/test/integration/targets/exos_lldp_global/tests/httpapi/_populate_config.yaml b/test/integration/targets/exos_lldp_global/tests/httpapi/_populate_config.yaml new file mode 100644 index 00000000000..3740e8e2c9c --- /dev/null +++ b/test/integration/targets/exos_lldp_global/tests/httpapi/_populate_config.yaml @@ -0,0 +1,7 @@ +--- +- name: Populate Config + exos_config: + lines: + - configure lldp transmit-interval 50 + - configure lldp ports all no-advertise all-tlvs + - configure lldp ports all advertise system-name system-description system-capabilities diff --git a/test/integration/targets/exos_lldp_global/tests/httpapi/_reset_config.yaml b/test/integration/targets/exos_lldp_global/tests/httpapi/_reset_config.yaml new file mode 100644 index 00000000000..e6ba1e41540 --- /dev/null +++ b/test/integration/targets/exos_lldp_global/tests/httpapi/_reset_config.yaml @@ -0,0 +1,7 @@ +--- +- name: Reset Config + exos_config: + lines: + - configure lldp transmit-interval 30 + - configure lldp ports all no-advertise all-tlvs + - configure lldp ports all advertise system-name system-description diff --git a/test/integration/targets/exos_lldp_global/tests/httpapi/deleted.yaml b/test/integration/targets/exos_lldp_global/tests/httpapi/deleted.yaml new file mode 100644 index 00000000000..f0b260ed35c --- /dev/null +++ b/test/integration/targets/exos_lldp_global/tests/httpapi/deleted.yaml @@ -0,0 +1,48 @@ +--- +- debug: + msg: "Start exos_lldp_global deleted integration tests ansible_connection={{ ansible_connection }}" + +- include_tasks: _reset_config.yaml + +- include_tasks: _populate_config.yaml + +- block: + - name: Delete attributes of LLDP service + exos_lldp_global: &deleted + config: + state: deleted + register: result + + - name: Assert that the before dicts were correctly generated + assert: + that: + - "{{ populate == result['before']}}" + + - name: Assert that correct set of requests were generated + assert: + that: + - "{{ deleted['requests'][0]['method'] == result['requests'][0]['method'] }}" + - "{{ deleted['requests'][0]['path'] == result['requests'][0]['path'] }}" + - "{{ deleted['requests'][0]['data'] == result['requests'][0]['data'] }}" + + - name: Assert that the after dicts were correctly generated + assert: + that: + - "{{ deleted['after'] == result['after']}}" + + - name: Delete attributes of all configured interfaces (IDEMPOTENT) + exos_lldp_global: *deleted + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result.changed == false" + + - name: Assert that the before dicts were correctly generated + assert: + that: + - "{{ deleted['after'] == result['before'] }}" + + always: + - include_tasks: _reset_config.yaml diff --git a/test/integration/targets/exos_lldp_global/tests/httpapi/merged.yaml b/test/integration/targets/exos_lldp_global/tests/httpapi/merged.yaml new file mode 100644 index 00000000000..2bf2a24491d --- /dev/null +++ b/test/integration/targets/exos_lldp_global/tests/httpapi/merged.yaml @@ -0,0 +1,49 @@ +--- +- debug: + msg: "START exos_lldp_global merged integration tests on connection={{ ansible_connection }}" + +- include_tasks: _reset_config.yaml + +- block: + - name: Merge the provided configuration with the exisiting running configuration + exos_lldp_global: &merged + config: + interval: 10 + tlv_select: + system_description: false + system_capabilities: false + state: merged + register: result + + - name: Assert that before dicts were correctly generated + assert: + that: "{{ merged['before'] == result['before'] }}" + + - name: Assert that correct set of requests were generated + assert: + that: + - "{{ merged['requests'][0]['method'] == result['requests'][0]['method'] }}" + - "{{ merged['requests'][0]['path'] == result['requests'][0]['path'] }}" + - "{{ merged['requests'][0]['data'] == result['requests'][0]['data'] }}" + + - name: Assert that after dicts was correctly generated + assert: + that: + - "{{ merged['after'] == result['after'] }}" + + - name: Merge the provided configuration with the existing running configuration (IDEMPOTENT) + exos_lldp_global: *merged + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - " result['changed'] == false" + + - name: Assert that before dicts were correctly generated + assert: + that: + - "{{ merged['after'] == result['before'] }}" + + always: + - include_tasks: _reset_config.yaml diff --git a/test/integration/targets/exos_lldp_global/tests/httpapi/replaced.yaml b/test/integration/targets/exos_lldp_global/tests/httpapi/replaced.yaml new file mode 100644 index 00000000000..595147b9eb2 --- /dev/null +++ b/test/integration/targets/exos_lldp_global/tests/httpapi/replaced.yaml @@ -0,0 +1,53 @@ +--- +- debug: + msg: "START exos_lldp_global replaced integration tests on connection={{ ansible_connection }}" + +- include_tasks: _reset_config.yaml + +- include_tasks: _populate_config.yaml + +- block: + - name: Replace device configurations of LLDP service with provided configurations + exos_lldp_global: &replaced + config: + interval: 20 + tlv_select: + system_name: false + system_description: true + system_capabilities: false + state: replaced + register: result + + - name: Assert that correct set of results were generated + assert: + that: + - "{{ replaced['requests'][0]['method'] == result['requests'][0]['method'] }}" + - "{{ replaced['requests'][0]['path'] == result['requests'][0]['path'] }}" + - "{{ replaced['requests'][0]['data'] == result['requests'][0]['data'] }}" + + - name: Assert that before dicts are correctly generated + assert: + that: + - "{{ populate == result['before'] }}" + + - name: Assert that after dict is correctly generated + assert: + that: + - "{{ replaced['after'] == result['after'] }}" + + - name: Replace device configurations of LLDP service with provided configurations (IDEMPOTENT) + exos_lldp_global: *replaced + register: result + + - name: Assert that task was idempotent + assert: + that: + - "result['changed'] == false" + + - name: Assert that before dict is correctly generated + assert: + that: + - "{{ replaced['after'] == result['before'] }}" + + always: + - include_tasks: _reset_config.yaml diff --git a/test/integration/targets/exos_lldp_global/vars/main.yaml b/test/integration/targets/exos_lldp_global/vars/main.yaml new file mode 100644 index 00000000000..cdcca8074b7 --- /dev/null +++ b/test/integration/targets/exos_lldp_global/vars/main.yaml @@ -0,0 +1,85 @@ +--- +merged: + before: + interval: 30 + tlv_select: + system_name: true + system_description: true + system_capabilities: false + port_description: false + management_address: false + + requests: + # 'suppress-tlv-advertisement' list is sorted to compare test + - data: '{"openconfig-lldp:config": {"suppress-tlv-advertisement": ["MANAGEMENT_ADDRESS", "PORT_DESCRIPTION", "SYSTEM_CAPABILITIES", "SYSTEM_DESCRIPTION"], "hello-timer": 10}}' + method: PUT + path: /rest/restconf/data/openconfig-lldp:lldp/config + + after: + interval: 10 + tlv_select: + system_name: true + system_description: false + system_capabilities: false + port_description: false + management_address: false + +populate: + interval: 50 + tlv_select: + system_name: true + system_description: true + system_capabilities: true + port_description: false + management_address: false + +replaced: + requests: + # 'suppress-tlv-advertisement' list is sorted to compare test + - data: '{"openconfig-lldp:config": {"suppress-tlv-advertisement": ["MANAGEMENT_ADDRESS", "PORT_DESCRIPTION", "SYSTEM_CAPABILITIES", "SYSTEM_NAME"], "hello-timer": 20}}' + method: PUT + path: /rest/restconf/data/openconfig-lldp:lldp/config + + after: + interval: 20 + tlv_select: + system_name: false + system_description: true + system_capabilities: false + port_description: false + management_address: false + +deleted: + before: + interval: 50 + tlv_select: + system_name: true + system_description: true + system_capabilities: true + port_description: false + management_address: false + + requests: + # 'suppress-tlv-advertisement' list is sorted to compare test + - data: '{"openconfig-lldp:config": {"suppress-tlv-advertisement": ["MANAGEMENT_ADDRESS", "PORT_DESCRIPTION", "SYSTEM_CAPABILITIES"], "hello-timer": 30}}' + method: PUT + path: /rest/restconf/data/openconfig-lldp:lldp/config + + + after: + interval: 30 + tlv_select: + system_name: true + system_description: true + system_capabilities: false + port_description: false + management_address: false + +round_trip: + interval: 30 + tlv_select: + system_name: true + system_description: true + system_capabilities: false + port_description: false + management_address: false diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt index 7db85707124..c6bc036fa7e 100644 --- a/test/sanity/ignore.txt +++ b/test/sanity/ignore.txt @@ -3636,7 +3636,6 @@ lib/ansible/modules/network/exos/exos_command.py validate-modules:E337 lib/ansible/modules/network/exos/exos_command.py validate-modules:E338 lib/ansible/modules/network/exos/exos_config.py validate-modules:E337 lib/ansible/modules/network/exos/exos_config.py validate-modules:E338 -lib/ansible/modules/network/exos/exos_facts.py validate-modules:E337 lib/ansible/modules/network/f5/_bigip_asm_policy.py validate-modules:E337 lib/ansible/modules/network/f5/_bigip_asm_policy.py validate-modules:E338 lib/ansible/modules/network/f5/_bigip_facts.py validate-modules:E337 diff --git a/test/units/modules/network/exos/test_exos_facts.py b/test/units/modules/network/exos/test_exos_facts.py index 59e101f466b..85880c3a440 100644 --- a/test/units/modules/network/exos/test_exos_facts.py +++ b/test/units/modules/network/exos/test_exos_facts.py @@ -36,9 +36,12 @@ class TestExosFactsModule(TestExosModule): def setUp(self): super(TestExosFactsModule, self).setUp() - self.mock_run_commands = patch('ansible.modules.network.exos.exos_facts.run_commands') + self.mock_run_commands = patch('ansible.module_utils.network.exos.facts.legacy.base.run_commands') self.run_commands = self.mock_run_commands.start() + self.mock_get_resource_connection = patch('ansible.module_utils.network.common.facts.facts.get_resource_connection') + self.get_resource_connection = self.mock_get_resource_connection.start() + def tearDown(self): super(TestExosFactsModule, self).tearDown() self.mock_run_commands.stop()