New module eos_vlans (#60652)

* Initial commit of module files

* Add tests, implement facts

* Implement config, fix issues

* Handle vlan ranges from the device

* Deprecate eos_vlan
This commit is contained in:
Nathaniel Case 2019-08-19 09:02:38 -04:00 committed by GitHub
parent 256db658b7
commit 446dcb7c96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 832 additions and 9 deletions

View file

@ -13,6 +13,8 @@ CHOICES = [
'!all', '!all',
'interfaces', 'interfaces',
'!interfaces', '!interfaces',
'vlans',
'!vlans',
] ]

View file

@ -0,0 +1,54 @@
# -*- 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 eos_vlans module
"""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
class VlansArgs(object):
"""The arg spec for the eos_vlans module
"""
def __init__(self, **kwargs):
pass
argument_spec = {
'config': {
'elements': 'dict',
'options': {
'vlan_id': {'required': True, 'type': 'int'},
'name': {'type': 'str'},
'state': {'choices': ['active', 'suspend'], 'type': 'str'},
},
'type': 'list',
},
'state': {
'choices': ['merged', 'replaced', 'overridden', 'deleted'],
'default': 'merged',
'type': 'str',
},
}

View file

@ -0,0 +1,208 @@
# -*- 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 eos_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, dict_diff, param_list_to_dict
from ansible.module_utils.network.eos.facts.facts import Facts
class Vlans(ConfigBase):
"""
The eos_vlans class
"""
gather_subset = [
'!all',
'!min',
]
gather_network_resources = [
'vlans',
]
def get_vlans_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)
vlans_facts = facts['ansible_network_resources'].get('vlans')
if not vlans_facts:
return []
return vlans_facts
def execute_module(self):
""" Execute the module
:rtype: A dictionary
:returns: The result from module execution
"""
result = {'changed': False}
warnings = list()
commands = list()
existing_vlans_facts = self.get_vlans_facts()
commands.extend(self.set_config(existing_vlans_facts))
if commands:
if not self._module.check_mode:
self._connection.edit_config(commands)
result['changed'] = True
result['commands'] = commands
changed_vlans_facts = self.get_vlans_facts()
result['before'] = existing_vlans_facts
if result['changed']:
result['after'] = changed_vlans_facts
result['warnings'] = warnings
return result
def set_config(self, existing_vlans_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_vlans_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']
want = param_list_to_dict(want, "vlan_id", False)
have = param_list_to_dict(have, "vlan_id", False)
if state == 'overridden':
commands = self._state_overridden(want, have)
elif state == 'deleted':
commands = self._state_deleted(want, have)
elif state == 'merged':
commands = self._state_merged(want, have)
elif state == 'replaced':
commands = self._state_replaced(want, have)
return commands
@staticmethod
def _state_replaced(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 = []
for key, desired in want.items():
if key in have:
extant = have[key]
else:
extant = dict(vlan_id=key)
add_config = dict_diff(extant, desired)
del_config = dict_diff(desired, extant)
commands.extend(generate_commands(key, add_config, del_config))
return commands
@staticmethod
def _state_overridden(want, have):
""" 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 key, extant in have.items():
if key in want:
desired = want[key]
else:
desired = dict(vlan_id=key)
add_config = dict_diff(extant, desired)
del_config = dict_diff(desired, extant)
commands.extend(generate_commands(key, add_config, del_config))
return commands
@staticmethod
def _state_merged(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 = []
for key, desired in want.items():
if key in have:
extant = have[key]
else:
extant = dict(vlan_id=key)
add_config = dict_diff(extant, desired)
commands.extend(generate_commands(key, add_config, {}))
return commands
@staticmethod
def _state_deleted(want, have):
""" The command generator when state is deleted
:rtype: A list
:returns: the commands necessary to remove the current configuration
of the provided objects
"""
commands = []
for key in want.keys():
desired = dict(vlan_id=key)
if key in have:
extant = have[key]
else:
extant = dict(vlan_id=key)
del_config = dict_diff(desired, extant)
commands.extend(generate_commands(key, {}, del_config))
return commands
def generate_commands(vlan_id, to_set, to_remove):
commands = []
for key, value in to_set.items():
commands.append("{0} {1}".format(key, value))
for key in to_remove.keys():
commands.append("no {0}".format(key))
if commands:
commands.insert(0, "vlan {0}".format(vlan_id))
return commands

View file

@ -13,6 +13,7 @@ __metaclass__ = type
from ansible.module_utils.network.common.facts.facts import FactsBase from ansible.module_utils.network.common.facts.facts import FactsBase
from ansible.module_utils.network.eos.argspec.facts.facts import FactsArgs from ansible.module_utils.network.eos.argspec.facts.facts import FactsArgs
from ansible.module_utils.network.eos.facts.interfaces.interfaces import InterfacesFacts from ansible.module_utils.network.eos.facts.interfaces.interfaces import InterfacesFacts
from ansible.module_utils.network.eos.facts.vlans.vlans import VlansFacts
from ansible.module_utils.network.eos.facts.legacy.base import Default, Hardware, Config, Interfaces from ansible.module_utils.network.eos.facts.legacy.base import Default, Hardware, Config, Interfaces
@ -24,6 +25,7 @@ FACT_LEGACY_SUBSETS = dict(
) )
FACT_RESOURCE_SUBSETS = dict( FACT_RESOURCE_SUBSETS = dict(
interfaces=InterfacesFacts, interfaces=InterfacesFacts,
vlans=VlansFacts,
) )

