diff --git a/library/__init__.py b/library/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/library/ios_facts.py b/library/ios_facts.py new file mode 100644 index 0000000..c3d3b3e --- /dev/null +++ b/library/ios_facts.py @@ -0,0 +1,108 @@ +#!/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) +""" +The module file for ios_facts +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': [u'preview'], + 'supported_by': 'network'} + + +DOCUMENTATION = """ +--- +module: ios_facts +version_added: 2.9 +short_description: Get facts about Cisco ios devices. +description: + - Collects facts from network devices running the ios operating + system. This module places the facts gathered in the fact tree keyed by the + respective resource name. The facts module will always collect a + base set of facts from the device and can enable or disable + collection of additional facts. +author: [u'Sumit Jaiswal (@justjais)'] +notes: + - Tested against Cisco IOSv Version 15.2 on VIRL +options: + gather_subset: + description: + - When supplied, this argument will restrict the facts collected + to a given subset. Possible values for this argument include + all, min, hardware, config, legacy, and lacp_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: 'all' + version_added: "2.2" + 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 lacp_interfaces, vlans etc. + 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 + version_added: "2.9" +""" + +EXAMPLES = """ +# Gather all facts +- ios_facts: + gather_subset: all + gather_network_resources: all +# Collect only the ios facts +- ios_facts: + gather_subset: + - !all + - !min + gather_network_resources: + - ios +# Do not collect ios facts +- ios_facts: + gather_network_resources: + - "!ios" +# Collect ios and minimal default facts +- ios_facts: + gather_subset: min + gather_network_resources: ios +""" + +RETURN = """ +See the respective resource module parameters for the tree. +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.ios.argspec.facts.facts import FactsArgs +from ansible.module_utils.network.ios.facts.facts import Facts + + +def main(): + """ + Main entry point for module execution + + :returns: ansible_facts + """ + module = AnsibleModule(argument_spec=FactsArgs.argument_spec, + supports_check_mode=True) + warnings = ['default value for `gather_subset` ' + 'will be changed to `min` from `!config` v2.11 onwards'] + + result = Facts(module).get_facts() + + ansible_facts, additional_warnings = result + warnings.extend(additional_warnings) + + module.exit_json(ansible_facts=ansible_facts, warnings=warnings) + + +if __name__ == '__main__': + main() diff --git a/library/ios_lacp.py b/library/ios_lacp.py new file mode 100644 index 0000000..9f6f4d8 --- /dev/null +++ b/library/ios_lacp.py @@ -0,0 +1,193 @@ +#!/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 ios_lacp +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network' +} + +NETWORK_OS = "ios" +RESOURCE = "lacp" +COPYRIGHT = "Copyright 2019 Red Hat" + +DOCUMENTATION = """ +--- +module: ios_lacp +version_added: 2.9 +short_description: Manage Global Link Aggregation Control Protocol (LACP) on Cisco IOS devices. +description: This module provides declarative management of Global LACP on Cisco IOS network devices. +author: Sumit Jaiswal (@justjais) +notes: + - 'Tested against Cisco IOSv Version 15.2 on VIRL +options: + config: + description: A dictionary of LACP options + type: list + elements: dict + suboptions: + system_priority: + description: + - LACP priority for the system. Note, Cisco default LACP System priority is 32768. + type: int + required: True + state: + description: + - The state the configuration should be left in + type: str + choices: + - merged + - replaced + - deleted + default: merged +""" + +EXAMPLES = """ +# Using Deleted +# +# Before state: +# ------------- +# +# vios#show lacp sys-id +# 123, 5e00.0000.8000 + +- name: Delete Global LACP attribute with config + ios_lacp: + config: + - system_priority: 123 + state: deleted + +# After state: +# ------------- +# +# vios#show lacp sys-id +# 32768, 5e00.0000.8000 + +# Using Deleted +# +# Before state: +# ------------- +# +# vios#show lacp sys-id +# 123, 5e00.0000.8000 + +- name: Delete Global LACP attribute without config + ios_lacp: + state: deleted + +# After state: +# ------------- +# +# vios#show lacp sys-id +# 32768, 5e00.0000.8000 + +# Using merged +# +# Before state: +# ------------- +# +# vios#show lacp sys-id +# 250, 5e00.0000.8000 + +- name: Merge provided configuration with device configuration + ios_lacp: + config: + - system_priority: 123 + state: merged + +# After state: +# ------------ +# +# vios#show lacp sys-id +# 123, 5e00.0000.8000 + +# Using replaced +# +# Before state: +# ------------- +# +# vios#show lacp sys-id +# 32768, 5e00.0000.8000 + +- name: Replaces Global LACP configuration + ios_lacp: + config: + - system_priority: 123 + state: replaced + +# After state: +# ------------ +# +# vios#show lacp sys-id +# 123, 5e00.0000.8000 + +""" +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. +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. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['command 1', 'command 2', 'command 3'] +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.ios.argspec.lacp.lacp import LacpArgs +from ansible.module_utils.network.ios.config.lacp.lacp import Lacp + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=LacpArgs.argument_spec, + supports_check_mode=True) + + result = Lacp(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/module_utils/__init__.py b/module_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/__init__.py b/module_utils/network/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/__init__.py b/module_utils/network/ios/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/argspec/__init__.py b/module_utils/network/ios/argspec/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/argspec/facts/__init__.py b/module_utils/network/ios/argspec/facts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/argspec/facts/facts.py b/module_utils/network/ios/argspec/facts/facts.py new file mode 100644 index 0000000..04d3e2f --- /dev/null +++ b/module_utils/network/ios/argspec/facts/facts.py @@ -0,0 +1,28 @@ +#!/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) +""" +The arg spec for the ios facts module. +""" + + +class FactsArgs(object): # pylint: disable=R0903 + """ The arg spec for the ios facts module + """ + + def __init__(self, **kwargs): + pass + + choices = [ + 'all', + 'interfaces', + ] + + argument_spec = { + 'gather_subset': dict(default=['!config'], type='list'), + 'gather_network_resources': dict(default=['all'], + choices=choices, + type='list'), + } diff --git a/module_utils/network/ios/argspec/lacp/__init__.py b/module_utils/network/ios/argspec/lacp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/argspec/lacp/lacp.py b/module_utils/network/ios/argspec/lacp/lacp.py new file mode 100644 index 0000000..25d703d --- /dev/null +++ b/module_utils/network/ios/argspec/lacp/lacp.py @@ -0,0 +1,42 @@ +#!/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 arg spec for the ios_lacp module +""" + + +class LacpArgs(object): + """The arg spec for the ios_lacp module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = {'config': {'elements': 'dict', + 'options': {'system_priority': {'required': True, 'type': 'int'}}, + 'type': 'list'}, + 'state': {'choices': ['merged', 'replaced', 'overridden', 'deleted'], + 'default': 'merged', + 'type': 'str'}} diff --git a/module_utils/network/ios/config/__init__.py b/module_utils/network/ios/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/config/lacp/__init__.py b/module_utils/network/ios/config/lacp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/config/lacp/lacp.py b/module_utils/network/ios/config/lacp/lacp.py new file mode 100644 index 0000000..4108463 --- /dev/null +++ b/module_utils/network/ios/config/lacp/lacp.py @@ -0,0 +1,216 @@ +# +# -*- 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 ios_lacp 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 ansible.module_utils.six import iteritems + +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.ios.facts.facts import Facts + + +class Lacp(ConfigBase): + """ + The ios_lacp class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'interfaces', + ] + + def __init__(self, module): + super(Lacp, self).__init__(module) + + def get_interfaces_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) + interfaces_facts = facts['ansible_network_resources'].get('lacp') + if not interfaces_facts: + return [] + + return interfaces_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + commands = list() + warnings = list() + + existing_interfaces_facts = self.get_interfaces_facts() + commands.extend(self.set_config(existing_interfaces_facts)) + if commands: + if not self._module.check_mode: + self._connection.edit_config(commands) + result['changed'] = True + result['commands'] = commands + + changed_interfaces_facts = self.get_interfaces_facts() + + result['before'] = existing_interfaces_facts + if result['changed']: + result['after'] = changed_interfaces_facts + result['warnings'] = warnings + + return result + + def set_config(self, existing_interfaces_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 commands necessary to migrate the current configuration + to the desired configuration + """ + want = self._module.params['config'] + have = existing_interfaces_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 commands necessary to migrate the current configuration + to the desired configuration + """ + state = self._module.params['state'] + if state == 'deleted': + kwargs = {'want': want, 'have': have, 'state': state} + commands = self._state_deleted(**kwargs) + elif state == 'merged': + kwargs = {'want': want, 'have': have, 'state': state} + commands = self._state_merged(**kwargs) + elif state == 'replaced': + kwargs = {'want': want, 'have': have, 'state': state} + commands = self._state_replaced(**kwargs) + + return commands + + @staticmethod + def _state_replaced(**kwargs): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + want = kwargs['want'] + have = kwargs['have'] + state = kwargs['state'] + + for w, h in zip(want, have): + kwargs = {'want': w, 'have': h, 'state': state} + commands.extend(Lacp.clear_interface(**kwargs)) + kwargs = {'want': w, 'have': h, 'state': state} + commands.extend(Lacp.set_interface(**kwargs)) + + return commands + + @staticmethod + def _state_merged(**kwargs): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = [] + want = kwargs['want'] + have = kwargs['have'] + state = kwargs['state'] + + for w, h in zip(want, have): + kwargs = {'want': w, 'have': h, 'state': state} + commands.extend(Lacp.set_interface(**kwargs)) + + return commands + + @staticmethod + def _state_deleted(**kwargs): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + commands = [] + want = kwargs['want'] + have = kwargs['have'] + state = kwargs['state'] + + if want: + for w, h in zip(want, have): + kwargs = {'want': w, 'have': h, 'state': state} + else: + for each in have: + kwargs = {'want': {}, 'have': each, 'state': state} + commands.extend(Lacp.clear_interface(**kwargs)) + + return commands + + @staticmethod + def _remove_command_from_interface(cmd, commands): + commands.append('no %s' % cmd) + return commands + + @staticmethod + def _add_command_to_interface(cmd, commands): + if cmd not in commands: + commands.append(cmd) + + @staticmethod + def set_interface(**kwargs): + # Set the interface config based on the want and have config + commands = [] + want = kwargs['want'] + have = kwargs['have'] + + want_dict = set(tuple({k: v for k, v in iteritems(want) if v is not None}.items())) + have_dict = set(tuple({k: v for k, v in iteritems(have) if v is not None}.items())) + diff = want_dict - have_dict + + if diff: + cmd = 'lacp system-priority {}'.format(dict(diff).get('system_priority')) + Lacp._add_command_to_interface(cmd, commands) + + return commands + + @staticmethod + def clear_interface(**kwargs): + # Delete the interface config based on the want and have config + commands = [] + have = kwargs['have'] + state = kwargs['state'] + + if have.get('system_priority') and have.get('system_priority') != 32768\ + and state == 'deleted' or state == 'replaced': + cmd = 'lacp system-priority' + Lacp._remove_command_from_interface(cmd, commands) + + return commands diff --git a/module_utils/network/ios/facts/__init__.py b/module_utils/network/ios/facts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/facts/facts.py b/module_utils/network/ios/facts/facts.py new file mode 100644 index 0000000..82eccb9 --- /dev/null +++ b/module_utils/network/ios/facts/facts.py @@ -0,0 +1,49 @@ +# +# -*- 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 ios +this file validates each subset of facts and selectively +calls the appropriate facts gathering function +""" + +from ansible.module_utils.network.ios.argspec.facts.facts import FactsArgs +from ansible.module_utils.network.common.facts.facts import FactsBase +from ansible.module_utils.network.ios.facts.lacp.lacp import LacpFacts + + +FACT_LEGACY_SUBSETS = {} +FACT_RESOURCE_SUBSETS = dict( + interfaces=LacpFacts, +) + + +class Facts(FactsBase): + """ The fact class for ios + """ + + 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 ios + + :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/module_utils/network/ios/facts/lacp/__init__.py b/module_utils/network/ios/facts/lacp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/facts/lacp/lacp.py b/module_utils/network/ios/facts/lacp/lacp.py new file mode 100644 index 0000000..7792905 --- /dev/null +++ b/module_utils/network/ios/facts/lacp/lacp.py @@ -0,0 +1,86 @@ +# +# -*- 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 ios lacp 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. +""" +import re +from copy import deepcopy + +from ansible.module_utils.network.common import utils +from ansible.module_utils.network.ios.argspec.lacp.lacp import LacpArgs + + +class LacpFacts(object): + """ The ios lacp fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = LacpArgs.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 lacp + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if connection: + pass + + objs = [] + if not data: + data = connection.get('show lacp sys-id') + # operate on a collection of resource x + config = data.split('interface ') + + for conf in config: + if conf: + obj = self.render_config(self.generated_spec, conf) + if obj: + objs.append(obj) + facts = {} + + if objs: + facts['lacp'] = [] + # params = utils.validate_config(self.argument_spec, {'config': objs}) + + params = {'config': objs} + + for cfg in params['config']: + facts['lacp'].append(cfg) + 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['system_priority'] = int(conf.split(',')[0]) + + return utils.remove_empties(config) \ No newline at end of file diff --git a/module_utils/network/ios/utils/__init__.py b/module_utils/network/ios/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/utils/utils.py b/module_utils/network/ios/utils/utils.py new file mode 100644 index 0000000..26cf801 --- /dev/null +++ b/module_utils/network/ios/utils/utils.py @@ -0,0 +1,100 @@ +#!/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) + +# utils + + +def search_obj_in_list(name, lst): + for o in lst: + if o['name'] == name: + return o + return None + + +def normalize_interface(name): + """Return the normalized interface name + """ + if not name: + return + + def _get_number(name): + digits = '' + for char in name: + if char.isdigit() or char in '/.': + digits += char + return digits + + if name.lower().startswith('gi'): + if_type = 'GigabitEthernet' + elif name.lower().startswith('te'): + if_type = 'TenGigabitEthernet' + elif name.lower().startswith('fa'): + if_type = 'FastEthernet' + elif name.lower().startswith('fo'): + if_type = 'FortyGigabitEthernet' + elif name.lower().startswith('long'): + if_type = 'LongReachEthernet' + elif name.lower().startswith('et'): + if_type = 'Ethernet' + elif name.lower().startswith('vl'): + if_type = 'Vlan' + elif name.lower().startswith('lo'): + if_type = 'loopback' + elif name.lower().startswith('po'): + if_type = 'port-channel' + elif name.lower().startswith('nv'): + if_type = 'nve' + elif name.lower().startswith('twe'): + if_type = 'TwentyFiveGigE' + elif name.lower().startswith('hu'): + if_type = 'HundredGigE' + else: + if_type = None + + number_list = name.split(' ') + if len(number_list) == 2: + number = number_list[-1].strip() + else: + number = _get_number(name) + + if if_type: + proper_interface = if_type + number + else: + proper_interface = name + + return proper_interface + + +def get_interface_type(interface): + """Gets the type of interface + """ + + if interface.upper().startswith('GI'): + return 'GigabitEthernet' + elif interface.upper().startswith('TE'): + return 'TenGigabitEthernet' + elif interface.upper().startswith('FA'): + return 'FastEthernet' + elif interface.upper().startswith('FO'): + return 'FortyGigabitEthernet' + elif interface.upper().startswith('LON'): + return 'LongReachEthernet' + elif interface.upper().startswith('ET'): + return 'Ethernet' + elif interface.upper().startswith('VL'): + return 'Vlan' + elif interface.upper().startswith('LO'): + return 'loopback' + elif interface.upper().startswith('PO'): + return 'port-channel' + elif interface.upper().startswith('NV'): + return 'nve' + elif interface.upper().startswith('TWE'): + return 'TwentyFiveGigE' + elif interface.upper().startswith('HU'): + return 'HundredGigE' + else: + return 'unknown'