Repository: ansible-collections/community.vmware

Issue:
vmware_deploy_ovf - Failure validating OVF import spec: The provided network mapping between OVF networks and the system network is not supported by any host
<!--- Verify first that your issue is not already reported on GitHub -->
<!--- Also test if the latest release and devel branch are affected too -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
We have a vCenter with 2 clusters configured and NSX-T on networking layer. The network is visible in both clusters and deploying an ovf template in vCenter works just fine. But when using the vmware_deploy_ovf module an error "Failure validating OVF import spec: The provided network mapping between OVF networks and the system network is not supported by any host" is thrown.  
This is a duplicate for issue: https://github.com/ansible-collections/community.vmware/issues/164 which is currently closed.

##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below, use your best guess if unsure -->
community.vmware.vmware_deploy_ovf

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes -->
```
ansible [core 2.13.7]
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/var/lib/awx/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /opt/my-envs/custom-venv-python38/lib64/python3.8/site-packages/ansible
  ansible collection location = /var/lib/awx/.ansible/collections:/usr/share/ansible/collections
  executable location = /opt/my-envs/custom-venv-python38/bin/ansible
  python version = 3.8.12 (default, May 10 2022, 23:46:40) [GCC 8.5.0 20210514 (Red Hat 8.5.0-10)]
  jinja version = 3.1.2
  libyaml = True
```

##### COLLECTION VERSION
<!--- Paste verbatim output from "ansible-galaxy collection list <namespace>.<collection>"  between the quotes
for example: ansible-galaxy collection list community.general
-->
```
community.vmware              3.2.0
```

##### CONFIGURATION
<!--- Paste verbatim output from "ansible-config dump --only-changed" between quotes -->
```

```

##### OS / ENVIRONMENT
<!--- Provide all relevant information below, e.g. target OS versions, network device firmware, etc. -->
Distributor ID: Red Hat
Description: Red Hat Entrerprise Linux 8
Automation framework AWX Ansible Tower 17.1.0 https://github.com/ansible/awx/releases/tag/17.1.0
Ansible role ```na_ots_deploy``` by NetApp https://github.com/netapp-automation/na_ots_deploy

##### STEPS TO REPRODUCE
<!--- Describe exactly how to reproduce the problem, using a minimal test-case -->

<!--- Paste example playbooks or commands between quotes below -->
```yaml
# tasks file for ots_deploy
- name: Import Deploy OVA - {{ ovf_path }}
  community.vmware.vmware_deploy_ovf:
    hostname: "{{ target_vcenter_or_esxi_host }}"
    username: "{{ host_login }}"
    password: "{{ host_password }}"
    validate_certs: no
    ovf: "{{ ovf_path }}"
    networks:
      "ONTAP Select Deploy VM Network": "{{ mgt_network }}"
    cluster: "{{ esx_cluster_name }}"
    datacenter: "{{ datacenter_name }}"
    datastore: "{{ datastore_name }}"
    name: "{{ deploy_name }}"
    properties:
      gateway: "{{ deploy_gateway }}"
      proxy_url: "{{ deploy_proxy_url }}"
      netMask: "{{ deploy_netMask }}"
      password: "{{ deploy_password }}"
      product_company: "{{ deploy_product_company }}"
      ipAddress: "{{ deploy_ipAddress }}"
      primaryDNS: "{{ deploy_primaryDNS }}"
      secondaryDNS: "{{ deploy_secondaryDNS }}"
      hostName: "{{ deploy_name }}"
      searchDomains: "{{ deploy_searchDomains }}"
    power_on: yes
    wait_for_ip_address: true
    allow_duplicates: no
  delegate_to: localhost
- name: wait for connectivity
  wait_for_connection:
    delay: 60
    timeout: 300
```

<!--- HINT: You can paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- Describe what you expected to happen when running the steps above -->
VM created

