Add junos vlans resource module (#59041)

pull/60161/head
Daniel Mellado Area 5 years ago committed by GitHub
parent 2a1393e0e1
commit e3681c049c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -10,7 +10,8 @@ CHOICES = [
'all',
'interfaces',
'lag_interfaces',
'l3_interfaces'
'l3_interfaces',
'vlans'
]

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

@ -0,0 +1,208 @@
# Copyright (C) 2019 Red Hat, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
The junos_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.junos.facts.facts import Facts
from ansible.module_utils.network.junos.junos import (locked_config,
load_config,
commit_configuration,
discard_changes,
tostring)
from ansible.module_utils.network.common.netconf import (build_root_xml_node,
build_child_xml_node)
class Vlans(ConfigBase):
"""
The junos_vlans class
"""
gather_subset = [
'!all',
'!min',
]
gather_network_resources = [
'vlans',
]
def __init__(self, module):
super(Vlans, self).__init__(module)
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()
existing_vlans_facts = self.get_vlans_facts()
config_xmls = self.set_config(existing_vlans_facts)
with locked_config(self._module):
for config_xml in to_list(config_xmls):
diff = load_config(self._module, config_xml, warnings)
commit = not self._module.check_mode
if diff:
if commit:
commit_configuration(self._module)
else:
discard_changes(self._module)
result['changed'] = True
if self._module._diff:
result['diff'] = {'prepared': diff}
result['xml'] = config_xmls
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
"""
root = build_root_xml_node('vlans')
state = self._module.params['state']
if state == 'overridden':
config_xmls = self._state_overridden(want, have)
elif state == 'deleted':
config_xmls = self._state_deleted(want, have)
elif state == 'merged':
config_xmls = self._state_merged(want, have)
elif state == 'replaced':
config_xmls = self._state_replaced(want, have)
for xml in config_xmls:
root.append(xml)
return tostring(root)
def _state_replaced(self, want, have):
""" The command generator when state is replaced
:rtype: A list
:returns: the xml necessary to migrate the current configuration
to the desired configuration
"""
intf_xml = []
intf_xml.extend(self._state_deleted(want, have))
intf_xml.extend(self._state_merged(want, have))
return intf_xml
def _state_overridden(self, want, have):
""" The command generator when state is overridden
:rtype: A list
:returns: the xml necessary to migrate the current configuration
to the desired configuration
"""
intf_xml = []
intf_xml.extend(self._state_deleted(have, have))
intf_xml.extend(self._state_merged(want, have))
return intf_xml
def _state_merged(self, want, have):
""" The command generator when state is merged
:rtype: A list
:returns: the xml necessary to merge the provided into
the current configuration
"""
intf_xml = []
for config in want:
vlan_name = str(config['name'])
vlan_id = str(config['vlan_id'])
vlan_description = config.get('description')
vlan_root = build_root_xml_node('vlan')
build_child_xml_node(vlan_root, 'name', vlan_name)
build_child_xml_node(vlan_root, 'vlan-id', vlan_id)
if vlan_description:
build_child_xml_node(vlan_root, 'description',
vlan_description)
intf_xml.append(vlan_root)
return intf_xml
def _state_deleted(self, want, have):
""" The command generator when state is deleted
:rtype: A list
:returns: the xml necessary to remove the current configuration
of the provided objects
"""
intf_xml = []
if not want:
want = have
for config in want:
vlan_name = config['name']
vlan_root = build_root_xml_node('vlan')
vlan_root.attrib.update({'delete': 'delete'})
build_child_xml_node(vlan_root, 'name', vlan_name)
intf_xml.append(vlan_root)
return intf_xml

@ -15,6 +15,7 @@ from ansible.module_utils.network.junos.facts.legacy.base import Default, Hardwa
from ansible.module_utils.network.junos.facts.interfaces.interfaces import InterfacesFacts
from ansible.module_utils.network.junos.facts.lag_interfaces.lag_interfaces import Lag_interfacesFacts
from ansible.module_utils.network.junos.facts.l3_interfaces.l3_interfaces import L3_interfacesFacts
from ansible.module_utils.network.junos.facts.vlans.vlans import VlansFacts
FACT_LEGACY_SUBSETS = dict(
default=Default,
@ -26,6 +27,7 @@ FACT_RESOURCE_SUBSETS = dict(
interfaces=InterfacesFacts,
lag_interfaces=Lag_interfacesFacts,
l3_interfaces=L3_interfacesFacts,
vlans=VlansFacts,
)

@ -0,0 +1,102 @@
#
# -*- 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 junos 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
from ansible.module_utils._text import to_bytes
from ansible.module_utils.network.common import utils
from ansible.module_utils.network.junos.argspec.vlans.vlans import VlansArgs
from ansible.module_utils.six import string_types
try:
from lxml import etree
HAS_LXML = True
except ImportError:
HAS_LXML = False
class VlansFacts(object):
""" The junos 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 HAS_LXML:
self._module.fail_json(msg='lxml is not installed.')
if not data:
config_filter = """
<configuration>
<vlans>
</vlans>
</configuration>
"""
data = connection.get_configuration(filter=config_filter)
if isinstance(data, string_types):
data = etree.fromstring(to_bytes(data,
errors='surrogate_then_replace'))
resources = data.xpath('configuration/vlans/vlan')
objs = []
for resource in resources:
if resource is not None:
obj = self.render_config(self.generated_spec, resource)
if obj:
objs.append(obj)
facts = {}
if 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):
"""
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['name'] = utils.get_xml_conf_arg(conf, 'name')
config['vlan_id'] = utils.get_xml_conf_arg(conf, 'vlan-id')
config['description'] = utils.get_xml_conf_arg(conf, 'description')
return utils.remove_empties(config)

@ -9,7 +9,7 @@ __metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'status': ['deprecated'],
'supported_by': 'network'}
DOCUMENTATION = """
@ -21,6 +21,10 @@ short_description: Manage VLANs on Juniper JUNOS network devices
description:
- This module provides declarative management of VLANs
on Juniper JUNOS network devices.
deprecated:
removed_in: "2.13"
why: Updated modules released with more functionality
alternative: Use M(junos_vlans) instead.
options:
name:
description:

