Add nxos_lacp resource module (#59717)

* Add nxos_lacp resource module

Signed-off-by: Trishna Guha <trishnaguha17@gmail.com>

* Add commands in module doc

Signed-off-by: Trishna Guha <trishnaguha17@gmail.com>
This commit is contained in:
Trishna Guha 2019-08-08 16:52:16 +05:30 committed by GitHub
parent a595c60d4c
commit 87a568da0b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 734 additions and 1 deletions

View file

@ -12,6 +12,7 @@ CHOICES = [
'lag_interfaces',
'telemetry',
'vlans',
'lacp',
]

View file

@ -0,0 +1,69 @@
#
# -*- 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 nxos_lacp module
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
class LacpArgs(object):
"""The arg spec for the nxos_lacp module
"""
def __init__(self, **kwargs):
pass
argument_spec = {
'config': {
'options': {
'system': {
'options': {
'mac': {
'options': {
'address': {
'type': 'str'
},
'role': {
'choices': ['primary', 'secondary'],
'type': 'str'
}
},
'type': 'dict'
},
'priority': {
'type': 'int'
}
},
'type': 'dict'
}
},
'type': 'dict'
},
'state': {
'choices': ['merged', 'replaced', 'deleted'],
'default': 'merged',
'type': 'str'
}
}

View file

@ -0,0 +1,201 @@
#
# -*- 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 nxos_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 __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 dict_diff, to_list, remove_empties
from ansible.module_utils.network.nxos.facts.facts import Facts
class Lacp(ConfigBase):
"""
The nxos_lacp class
"""
gather_subset = [
'!all',
'!min',
]
gather_network_resources = [
'lacp',
]
exclude_params = [
'priority',
'mac',
]
def __init__(self, module):
super(Lacp, self).__init__(module)
def get_lacp_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)
lacp_facts = facts['ansible_network_resources'].get('lacp')
if not lacp_facts:
return []
return lacp_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_lacp_facts = self.get_lacp_facts()
commands.extend(self.set_config(existing_lacp_facts))
if commands:
if not self._module.check_mode:
self._connection.edit_config(commands)
result['changed'] = True
result['commands'] = commands
changed_lacp_facts = self.get_lacp_facts()
result['before'] = existing_lacp_facts
if result['changed']:
result['after'] = changed_lacp_facts
result['warnings'] = warnings
return result
def set_config(self, existing_lacp_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 = remove_empties(self._module.params['config'])
have = existing_lacp_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']
commands = list()
if state == 'overridden':
commands.extend(self._state_overridden(want, have))
elif state == 'deleted':
commands.extend(self._state_deleted(want, have))
elif state == 'merged':
commands.extend(self._state_merged(want, have))
elif state == 'replaced':
commands.extend(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 = []
diff = dict_diff(want, have)
wkeys = want.keys()
dkeys = diff.keys()
for k in wkeys:
if k in self.exclude_params and k in dkeys:
del diff[k]
deleted_commands = self.del_all(diff)
merged_commands = self._state_merged(want, have)
if merged_commands:
commands.extend(deleted_commands)
commands.extend(merged_commands)
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
"""
return self.set_commands(want, have)
def _state_deleted(self, 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 = []
if not have:
return commands
commands.extend(self.del_all(have))
return commands
def get_diff(self, comparable, base):
diff = {}
if not base:
diff = comparable
else:
diff = dict_diff(base, comparable)
return diff
def del_all(self, diff):
commands = []
base = 'no lacp system-'
diff = diff.get('system')
if diff:
if 'priority' in diff:
commands.append(base + 'priority')
if 'mac' in diff:
commands.append(base + 'mac')
return commands
def add_commands(self, diff):
commands = []
base = 'lacp system-'
diff = diff.get('system')
if diff and 'priority' in diff:
cmd = base + 'priority' + ' ' + str(diff['priority'])
commands.append(cmd)
if diff and 'mac' in diff:
cmd = ''
if 'address' in diff['mac']:
cmd += base + 'mac' + ' ' + diff['mac']['address']
if 'role' in diff['mac']:
cmd += ' ' + 'role' + ' ' + diff['mac']['role']
if cmd:
commands.append(cmd)
return commands
def set_commands(self, want, have):
if not want:
return []
diff = self.get_diff(want, have)
return self.add_commands(diff)

View file

@ -12,6 +12,7 @@ calls the appropriate facts gathering function
from ansible.module_utils.network.nxos.argspec.facts.facts import FactsArgs
from ansible.module_utils.network.common.facts.facts import FactsBase
from ansible.module_utils.network.nxos.facts.legacy.base import Default, Legacy, Hardware, Config, Interfaces, Features
from ansible.module_utils.network.nxos.facts.lacp.lacp import LacpFacts
from ansible.module_utils.network.nxos.facts.lag_interfaces.lag_interfaces import Lag_interfacesFacts
from ansible.module_utils.network.nxos.facts.telemetry.telemetry import TelemetryFacts
from ansible.module_utils.network.nxos.facts.vlans.vlans import VlansFacts
@ -29,6 +30,7 @@ FACT_RESOURCE_SUBSETS = dict(
lag_interfaces=Lag_interfacesFacts,
telemetry=TelemetryFacts,
vlans=VlansFacts,
lacp=LacpFacts,
)

View file

@ -0,0 +1,85 @@
#
# -*- 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 nxos 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.
"""
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.nxos.argspec.lacp.lacp import LacpArgs
class LacpFacts(object):
""" The nxos 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
"""
obj = []
if not data:
data = connection.get("show running-config | include lacp")
resources = data.strip()
objs = self.render_config(self.generated_spec, resources)
ansible_facts['ansible_network_resources'].pop('lacp', None)
facts = {}
if objs:
params = utils.validate_config(self.argument_spec, {'config': objs})
facts['lacp'] = utils.remove_empties(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)
p_match = re.search(r'lacp system-priority (\d+)', conf, re.M)
if p_match:
config['system']['priority'] = p_match.group(1)
a_match = re.search(r'lacp system-mac (\S+)', conf, re.M)
if a_match:
address = a_match.group(1)
config['system']['mac']['address'] = address
r_match = re.search(r'lacp system-mac {0} role (\S+)'.format(address), conf, re.M)
if r_match:
config['system']['mac']['role'] = r_match.group(1)
return utils.remove_empties(config)

View file

@ -57,7 +57,7 @@ options:
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', 'lag_interfaces', 'telemetry', 'vlans']
choices: ['all', 'lag_interfaces', 'telemetry', 'vlans', 'lacp']
required: false
version_added: "2.9"
"""

View file

@ -0,0 +1,188 @@
#!/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 nxos_lacp
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'network'
}
DOCUMENTATION = """
---
module: nxos_lacp
version_added: 2.9
short_description: Manage Global Link Aggregation Control Protocol (LACP) on Cisco NX-OS devices.
description: This module manages Global Link Aggregation Control Protocol (LACP) on NX-OS devices.
author: Trishna Guha (@trishnaguha)
notes:
- Tested against NXOS 7.3.(0)D1(1) on VIRL.
- Feature lacp should be enabled for this module.
options:
config:
description: LACP global options.
type: dict
suboptions:
system:
description:
- LACP system options
type: dict
suboptions:
priority:
description:
- The system priority to use in LACP negotiations.
type: int
mac:
description:
- MAC address to be used for the LACP Protocol exchanges
type: dict
suboptions:
address:
description:
- MAC-address (FORMAT :xxxx.xxxx.xxxx).
type: str
role:
description:
- The role for the Switch.
type: str
choices: ['primary', 'secondary']
state:
description:
- The state the configuration should be left in.
type: str
choices:
- merged
- replaced
- deleted
default: merged
"""
EXAMPLES = """
# Using merged
# Before state:
# -------------
#
- name: Merge provided configuration with device configuration.
nxos_lacp:
config:
system:
priority: 10
mac:
address: 00c1.4c00.bd15
state: merged
# After state:
# ------------
#
# lacp system-priority 10
# lacp system-mac 00c1.4c00.bd15
# Using replaced
# Before state:
# -------------
#
# lacp system-priority 10
- name: Replace device global lacp configuration with the given configuration.
nxos_lacp:
config:
system:
mac:
address: 00c1.4c00.bd15
state: replaced
# After state:
# ------------
#
# lacp system-mac 00c1.4c00.bd15
# Using deleted
# Before state:
# -------------
#
# lacp system-priority 10
- name: Delete global LACP configurations.
nxos_lacp:
state: deleted
# After state:
# ------------
#
"""
RETURN = """
before:
description: The configuration prior to the model invocation.
returned: always
type: dict
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: dict
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: ['lacp system-priority 15', 'lacp system-mac 00c1.4c00.bd15 role primary']
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.nxos.argspec.lacp.lacp import LacpArgs
from ansible.module_utils.network.nxos.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()

View file

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

View file

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

View file

@ -0,0 +1,20 @@
---
- name: collect cli test cases
find:
paths: "{{ role_path }}/tests/cli"
patterns: "{{ testcase }}.yaml"
connection: local
register: test_cases
- set_fact:
test_cases:
files: "{{ test_cases.files }}"
- 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 connection={{ cli }}"
with_items: "{{ test_items }}"
loop_control:
loop_var: test_case_to_run

View file

@ -0,0 +1,2 @@
---
- { include: cli.yaml, tags: ['cli'] }

View file

@ -0,0 +1,53 @@
---
- debug:
msg: "Start nxos_lacp deleted integration tests connection={{ ansible_connection }}"
- name: Enable lacp feature
nxos_feature:
feature: lacp
- block:
- name: Setup
cli_config:
config: lacp system-priority 11
- name: Gather lacp facts
nxos_facts: &facts
gather_subset:
- '!all'
- '!min'
gather_network_resources: lacp
- name: deleted
nxos_lacp: &deleted
state: deleted
register: result
- assert:
that:
- "ansible_facts.network_resources.lacp == result.before"
- "'no lacp system-priority' in result.commands"
- "result.changed == true"
- "result.commands|length == 1"
- name: Gather lacp post facts
nxos_facts: *facts
- assert:
that:
- "result.after|length == 0"
- name: Idempotence - deleted
nxos_lacp: *deleted
register: result
- assert:
that:
- "result.changed == false"
- "result.commands|length == 0"
always:
- name: teardown
nxos_feature:
feature: lacp
state: disabled

View file

@ -0,0 +1,49 @@
---
- debug:
msg: "Start nxos_lacp merged integration tests connection={{ ansible_connection }}"
- name: Enable lacp
nxos_feature:
feature: lacp
- block:
- name: Merged
nxos_lacp: &merged
config:
system:
priority: 11
state: merged
register: result
- assert:
that:
- "result.before|length == 0"
- "result.changed == true"
- "'lacp system-priority 11' in result.commands"
- "result.commands|length == 1"
- name: Gather lacp facts
nxos_facts:
gather_subset:
- '!all'
- '!min'
gather_network_resources: lacp
- assert:
that:
- "ansible_facts.network_resources.lacp == result.after"
- name: Idempotence - Merged
nxos_lacp: *merged
register: result
- assert:
that:
- "result.changed == false"
- "result.commands|length == 0"
always:
- name: teardown
nxos_feature:
feature: lacp
state: disabled

View file

@ -0,0 +1,59 @@
---
- debug:
msg: "Start nxos_lacp replaced integration tests connection={{ ansible_connection }}"
- name: Enable lacp feature
nxos_feature:
feature: lacp
- block:
- name: Setup
cli_config:
config: lacp system-priority 11
- name: Gather lacp facts
nxos_facts: &facts
gather_subset:
- '!all'
- '!min'
gather_network_resources: lacp
- name: Replaced
nxos_lacp: &replaced
config:
system:
mac:
address: 00c1.4c00.bd15
role: primary
state: replaced
register: result
- assert:
that:
- "ansible_facts.network_resources.lacp == result.before"
- "result.changed == true"
- "'no lacp system-priority' in result.commands"
- "'lacp system-mac 00c1.4c00.bd15 role primary' in result.commands"
- "result.commands|length == 2"
- name: Gather lacp interfaces post facts
nxos_facts: *facts
- assert:
that:
- "ansible_facts.network_resources.lacp == result.after"
- name: Idempotence - Replaced
nxos_lacp: *replaced
register: result
- assert:
that:
- "result.changed == false"
- "result.commands|length == 0"
always:
- name: teardown
nxos_feature:
feature: lacp
state: disabled