From 930fde5f7079601c5f174bd510ef434435343dc2 Mon Sep 17 00:00:00 2001
From: Philippe Dellaert <philippe@dellaert.org>
Date: Mon, 6 Nov 2017 20:46:51 +0400
Subject: [PATCH] vmware_dvs_portgroup: Add configuration of vlan trunk,
 security settings and port policies and integration tests (#32298)

* Add configuration of vlan trunk, security settings and port policies, and tests

This commit adds the following capabilities to the
vmware_dvs_portgroup module:
- Support for VLAN trunk portgroup
- Support for all security settings (promiscuous, forged transmits & mac
address changes)
- Support for all the port specific policies
- port specific policies match the vCenter UI behaviour (for instance:
block override is enabled by default)
- Cleanup and use of proper API entities not root entities
- Integration testing

* Cleanup of docs and adding more examples
---
 .../cloud/vmware/vmware_dvs_portgroup.py      | 235 +++++++++++++++--
 .../targets/vmware_dvs_portgroup/aliases      |   3 +
 .../vmware_dvs_portgroup/tasks/main.yml       | 244 ++++++++++++++++++
 3 files changed, 456 insertions(+), 26 deletions(-)
 create mode 100644 test/integration/targets/vmware_dvs_portgroup/aliases
 create mode 100644 test/integration/targets/vmware_dvs_portgroup/tasks/main.yml

diff --git a/lib/ansible/modules/cloud/vmware/vmware_dvs_portgroup.py b/lib/ansible/modules/cloud/vmware/vmware_dvs_portgroup.py
index 54c2094ec19..b2832e700a5 100644
--- a/lib/ansible/modules/cloud/vmware/vmware_dvs_portgroup.py
+++ b/lib/ansible/modules/cloud/vmware/vmware_dvs_portgroup.py
@@ -17,20 +17,23 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
 DOCUMENTATION = '''
 ---
 module: vmware_dvs_portgroup
-short_description: Create or remove a Distributed vSwitch portgroup
+short_description: Create or remove a Distributed vSwitch portgroup.
 description:
-    - Create or remove a Distributed vSwitch portgroup
+    - Create or remove a Distributed vSwitch portgroup.
 version_added: 2.0
-author: "Joseph Callen (@jcpowermac)"
+author:
+    - Joseph Callen (@jcpowermac)
+    - Philippe Dellaert (@pdellaert) <philippe@dellaert.org>
 notes:
     - Tested on vSphere 5.5
+    - Tested on vSphere 6.5
 requirements:
     - "python >= 2.6"
     - PyVmomi
 options:
     portgroup_name:
         description:
-            - The name of the portgroup that is to be created or deleted
+            - The name of the portgroup that is to be created or deleted.
         required: True
     switch_name:
         description:
@@ -38,36 +41,133 @@ options:
         required: True
     vlan_id:
         description:
-            - The VLAN ID that should be configured with the portgroup
+            - The VLAN ID that should be configured with the portgroup, use 0 for no VLAN.
+            - 'If C(vlan_trunk) is configured to be I(true), this can be a range, example: 1-4094.'
         required: True
     num_ports:
         description:
-            - The number of ports the portgroup should contain
+            - The number of ports the portgroup should contain.
         required: True
     portgroup_type:
         description:
-            - See VMware KB 1022312 regarding portgroup types
+            - See VMware KB 1022312 regarding portgroup types.
         required: True
         choices:
             - 'earlyBinding'
             - 'lateBinding'
             - 'ephemeral'
+    state:
+        description:
+            - Determines if the portgroup should be present or not.
+        required: True
+        choices:
+            - 'present'
+            - 'absent'
+        version_added: '2.5'
+    vlan_trunk:
+        description:
+            - Indicates whether this is a VLAN trunk or not.
+        required: False
+        default: False
+        version_added: '2.5'
+    security:
+        description:
+            - Dict which configures the different security values for portgroup.
+            - 'Valid attributes are:'
+            - '- C(promiscuous) (bool): indicates whether promiscuous mode is allowed. (default: false)'
+            - '- C(forged_transmits) (bool): indicates whether forged transmits are allowed. (default: false)'
+            - '- C(mac_changes) (bool): indicates whether mac changes are allowed. (default: false)'
+        required: False
+        version_added: '2.5'
+    port_policy:
+        description:
+            - Dict which configures the advanced policy settings for the portgroup.
+            - 'Valid attributes are:'
+            - '- C(block_override) (bool): indicates if the block policy can be changed per port. (default: true)'
+            - '- C(ipfix_override) (bool): indicates if the ipfix policy can be changed per port. (default: false)'
+            - '- C(live_port_move) (bool): indicates if a live port can be moved in or out of the portgroup. (default: false)'
+            - '- C(network_rp_override) (bool): indicates if the network resource pool can be changed per port. (default: false)'
+            - '- C(port_config_reset_at_disconnect) (bool): indicates if the configuration of a port is reset automatically after disconnect. (default: true)'
+            - '- C(security_override) (bool): indicates if the security policy can be changed per port. (default: false)'
+            - '- C(shaping_override) (bool): indicates if the shaping policy can be changed per port. (default: false)'
+            - '- C(traffic_filter_override) (bool): indicates if the traffic filter can be changed per port. (default: false)'
+            - '- C(uplink_teaming_override) (bool): indicates if the uplink teaming policy can be changed per port. (default: false)'
+            - '- C(vendor_config_override) (bool): indicates if the vendor config can be changed per port. (default: false)'
+            - '- C(vlan_override) (bool): indicates if the vlan can be changed per port. (default: false)'
+        required: False
+        version_added: '2.5'
 extends_documentation_fragment: vmware.documentation
 '''
 
 EXAMPLES = '''
-   - name: Create Management portgroup
-     local_action:
-        module: vmware_dvs_portgroup
+   - name: Create vlan portgroup
+     connection: local
+     vmware_dvs_portgroup:
         hostname: vcenter_ip_or_hostname
         username: vcenter_username
         password: vcenter_password
-        portgroup_name: Management
+        portgroup_name: vlan-123-portrgoup
         switch_name: dvSwitch
         vlan_id: 123
         num_ports: 120
         portgroup_type: earlyBinding
         state: present
+
+   - name: Create vlan trunk portgroup
+     connection: local
+     vmware_dvs_portgroup:
+        hostname: vcenter_ip_or_hostname
+        username: vcenter_username
+        password: vcenter_password
+        portgroup_name: vlan-trunk-portrgoup
+        switch_name: dvSwitch
+        vlan_id: 1-1000
+        vlan_trunk: True
+        num_ports: 120
+        portgroup_type: earlyBinding
+        state: present
+
+   - name: Create no-vlan portgroup
+     connection: local
+     vmware_dvs_portgroup:
+        hostname: vcenter_ip_or_hostname
+        username: vcenter_username
+        password: vcenter_password
+        portgroup_name: no-vlan-portrgoup
+        switch_name: dvSwitch
+        vlan_id: 0
+        num_ports: 120
+        portgroup_type: earlyBinding
+        state: present
+
+   - name: Create vlan portgroup with all security and port policies
+     connection: local
+     vmware_dvs_portgroup:
+        hostname: vcenter_ip_or_hostname
+        username: vcenter_username
+        password: vcenter_password
+        portgroup_name: vlan-123-portrgoup
+        switch_name: dvSwitch
+        vlan_id: 123
+        num_ports: 120
+        portgroup_type: earlyBinding
+        state: present
+        security:
+          promiscuous: yes
+          forged_transmits: yes
+          mac_changes: yes
+        port_policy:
+          block_override: yes
+          ipfix_override: yes
+          live_port_move: yes
+          network_rp_override: yes
+          port_config_reset_at_disconnect: yes
+          security_override: yes
+          shaping_override: yes
+          traffic_filter_override: yes
+          uplink_teaming_override: yes
+          vendor_config_override: yes
+          vlan_override: yes
 '''
 
 try:
@@ -92,6 +192,21 @@ class VMwareDvsPortgroup(object):
         self.portgroup_type = self.module.params['portgroup_type']
         self.dv_switch = None
         self.state = self.module.params['state']
+        self.vlan_trunk = self.module.params['vlan_trunk']
+        self.security_promiscuous = self.module.params['security']['promiscuous']
+        self.security_forged_transmits = self.module.params['security']['forged_transmits']
+        self.security_mac_changes = self.module.params['security']['mac_changes']
+        self.policy_block_override = self.module.params['port_policy']['block_override']
+        self.policy_ipfix_override = self.module.params['port_policy']['ipfix_override']
+        self.policy_live_port_move = self.module.params['port_policy']['live_port_move']
+        self.policy_network_rp_override = self.module.params['port_policy']['network_rp_override']
+        self.policy_port_config_reset_at_disconnect = self.module.params['port_policy']['port_config_reset_at_disconnect']
+        self.policy_security_override = self.module.params['port_policy']['security_override']
+        self.policy_shaping_override = self.module.params['port_policy']['shaping_override']
+        self.policy_traffic_filter_override = self.module.params['port_policy']['traffic_filter_override']
+        self.policy_uplink_teaming_override = self.module.params['port_policy']['uplink_teaming_override']
+        self.policy_vendor_config_override = self.module.params['port_policy']['vendor_config_override']
+        self.policy_vlan_override = self.module.params['port_policy']['vlan_override']
         self.content = connect_to_api(module)
 
     def process_state(self):
@@ -118,19 +233,40 @@ class VMwareDvsPortgroup(object):
     def create_port_group(self):
         config = vim.dvs.DistributedVirtualPortgroup.ConfigSpec()
 
+        # Basic config
         config.name = self.portgroup_name
         config.numPorts = self.num_ports
 
-        # vim.VMwareDVSPortSetting() does not exist in the pyvmomi documentation
-        # but this is the correct managed object type.
-
-        config.defaultPortConfig = vim.VMwareDVSPortSetting()
-
-        # vim.VmwareDistributedVirtualSwitchVlanIdSpec() does not exist in the
-        # pyvmomi documentation but this is the correct managed object type
-        config.defaultPortConfig.vlan = vim.VmwareDistributedVirtualSwitchVlanIdSpec()
+        # Default port config
+        config.defaultPortConfig = vim.dvs.VmwareDistributedVirtualSwitch.VmwarePortConfigPolicy()
+        if self.vlan_trunk:
+            config.defaultPortConfig.vlan = vim.dvs.VmwareDistributedVirtualSwitch.TrunkVlanSpec()
+            vlan_id_start, vlan_id_end = self.vlan_id.split('-')
+            config.defaultPortConfig.vlan.vlanId = [vim.NumericRange(start=int(vlan_id_start.strip()), end=int(vlan_id_end.strip()))]
+        else:
+            config.defaultPortConfig.vlan = vim.dvs.VmwareDistributedVirtualSwitch.VlanIdSpec()
+            config.defaultPortConfig.vlan.vlanId = int(self.vlan_id)
         config.defaultPortConfig.vlan.inherited = False
-        config.defaultPortConfig.vlan.vlanId = self.vlan_id
+        config.defaultPortConfig.securityPolicy = vim.dvs.VmwareDistributedVirtualSwitch.SecurityPolicy()
+        config.defaultPortConfig.securityPolicy.allowPromiscuous = vim.BoolPolicy(value=self.security_promiscuous)
+        config.defaultPortConfig.securityPolicy.forgedTransmits = vim.BoolPolicy(value=self.security_forged_transmits)
+        config.defaultPortConfig.securityPolicy.macChanges = vim.BoolPolicy(value=self.security_mac_changes)
+
+        # PG policy (advanced_policy)
+        config.policy = vim.dvs.VmwareDistributedVirtualSwitch.VMwarePortgroupPolicy()
+        config.policy.blockOverrideAllowed = self.policy_block_override
+        config.policy.ipfixOverrideAllowed = self.policy_ipfix_override
+        config.policy.livePortMovingAllowed = self.policy_live_port_move
+        config.policy.networkResourcePoolOverrideAllowed = self.policy_network_rp_override
+        config.policy.portConfigResetAtDisconnect = self.policy_port_config_reset_at_disconnect
+        config.policy.securityPolicyOverrideAllowed = self.policy_security_override
+        config.policy.shapingOverrideAllowed = self.policy_shaping_override
+        config.policy.trafficFilterOverrideAllowed = self.policy_traffic_filter_override
+        config.policy.uplinkTeamingOverrideAllowed = self.policy_uplink_teaming_override
+        config.policy.vendorConfigOverrideAllowed = self.policy_vendor_config_override
+        config.policy.vlanOverrideAllowed = self.policy_vlan_override
+
+        # PG Type
         config.type = self.portgroup_type
 
         spec = [config]
@@ -176,12 +312,59 @@ class VMwareDvsPortgroup(object):
 
 def main():
     argument_spec = vmware_argument_spec()
-    argument_spec.update(dict(portgroup_name=dict(required=True, type='str'),
-                         switch_name=dict(required=True, type='str'),
-                         vlan_id=dict(required=True, type='int'),
-                         num_ports=dict(required=True, type='int'),
-                         portgroup_type=dict(required=True, choices=['earlyBinding', 'lateBinding', 'ephemeral'], type='str'),
-                         state=dict(default='present', choices=['present', 'absent'], type='str')))
+    argument_spec.update(
+        dict(
+            portgroup_name=dict(required=True, type='str'),
+            switch_name=dict(required=True, type='str'),
+            vlan_id=dict(required=True, type='str'),
+            num_ports=dict(required=True, type='int'),
+            portgroup_type=dict(required=True, choices=['earlyBinding', 'lateBinding', 'ephemeral'], type='str'),
+            state=dict(required=True, choices=['present', 'absent'], type='str'),
+            vlan_trunk=dict(type='bool', default=False),
+            security=dict(
+                type='dict',
+                options=dict(
+                    promiscuous=dict(type='bool', default=False),
+                    forged_transmits=dict(type='bool', default=False),
+                    mac_changes=dict(type='bool', default=False)
+                ),
+                default=dict(
+                    promiscuous=False,
+                    forged_transmits=False,
+                    mac_changes=False
+                )
+            ),
+            port_policy=dict(
+                type='dict',
+                options=dict(
+                    block_override=dict(type='bool', default=True),
+                    ipfix_override=dict(type='bool', default=False),
+                    live_port_move=dict(type='bool', default=False),
+                    network_rp_override=dict(type='bool', default=False),
+                    port_config_reset_at_disconnect=dict(type='bool', default=True),
+                    security_override=dict(type='bool', default=False),
+                    shaping_override=dict(type='bool', default=False),
+                    traffic_filter_override=dict(type='bool', default=False),
+                    uplink_teaming_override=dict(type='bool', default=False),
+                    vendor_config_override=dict(type='bool', default=False),
+                    vlan_override=dict(type='bool', default=False)
+                ),
+                default=dict(
+                    block_override=True,
+                    ipfix_override=False,
+                    live_port_move=False,
+                    network_rp_override=False,
+                    port_config_reset_at_disconnect=True,
+                    security_override=False,
+                    shaping_override=False,
+                    traffic_filter_override=False,
+                    uplink_teaming_override=False,
+                    vendor_config_override=False,
+                    vlan_override=False
+                )
+            )
+        )
+    )
 
     module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
 
diff --git a/test/integration/targets/vmware_dvs_portgroup/aliases b/test/integration/targets/vmware_dvs_portgroup/aliases
new file mode 100644
index 00000000000..4c6228bf7a7
--- /dev/null
+++ b/test/integration/targets/vmware_dvs_portgroup/aliases
@@ -0,0 +1,3 @@
+posix/ci/cloud/group1/vcenter
+cloud/vcenter
+destructive
diff --git a/test/integration/targets/vmware_dvs_portgroup/tasks/main.yml b/test/integration/targets/vmware_dvs_portgroup/tasks/main.yml
new file mode 100644
index 00000000000..fc0875e9df2
--- /dev/null
+++ b/test/integration/targets/vmware_dvs_portgroup/tasks/main.yml
@@ -0,0 +1,244 @@
+# Test code for the vmware_dvs_portgroup module.
+# (c) 2017, Philippe Dellaert <philippe@dellaert.org>
+
+# This file is part of Ansible
+#
+# Ansible 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.
+#
+# Ansible 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 Ansible.  If not, see <http://www.gnu.org/licenses/>.
+#
+- name: make sure pyvmomi is installed
+  pip:
+    name: pyvmomi
+    state: latest
+  when: "{{ ansible_user_id == 'root' }}"
+
+- 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
+
+- debug: var=vcsim_instance
+
+- 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'] }}"
+
+# Testcase 0001: Add basic portgroup
+- name: create basic portgroup
+  vmware_dvs_portgroup:
+    validate_certs: False
+    hostname: "{{ vcsim }}"
+    username: "{{ vcsim_instance['json']['username'] }}"
+    password: "{{ vcsim_instance['json']['password'] }}"
+    switch_name: "{{ new_dvs_0001['json'][0] | basename }}"
+    portgroup_name: "basic"
+    vlan_id: 0
+    num_ports: 32
+    portgroup_type: earlyBinding
+    state: present
+  register: dvs_pg_result_0001
+
+- name: ensure dvs portgroup is present
+  assert:
+    that:
+        - "{{ dvs_pg_result_0001.changed == true }}"
+
+# Testcase 0002: Add basic VLAN portgroup
+- name: create basic VLAN portgroup
+  vmware_dvs_portgroup:
+    validate_certs: False
+    hostname: "{{ vcsim }}"
+    username: "{{ vcsim_instance['json']['username'] }}"
+    password: "{{ vcsim_instance['json']['password'] }}"
+    switch_name: "{{ new_dvs_0001['json'][0] | basename }}"
+    portgroup_name: "basic-vlan10"
+    vlan_id: 10
+    num_ports: 32
+    portgroup_type: earlyBinding
+    state: present
+  register: dvs_pg_result_0002
+
+- name: ensure dvs portgroup is present
+  assert:
+    that:
+        - "{{ dvs_pg_result_0002.changed == true }}"
+
+# Testcase 0003: Add basic trunk portgroup
+- name: create basic trunk portgroup
+  vmware_dvs_portgroup:
+    validate_certs: False
+    hostname: "{{ vcsim }}"
+    username: "{{ vcsim_instance['json']['username'] }}"
+    password: "{{ vcsim_instance['json']['password'] }}"
+    switch_name: "{{ new_dvs_0001['json'][0] | basename }}"
+    portgroup_name: "basic-trunk"
+    vlan_id: 1-4096
+    vlan_trunk: True
+    num_ports: 32
+    portgroup_type: earlyBinding
+    state: present
+  register: dvs_pg_result_0003
+
+- name: ensure dvs portgroup is present
+  assert:
+    that:
+        - "{{ dvs_pg_result_0003.changed == true }}"
+
+# Testcase 0004: Add basic portgroup again
+- name: create basic portgroup again
+  vmware_dvs_portgroup:
+    validate_certs: False
+    hostname: "{{ vcsim }}"
+    username: "{{ vcsim_instance['json']['username'] }}"
+    password: "{{ vcsim_instance['json']['password'] }}"
+    switch_name: "{{ new_dvs_0001['json'][0] | basename }}"
+    portgroup_name: "basic"
+    vlan_id: 0
+    num_ports: 32
+    portgroup_type: earlyBinding
+    state: present
+  register: dvs_pg_result_0004
+
+- name: ensure dvs portgroup is present
+  assert:
+    that:
+        - "{{ dvs_pg_result_0004.changed == false }}"
+
+# Testcase 0005: Add basic portgroup with all security and policy settings enabled
+- name: create basic portgroup with all security and policy settings enabled
+  vmware_dvs_portgroup:
+    validate_certs: False
+    hostname: "{{ vcsim }}"
+    username: "{{ vcsim_instance['json']['username'] }}"
+    password: "{{ vcsim_instance['json']['password'] }}"
+    switch_name: "{{ new_dvs_0001['json'][0] | basename }}"
+    portgroup_name: "basic-all-enabled"
+    vlan_id: 0
+    num_ports: 32
+    portgroup_type: earlyBinding
+    state: present
+    security:
+      promiscuous: yes
+      forged_transmits: yes
+      mac_changes: yes
+    port_policy:
+      block_override: yes
+      ipfix_override: yes
+      live_port_move: yes
+      network_rp_override: yes
+      port_config_reset_at_disconnect: yes
+      security_override: yes
+      shaping_override: yes
+      traffic_filter_override: yes
+      uplink_teaming_override: yes
+      vendor_config_override: yes
+      vlan_override: yes
+  register: dvs_pg_result_0005
+
+- name: ensure dvs portgroup is present
+  assert:
+    that:
+        - "{{ dvs_pg_result_0005.changed == true }}"
+
+# Testcase 0006: Add basic portgroup with some settings enabled
+- name: create basic portgroup with all security and policy settings enabled
+  vmware_dvs_portgroup:
+    validate_certs: False
+    hostname: "{{ vcsim }}"
+    username: "{{ vcsim_instance['json']['username'] }}"
+    password: "{{ vcsim_instance['json']['password'] }}"
+    switch_name: "{{ new_dvs_0001['json'][0] | basename }}"
+    portgroup_name: "basic-some-enabled"
+    vlan_id: 0
+    num_ports: 32
+    portgroup_type: earlyBinding
+    state: present
+    security:
+      promiscuous: yes
+      forged_transmits: yes
+      mac_changes: no
+    port_policy:
+      vlan_override: yes
+  register: dvs_pg_result_0006
+
+- name: ensure dvs portgroup is present
+  assert:
+    that:
+        - "{{ dvs_pg_result_0006.changed == true }}"
+
+# Testcase 0007: Delete basic portgroup
+- name: delete basic portgroup
+  vmware_dvs_portgroup:
+    validate_certs: False
+    hostname: "{{ vcsim }}"
+    username: "{{ vcsim_instance['json']['username'] }}"
+    password: "{{ vcsim_instance['json']['password'] }}"
+    switch_name: "{{ new_dvs_0001['json'][0] | basename }}"
+    portgroup_name: "basic"
+    vlan_id: 0
+    num_ports: 32
+    portgroup_type: earlyBinding
+    state: absent
+  register: dvs_pg_result_0007
+
+- name: ensure dvs portgroup is removed
+  assert:
+    that:
+        - "{{ dvs_pg_result_0007.changed == true }}"
+
+# Testcase 0008: Delete basic portgroup again
+- name: delete basic portgroup again
+  vmware_dvs_portgroup:
+    validate_certs: False
+    hostname: "{{ vcsim }}"
+    username: "{{ vcsim_instance['json']['username'] }}"
+    password: "{{ vcsim_instance['json']['password'] }}"
+    switch_name: "{{ new_dvs_0001['json'][0] | basename }}"
+    portgroup_name: "basic"
+    vlan_id: 0
+    num_ports: 32
+    portgroup_type: earlyBinding
+    state: absent
+  register: dvs_pg_result_0008
+
+- name: ensure dvs portgroup is removed
+  assert:
+    that:
+        - "{{ dvs_pg_result_0008.changed == false }}"