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..ee3a73c --- /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 myos_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: + - interfaces +# Do not collect ios facts +- ios_facts: + gather_network_resources: + - "!interfaces" +# Collect ios and minimal default facts +- ios_facts: + gather_subset: min + gather_network_resources: interfaces +""" + +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_vlans.py b/library/ios_vlans.py new file mode 100644 index 0000000..0e0b25c --- /dev/null +++ b/library/ios_vlans.py @@ -0,0 +1,403 @@ +#!/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_vlans +""" + +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 = "vlans" +COPYRIGHT = "Copyright 2019 Red Hat" + +DOCUMENTATION = """ +module: ios_vlans +version_added: 2.9 +short_description: Manage VLANs on Cisco IOS devices. +description: This module provides declarative management of VLANs 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 VLANs options + type: list + elements: dict + suboptions: + name: + description: + - Ascii name of the VLAN. + - NOTE, I(name) should not be named/appended with I(default) as it is reserved for device default vlans. + type: str + vlan_id: + description: + - ID of the VLAN. Range 1-4094 + type: int + required: True + mtu: + description: + - VLAN Maximum Transmission Unit. + - Refer to vendor documentation for valid values. + type: int + state: + description: + - Operational state of the VLAN + type: str + choices: + - active + - suspend + remote_span: + description: + - Configure as Remote SPAN VLAN + type: bool + shutdown: + description: + - Shutdown VLAN switching. + type: str + choices: + - enabled + - disabled +state: + description: + - The state the configuration should be left in + type: str + choices: + - merged + - replaced + - overridden + - deleted + default: merged +""" +EXAMPLES = """ +# Using deleted + +# Before state: +# ------------- +# +# vios#show vlan +# VLAN Name Status Ports +# ---- -------------------------------- --------- ------------------------------- +# 1 default active Gi0/1, Gi0/2 +# 10 vlan_10 active +# 20 vlan_20 act/lshut +# 30 vlan_30 sus/lshut +# 1002 fddi-default act/unsup +# 1003 token-ring-default act/unsup +# 1004 fddinet-default act/unsup +# 1005 trnet-default act/unsup +# +# VLAN Type SAID MTU Parent RingNo BridgeNo Stp BrdgMode Trans1 Trans2 +# ---- ----- ---------- ----- ------ ------ -------- ---- -------- ------ ------ +# 1 enet 100001 1500 - - - - - 0 0 +# 10 enet 100010 1500 - - - - - 0 0 +# 20 enet 100020 610 - - - - - 0 0 +# 30 enet 100030 1500 - - - - - 0 0 +# 1002 fddi 101002 1500 - - - - - 0 0 +# 1003 tr 101003 1500 - - - - - 0 0 +# 1004 fdnet 101004 1500 - - - ieee - 0 0 +# 1005 trnet 101005 1500 - - - ibm - 0 0 +# +# Remote SPAN VLANs +# ------------------------------------------------------------------------------ +# 10 + +- name: Delete attributes of given VLANs + ios_vlans: + config: + - vlan_id: 10 + - vlan_id: 20 + - vlan_id: 30 + state: deleted + +# After state: +# ------------- +# +# vios#show vlan +# VLAN Name Status Ports +# ---- -------------------------------- --------- ------------------------------- +# 1 default active Gi0/1, Gi0/2 +# 1002 fddi-default act/unsup +# 1003 token-ring-default act/unsup +# 1004 fddinet-default act/unsup +# 1005 trnet-default act/unsup +# +# VLAN Type SAID MTU Parent RingNo BridgeNo Stp BrdgMode Trans1 Trans2 +# ---- ----- ---------- ----- ------ ------ -------- ---- -------- ------ ------ +# 1 enet 100001 1500 - - - - - 0 0 +# 1002 fddi 101002 1500 - - - - - 0 0 +# 1003 tr 101003 1500 - - - - - 0 0 +# 1004 fdnet 101004 1500 - - - ieee - 0 0 +# 1005 trnet 101005 1500 - - - ibm - 0 0 + +# Using merged + +# Using merged + +# Before state: +# ------------- +# +# vios#show vlan +# VLAN Name Status Ports +# ---- -------------------------------- --------- ------------------------------- +# 1 default active Gi0/1, Gi0/2 +# 1002 fddi-default act/unsup +# 1003 token-ring-default act/unsup +# 1004 fddinet-default act/unsup +# 1005 trnet-default act/unsup +# +# VLAN Type SAID MTU Parent RingNo BridgeNo Stp BrdgMode Trans1 Trans2 +# ---- ----- ---------- ----- ------ ------ -------- ---- -------- ------ ------ +# 1 enet 100001 1500 - - - - - 0 0 +# 1002 fddi 101002 1500 - - - - - 0 0 +# 1003 tr 101003 1500 - - - - - 0 0 +# 1004 fdnet 101004 1500 - - - ieee - 0 0 +# 1005 trnet 101005 1500 - - - ibm - 0 0 + +- name: Merge provided configuration with device configuration + ios_vlans: + config: + - name: Vlan_10 + vlan_id: 10 + state: active + shutdown: disabled + remote_span: 10 + - name: Vlan_20 + vlan_id: 20 + mtu: 610 + state: active + shutdown: enabled + - name: Vlan_30 + vlan_id: 30 + state: suspend + shutdown: enabled + state: merged + +# After state: +# ------------ +# +# vios#show vlan +# VLAN Name Status Ports +# ---- -------------------------------- --------- ------------------------------- +# 1 default active Gi0/1, Gi0/2 +# 10 vlan_10 active +# 20 vlan_20 act/lshut +# 30 vlan_30 sus/lshut +# 1002 fddi-default act/unsup +# 1003 token-ring-default act/unsup +# 1004 fddinet-default act/unsup +# 1005 trnet-default act/unsup +# +# VLAN Type SAID MTU Parent RingNo BridgeNo Stp BrdgMode Trans1 Trans2 +# ---- ----- ---------- ----- ------ ------ -------- ---- -------- ------ ------ +# 1 enet 100001 1500 - - - - - 0 0 +# 10 enet 100010 1500 - - - - - 0 0 +# 20 enet 100020 610 - - - - - 0 0 +# 30 enet 100030 1500 - - - - - 0 0 +# 1002 fddi 101002 1500 - - - - - 0 0 +# 1003 tr 101003 1500 - - - - - 0 0 +# 1004 fdnet 101004 1500 - - - ieee - 0 0 +# 1005 trnet 101005 1500 - - - ibm - 0 0 +# +# Remote SPAN VLANs +# ------------------------------------------------------------------------------ +# 10 + +# Using overridden + +# Before state: +# ------------- +# +# vios#show vlan +# VLAN Name Status Ports +# ---- -------------------------------- --------- ------------------------------- +# 1 default active Gi0/1, Gi0/2 +# 10 vlan_10 active +# 20 vlan_20 act/lshut +# 30 vlan_30 sus/lshut +# 1002 fddi-default act/unsup +# 1003 token-ring-default act/unsup +# 1004 fddinet-default act/unsup +# 1005 trnet-default act/unsup +# +# VLAN Type SAID MTU Parent RingNo BridgeNo Stp BrdgMode Trans1 Trans2 +# ---- ----- ---------- ----- ------ ------ -------- ---- -------- ------ ------ +# 1 enet 100001 1500 - - - - - 0 0 +# 10 enet 100010 1500 - - - - - 0 0 +# 20 enet 100020 610 - - - - - 0 0 +# 30 enet 100030 1500 - - - - - 0 0 +# 1002 fddi 101002 1500 - - - - - 0 0 +# 1003 tr 101003 1500 - - - - - 0 0 +# 1004 fdnet 101004 1500 - - - ieee - 0 0 +# 1005 trnet 101005 1500 - - - ibm - 0 0 +# +# Remote SPAN VLANs +# ------------------------------------------------------------------------------ +# 10 + +- name: Override device configuration of all VLANs with provided configuration + ios_vlans: + config: + - name: Vlan_10 + vlan_id: 10 + mtu: 1000 + state: overridden + +# After state: +# ------------ +# +# vios#show vlan +# VLAN Name Status Ports +# ---- -------------------------------- --------- ------------------------------- +# 10 Vlan_10 active +# +# VLAN Type SAID MTU Parent RingNo BridgeNo Stp BrdgMode Trans1 Trans2 +# ---- ----- ---------- ----- ------ ------ -------- ---- -------- ------ ------ +# 10 enet 100010 1000 - - - - - 0 0 + +# Using replaced + +# Before state: +# ------------- +# +# vios#show vlan +# VLAN Name Status Ports +# ---- -------------------------------- --------- ------------------------------- +# 1 default active Gi0/1, Gi0/2 +# 10 vlan_10 active +# 20 vlan_20 act/lshut +# 30 vlan_30 sus/lshut +# 1002 fddi-default act/unsup +# 1003 token-ring-default act/unsup +# 1004 fddinet-default act/unsup +# 1005 trnet-default act/unsup +# +# VLAN Type SAID MTU Parent RingNo BridgeNo Stp BrdgMode Trans1 Trans2 +# ---- ----- ---------- ----- ------ ------ -------- ---- -------- ------ ------ +# 1 enet 100001 1500 - - - - - 0 0 +# 10 enet 100010 1500 - - - - - 0 0 +# 20 enet 100020 610 - - - - - 0 0 +# 30 enet 100030 1500 - - - - - 0 0 +# 1002 fddi 101002 1500 - - - - - 0 0 +# 1003 tr 101003 1500 - - - - - 0 0 +# 1004 fdnet 101004 1500 - - - ieee - 0 0 +# 1005 trnet 101005 1500 - - - ibm - 0 0 +# +# Remote SPAN VLANs +# ------------------------------------------------------------------------------ +# 10 + +- name: Replaces device configuration of listed VLANs with provided configuration + ios_vlans: + config: + - name: Test_VLAN20 + vlan_id: 20 + mtu: 700 + shutdown: disabled + - vlan_id: 30 + name: Test_VLAN30 + mtu: 1000 + state: replaced + +# After state: +# ------------ +# +# vios#show vlan +# VLAN Name Status Ports +# ---- -------------------------------- --------- ------------------------------- +# 1 default active Gi0/1, Gi0/2 +# 10 vlan_10 active +# 20 Test_VLAN20 active +# 30 Test_VLAN30 sus/lshut +# 1002 fddi-default act/unsup +# 1003 token-ring-default act/unsup +# 1004 fddinet-default act/unsup +# 1005 trnet-default act/unsup +# +# VLAN Type SAID MTU Parent RingNo BridgeNo Stp BrdgMode Trans1 Trans2 +# ---- ----- ---------- ----- ------ ------ -------- ---- -------- ------ ------ +# 1 enet 100001 1500 - - - - - 0 0 +# 10 enet 100010 1500 - - - - - 0 0 +# 20 enet 100020 700 - - - - - 0 0 +# 30 enet 100030 1000 - - - - - 0 0 +# 1002 fddi 101002 1500 - - - - - 0 0 +# 1003 tr 101003 1500 - - - - - 0 0 +# 1004 fdnet 101004 1500 - - - ieee - 0 0 +# 1005 trnet 101005 1500 - - - ibm - 0 0 +# +# Remote SPAN VLANs +# ------------------------------------------------------------------------------ +# 10 + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + 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 + type: list + 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: ['vlan 20', 'name vlan_20', 'mtu 600', 'remote-span'] +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.ios.argspec.vlans.vlans import VlansArgs +from ansible.module_utils.network.ios.config.vlans.vlans import Vlans + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=VlansArgs.argument_spec, + supports_check_mode=True) + + result = Vlans(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/__init__.pyc b/module_utils/__init__.pyc new file mode 100644 index 0000000..48d15ca Binary files /dev/null and b/module_utils/__init__.pyc differ 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/__init__.pyc b/module_utils/network/__init__.pyc new file mode 100644 index 0000000..f6f7563 Binary files /dev/null and b/module_utils/network/__init__.pyc differ 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/__init__.pyc b/module_utils/network/ios/__init__.pyc new file mode 100644 index 0000000..58061c0 Binary files /dev/null and b/module_utils/network/ios/__init__.pyc differ 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..d917039 --- /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', + 'vlans', + ] + + 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/vlans/__init__.py b/module_utils/network/ios/argspec/vlans/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/argspec/vlans/vlans.py b/module_utils/network/ios/argspec/vlans/vlans.py new file mode 100644 index 0000000..6c4519a --- /dev/null +++ b/module_utils/network/ios/argspec/vlans/vlans.py @@ -0,0 +1,47 @@ +#!/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_vlans module +""" + + +class VlansArgs(object): + """The arg spec for the ios_vlans module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = {'config': {'elements': 'dict', + 'options': {'name': {'type': 'str'}, + 'vlan_id': {'required': True, 'type': int}, + 'mtu': {'type': int}, + 'remote_span': {'type': bool}, + 'state':{'type': 'str', 'choices':['active', 'suspend']}, + 'shutdown': {'type': 'str', 'choices':['enabled', 'disabled']}}, + '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/__init__.pyc b/module_utils/network/ios/config/__init__.pyc new file mode 100644 index 0000000..b101587 Binary files /dev/null and b/module_utils/network/ios/config/__init__.pyc differ diff --git a/module_utils/network/ios/config/vlans/__init__.py b/module_utils/network/ios/config/vlans/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/config/vlans/__init__.pyc b/module_utils/network/ios/config/vlans/__init__.pyc new file mode 100644 index 0000000..f102dac Binary files /dev/null and b/module_utils/network/ios/config/vlans/__init__.pyc differ diff --git a/module_utils/network/ios/config/vlans/vlans.py b/module_utils/network/ios/config/vlans/vlans.py new file mode 100644 index 0000000..3b523b9 --- /dev/null +++ b/module_utils/network/ios/config/vlans/vlans.py @@ -0,0 +1,277 @@ +# +# -*- 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_vlans 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.ios.facts.facts import Facts +from ansible.module_utils.network.ios.utils.utils import dict_to_set + + +class Vlans(ConfigBase): + """ + The ios_vlans class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'vlans', + ] + + def __init__(self, module): + super(Vlans, 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('vlans') + 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 == 'overridden': + commands = self._state_overridden(want, have, state) + elif state == 'deleted': + commands = self._state_deleted(want, have, state) + elif state == 'merged': + commands = self._state_merged(want, have) + elif state == 'replaced': + commands = self._state_replaced(want, have) + return commands + + def _state_replaced(self, want, have): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + + check = False + for each in want: + for every in have: + if every['vlan_id'] == each['vlan_id']: + check = True + break + else: + continue + if check: + commands.extend(self._set_config(each, every)) + else: + commands.extend(self._set_config(each, dict())) + + return commands + + def _state_overridden(self, want, have, state): + """ The command generator when state is overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + + for each in have: + for every in want: + if each['vlan_id'] == every['vlan_id']: + break + else: + # We didn't find a matching desired state, which means we can + # pretend we recieved an empty desired state. + commands.extend(self._clear_config(every, each, state)) + continue + commands.extend(self._set_config(every, each)) + + return commands + + def _state_merged(self, want, have): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = [] + + check = False + for each in want: + for every in have: + if each.get('vlan_id') == every.get('vlan_id'): + check = True + break + else: + continue + if check: + commands.extend(self._set_config(each, every)) + else: + commands.extend(self._set_config(each, dict())) + + return commands + + def _state_deleted(self, want, have, state): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + commands = [] + + if want: + check = False + for each in want: + for every in have: + if each.get('vlan_id') == every.get('vlan_id'): + check = True + break + else: + check = False + continue + if check: + commands.extend(self._clear_config(each, every, state)) + else: + for each in have: + commands.extend(self._clear_config(dict(), each, state)) + + return commands + + def remove_command_from_config_list(self, vlan, cmd, commands): + if vlan not in commands and cmd != 'vlan': + commands.insert(0, vlan) + elif cmd == 'vlan': + commands.append('no %s' % vlan) + return commands + commands.append('no %s' % cmd) + return commands + + def add_command_to_config_list(self, vlan_id, cmd, commands): + if vlan_id not in commands: + commands.insert(0, vlan_id) + if cmd not in commands: + commands.append(cmd) + + def _set_config(self, want, have): + # Set the interface config based on the want and have config + commands = [] + vlan = 'vlan {0}'.format(want.get('vlan_id')) + + # Get the diff b/w want n have + want_dict = dict_to_set(want) + have_dict = dict_to_set(have) + diff = want_dict - have_dict + + if diff: + name = dict(diff).get('name') + state = dict(diff).get('state') + shutdown = dict(diff).get('shutdown') + mtu = dict(diff).get('mtu') + remote_span = dict(diff).get('remote_span') + if name: + cmd = 'name {0}'.format(name) + self.add_command_to_config_list(vlan, cmd, commands) + if state: + cmd = 'state {0}'.format(state) + self.add_command_to_config_list(vlan, cmd, commands) + if mtu: + cmd = 'mtu {0}'.format(mtu) + self.add_command_to_config_list(vlan, cmd, commands) + if remote_span: + self.add_command_to_config_list(vlan, 'remote-span', commands) + if shutdown == 'enabled': + self.add_command_to_config_list(vlan, 'shutdown', commands) + elif shutdown == 'disabled': + self.add_command_to_config_list(vlan, 'no shutdown', commands) + + return commands + + def _clear_config(self, want, have, state): + # Delete the interface config based on the want and have config + commands = [] + vlan = 'vlan {0}'.format(have.get('vlan_id')) + + if have.get('vlan_id') and 'default' not in have.get('name')\ + and (have.get('vlan_id') != want.get('vlan_id') or state == 'deleted'): + self.remove_command_from_config_list(vlan, 'vlan', commands) + elif 'default' not in have.get('name'): + if have.get('mtu') != want.get('mtu'): + self.remove_command_from_config_list(vlan, 'mtu', commands) + if have.get('remote_span') != want.get('remote_span') and want.get('remote_span'): + self.remove_command_from_config_list(vlan, 'remote-span', commands) + if have.get('shutdown') != want.get('shutdown') and want.get('shutdown'): + self.remove_command_from_config_list(vlan, 'shutdown', commands) + if have.get('state') != want.get('state') and want.get('state'): + self.remove_command_from_config_list(vlan, 'state', commands) + + return commands diff --git a/module_utils/network/ios/config/vlans/vlans.pyc b/module_utils/network/ios/config/vlans/vlans.pyc new file mode 100644 index 0000000..c2fca98 Binary files /dev/null and b/module_utils/network/ios/config/vlans/vlans.pyc differ 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..d381cc8 --- /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.vlans.vlans import VlansFacts + + +FACT_LEGACY_SUBSETS = {} +FACT_RESOURCE_SUBSETS = dict( + vlans=VlansFacts, +) + + +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/vlans/__init__.py b/module_utils/network/ios/facts/vlans/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/facts/vlans/vlans.py b/module_utils/network/ios/facts/vlans/vlans.py new file mode 100644 index 0000000..dac4822 --- /dev/null +++ b/module_utils/network/ios/facts/vlans/vlans.py @@ -0,0 +1,141 @@ +# +# -*- 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 vlans 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 copy import deepcopy + +from ansible.module_utils.network.common import utils +from ansible.module_utils.network.ios.argspec.vlans.vlans import VlansArgs + + +class VlansFacts(object): + """ The ios vlans fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = VlansArgs.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 vlans + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if connection: + pass + + objs = [] + mtu_objs = [] + remote_objs = [] + final_objs = [] + if not data: + data = connection.get('show vlan') + # operate on a collection of resource x + config = data.split('\n') + # Get individual vlan configs separately + vlan_info = '' + for conf in config: + if 'Name' in conf: + vlan_info = 'Name' + elif 'Type' in conf: + vlan_info = 'Type' + elif 'Remote' in conf: + vlan_info = 'Remote' + if conf and not ' ' in filter(None,conf.split('-')): + obj = self.render_config(self.generated_spec, conf, vlan_info) + if 'mtu' in obj: + mtu_objs.append(obj) + elif 'remote_span' in obj: + remote_objs = obj + elif obj: + objs.append(obj) + # Appending MTU value to the retrieved dictionary + for o, m in zip(objs, mtu_objs): + o.update(m) + final_objs.append(o) + + # Appending Remote Span value to related VLAN: + if remote_objs: + if remote_objs.get('remote_span'): + for each in remote_objs.get('remote_span'): + for every in final_objs: + if each == every.get('vlan_id'): + every.update({'remote_span': True}) + break + + facts = {} + if final_objs: + facts['vlans'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + + for cfg in params['config']: + facts['vlans'].append(utils.remove_empties(cfg)) + ansible_facts['ansible_network_resources'].update(facts) + + return ansible_facts + + def render_config(self, spec, conf, vlan_info): + """ + 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) + + if vlan_info == 'Name' and 'Name' not in conf: + conf = filter(None, conf.split(' ')) + config['vlan_id'] = int(conf[0]) + config['name'] = conf[1] + if len(conf[2].split('/')) > 1: + if conf[2].split('/')[0] == 'sus': + config['state'] = 'suspend' + elif conf[2].split('/')[0] == 'act': + config['state'] = 'active' + config['shutdown'] = 'enabled' + else: + if conf[2] == 'suspended': + config['state'] = 'suspend' + elif conf[2] == 'active': + config['state'] = 'active' + config['shutdown'] = 'disabled' + elif vlan_info == 'Type' and 'Type' not in conf: + conf = filter(None, conf.split(' ')) + config['mtu'] = int(conf[3]) + elif vlan_info == 'Remote': + if len(conf.split(',')) > 1 or conf.isdigit(): + remote_span_vlan = [] + if len(conf.split(',')) > 1: + remote_span_vlan = conf.split(',') + else: + remote_span_vlan.append(conf) + remote_span = [] + for each in remote_span_vlan: + remote_span.append(int(each)) + config['remote_span'] = remote_span + + return utils.remove_empties(config) 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..946e9aa --- /dev/null +++ b/module_utils/network/ios/utils/utils.py @@ -0,0 +1,175 @@ +#!/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 + + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.six import iteritems + + +def remove_command_from_config_list(interface, cmd, commands): + # To delete the passed config + if interface not in commands: + commands.insert(0, interface) + commands.append('no %s' % cmd) + return commands + + +def add_command_to_config_list(interface, cmd, commands): + # To set the passed config + if interface not in commands: + commands.insert(0, interface) + commands.append(cmd) + + +def dict_to_set(sample_dict): + # Generate a set with passed dictionary for comparison + test_dict = {} + for k, v in iteritems(sample_dict): + if v is not None: + if isinstance(v, list) and v: + if isinstance(v[0], dict): + li = [] + for each in v: + for key, value in iteritems(each): + if isinstance(value, list): + each[key] = tuple(value) + li.append(tuple(each.items())) + v = tuple(li) + else: + v = tuple(v) + elif isinstance(v, dict): + li = [] + for key, value in iteritems(v): + if isinstance(value, list): + v[key] = tuple(value) + li.extend(tuple(v.items())) + v = tuple(li) + elif isinstance(v, list): + v = tuple(v) + test_dict.update({k: v}) + return_set = set(tuple(test_dict.items())) + return return_set + + +def filter_dict_having_none_value(want, have): + # Generate dict with have dict value which is None in want dict + test_dict = dict() + test_key_dict = dict() + test_dict['name'] = want.get('name') + for k, v in iteritems(want): + if isinstance(v, dict): + for key, value in iteritems(v): + if value is None: + dict_val = have.get(k).get(key) + test_key_dict.update({key: dict_val}) + test_dict.update({k: test_key_dict}) + if v is None: + val = have.get(k) + test_dict.update({k: val}) + return test_dict + + +def remove_duplicate_interface(commands): + # Remove duplicate interface from commands + set_cmd = [] + for each in commands: + if 'interface' in each: + if each not in set_cmd: + set_cmd.append(each) + else: + set_cmd.append(each) + + return set_cmd + + +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'