Repository: ansible-collections/community.vmware

Issue:
vmware_dvswitch_lacp, map vmnic to lag
_From @Aglidic on Apr 09, 2020 20:26_

SUMMARY

Really would like to be able to map vmnic to lag memeber during creation of dvswitch.

ISSUE TYPE

    Feature Idea

COMPONENT NAME

vmware_dvswitch_lacp

ADDITIONAL INFORMATION

If we enable the enhanced vmotion and create a lag we have no possibility to map our vmnic to this lag so we need to do it manually. Would be helpful to add option in the module for that

_Copied from original issue: ansible/ansible#68824_


Discussion:
Files identified in the description:
* [`plugins/modules/vmware_dvswitch_lacp.py`](https://github.com/ansible-collections/vmware/blob/main/plugins/modules/vmware_dvswitch_lacp.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @Tomorrow9 @goneri @lparkes @pgbidkar @warthog9
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
I would like to have this feature as well.
But should it not be handled by vmware_dvs_host. Just add an uplink to the vmnic (lag-0, lag-1) and if nothing defined, just use the default (Uplink 1, Uplink 2)
@Aglidic I'm not sure I understand this issue. Could you please explain what exactly you have to do manually?

@noesberger 
> But should it not be handled by vmware_dvs_host.

I'm not sure since I don't understand the problem here. But I agree with you, assigning physical NICs of an ESXi hosts to LAG uplinks looks like a job for `vmware_dvs_host` to me. For example, you might want (or have) to use vmnic0 and vmnic2 for lag-0 and lag-1 on one host, but vmnic1 and vmnic3 on another. So that's clearly something that's host specific and can't be done on the LAG level.

> Just add an uplink to the vmnic (lag-0, lag-1) and if nothing defined, just use the default (Uplink 1, Uplink 2)

I'm not sure if a lot of people are doing this, but as far as I know you can have more than just one LAG on a virtual switch. If I understand your proposal correct, we couldn't support this. I haven't put much thought into this but what do you think about something like this:

```
vmnic:
    - vmnic0
    - vmnic1
lag_uplinks:
    - lag: foo
      vmnic:
          - vmnic2
          - vmnic3
    - lag: bar
      vmnic:
          - vmnic4
          - vmnic5
```

What do you think? I'm not really sure myself, I've just thought that up on the spur of the moment.
Hi, I am also interested in this. It is exactly as @mariolenz  explained. When using 'community.vmware.vmware_dvs_host' module to add host vmnics to DVSwitch, they are added as plain uplink ports. There is no possibility to add them to a already created LAG uplink port. This would be perfect: 
```
vmnic:
    - vmnic0
    - vmnic1
lag_uplinks:
    - lag: foo
      vmnic:
          - vmnic2
          - vmnic3
    - lag: bar
      vmnic:
          - vmnic4
          - vmnic5

```
Sorry @miguelgmedina but this turned out to be trickier to implement than I originally expected. I think I'm making some progress, but only slowly.

Resolving patch:
diff --git a/plugins/modules/vmware_dvs_host.py b/plugins/modules/vmware_dvs_host.py
--- a/plugins/modules/vmware_dvs_host.py
+++ b/plugins/modules/vmware_dvs_host.py
@@ -43,6 +43,25 @@
         required: False
         type: list
         elements: str
+    lag_uplinks:
+        version_added: '1.12.0'
+        required: False
+        type: list
+        elements: dict
+        description:
+        - The ESXi hosts vmnics to use with specific LAGs.
+        suboptions:
+            lag:
+                description:
+                - Name of the LAG.
+                type: str
+                required: True
+            vmnics:
+                description:
+                - The ESXi hosts vmnics to use with the LAG.
+                required: False
+                type: list
+                elements: str
     state:
         description:
         - If the host should be present or absent attached to the vSwitch.
@@ -85,6 +104,25 @@
     state: present
   delegate_to: localhost
 
+- name: Add vmnics to LAGs
+  community.vmware.vmware_dvs_host:
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    esxi_hostname: '{{ esxi_hostname }}'
+    switch_name: dvSwitch
+    lag_uplinks:
+        - lag: lag1
+          vmnics:
+              - vmnic0
+              - vmnic1
+        - lag: lag2
+          vmnics:
+              - vmnic2
+              - vmnic3
+    state: present
+  delegate_to: localhost
+
 - name: Add Host to dVS/enable learnswitch (https://labs.vmware.com/flings/learnswitch)
   community.vmware.vmware_dvs_host:
     hostname: '{{ vcenter_hostname }}'
@@ -102,12 +140,6 @@
   delegate_to: localhost
 '''
 
-try:
-    from collections import Counter
-    HAS_COLLECTIONS_COUNTER = True
-except ImportError:
-    HAS_COLLECTIONS_COUNTER = False
-
 try:
     from pyVmomi import vim, vmodl
 except ImportError:
@@ -126,17 +158,32 @@
 class VMwareDvsHost(PyVmomi):
     def __init__(self, module):
         super(VMwareDvsHost, self).__init__(module)
-        self.dv_switch = None
         self.uplink_portgroup = None
         self.host = None
         self.dv_switch = None
-        self.nic = None
+        self.desired_state = {}
+
         self.state = self.module.params['state']
         self.switch_name = self.module.params['switch_name']
         self.esxi_hostname = self.module.params['esxi_hostname']
         self.vmnics = self.module.params['vmnics']
+        self.lag_uplinks = self.module.params['lag_uplinks']
         self.vendor_specific_config = self.module.params['vendor_specific_config']
 
+        self.dv_switch = find_dvs_by_name(self.content, self.switch_name)
+
+        if self.dv_switch is None:
+            self.module.fail_json(msg="A distributed virtual switch %s "
+                                      "does not exist" % self.switch_name)
+
+        self.lags = {}
+        for lag in self.dv_switch.config.lacpGroupConfig:
+            self.lags[lag.name] = lag
+
+        for lag_uplink in self.lag_uplinks:
+            if lag_uplink['lag'] not in self.lags:
+                self.module.fail_json(msg="LAG %s not found" % lag_uplink['lag'])
+
     def process_state(self):
         dvs_host_states = {
             'absent': {
@@ -179,15 +226,15 @@ def modify_dvs_host(self, operation):
                 config.append(vim.dvs.KeyedOpaqueBlob(key=item['key'], opaqueData=item['value']))
             spec.host[0].vendorSpecificConfig = config
 
-        if operation in ("edit", "add"):
+        if operation == "edit":
             spec.host[0].backing = vim.dvs.HostMember.PnicBacking()
-            count = 0
 
-            for nic in self.vmnics:
-                spec.host[0].backing.pnicSpec.append(vim.dvs.HostMember.PnicSpec())
-                spec.host[0].backing.pnicSpec[count].pnicDevice = nic
-                spec.host[0].backing.pnicSpec[count].uplinkPortgroupKey = self.uplink_portgroup.key
-                count += 1
+            for nic, uplinkPortKey in self.desired_state.items():
+                pnicSpec = vim.dvs.HostMember.PnicSpec()
+                pnicSpec.pnicDevice = nic
+                pnicSpec.uplinkPortgroupKey = self.uplink_portgroup.key
+                pnicSpec.uplinkPortKey = uplinkPortKey
+                spec.host[0].backing.pnicSpec.append(pnicSpec)
 
         try:
             task = self.dv_switch.ReconfigureDvs_Task(spec)
@@ -220,6 +267,12 @@ def state_create_dvs_host(self):
 
         if not self.module.check_mode:
             changed, result = self.modify_dvs_host(operation)
+            if changed:
+                self.set_desired_state()
+                changed, result = self.modify_dvs_host("edit")
+            else:
+                self.module.exit_json(changed=changed, result=to_native(result))
+
         self.module.exit_json(changed=changed, result=to_native(result))
 
     def find_host_attached_dvs(self):
@@ -229,23 +282,62 @@ def find_host_attached_dvs(self):
 
         return None
 
+    def set_desired_state(self):
+        lag_uplinks = []
+        switch_uplink_ports = {'non_lag': []}
+
+        portCriteria = vim.dvs.PortCriteria()
+        portCriteria.host = [self.host]
+        portCriteria.portgroupKey = self.uplink_portgroup.key
+        portCriteria.uplinkPort = True
+        ports = self.dv_switch.FetchDVPorts(portCriteria)
+
+        for name, lag in self.lags.items():
+            switch_uplink_ports[name] = []
+            for uplinkName in lag.uplinkName:
+                for port in ports:
+                    if port.config.name == uplinkName:
+                        switch_uplink_ports[name].append(port.key)
+                        lag_uplinks.append(port.key)
+
+        for port in ports:
+            if port.key in self.uplink_portgroup.portKeys and port.key not in lag_uplinks:
+                switch_uplink_ports['non_lag'].append(port.key)
+
+        count = 0
+        for vmnic in self.vmnics:
+            self.desired_state[vmnic] = switch_uplink_ports['non_lag'][count]
+            count += 1
+
+        for lag in self.lag_uplinks:
+            count = 0
+            for vmnic in lag['vmnics']:
+                self.desired_state[vmnic] = switch_uplink_ports[lag['lag']][count]
+                count += 1
+
     def check_uplinks(self):
         pnic_device = []
 
+        self.set_desired_state()
+
         for dvs_host_member in self.dv_switch.config.host:
-            if dvs_host_member.config.host == self.host:
-                for pnicSpec in dvs_host_member.config.backing.pnicSpec:
-                    pnic_device.append(pnicSpec.pnicDevice)
+            if dvs_host_member.config.host.name == self.esxi_hostname:
+                break
 
-        return Counter(pnic_device) == Counter(self.vmnics)
+        for pnicSpec in dvs_host_member.config.backing.pnicSpec:
+            pnic_device.append(pnicSpec.pnicDevice)
+            if pnicSpec.pnicDevice not in self.desired_state:
+                return False
+            if pnicSpec.uplinkPortKey != self.desired_state[pnicSpec.pnicDevice]:
+                return False
 
-    def check_dvs_host_state(self):
-        self.dv_switch = find_dvs_by_name(self.content, self.switch_name)
+        for vmnic in self.desired_state:
+            if vmnic not in pnic_device:
+                return False
 
-        if self.dv_switch is None:
-            self.module.fail_json(msg="A distributed virtual switch %s "
-                                      "does not exist" % self.switch_name)
+        return True
 
+    def check_dvs_host_state(self):
         self.uplink_portgroup = self.find_dvs_uplink_pg()
 
         if self.uplink_portgroup is None:
@@ -262,6 +354,9 @@ def check_dvs_host_state(self):
                 self.module.fail_json(msg="The esxi_hostname %s does not exist "
                                           "in vCenter" % self.esxi_hostname)
             return 'absent'
+        # Skip checking uplinks if the host should be absent, anyway
+        elif self.state == 'absent':
+            return 'present'
         else:
             if self.check_uplinks():
                 return 'present'
@@ -286,15 +381,30 @@ def main():
                     value=dict(type='str', required=True),
                 ),
             ),
+            lag_uplinks=dict(
+                type='list',
+                default=[],
+                required=False,
+                elements='dict',
+                options=dict(
+                    lag=dict(
+                        type='str',
+                        required=True,
+                    ),
+                    vmnics=dict(
+                        type='list',
+                        required=False,
+                        elements='str',
+                        default=[],
+                    ),
+                ),
+            ),
         )
     )
 
     module = AnsibleModule(argument_spec=argument_spec,
                            supports_check_mode=True)
 
-    if not HAS_COLLECTIONS_COUNTER:
-        module.fail_json(msg='collections.Counter from Python-2.7 is required for this module')
-
     vmware_dvs_host = VMwareDvsHost(module)
     vmware_dvs_host.process_state()
 


Test patch:
diff --git a/tests/integration/targets/vmware_dvs_host/tasks/main.yml b/tests/integration/targets/vmware_dvs_host/tasks/main.yml
--- a/tests/integration/targets/vmware_dvs_host/tasks/main.yml
+++ b/tests/integration/targets/vmware_dvs_host/tasks/main.yml
@@ -3,8 +3,8 @@
     name: prepare_vmware_tests
   vars:
     setup_attach_host: true
-- name: 'Attach the hosts to the DVSwitch without specifying any vnics (Issue #185)'
-  when: vcsim is not defined
+
+- when: vcsim is not defined
   block:
   - name: Create the DVSwitch
     vmware_dvswitch:
@@ -16,7 +16,17 @@
       discovery_operation: both
       state: present
 
-  - name: 'Attach the hosts to the DVSwitch without specifying any vnics'
+  - name: Create LAG
+    vmware_dvswitch_lacp:
+      switch: 'dvs_host_test_switch'
+      support_mode: enhanced
+      link_aggregation_groups:
+          - name: lag1
+            mode: active
+            uplink_number: 2
+            load_balancing_mode: srcDestIpTcpUdpPortVlan
+
+  - name: 'Attach host to the DVSwitch without specifying any vmnics (Issue #185)'
     vmware_dvs_host:
       esxi_hostname: '{{ esxi1 }}'
       switch_name: 'dvs_host_test_switch'
@@ -26,9 +36,71 @@
       that:
         - host_dvs_attachment is changed
 
+  - name: 'Attach hosts to the DVSwitch without specifying any vmnics again (idempotency)'
+    vmware_dvs_host:
+      esxi_hostname: '{{ esxi1 }}'
+      switch_name: 'dvs_host_test_switch'
+      state: present
+    register: host_dvs_attachment_again
+  - assert:
+      that:
+        - host_dvs_attachment_again is not changed
+
+  - name: 'Add vmnic to uplink'
+    vmware_dvs_host:
+      esxi_hostname: '{{ esxi1 }}'
+      switch_name: 'dvs_host_test_switch'
+      vmnics:
+          - vmnic1
+      state: present
+    register: host_dvs_uplink
+  - assert:
+      that:
+        - host_dvs_uplink is changed
+
+  - name: 'Add vmnic to uplink again (idempotency)'
+    vmware_dvs_host:
+      esxi_hostname: '{{ esxi1 }}'
+      switch_name: 'dvs_host_test_switch'
+      vmnics:
+          - vmnic1
+      state: present
+    register: host_dvs_uplink_again
+  - assert:
+      that:
+        - host_dvs_uplink_again is not changed
+
+  - name: 'Add vmnic to LAG uplink'
+    vmware_dvs_host:
+      esxi_hostname: '{{ esxi1 }}'
+      switch_name: 'dvs_host_test_switch'
+      lag_uplinks:
+          - lag: lag1
+            vmnics:
+                - vmnic1
+      state: present
+    register: host_dvs_lag_uplink
+  - assert:
+      that:
+        - host_dvs_lag_uplink is changed
+
+  - name: 'Add vmnic to LAG uplink again (idempotency)'
+    vmware_dvs_host:
+      esxi_hostname: '{{ esxi1 }}'
+      switch_name: 'dvs_host_test_switch'
+      lag_uplinks:
+          - lag: lag1
+            vmnics:
+                - vmnic1
+      state: present
+    register: host_dvs_lag_uplink_again
+  - assert:
+      that:
+        - host_dvs_lag_uplink_again is not changed
+
 # Cleanup
   always:
-    - name: 'Cleanup: detaching hosts from dvs'
+    - name: 'Cleanup: detaching host from dvs'
       vmware_dvs_host:
         esxi_hostname: "{{ esxi1 }}"
         switch_name: 'dvs_host_test_switch'