new module vmware_dvswitch_pvlans (#48855)

This commit is contained in:
Christian Kotte 2018-12-12 10:13:54 +01:00 committed by John R Barker
parent b67719ba1d
commit 1b9b0b85c4
3 changed files with 667 additions and 0 deletions

View file

@ -0,0 +1,533 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright: (c) 2018, Christian Kotte <christian.kotte@gmx.de>
#
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: vmware_dvswitch_pvlans
short_description: Manage Private VLAN configuration of a Distributed Switch
description:
- This module can be used to configure Private VLANs (PVLANs) on a Distributed Switch.
version_added: 2.8
author:
- Christian Kotte (@ckotte)
notes:
- Tested on vSphere 6.5 and 6.7
requirements:
- "python >= 2.6"
- PyVmomi
options:
switch:
description:
- The name of the Distributed Switch.
type: str
required: True
aliases: ['dvswitch']
primary_pvlans:
description:
- A list of VLAN IDs that should be configured as Primary PVLANs.
- If C(primary_pvlans) isn't specified, all PVLANs will be deleted if present.
- Each member of the list requires primary_pvlan_id (int) set.
- The secondary promiscuous PVLAN will be created automatically.
- If C(secondary_pvlans) isn't specified, the primary PVLANs and each secondary promiscuous PVLAN will be created.
- Please see examples for more information.
type: list
default: []
secondary_pvlans:
description:
- A list of VLAN IDs that should be configured as Secondary PVLANs.
- 'C(primary_pvlans) need to be specified to create any Secodary PVLAN.'
- If C(primary_pvlans) isn't specified, all PVLANs will be deleted if present.
- Each member of the list requires primary_pvlan_id (int), secondary_pvlan_id (int), and pvlan_type (str) to be set.
- The type of the secondary PVLAN can be isolated or community. The secondary promiscuous PVLAN will be created automatically.
- Please see examples for more information.
type: list
default: []
extends_documentation_fragment: vmware.documentation
'''
EXAMPLES = '''
- name: Create PVLANs on a Distributed Switch
vmware_dvswitch_pvlans:
hostname: '{{ inventory_hostname }}'
username: '{{ vcenter_username }}'
password: '{{ vcenter_password }}'
switch: dvSwitch
primary_pvlans:
- primary_pvlan_id: 1
- primary_pvlan_id: 4
secondary_pvlans:
- primary_pvlan_id: 1
secondary_pvlan_id: 2
pvlan_type: isolated
- primary_pvlan_id: 1
secondary_pvlan_id: 3
pvlan_type: community
- primary_pvlan_id: 4
secondary_pvlan_id: 5
pvlan_type: community
delegate_to: localhost
- name: Create primary PVLAN and secondary promiscuous PVLAN on a Distributed Switch
vmware_dvswitch_pvlans:
hostname: '{{ inventory_hostname }}'
username: '{{ vcenter_username }}'
password: '{{ vcenter_password }}'
switch: dvSwitch
primary_pvlans:
- primary_pvlan_id: 1
delegate_to: localhost
- name: Remove all PVLANs from a Distributed Switch
vmware_dvswitch_pvlans:
hostname: '{{ inventory_hostname }}'
username: '{{ vcenter_username }}'
password: '{{ vcenter_password }}'
switch: dvSwitch
primary_pvlans: []
secondary_pvlans: []
delegate_to: localhost
'''
RETURN = """
result:
description: information about performed operation
returned: always
type: string
sample: {
"changed": true,
"dvswitch": "dvSwitch",
"private_vlans": [
{
"primary_pvlan_id": 1,
"pvlan_type": "promiscuous",
"secondary_pvlan_id": 1
},
{
"primary_pvlan_id": 1,
"pvlan_type": "isolated",
"secondary_pvlan_id": 2
},
{
"primary_pvlan_id": 1,
"pvlan_type": "community",
"secondary_pvlan_id": 3
}
],
"private_vlans_previous": [],
"result": "All private VLANs added"
}
"""
try:
from pyVmomi import vim
except ImportError:
pass
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible.module_utils.vmware import (
PyVmomi, TaskError, find_dvs_by_name, vmware_argument_spec, wait_for_task
)
class VMwareDvSwitchPvlans(PyVmomi):
"""Class to manage Private VLANs on a Distributed Virtual Switch"""
def __init__(self, module):
super(VMwareDvSwitchPvlans, self).__init__(module)
self.switch_name = self.module.params['switch']
if self.module.params['primary_pvlans']:
self.primary_pvlans = self.module.params['primary_pvlans']
if self.module.params['secondary_pvlans']:
self.secondary_pvlans = self.module.params['secondary_pvlans']
else:
self.secondary_pvlans = None
self.do_pvlan_sanity_checks()
else:
self.primary_pvlans = None
self.secondary_pvlans = None
self.dvs = find_dvs_by_name(self.content, self.switch_name)
if self.dvs is None:
self.module.fail_json(msg="Failed to find DVS %s" % self.switch_name)
def do_pvlan_sanity_checks(self):
"""Do sanity checks for primary and secondary PVLANs"""
# Check if primary PVLANs are unique
for primary_vlan in self.primary_pvlans:
count = 0
primary_pvlan_id = self.get_primary_pvlan_option(primary_vlan)
for primary_vlan_2 in self.primary_pvlans:
primary_pvlan_id_2 = self.get_primary_pvlan_option(primary_vlan_2)
if primary_pvlan_id == primary_pvlan_id_2:
count += 1
if count > 1:
self.module.fail_json(
msg="The primary PVLAN ID '%s' must be unique!" % primary_pvlan_id
)
if self.secondary_pvlans:
# Check if secondary PVLANs are unique
for secondary_pvlan in self.secondary_pvlans:
count = 0
result = self.get_secondary_pvlan_options(secondary_pvlan)
for secondary_pvlan_2 in self.secondary_pvlans:
result_2 = self.get_secondary_pvlan_options(secondary_pvlan_2)
if result[0] == result_2[0]:
count += 1
if count > 1:
self.module.fail_json(
msg="The secondary PVLAN ID '%s' must be unique!" % result[0]
)
# Check if secondary PVLANs are already used as primary PVLANs
for primary_vlan in self.primary_pvlans:
primary_pvlan_id = self.get_primary_pvlan_option(primary_vlan)
for secondary_pvlan in self.secondary_pvlans:
result = self.get_secondary_pvlan_options(secondary_pvlan)
if primary_pvlan_id == result[0]:
self.module.fail_json(
msg="The secondary PVLAN ID '%s' is already used as a primary PVLAN!" %
result[0]
)
# Check if a primary PVLAN is present for every secondary PVLANs
for secondary_pvlan in self.secondary_pvlans:
primary_pvlan_found = False
result = self.get_secondary_pvlan_options(secondary_pvlan)
for primary_vlan in self.primary_pvlans:
primary_pvlan_id = self.get_primary_pvlan_option(primary_vlan)
if result[1] == primary_pvlan_id:
primary_pvlan_found = True
break
if not primary_pvlan_found:
self.module.fail_json(
msg="The primary PVLAN ID '%s' isn't defined for the secondary PVLAN ID '%s'!" %
(result[1], result[0])
)
def ensure(self):
"""Manage Private VLANs"""
changed = False
results = dict(changed=changed)
results['dvswitch'] = self.switch_name
changed_list_add = []
changed_list_remove = []
config_spec = vim.dvs.VmwareDistributedVirtualSwitch.ConfigSpec()
# Use the same version in the new spec; The version will be increased by one by the API automatically
config_spec.configVersion = self.dvs.config.configVersion
# Check Private VLANs
results['private_vlans'] = None
if self.primary_pvlans:
desired_pvlan_list = []
for primary_vlan in self.primary_pvlans:
primary_pvlan_id = self.get_primary_pvlan_option(primary_vlan)
temp_pvlan = dict()
temp_pvlan['primary_pvlan_id'] = primary_pvlan_id
temp_pvlan['secondary_pvlan_id'] = primary_pvlan_id
temp_pvlan['pvlan_type'] = 'promiscuous'
desired_pvlan_list.append(temp_pvlan)
if self.secondary_pvlans:
for secondary_pvlan in self.secondary_pvlans:
(secondary_pvlan_id,
secondary_vlan_primary_vlan_id,
pvlan_type) = self.get_secondary_pvlan_options(secondary_pvlan)
temp_pvlan = dict()
temp_pvlan['primary_pvlan_id'] = secondary_vlan_primary_vlan_id
temp_pvlan['secondary_pvlan_id'] = secondary_pvlan_id
temp_pvlan['pvlan_type'] = pvlan_type
desired_pvlan_list.append(temp_pvlan)
results['private_vlans'] = desired_pvlan_list
if self.dvs.config.pvlanConfig:
pvlan_spec_list = []
# Check if desired PVLANs are configured
for primary_vlan in self.primary_pvlans:
primary_pvlan_id = self.get_primary_pvlan_option(primary_vlan)
promiscuous_found = other_found = False
for pvlan_object in self.dvs.config.pvlanConfig:
if pvlan_object.primaryVlanId == primary_pvlan_id and pvlan_object.pvlanType == 'promiscuous':
promiscuous_found = True
break
if not promiscuous_found:
changed = True
changed_list_add.append('promiscuous (%s, %s)' % (primary_pvlan_id, primary_pvlan_id))
pvlan_spec_list.append(
self.create_pvlan_config_spec(
operation='add',
primary_pvlan_id=primary_pvlan_id,
secondary_pvlan_id=primary_pvlan_id,
pvlan_type='promiscuous'
)
)
if self.secondary_pvlans:
for secondary_pvlan in self.secondary_pvlans:
(secondary_pvlan_id,
secondary_vlan_primary_vlan_id,
pvlan_type) = self.get_secondary_pvlan_options(secondary_pvlan)
if primary_pvlan_id == secondary_vlan_primary_vlan_id:
for pvlan_object_2 in self.dvs.config.pvlanConfig:
if (pvlan_object_2.primaryVlanId == secondary_vlan_primary_vlan_id
and pvlan_object_2.secondaryVlanId == secondary_pvlan_id
and pvlan_object_2.pvlanType == pvlan_type):
other_found = True
break
if not other_found:
changed = True
changed_list_add.append(
'%s (%s, %s)' % (pvlan_type, primary_pvlan_id, secondary_pvlan_id)
)
pvlan_spec_list.append(
self.create_pvlan_config_spec(
operation='add',
primary_pvlan_id=primary_pvlan_id,
secondary_pvlan_id=secondary_pvlan_id,
pvlan_type=pvlan_type
)
)
# Check if a PVLAN needs to be removed
for pvlan_object in self.dvs.config.pvlanConfig:
promiscuous_found = other_found = False
if (pvlan_object.primaryVlanId == pvlan_object.secondaryVlanId
and pvlan_object.pvlanType == 'promiscuous'):
for primary_vlan in self.primary_pvlans:
primary_pvlan_id = self.get_primary_pvlan_option(primary_vlan)
if pvlan_object.primaryVlanId == primary_pvlan_id and pvlan_object.pvlanType == 'promiscuous':
promiscuous_found = True
break
if not promiscuous_found:
changed = True
changed_list_remove.append(
'promiscuous (%s, %s)' % (pvlan_object.primaryVlanId, pvlan_object.secondaryVlanId)
)
pvlan_spec_list.append(
self.create_pvlan_config_spec(
operation='remove',
primary_pvlan_id=pvlan_object.primaryVlanId,
secondary_pvlan_id=pvlan_object.secondaryVlanId,
pvlan_type='promiscuous'
)
)
elif self.secondary_pvlans:
for secondary_pvlan in self.secondary_pvlans:
(secondary_pvlan_id,
secondary_vlan_primary_vlan_id,
pvlan_type) = self.get_secondary_pvlan_options(secondary_pvlan)
if (pvlan_object.primaryVlanId == secondary_vlan_primary_vlan_id
and pvlan_object.secondaryVlanId == secondary_pvlan_id
and pvlan_object.pvlanType == pvlan_type):
other_found = True
break
if not other_found:
changed = True
changed_list_remove.append(
'%s (%s, %s)' % (
pvlan_object.pvlanType, pvlan_object.primaryVlanId, pvlan_object.secondaryVlanId
)
)
pvlan_spec_list.append(
self.create_pvlan_config_spec(
operation='remove',
primary_pvlan_id=pvlan_object.primaryVlanId,
secondary_pvlan_id=pvlan_object.secondaryVlanId,
pvlan_type=pvlan_object.pvlanType
)
)
else:
changed = True
changed_list_remove.append(
'%s (%s, %s)' % (
pvlan_object.pvlanType, pvlan_object.primaryVlanId, pvlan_object.secondaryVlanId
)
)
pvlan_spec_list.append(
self.create_pvlan_config_spec(
operation='remove',
primary_pvlan_id=pvlan_object.primaryVlanId,
secondary_pvlan_id=pvlan_object.secondaryVlanId,
pvlan_type=pvlan_object.pvlanType
)
)
else:
changed = True
changed_list_add.append('All private VLANs')
pvlan_spec_list = []
for primary_vlan in self.primary_pvlans:
# the first secondary VLAN's type is always promiscuous
primary_pvlan_id = self.get_primary_pvlan_option(primary_vlan)
pvlan_spec_list.append(
self.create_pvlan_config_spec(
operation='add',
primary_pvlan_id=primary_pvlan_id,
secondary_pvlan_id=primary_pvlan_id,
pvlan_type='promiscuous'
)
)
if self.secondary_pvlans:
for secondary_pvlan in self.secondary_pvlans:
(secondary_pvlan_id,
secondary_vlan_primary_vlan_id,
pvlan_type) = self.get_secondary_pvlan_options(secondary_pvlan)
if primary_pvlan_id == secondary_vlan_primary_vlan_id:
pvlan_spec_list.append(
self.create_pvlan_config_spec(
operation='add',
primary_pvlan_id=primary_pvlan_id,
secondary_pvlan_id=secondary_pvlan_id,
pvlan_type=pvlan_type
)
)
else:
# Remove PVLAN configuration if present
if self.dvs.config.pvlanConfig:
changed = True
changed_list_remove.append('All private VLANs')
pvlan_spec_list = []
for pvlan_object in self.dvs.config.pvlanConfig:
pvlan_spec_list.append(
self.create_pvlan_config_spec(
operation='remove',
primary_pvlan_id=pvlan_object.primaryVlanId,
secondary_pvlan_id=pvlan_object.secondaryVlanId,
pvlan_type=pvlan_object.pvlanType
)
)
if changed:
message_add = message_remove = None
if changed_list_add:
message_add = self.build_change_message('add', changed_list_add)
if changed_list_remove:
message_remove = self.build_change_message('remove', changed_list_remove)
if message_add and message_remove:
message = message_add + '. ' + message_remove + '.'
elif message_add:
message = message_add
elif message_remove:
message = message_remove
current_pvlan_list = []
for pvlan_object in self.dvs.config.pvlanConfig:
temp_pvlan = dict()
temp_pvlan['primary_pvlan_id'] = pvlan_object.primaryVlanId
temp_pvlan['secondary_pvlan_id'] = pvlan_object.secondaryVlanId
temp_pvlan['pvlan_type'] = pvlan_object.pvlanType
current_pvlan_list.append(temp_pvlan)
results['private_vlans_previous'] = current_pvlan_list
config_spec.pvlanConfigSpec = pvlan_spec_list
if not self.module.check_mode:
try:
task = self.dvs.ReconfigureDvs_Task(config_spec)
wait_for_task(task)
except TaskError as invalid_argument:
self.module.fail_json(
msg="Failed to update DVS : %s" % to_native(invalid_argument)
)
else:
message = "PVLANs already configured properly"
results['changed'] = changed
results['result'] = message
self.module.exit_json(**results)
def get_primary_pvlan_option(self, primary_vlan):
"""Get Primary PVLAN option"""
primary_pvlan_id = primary_vlan.get('primary_pvlan_id', None)
if primary_pvlan_id is None:
self.module.fail_json(
msg="Please specify primary_pvlan_id in primary_pvlans options as it's a required parameter"
)
if primary_pvlan_id in (0, 4095):
self.module.fail_json(msg="The VLAN IDs of 0 and 4095 are reserved and cannot be used as a primary PVLAN.")
return primary_pvlan_id
def get_secondary_pvlan_options(self, secondary_pvlan):
"""Get Secondary PVLAN option"""
secondary_pvlan_id = secondary_pvlan.get('secondary_pvlan_id', None)
if secondary_pvlan_id is None:
self.module.fail_json(
msg="Please specify secondary_pvlan_id in secondary_pvlans options as it's a required parameter"
)
primary_pvlan_id = secondary_pvlan.get('primary_pvlan_id', None)
if primary_pvlan_id is None:
self.module.fail_json(
msg="Please specify primary_pvlan_id in secondary_pvlans options as it's a required parameter"
)
if secondary_pvlan_id in (0, 4095) or primary_pvlan_id in (0, 4095):
self.module.fail_json(
msg="The VLAN IDs of 0 and 4095 are reserved and cannot be used as a primary or secondary PVLAN."
)
pvlan_type = secondary_pvlan.get('pvlan_type', None)
supported_pvlan_types = ['isolated', 'community']
if pvlan_type is None:
self.module.fail_json(msg="Please specify pvlan_type in secondary_pvlans options as it's a required parameter")
elif pvlan_type not in supported_pvlan_types:
self.module.fail_json(msg="The specified PVLAN type '%s' isn't supported!" % pvlan_type)
return secondary_pvlan_id, primary_pvlan_id, pvlan_type
@staticmethod
def create_pvlan_config_spec(operation, primary_pvlan_id, secondary_pvlan_id, pvlan_type):
"""
Create PVLAN config spec
operation: add, edit, or remove
Returns: PVLAN config spec
"""
pvlan_spec = vim.dvs.VmwareDistributedVirtualSwitch.PvlanConfigSpec()
pvlan_spec.operation = operation
pvlan_spec.pvlanEntry = vim.dvs.VmwareDistributedVirtualSwitch.PvlanMapEntry()
pvlan_spec.pvlanEntry.primaryVlanId = primary_pvlan_id
pvlan_spec.pvlanEntry.secondaryVlanId = secondary_pvlan_id
pvlan_spec.pvlanEntry.pvlanType = pvlan_type
return pvlan_spec
def build_change_message(self, operation, changed_list):
"""Build the changed message"""
if operation == 'add':
changed_operation = 'added'
elif operation == 'remove':
changed_operation = 'removed'
if self.module.check_mode:
changed_suffix = ' would be %s' % changed_operation
else:
changed_suffix = ' %s' % changed_operation
if len(changed_list) > 2:
message = ', '.join(changed_list[:-1]) + ', and ' + str(changed_list[-1])
elif len(changed_list) == 2:
message = ' and '.join(changed_list)
elif len(changed_list) == 1:
message = changed_list[0]
message += changed_suffix
return message
def main():
"""Main"""
argument_spec = vmware_argument_spec()
argument_spec.update(
dict(
switch=dict(required=True, aliases=['dvswitch']),
primary_pvlans=dict(type='list', default=list(), required=False),
secondary_pvlans=dict(type='list', default=list(), required=False),
)
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
vmware_dvswitch_pvlans = VMwareDvSwitchPvlans(module)
vmware_dvswitch_pvlans.ensure()
if __name__ == '__main__':
main()

View file

@ -0,0 +1,2 @@
cloud/vcenter
unsupported

View file

@ -0,0 +1,132 @@
# Test code for the vmware_dvswitch_pvlans module.
# Copyright: (c) 2018, Christian Kotte <christian.kotte@gmx.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
- name: store the vcenter container ip
set_fact:
vcsim: "{{ lookup('env', 'vcenter_host') }}"
- debug: var=vcsim
- name: Wait for Flask controller to come up online
wait_for:
host: "{{ vcsim }}"
port: 5000
state: started
- name: kill vcsim
uri:
url: http://{{ vcsim }}:5000/killall
- name: start vcsim
uri:
url: http://{{ vcsim }}:5000/spawn?cluster=2
register: vcsim_instance
- name: Wait for vcsim server to come up online
wait_for:
host: "{{ vcsim }}"
port: 443
state: started
- name: get a list of Datacenter from vcsim
uri:
url: http://{{ vcsim }}:5000/govc_find?filter=DC
register: datacenters
- debug: var=vcsim_instance
- debug: var=datacenters
# Testcase 0001: Add Distributed vSwitch
- name: add distributed vSwitch
vmware_dvswitch:
validate_certs: False
hostname: "{{ vcsim }}"
username: "{{ vcsim_instance['json']['username'] }}"
password: "{{ vcsim_instance['json']['password'] }}"
datacenter_name: "{{ item | basename }}"
state: present
switch_name: dvswitch_0001
mtu: 9000
uplink_quantity: 2
discovery_proto: lldp
discovery_operation: both
register: dvs_result_0001
with_items:
- "{{ datacenters['json'] }}"
- name: ensure distributed vswitch is present
assert:
that:
- "{{ dvs_result_0001.changed == true }}"
- name: get a list of distributed vswitch from vcsim after adding
uri:
url: http://{{ vcsim }}:5000/govc_find?filter=DVS
register: new_dvs_0001
- debug:
msg: "{{ item | basename }}"
with_items: "{{ new_dvs_0001['json'] }}"
- set_fact: new_dvs_name="{% for dvs in new_dvs_0001['json'] %} {{ True if (dvs | basename) == 'dvswitch_0001' else False }}{% endfor %}"
- debug: var=new_dvs_name
- assert:
that:
- "{{ 'True' in new_dvs_name }}"
- name: Configure PVLANs in check mode
vmware_dvswitch_pvlans:
hostname: "{{ vcsim }}"
username: "{{ vcsim_instance['json']['username'] }}"
password: "{{ vcsim_instance['json']['password'] }}"
switch: dvswitch_0001
primary_pvlans:
- primary_pvlan_id: 1
- primary_pvlan_id: 4
secondary_pvlans:
- primary_pvlan_id: 1
secondary_pvlan_id: 2
pvlan_type: isolated
- primary_pvlan_id: 1
secondary_pvlan_id: 3
pvlan_type: community
- primary_pvlan_id: 4
secondary_pvlan_id: 5
pvlan_type: community
validate_certs: no
register: pvlans_result_check_mode
check_mode: yes
- name: ensure pvlans were configured
assert:
that:
- pvlans_result_check_mode.changed
- name: Configure PVLANs
vmware_dvswitch_pvlans:
hostname: "{{ vcsim }}"
username: "{{ vcsim_instance['json']['username'] }}"
password: "{{ vcsim_instance['json']['password'] }}"
switch: dvswitch_0001
primary_pvlans:
- primary_pvlan_id: 1
- primary_pvlan_id: 4
secondary_pvlans:
- primary_pvlan_id: 1
secondary_pvlan_id: 2
pvlan_type: isolated
- primary_pvlan_id: 1
secondary_pvlan_id: 3
pvlan_type: community
- primary_pvlan_id: 4
secondary_pvlan_id: 5
pvlan_type: community
validate_certs: no
register: pvlans_result
- name: ensure pvlans were configured
assert:
that:
- pvlans_result.changed