##### ACTUAL RESULTS
<!--- Describe what actually happened. If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes -->
```
TASK [na_ots_deploy : Import Deploy OVA - /opt/data/deploy/ONTAPSELECT9.11.1.ova] ***
task path: /tmp/bwrap_58_q9cwyu7x/awx_58_wdzr21kj/project/select/roles/na_ots_deploy/tasks/main.yml:2
<localhost> ESTABLISH LOCAL CONNECTION FOR USER: root
<localhost> EXEC /bin/sh -c 'echo ~root && sleep 0'
<localhost> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /root/.ansible/tmp `"&& mkdir "` echo /root/.ansible/tmp/ansible-tmp-1672250337.1466973-882-163500159704009 `" && echo ansible-tmp-1672250337.1466973-882-163500159704009="` echo /root/.ansible/tmp/ansible-tmp-1672250337.1466973-882-163500159704009 `" ) && sleep 0'
Using module file /opt/my-envs/custom-venv-python38/lib/python3.8/site-packages/ansible_collections/community/vmware/plugins/modules/vmware_deploy_ovf.py
<localhost> PUT /var/lib/awx/.ansible/tmp/ansible-local-876a8zkvpqa/tmpv4vq5_hx TO /root/.ansible/tmp/ansible-tmp-1672250337.1466973-882-163500159704009/AnsiballZ_vmware_deploy_ovf.py
<localhost> EXEC /bin/sh -c 'chmod u+x /root/.ansible/tmp/ansible-tmp-1672250337.1466973-882-163500159704009/ /root/.ansible/tmp/ansible-tmp-1672250337.1466973-882-163500159704009/AnsiballZ_vmware_deploy_ovf.py && sleep 0'
<localhost> EXEC /bin/sh -c '/opt/my-envs/custom-venv-python38/bin/python3.8 /root/.ansible/tmp/ansible-tmp-1672250337.1466973-882-163500159704009/AnsiballZ_vmware_deploy_ovf.py && sleep 0'
<localhost> EXEC /bin/sh -c 'rm -f -r /root/.ansible/tmp/ansible-tmp-1672250337.1466973-882-163500159704009/ > /dev/null 2>&1 && sleep 0'
fatal: [localhost]: FAILED! => {
    "changed": false,
    "invocation": {
        "module_args": {
            "allow_duplicates": false,
            "cluster": "c023-muce-cl01",
            "datacenter": "c023-muce",
            "datastore": "vds001c023",
            "deployment_option": null,
            "disk_provisioning": "thin",
            "esxi_hostname": null,
            "fail_on_spec_warnings": false,
            "folder": null,
            "hostname": "vce001c023.company.net",
            "inject_ovf_env": false,
            "name": "otd001c023",
            "networks": {
                "ONTAP Select Deploy VM Network": "c023_nvl182-uplink_faas"
            },
            "ovf": "/opt/data/deploy/ONTAPSELECT9.11.1.ova",
            "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
            "port": 443,
            "power_on": true,
            "properties": {
                "gateway": "100.100.100.1",
                "hostName": "otd001c023",
                "ipAddress": "100.100.100.4",
                "netMask": "255.255.254.0",
                "password": "<password>",
                "primaryDNS": "100.127.0.10",
                "product_company": "The Company",
                "proxy_url": "http://proxy:3128",
                "searchDomains": "c023.company.net",
                "secondaryDNS": "100.127.0.11"
            },
            "proxy_host": null,
            "proxy_port": null,
            "resource_pool": "Resources",
            "username": "svc-user@vsphere.local",
            "validate_certs": false,
            "wait": true,
            "wait_for_ip_address": true
        }
    },
    "msg": "Failure validating OVF import spec: The provided network mapping between OVF networks and the system network is not supported by any host."
}

```

##### POSSIBLE ROOT CAUSE (TRIAGE)

The function ```find_network_by_name``` https://github.com/ansible-collections/community.vmware/blob/7eb0ef3bea08eaf422ed0a4bd011193cca5d6793/plugins/modules/vmware_deploy_ovf.py#L388 returns the network defined in the second cluster, not in the one that we have defined in ```cluster``` parameter of ```community.vmware.vmware_deploy_ovf```.  <br/>
In our case, the function returns a network in a portgroup that resides on in the cluster ```c023-muce-cl02``` but we defined cluster ```c023-muce-cl01``` as a parameter of the module. 
The same behavior can be observed in the vcenter search toolbar. When searching for the required network, we get two entries, the first one comes from the second cluster, the second one is from the first cluster.<br/>

Analyzing the code, we see that ```find_network_by_name``` https://github.com/ansible-collections/community.vmware/blob/7eb0ef3bea08eaf422ed0a4bd011193cca5d6793/plugins/module_utils/vmware.py#L323 is an alias to https://github.com/ansible-collections/community.vmware/blob/7eb0ef3bea08eaf422ed0a4bd011193cca5d6793/plugins/module_utils/vmware.py#L203 

The function's code:

```python
def find_object_by_name(content, name, obj_type, folder=None, recurse=True):
    if not isinstance(obj_type, list):
        obj_type = [obj_type]

    name = name.strip()

    objects = get_all_objs(content, obj_type, folder=folder, recurse=recurse)
    for obj in objects:
        try:
            if unquote(obj.name) == name:
                return obj
        except vmodl.fault.ManagedObjectNotFound:
            pass

    return None