View file

@ -0,0 +1,108 @@
# -*- 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 eos 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 __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from copy import deepcopy
import re
from ansible.module_utils.network.common import utils
from ansible.module_utils.network.eos.argspec.vlans.vlans import VlansArgs
class VlansFacts(object):
""" The eos 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 not data:
data = connection.get('show running-config | section ^vlan')
# split the config into instances of the resource
resource_delim = 'vlan'
find_pattern = r'(?:^|\n)%s.*?(?=(?:^|\n)%s|$)' % (resource_delim,
resource_delim)
resources = [p.strip() for p in re.findall(find_pattern,
data,
re.DOTALL)]
objs = []
for resource in resources:
if resource:
obj = self.render_config(self.generated_spec, resource)
if obj:
objs.extend(obj)
ansible_facts['ansible_network_resources'].pop('vlans', None)
facts = {}
if objs:
params = utils.validate_config(self.argument_spec, {'config': objs})
facts['vlans'] = [utils.remove_empties(cfg) for cfg in 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)
vlans = []
vlan_list = vlan_to_list(utils.parse_conf_arg(conf, 'vlan'))
for vlan in vlan_list:
config['vlan_id'] = vlan
config['name'] = utils.parse_conf_arg(conf, 'name')
config['state'] = utils.parse_conf_arg(conf, 'state')
vlans.append(utils.remove_empties(config))
return vlans
def vlan_to_list(vlan_str):
vlans = []
for vlan in vlan_str.split(','):
if '-' in vlan:
start, stop = vlan.split('-')
vlans.extend(range(int(start), int(stop) + 1))
else:
vlans.append(int(vlan))
return vlans

View file

@ -20,7 +20,7 @@
# #
ANSIBLE_METADATA = {'metadata_version': '1.1', ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'], 'status': ['deprecated'],
'supported_by': 'network'} 'supported_by': 'network'}
@ -33,6 +33,10 @@ short_description: Manage VLANs on Arista EOS network devices
description: description:
- This module provides declarative management of VLANs - This module provides declarative management of VLANs
on Arista EOS network devices. on Arista EOS network devices.
deprecated:
removed_in: "2.13"
alternative: eos_vlans
why: Updated modules released with more functionality
notes: notes:
- Tested against EOS 4.15 - Tested against EOS 4.15
options: options:

View file

@ -47,7 +47,7 @@ options:
can also be used with an initial C(M(!)) to specify that a can also be used with an initial C(M(!)) to specify that a
specific subset should not be collected. specific subset should not be collected.
required: false required: false
choices: ['all', '!all', 'interfaces', '!interfaces'] choices: ['all', '!all', 'interfaces', '!interfaces', 'vlans', '!vlans']
type: list type: list
version_added: "2.9" version_added: "2.9"
""" """

View file

@ -0,0 +1,240 @@
#!/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 eos_vlans
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'network'
}
DOCUMENTATION = """
---
module: eos_vlans
version_added: 2.9
short_description: Manage VLANs on Arista EOS devices.
description: This module provides declarative management of VLANs on Arista EOS network devices.
author: Nathaniel Case (@qalthos)
notes:
- Tested against Arista EOS 4.20.10M
- This module works with connection C(network_cli). See the
L(EOS Platform Options,../network/user_guide/platform_eos.html).
options:
config:
description: A dictionary of VLANs options
type: list
elements: dict
suboptions:
name:
description:
- Name of the VLAN.
type: str
vlan_id:
description:
- ID of the VLAN. Range 1-4094
type: int
required: true
state:
description:
- Operational state of the VLAN
type: str
choices:
- active
- suspend
state:
description:
- The state the configuration should be left in
type: str
choices:
- merged
- replaced
- overridden
- deleted
default: merged
"""
EXAMPLES = """
# Using deleted
# Before state:
# -------------
#
# veos(config-vlan-20)#show running-config | section vlan
# vlan 10
# name ten
# !
# vlan 20
# name twenty
- name: Delete attributes of the given VLANs.
ios_vlans:
config:
- vlan_id: 20
state: deleted
# After state:
# ------------
#
# veos(config-vlan-20)#show running-config | section vlan
# vlan 10
# name ten
# Using merged
# Before state:
# -------------
#
# veos(config-vlan-20)#show running-config | section vlan
# vlan 10
# name ten
# !
# vlan 20
# name twenty
- name: Merge given VLAN attributes with device configuration
ios_vlans:
config:
- vlan_id: 20
state: suspend
state: merged
# After state:
# ------------
#
# veos(config-vlan-20)#show running-config | section vlan
# vlan 10
# name ten
# !
# vlan 20
# name twenty
# state suspend
# Using overridden
# Before state:
# -------------
#
# veos(config-vlan-20)#show running-config | section vlan
# vlan 10
# name ten
# !
# vlan 20
# name twenty
- name: Override device configuration of all VLANs with provided configuration
ios_vlans:
config:
- vlan_id: 20
state: suspend
state: overridden
# After state:
# ------------
#
# veos(config-vlan-20)#show running-config | section vlan
# vlan 20
# state suspend
# Using replaced
# Before state:
# -------------
#
# veos(config-vlan-20)#show running-config | section vlan
# vlan 10
# name ten
# !
# vlan 20
# name twenty
- name: Replace all attributes of specified VLANs with provided configuration
ios_vlans:
config:
- vlan_id: 20
state: suspend
state: replaced
# After state:
# ------------
#
# veos(config-vlan-20)#show running-config | section vlan
# vlan 10
# name ten
# !
# vlan 20
# state suspend
"""
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 10', 'no name', 'vlan 11', 'name Eleven']
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.eos.argspec.vlans.vlans import VlansArgs
from ansible.module_utils.network.eos.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()

View file

@ -0,0 +1,2 @@
---
testcase: "*"

View file

@ -0,0 +1,2 @@
dependencies:
- prepare_eos_tests

View file

@ -0,0 +1,16 @@
---
- name: collect all cli test cases
find:
paths: "{{ role_path }}/tests/cli"
patterns: "{{ testcase }}.yaml"
delegate_to: localhost
register: test_cases
- name: set test_items
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
- name: run test cases (connection=network_cli)
include: "{{ test_case_to_run }} ansible_connection=network_cli"
with_items: "{{ test_items }}"
loop_control:
loop_var: test_case_to_run

View file

@ -0,0 +1,39 @@
---
- include_tasks: reset_config.yml
- set_fact:
config:
- vlan_id: 20
- eos_facts:
gather_network_resources: vlans
become: yes
- name: Returns vlans to default parameters
eos_vlans:
config: "{{ config }}"
state: deleted
register: result
become: yes
- assert:
that:
- "ansible_facts.network_resources.vlans|symmetric_difference(result.before) == []"
- eos_facts:
gather_network_resources: vlans
become: yes
- assert:
that:
- "ansible_facts.network_resources.vlans|symmetric_difference(result.after) == []"
- set_fact:
expected_config:
- vlan_id: 10
name: ten
- vlan_id: 20
- assert:
that:
- "expected_config|symmetric_difference(ansible_facts.network_resources.vlans) == []"

View file

@ -0,0 +1,42 @@
---
- include_tasks: reset_config.yml
- set_fact:
config:
- vlan_id: 20
state: suspend
- eos_facts:
gather_network_resources: vlans
become: yes
- name: Merge provided configuration with device configuration
eos_vlans:
config: "{{ config }}"
state: merged
register: result
become: yes
- assert:
that:
- "ansible_facts.network_resources.vlans|symmetric_difference(result.before) == []"
- eos_facts:
gather_network_resources: vlans
become: yes
- assert:
that:
- "ansible_facts.network_resources.vlans|symmetric_difference(result.after) == []"
- set_fact:
expected_config:
- vlan_id: 10
name: ten
- vlan_id: 20
name: twenty
state: suspend
- assert:
that:
- "expected_config|symmetric_difference(ansible_facts.network_resources.vlans) == []"

View file

@ -0,0 +1,39 @@
---
- include_tasks: reset_config.yml
- set_fact:
config:
- vlan_id: 20
state: suspend
other_config:
- vlan_id: 10
- eos_facts:
gather_network_resources: vlans
become: yes
- name: Overrides device configuration of all vlans with provided configuration
eos_vlans:
config: "{{ config }}"
state: overridden
register: result
become: yes
- assert:
that:
- "ansible_facts.network_resources.vlans|symmetric_difference(result.before) == []"
- eos_facts:
gather_network_resources: vlans
become: yes
- assert:
that:
- "ansible_facts.network_resources.vlans|symmetric_difference(result.after) == []"
- set_fact:
expected_config: "{{ config }} + {{ other_config }}"
- assert:
that:
- "expected_config|symmetric_difference(ansible_facts.network_resources.vlans) == []"

View file

@ -0,0 +1,40 @@
---
- include_tasks: reset_config.yml
- set_fact:
config:
- vlan_id: 20
state: suspend
other_config:
- vlan_id: 10
name: ten
- eos_facts:
gather_network_resources: vlans
become: yes
- name: Replaces device configuration of listed vlans with provided configuration
eos_vlans:
config: "{{ config }}"
state: replaced
register: result
become: yes
- assert:
that:
- "ansible_facts.network_resources.vlans|symmetric_difference(result.before) == []"
- eos_facts:
gather_network_resources: vlans
become: yes
- assert:
that:
- "ansible_facts.network_resources.vlans|symmetric_difference(result.after) == []"
- set_fact:
expected_config: "{{ config }} + {{ other_config }}"
- assert:
that:
- "expected_config|symmetric_difference(ansible_facts.network_resources.vlans) == []"

View file

@ -0,0 +1,25 @@
---
- name: Reset initial config
cli_config:
config: |
no vlan 1-4094
vlan 10
name ten
vlan 20
name twenty
become: yes
- eos_facts:
gather_network_resources: vlans
become: yes
- set_fact:
expected_config:
- vlan_id: 10
name: ten
- vlan_id: 20
name: twenty
- assert:
that:
- "expected_config|symmetric_difference(ansible_facts.network_resources.vlans) == []"

View file

@ -3617,13 +3617,13 @@ lib/ansible/modules/network/eos/eos_user.py validate-modules:E326
lib/ansible/modules/network/eos/eos_user.py validate-modules:E337 lib/ansible/modules/network/eos/eos_user.py validate-modules:E337
lib/ansible/modules/network/eos/eos_user.py validate-modules:E338 lib/ansible/modules/network/eos/eos_user.py validate-modules:E338
lib/ansible/modules/network/eos/eos_user.py validate-modules:E340 lib/ansible/modules/network/eos/eos_user.py validate-modules:E340
lib/ansible/modules/network/eos/eos_vlan.py future-import-boilerplate lib/ansible/modules/network/eos/_eos_vlan.py future-import-boilerplate
lib/ansible/modules/network/eos/eos_vlan.py metaclass-boilerplate lib/ansible/modules/network/eos/_eos_vlan.py metaclass-boilerplate
lib/ansible/modules/network/eos/eos_vlan.py validate-modules:E322 lib/ansible/modules/network/eos/_eos_vlan.py validate-modules:E322
lib/ansible/modules/network/eos/eos_vlan.py validate-modules:E326 lib/ansible/modules/network/eos/_eos_vlan.py validate-modules:E326
lib/ansible/modules/network/eos/eos_vlan.py validate-modules:E337 lib/ansible/modules/network/eos/_eos_vlan.py validate-modules:E337
lib/ansible/modules/network/eos/eos_vlan.py validate-modules:E338 lib/ansible/modules/network/eos/_eos_vlan.py validate-modules:E338
lib/ansible/modules/network/eos/eos_vlan.py validate-modules:E340 lib/ansible/modules/network/eos/_eos_vlan.py validate-modules:E340
lib/ansible/modules/network/eos/eos_vrf.py future-import-boilerplate lib/ansible/modules/network/eos/eos_vrf.py future-import-boilerplate
lib/ansible/modules/network/eos/eos_vrf.py metaclass-boilerplate lib/ansible/modules/network/eos/eos_vrf.py metaclass-boilerplate
lib/ansible/modules/network/eos/eos_vrf.py validate-modules:E322 lib/ansible/modules/network/eos/eos_vrf.py validate-modules:E322