@ -63,7 +63,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', 'interfaces', 'lag_interfaces', 'l3_interfaces']
choices: ['all', 'interfaces', 'lag_interfaces', 'l3_interfaces', 'vlans']
required: false
version_added: "2.9"
requirements:

@ -0,0 +1,278 @@
#!/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 junos_vlans
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'network'
}
DOCUMENTATION = """
---
module: junos_vlans
version_added: 2.9
short_description: Create and manage VLAN configurations on Junos OS
description: This module creates and manages VLAN configurations on Junos OS.
author: Daniel Mellado (@danielmellado)
requirements:
- ncclient (>=v0.6.4)
notes:
- This module requires the netconf system service be enabled on
the remote device being managed
- Tested against Junos OS 18.4R1
- This module works with connection C(netconf). See L(the Junos OS
Platform Options,../network/user_guide/platform_junos.html).
options:
config:
description: A dictionary of Vlan options
type: list
elements: dict
suboptions:
vlan_id:
description:
- IEEE 802.1q VLAN identifier for VLAN (1..4094).
type: int
required: true
name:
description:
- Name of VLAN.
type: str
required: true
description:
description:
- Text description of VLANs
type: str
state:
description:
- The state the configuration should be left in.
type: str
choices:
- merged
- replaced
- overridden
- deleted
default: merged
"""
EXAMPLES = """
# Using merged
#############
# Before State
# ------------
#
# admin# show vlans
# vlan-2 {
# vlan-id 2;
# }
# vlan-3 {
# vlan-id 3;
# }
- name: Merge JUNOS vlan
junos_vlans:
config:
- name: vlan-1
vlan-id: 1
state: merged
# After State
# -----------
#
# admin# show vlans
# vlan-1 {
# vlan-id 1;
# }
# vlan-2 {
# vlan-id 2;
# }
# vlan-3 {
# vlan-id 3;
# }
# Using replaced
################
# Before State
# ------------
#
# admin# show vlans
# vlan-1 {
# vlan-id 1;
# }
# vlan-2 {
# vlan-id 2;
# }
# vlan-3 {
# vlan-id 3;
# }
- name: Replace JUNOS vlan
junos_vlans:
config:
- name: vlan-1
vlan-id: 10
- name: vlan-3
vlan-id: 30
state: replaced
# After State
# -----------
#
# admin# show vlans
# vlan-1 {
# vlan-id 10;
# }
# vlan-2 {
# vlan-id 2;
# }
# vlan-3 {
# vlan-id 30;
# }
# Using overridden
##################
# Before State
# ------------
#
# admin# show vlans
# vlan-1 {
# vlan-id 1;
# }
# vlan-2 {
# vlan-id 2;
# }
# vlan-3 {
# vlan-id 3;
# }
- name: Override JUNOS vlan
junos_vlans:
config:
- name: vlan-4
vlan-id: 100
- name: vlan-2
vlan-id: 200
state: overridden
# After State
# -----------
#
# admin# show vlans
# vlan-2 {
# vlan-id 200;
# }
# vlan-4 {
# vlan-id 100;
# }
#Using deleted
##############
# Before State
# ------------
#
# admin# show vlans
# vlan-1 {
# vlan-id 1;
# }
# vlan-2 {
# vlan-id 2;
# }
# vlan-3 {
# vlan-id 3;
# }
- name: Delete JUNOS vlan
junos_vlans:
config:
- name: vlan-1
state: deleted
# After State
# -----------
#
# admin# show vlans
# vlan-2 {
# vlan-id 2;
# }
# vlan-3 {
# vlan-id 3;
# }
"""
RETURN = """
before:
description: The configuration prior to the model invocation.
returned: always
type: str
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: str
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: ['xml 1', 'xml 2', 'xml 3']
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.junos.argspec.vlans.vlans import VlansArgs
from ansible.module_utils.network.junos.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()

@ -0,0 +1,3 @@
---
testcase: "[^_].*"
test_items: []

@ -0,0 +1,2 @@
dependencies:
# - prepare_junos_tests

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

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

@ -0,0 +1,12 @@
---
- debug:
msg: "Start junos_vlans base config ansible_connection={{ ansible_connection }}"
- name: Configure base vlans
junos_config:
lines:
- set vlans vlan1 vlan-id 1
- set vlans vlan2 vlan-id 2
- debug:
msg: "End junos_vlans base config ansible_connection={{ ansible_connection }}"

@ -0,0 +1,12 @@
---
- debug:
msg: "Start junos_vlans teardown ansible_connection={{ ansible_connection }}"
- name: Remove interface config
junos_config:
lines:
- delete vlan vlan1
- delete vlan vlan2
- debug:
msg: "End junos_vlans teardown ansible_connection={{ ansible_connection }}"

@ -0,0 +1,33 @@
---
- debug:
msg: "START junos_vlans deleted integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _base_config.yaml
- block:
- name: Delete the provided configuration with the exisiting running configuration
junos_vlans: &deleted
config:
state: deleted
register: result
- name: Assert the configuration is reflected on host
assert:
that:
- not result.after
- name: Delete the provided configuration with the existing running configuration (IDEMPOTENT)
junos_vlans: *deleted
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result['changed'] == false"
always:
- include_tasks: _remove_config.yaml
- debug:
msg: "END junos_vlans deleted integration tests on connection={{ ansible_connection }}"

@ -0,0 +1,43 @@
---
- debug:
msg: "START junos_vlans merged integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- set_fact:
expected_merged_output:
- name: vlan1
vlan_id: 1
- name: vlan2
vlan_id: 2
- block:
- name: Merge the provided configuration with the exisiting running configuration
junos_vlans: &merged
config:
- name: vlan1
vlan_id: 1
- name: vlan2
vlan_id: 2
state: merged
register: result
- name: Assert the configuration is reflected on host
assert:
that:
- "{{ expected_merged_output | symmetric_difference(result['after']) |length == 0 }}"
- name: Merge the provided configuration with the existing running configuration (IDEMPOTENT)
junos_vlans: *merged
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result['changed'] == false"
always:
- include_tasks: _remove_config.yaml
- debug:
msg: "END junos_vlans merged integration tests on connection={{ ansible_connection }}"

@ -0,0 +1,40 @@
---
- debug:
msg: "START junos_vlans overridden integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _base_config.yaml
- set_fact:
expected_overridden_output:
- name: vlan1
vlan_id: 100
- block:
- name: Override the provided configuration with the exisiting running configuration
junos_vlans: &overridden
config:
- name: vlan1
vlan_id: 100
state: overridden
register: result
- name: Assert the configuration is reflected on host
assert:
that:
- "{{ expected_overridden_output | symmetric_difference(result['after']) |length == 0 }}"
- name: Override the provided configuration with the existing running configuration (IDEMPOTENT)
junos_vlans: *overridden
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result['changed'] == false"
always:
- include_tasks: _remove_config.yaml
- debug:
msg: "END junos_vlans overridden integration tests on connection={{ ansible_connection }}"

@ -0,0 +1,42 @@
---
- debug:
msg: "START junos_vlans replaced integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _base_config.yaml
- set_fact:
expected_replaced_output:
- name: vlan1
vlan_id: 10
- name: vlan2
vlan_id: 2
- block:
- name: Replace the provided configuration with the exisiting running configuration
junos_vlans: &replaced
config:
- name: vlan1
vlan_id: 10
state: replaced
register: result
- name: Assert the configuration is reflected on host
assert:
that:
- "{{ expected_replaced_output | symmetric_difference(result['after']) |length == 0 }}"
- name: Replace the provided configuration with the existing running configuration (IDEMPOTENT)
junos_vlans: *replaced
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result['changed'] == false"
always:
- include_tasks: _remove_config.yaml
- debug:
msg: "END junos_vlans replaced integration tests on connection={{ ansible_connection }}"

@ -4931,12 +4931,12 @@ lib/ansible/modules/network/junos/junos_user.py validate-modules:E326
lib/ansible/modules/network/junos/junos_user.py validate-modules:E337
lib/ansible/modules/network/junos/junos_user.py validate-modules:E338
lib/ansible/modules/network/junos/junos_user.py validate-modules:E340
lib/ansible/modules/network/junos/junos_vlan.py validate-modules:E322
lib/ansible/modules/network/junos/junos_vlan.py validate-modules:E324
lib/ansible/modules/network/junos/junos_vlan.py validate-modules:E326
lib/ansible/modules/network/junos/junos_vlan.py validate-modules:E337
lib/ansible/modules/network/junos/junos_vlan.py validate-modules:E338
lib/ansible/modules/network/junos/junos_vlan.py validate-modules:E340
lib/ansible/modules/network/junos/_junos_vlan.py validate-modules:E322
lib/ansible/modules/network/junos/_junos_vlan.py validate-modules:E324
lib/ansible/modules/network/junos/_junos_vlan.py validate-modules:E326
lib/ansible/modules/network/junos/_junos_vlan.py validate-modules:E337
lib/ansible/modules/network/junos/_junos_vlan.py validate-modules:E338
lib/ansible/modules/network/junos/_junos_vlan.py validate-modules:E340
lib/ansible/modules/network/junos/junos_vrf.py validate-modules:E322
lib/ansible/modules/network/junos/junos_vrf.py validate-modules:E324
lib/ansible/modules/network/junos/junos_vrf.py validate-modules:E326

Loading…
Cancel
Save