```

We see that the function returns first possible occurence of the network which is a problem in case when the second cluster has been added later, thus it has "higher id" than the same network defined in the first cluster. 
Expected result however is to get the network defined on the first cluster.


##### PROPOSED SOLUTION
We have introduced small code modifications that solved this issue. The solution has been tested and resulted into a correctly deployed VM.  Check the code changes here: https://github.com/ansible-collections/community.vmware/compare/main...palkoc:community.vmware:main



Discussion:
Files identified in the description:
None

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 --->
@palkoc: Greetings! Thanks for taking the time to open this issue. In order for the community to handle your issue effectively, we need a bit more information.

Here are the items we could not find in your description:
- component name

Please set the description of this issue with this template:
https://raw.githubusercontent.com/ansible/ansible/devel/.github/ISSUE_TEMPLATE/bug_report.md

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: issue_missing_data --->

Resolving patch:
diff --git a/plugins/module_utils/vmware.py b/plugins/module_utils/vmware.py
--- a/plugins/module_utils/vmware.py
+++ b/plugins/module_utils/vmware.py
@@ -24,6 +24,7 @@
 from ansible_collections.community.vmware.plugins.module_utils.version import StrictVersion
 from random import randint
 
+
 REQUESTS_IMP_ERR = None
 try:
     # requests is required for exception handling of the ConnectionError
@@ -219,6 +220,23 @@ def find_object_by_name(content, name, obj_type, folder=None, recurse=True):
     return None
 
 
+def find_all_objects_by_name(content, name, obj_type, folder=None, recurse=True):
+    if not isinstance(obj_type, list):
+        obj_type = [obj_type]
+
+    name = name.strip()
+    result = []
+
+    objects = get_all_objs(content, obj_type, folder=folder, recurse=recurse)
+    for obj in objects:
+        try:
+            if unquote(obj.name) == name:
+                result.append(obj)
+        except vmodl.fault.ManagedObjectNotFound:
+            pass
+    return result
+
+
 def find_cluster_by_name(content, cluster_name, datacenter=None):
     if datacenter and hasattr(datacenter, 'hostFolder'):
         folder = datacenter.hostFolder
@@ -326,6 +344,10 @@ def find_network_by_name(content, network_name, datacenter_name=None):
     return find_object_by_name(content, network_name, [vim.Network], datacenter_name)
 
 
+def find_all_networks_by_name(content, network_name, datacenter_name=None):
+    return find_all_objects_by_name(content, network_name, [vim.Network], datacenter_name)
+
+
 def find_vm_by_id(content, vm_id, vm_id_type="vm_name", datacenter=None,
                   cluster=None, folder=None, match_first=False):
     """ UUID is unique to a VM, every other id returns the first match. """
diff --git a/plugins/modules/vmware_deploy_ovf.py b/plugins/modules/vmware_deploy_ovf.py
--- a/plugins/modules/vmware_deploy_ovf.py
+++ b/plugins/modules/vmware_deploy_ovf.py
@@ -197,7 +197,7 @@
 from ansible.module_utils.six import string_types
 from ansible.module_utils.urls import generic_urlparse, open_url, urlparse, urlunparse
 from ansible_collections.community.vmware.plugins.module_utils.vmware import (
-    find_network_by_name,
+    find_all_networks_by_name,
     find_vm_by_name,
     PyVmomi,
     gather_vm_facts,
@@ -385,14 +385,24 @@ def get_objects(self):
                                       " datacenter" % self.params)
 
         for key, value in self.params['networks'].items():
-            network = find_network_by_name(self.content, value, datacenter_name=self.datacenter)
-            if not network:
+            # If we have the same network name defined in multiple clusters, check all networks to get the right one
+            networks = find_all_networks_by_name(self.content, value, datacenter_name=self.datacenter)
+            if not networks:
                 self.module.fail_json(msg='%(networks)s could not be located' % self.params)
-            network_mapping = vim.OvfManager.NetworkMapping()
-            network_mapping.name = key
-            network_mapping.network = network
-            self.network_mappings.append(network_mapping)
-
+            # Search for the network key of the same network name, that resides in a cluster parameter
+            for network in networks:
+                if self.params['cluster']:
+                    for cnet in cluster.network:
+                        if network.key in cnet.key:
+                            network_mapping = vim.OvfManager.NetworkMapping()
+                            network_mapping.name = key
+                            network_mapping.network = network
+                            self.network_mappings.append(network_mapping)
+                else:
+                    network_mapping = vim.OvfManager.NetworkMapping()
+                    network_mapping.name = key
+                    network_mapping.network = network
+                    self.network_mappings.append(network_mapping)
         return self.datastore, self.datacenter, self.resource_pool, self.network_mappings
 
     def get_ovf_descriptor(self):


Test patch: