diff --git a/lib/ansible/module_utils/vmware.py b/lib/ansible/module_utils/vmware.py index 592d35d05e..3a0acc85fa 100644 --- a/lib/ansible/module_utils/vmware.py +++ b/lib/ansible/module_utils/vmware.py @@ -52,6 +52,30 @@ def wait_for_task(task): time.sleep(15) +def find_obj(content, vimtype, name, first=True): + container = content.viewManager.CreateContainerView(container=content.rootFolder, recursive=True, type=vimtype) + obj_list = container.view + container.Destroy() + + # Backward compatible with former get_obj() function + if name is None: + if obj_list: + return obj_list[0] + return None + + # Select the first match + if first is True: + for obj in obj_list: + if obj.name == name: + return obj + + # If no object found, return None + return None + + # Return all matching objects if needed + return [obj for obj in obj_list if obj.name == name] + + def find_dvspg_by_name(dv_switch, portgroup_name): portgroups = dv_switch.portgroup diff --git a/lib/ansible/modules/cloud/vmware/vmware_guest.py b/lib/ansible/modules/cloud/vmware/vmware_guest.py index 4afb8f2d70..d16ac70e65 100644 --- a/lib/ansible/modules/cloud/vmware/vmware_guest.py +++ b/lib/ansible/modules/cloud/vmware/vmware_guest.py @@ -23,281 +23,289 @@ ANSIBLE_METADATA = {'metadata_version': '1.0', 'supported_by': 'community'} -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: vmware_guest -short_description: Manages virtual machines in vcenter +short_description: Manages virtual machines in vCenter description: - - Create new virtual machines (from templates or not) - - Power on/power off/restart a virtual machine - - Modify, rename or remove a virtual machine -version_added: 2.2 +- Create new virtual machines (from templates or not). +- Power on/power off/restart a virtual machine. +- Modify, rename or remove a virtual machine. +version_added: '2.2' author: - - James Tanner (@jctanner) - - Loic Blot (@nerzhul) +- James Tanner (@jctanner) +- Loic Blot (@nerzhul) notes: - - Tested on vSphere 5.5 and 6.0 +- Tested on vSphere 5.5 and 6.0 requirements: - - "python >= 2.6" - - PyVmomi +- python >= 2.6 +- PyVmomi options: - state: - description: - - What state should the virtual machine be in? - - If C(state) is set to C(present) and VM exists, ensure the VM configuration conforms to task arguments - required: True - choices: ['present', 'absent', 'poweredon', 'poweredoff', 'restarted', 'suspended', 'shutdownguest', 'rebootguest'] - name: - description: - - Name of the VM to work with - required: True - name_match: - description: - - If multiple VMs matching the name, use the first or last found - default: 'first' - choices: ['first', 'last'] - uuid: - description: - - UUID of the instance to manage if known, this is VMware's unique identifier. - - This is required if name is not supplied. - template: - description: - - Template used to create VM. - - If this value is not set, VM is created without using a template. - - If the VM exists already this setting will be ignored. - is_template: - description: - - Flag the instance as a template - default: False - version_added: "2.3" - folder: - description: - - Destination folder, absolute path to find an existing guest or create the new guest - hardware: - description: - - "Manage some VM hardware attributes." - - "Valid attributes are: memory_mb, num_cpus and scsi" - - "scsi: Valid values are buslogic, lsilogic, lsilogicsas and paravirtual (default)" - guest_id: - description: - - "Set the guest ID (Debian, RHEL, Windows...)" - - "This field is required when creating a VM" - - > - Valid values are referenced here: - https://www.vmware.com/support/developer/converter-sdk/conv55_apireference/vim.vm.GuestOsDescriptor.GuestOsIdentifier.html - version_added: "2.3" - disk: - description: - - "A list of disks to add" - - "Valid attributes are: size_[tb,gb,mb,kb], type, datastore and autoselect_datastore" - - "type: Valid value is thin (default: None)" - - "datastore: Datastore to use for the disk. If autoselect_datastore is True, filter datastore selection." - - "autoselect_datastore (bool): select the less used datastore." - resource_pool: - description: - - Affect machine to the given resource pool - - Resource pool should be child of the selected host parent - default: None - version_added: "2.3" - wait_for_ip_address: - description: - - Wait until vCenter detects an IP address for the VM - - This requires vmware-tools (vmtoolsd) to properly work after creation - default: False - snapshot_src: - description: - - Name of an existing snapshot to use to create a clone of a VM. - default: None - version_added: "2.4" - linked_clone: - description: - - Whether to create a Linked Clone from the snapshot specified - default: False - version_added: "2.4" - force: - description: - - Ignore warnings and complete the actions - datacenter: - description: - - Destination datacenter for the deploy operation - default: ha-datacenter - cluster: - description: - - The cluster name where the VM will run. - version_added: "2.3" - esxi_hostname: - description: - - The esxi hostname where the VM will run. - annotation: - description: - - A note or annotation to include in the VM - version_added: "2.3" - customvalues: - description: - - Define a list of customvalues to set on VM. - - "A customvalue object takes 2 fields 'key' and 'value'." - version_added: "2.3" - networks: - description: - - Network to use should include C(name) or C(vlan) entry - - Add an optional C(ip) and C(netmask) for network configuration - - Add an optional C(gateway) entry to configure a gateway - - Add an optional C(mac) entry to customize mac address - - Add an optional C(dns_servers) or C(domain) entry per interface (Windows) - - Add an optional C(device_type) to configure the virtual NIC (pcnet32, vmxnet2, vmxnet3, e1000, e1000e) - version_added: "2.3" - customization: - description: - - "Parameters to customize template" - - "Common parameters (Linux/Windows):" - - " C(dns_servers) (list): List of DNS servers to configure" - - " C(dns_suffix) (list): List of domain suffixes, aka DNS search path (default: C(domain) parameter)" - - " C(domain) (string): DNS domain name to use" - - " C(hostname) (string): Computer hostname (default: C(name) parameter)" - - "Parameters related to windows customization:" - - " C(autologon) (bool): Auto logon after VM customization (default: False)" - - " C(autologoncount) (int): Number of autologon after reboot (default: 1)" - - " C(domainadmin) (string): User used to join in AD domain (mandatory with joindomain)" - - " C(domainadminpassword) (string): Password used to join in AD domain (mandatory with joindomain)" - - " C(fullname) (string): Server owner name (default: Administrator)" - - " C(joindomain) (string): AD domain to join (Not compatible with C(joinworkgroup))" - - " C(joinworkgroup) (string): Workgroup to join (Not compatible with C(joindomain), default: WORKGROUP)" - - " C(orgname) (string): Organisation name (default: ACME)" - - " C(password) (string): Local administrator password (mandatory)" - - " C(productid) (string): Product ID" - - " C(runonce) (list): List of commands to run at first user logon" - - " C(timezone) (int): Timezone (default: 85) See U(https://msdn.microsoft.com/en-us/library/ms912391(v=winembedded.11).aspx)" - version_added: "2.3" + state: + description: + - What state should the virtual machine be in? + - If C(state) is set to C(present) and VM exists, ensure the VM configuration conforms to task arguments. + required: yes + choices: [ 'present', 'absent', 'poweredon', 'poweredoff', 'restarted', 'suspended', 'shutdownguest', 'rebootguest' ] + name: + description: + - Name of the VM to work with. + - VM names in vCenter are not necessarily unique, which may be problematic, see C(name_match). + required: yes + name_match: + description: + - If multiple VMs matching the name, use the first or last found. + default: 'first' + choices: [ 'first', 'last' ] + uuid: + description: + - UUID of the instance to manage if known, this is VMware's unique identifier. + - This is required if name is not supplied. + template: + description: + - Template used to create VM. + - If this value is not set, VM is created without using a template. + - If the VM exists already this setting will be ignored. + is_template: + description: + - Flag the instance as a template. + default: 'no' + type: bool + version_added: '2.3' + folder: + description: + - Destination folder, absolute path to find an existing guest or create the new guest. + default: / + hardware: + description: + - Manage some VM hardware attributes. + - 'Valid attributes are:' + - ' - C(memory_mb) (integer): Amount of memory in MB.' + - ' - C(num_cpus) (integer): Number of CPUs.' + - ' - C(scsi) (string): Valid values are C(buslogic), C(lsilogic), C(lsilogicsas) and C(paravirtual) (default).' + guest_id: + description: + - Set the guest ID (Debian, RHEL, Windows...). + - This field is required when creating a VM. + - > + Valid values are referenced here: + https://www.vmware.com/support/developer/converter-sdk/conv55_apireference/vim.vm.GuestOsDescriptor.GuestOsIdentifier.html + version_added: '2.3' + disk: + description: + - A list of disks to add. + - 'Valid attributes are:' + - ' - C(size_[tb,gb,mb,kb]) (integer): Disk storage size in specified unit.' + - ' - C(type) (string): Valid value is C(thin) (default: None).' + - ' - C(datastore) (string): Datastore to use for the disk. If C(autoselect_datastore) is enabled, filter datastore selection.' + - ' - C(autoselect_datastore) (bool): select the less used datastore.' + resource_pool: + description: + - Affect machine to the given resource pool. + - Resource pool should be child of the selected host parent. + version_added: '2.3' + wait_for_ip_address: + description: + - Wait until vCenter detects an IP address for the VM. + - This requires vmware-tools (vmtoolsd) to properly work after creation. + default: 'no' + type: bool + snapshot_src: + description: + - Name of an existing snapshot to use to create a clone of a VM. + version_added: '2.4' + linked_clone: + description: + - Whether to create a Linked Clone from the snapshot specified. + default: 'no' + type: bool + version_added: '2.4' + force: + description: + - Ignore warnings and complete the actions. + default: 'no' + type: bool + datacenter: + description: + - Destination datacenter for the deploy operation. + default: ha-datacenter + cluster: + description: + - The cluster name where the VM will run. + version_added: '2.3' + esxi_hostname: + description: + - The ESXi hostname where the VM will run. + annotation: + description: + - A note or annotation to include in the VM. + version_added: '2.3' + customvalues: + description: + - Define a list of customvalues to set on VM. + - A customvalue object takes 2 fields C(key) and C(value). + version_added: '2.3' + networks: + description: + - A list of networks (in the order of the NICs). + - 'One of the below parameters is required per entry:' + - ' - C(name) (string): Name of the portgroup for this interface.' + - ' - C(vlan) (integer): VLAN number for this interface.' + - 'Optional parameters per entry (used for virtual hardware):' + - ' - C(device_type) (string): Virtual network device (one of C(e1000), C(e1000e), C(pcnet32), C(vmxnet2), C(vmxnet3) (default), C(sriov)).' + - ' - C(mac) (string): Customize mac address.' + - 'Optional parameters per entry (used for OS customization):' + - ' - C(type) (string): Type of IP assignment (either C(dhcp) or C(static)).' + - ' - C(ip) (string): Static IP address (implies C(type: static)).' + - ' - C(netmask) (string): Static netmask required for C(ip).' + - ' - C(gateway) (string): Static gateway.' + - ' - C(dns_servers) (string): DNS servers for this network interface (Windows).' + - ' - C(domain) (string): Domain name for this network interface (Windows).' + version_added: '2.3' + customization: + description: + - Parameters for OS customization when cloning from template. + - 'Common parameters (Linux/Windows):' + - ' - C(dns_servers) (list): List of DNS servers to configure.' + - ' - C(dns_suffix) (list): List of domain suffixes, aka DNS search path (default: C(domain) parameter).' + - ' - C(domain) (string): DNS domain name to use.' + - ' - C(hostname) (string): Computer hostname (default: shorted C(name) parameter).' + - 'Parameters related to Windows customization:' + - ' - C(autologon) (bool): Auto logon after VM customization (default: False).' + - ' - C(autologoncount) (int): Number of autologon after reboot (default: 1).' + - ' - C(domainadmin) (string): User used to join in AD domain (mandatory with C(joindomain)).' + - ' - C(domainadminpassword) (string): Password used to join in AD domain (mandatory with C(joindomain)).' + - ' - C(fullname) (string): Server owner name (default: Administrator).' + - ' - C(joindomain) (string): AD domain to join (Not compatible with C(joinworkgroup)).' + - ' - C(joinworkgroup) (string): Workgroup to join (Not compatible with C(joindomain), default: WORKGROUP).' + - ' - C(orgname) (string): Organisation name (default: ACME).' + - ' - C(password) (string): Local administrator password.' + - ' - C(productid) (string): Product ID.' + - ' - C(runonce) (list): List of commands to run at first user logon.' + - ' - C(timezone) (int): Timezone (See U(https://msdn.microsoft.com/en-us/library/ms912391.aspx)).' + version_added: '2.3' extends_documentation_fragment: vmware.documentation ''' -EXAMPLES = ''' -# Create a VM from a template - - name: create the VM - vmware_guest: - hostname: 192.0.2.44 - username: administrator@vsphere.local - password: vmware - validate_certs: no - esxi_hostname: 192.0.2.117 - datacenter: datacenter1 - folder: testvms - name: testvm_2 - state: poweredon - guest_id: centos64guest - disk: - - size_gb: 10 - type: thin - datastore: g73_datastore - hardware: - memory_mb: 512 - num_cpus: 1 - scsi: paravirtual - networks: - - name: VM Network - ip: 192.168.1.100 - netmask: 255.255.255.0 - mac: 'aa:bb:dd:aa:00:14' - template: template_el7 - wait_for_ip_address: yes - delegate_to: localhost - register: deploy +EXAMPLES = r''' +- name: Create a VM from a template + vmware_guest: + hostname: 192.0.2.44 + username: administrator@vsphere.local + password: vmware + validate_certs: no + folder: /testvms + name: testvm_2 + state: poweredon + template: template_el7 + disk: + - size_gb: 10 + type: thin + datastore: g73_datastore + hardware: + memory_mb: 512 + num_cpus: 1 + scsi: paravirtual + networks: + - name: VM Network + mac: aa:bb:dd:aa:00:14 + wait_for_ip_address: yes + delegate_to: localhost + register: deploy -# Clone a VM from Template and customize - - name: Clone template and customize - vmware_guest: - hostname: 192.168.1.209 - username: administrator@vsphere.local - password: vmware - validate_certs: no - datacenter: datacenter1 - cluster: cluster - name: testvm-2 - template: template_windows - networks: - - name: VM Network - ip: 192.168.1.100 - netmask: 255.255.255.0 - gateway: 192.168.1.1 - mac: 'aa:bb:dd:aa:00:14' - domain: my_domain - dns_servers: - - 192.168.1.1 - - 192.168.1.2 - customization: - autologon: True - dns_servers: - - 192.168.1.1 - - 192.168.1.2 - domain: my_domain - password: new_vm_password - runonce: - - powershell.exe -ExecutionPolicy Unrestricted -File C:\Windows\Temp\Enable-WinRM.ps1 -ForceNewSSLCert - delegate_to: localhost +- name: Clone a VM from Template and customize + vmware_guest: + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + validate_certs: no + datacenter: datacenter1 + cluster: cluster + name: testvm-2 + template: template_windows + networks: + - name: VM Network + ip: 192.168.1.100 + netmask: 255.255.255.0 + gateway: 192.168.1.1 + mac: aa:bb:dd:aa:00:14 + domain: my_domain + dns_servers: + - 192.168.1.1 + - 192.168.1.2 + - vlan: 1234 + type: dhcp + customization: + autologon: yes + dns_servers: + - 192.168.1.1 + - 192.168.1.2 + domain: my_domain + password: new_vm_password + runonce: + - powershell.exe -ExecutionPolicy Unrestricted -File C:\Windows\Temp\ConfigureRemotingForAnsible.ps1 -ForceNewSSLCert -EnableCredSSP + delegate_to: localhost -# Create a VM template - - name: create a VM template - vmware_guest: - hostname: 192.0.2.88 - username: administrator@vsphere.local - password: vmware - validate_certs: no - datacenter: datacenter1 - cluster: vmware_cluster_esx - resource_pool: highperformance_pool - folder: testvms - name: testvm_6 - is_template: yes - guest_id: debian6_64Guest - disk: - - size_gb: 10 - type: thin - datastore: g73_datastore - hardware: - memory_mb: 512 - num_cpus: 1 - scsi: lsilogic - wait_for_ip_address: yes - delegate_to: localhost - register: deploy +- name: Create a VM template + vmware_guest: + hostname: 192.0.2.88 + username: administrator@vsphere.local + password: vmware + validate_certs: no + datacenter: datacenter1 + cluster: vmware_cluster_esx + resource_pool: highperformance_pool + folder: /testvms + name: testvm_6 + is_template: yes + guest_id: debian6_64Guest + disk: + - size_gb: 10 + type: thin + datastore: g73_datastore + hardware: + memory_mb: 512 + num_cpus: 1 + scsi: lsilogic + delegate_to: localhost + register: deploy -# Rename a VM (requires the VM's uuid) - - vmware_guest: - hostname: 192.168.1.209 - username: administrator@vsphere.local - password: vmware - uuid: 421e4592-c069-924d-ce20-7e7533fab926 - name: new_name - state: present - delegate_to: localhost +- name: Rename a VM (requires the VM's uuid) + vmware_guest: + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + uuid: 421e4592-c069-924d-ce20-7e7533fab926 + name: new_name + state: present + delegate_to: localhost -# Remove a VM by uuid - - vmware_guest: - hostname: 192.168.1.209 - username: administrator@vsphere.local - password: vmware - uuid: 421e4592-c069-924d-ce20-7e7533fab926 - state: absent - delegate_to: localhost +- name: Remove a VM by uuid + vmware_guest: + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + uuid: 421e4592-c069-924d-ce20-7e7533fab926 + state: absent + delegate_to: localhost ''' -RETURN = """ +RETURN = r''' instance: description: metadata about the new virtualmachine returned: always type: dict sample: None -""" +''' import os import time -# import module snippets from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.pycompat24 import get_exception from ansible.module_utils.six import iteritems from ansible.module_utils.urls import fetch_url -from ansible.module_utils.vmware import get_all_objs, connect_to_api, gather_vm_facts +from ansible.module_utils.vmware import connect_to_api, find_obj, gather_vm_facts, get_all_objs try: import json @@ -395,8 +403,7 @@ class PyVmomiDeviceHelper(object): elif device_type == 'sriov': nic.device = vim.vm.device.VirtualSriovEthernetCard() else: - self.module.fail_json(msg="Invalid device_type '%s' for network %s" % - (device_type, device_infos['name'])) + self.module.fail_json(msg='Invalid device_type "%s" for network "%s"' % (device_type, device_infos['name'])) nic.device.wakeOnLanEnabled = True nic.device.deviceInfo = vim.Description() @@ -425,19 +432,19 @@ class PyVmomiCache(object): def get_network(self, network): if network not in self.networks: - self.networks[network] = get_obj(self.content, [vim.Network], network) + self.networks[network] = find_obj(self.content, [vim.Network], network) return self.networks[network] def get_cluster(self, cluster): if cluster not in self.clusters: - self.clusters[cluster] = get_obj(self.content, [vim.ClusterComputeResource], cluster) + self.clusters[cluster] = find_obj(self.content, [vim.ClusterComputeResource], cluster) return self.clusters[cluster] def get_esx_host(self, host): if host not in self.esx_hosts: - self.esx_hosts[host] = get_obj(self.content, [vim.HostSystem], host) + self.esx_hosts[host] = find_obj(self.content, [vim.HostSystem], host) return self.esx_hosts[host] @@ -458,9 +465,6 @@ class PyVmomiHelper(object): self.current_vm_obj = None self.cache = PyVmomiCache(self.content) - def should_deploy_from_template(self): - return self.params.get('template') is not None - def getvm(self, name=None, uuid=None, folder=None): # https://www.vmware.com/support/developer/vc-sdk/visdk2xpubs/ReferenceGuide/vim.SearchIndex.html @@ -584,7 +588,7 @@ class PyVmomiHelper(object): def configure_guestid(self, vm_obj, vm_creation=False): # guest_id is not required when using templates - if self.should_deploy_from_template() and self.params.get('guest_id') is None: + if self.params['template'] and not self.params['guest_id']: return # guest_id is only mandatory on VM creation @@ -603,7 +607,7 @@ class PyVmomiHelper(object): if vm_obj is None or self.configspec.numCPUs != vm_obj.config.hardware.numCPU: self.change_detected = True # num_cpu is mandatory for VM creation - elif vm_creation and not self.should_deploy_from_template(): + elif vm_creation and not self.params['template']: self.module.fail_json(msg="hardware.num_cpus attribute is mandatory for VM creation") if 'memory_mb' in self.params['hardware']: @@ -611,10 +615,9 @@ class PyVmomiHelper(object): if vm_obj is None or self.configspec.memoryMB != vm_obj.config.hardware.memoryMB: self.change_detected = True # memory_mb is mandatory for VM creation - elif vm_creation and not self.should_deploy_from_template(): + elif vm_creation and not self.params['template']: self.module.fail_json(msg="hardware.memory_mb attribute is mandatory for VM creation") - def get_vm_network_interfaces(self, vm=None): if vm is None: return [] @@ -639,11 +642,11 @@ class PyVmomiHelper(object): network_devices = list() for network in self.params['networks']: if 'ip' in network or 'netmask' in network: - if 'ip' not in network or not 'netmask' in network: + if 'ip' not in network or 'netmask' not in network: self.module.fail_json(msg="Both 'ip' and 'netmask' are required together.") if 'name' in network: - if get_obj(self.content, [vim.Network], network['name']) is None: + if find_obj(self.content, [vim.Network], network['name']) is None: self.module.fail_json(msg="Network '%(name)s' does not exists" % network) elif 'vlan' in network: @@ -677,7 +680,7 @@ class PyVmomiHelper(object): network_devices[key]) nic_change_detected = False - if key < len(current_net_devices) and (vm_obj or self.should_deploy_from_template()): + if key < len(current_net_devices) and (vm_obj or self.params['template']): nic.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit # Changing mac address has no effect when editing interface if 'mac' in network_devices[key] and nic.device.macAddress != current_net_devices[key].macAddress: @@ -692,7 +695,7 @@ class PyVmomiHelper(object): if hasattr(self.cache.get_network(network_devices[key]['name']), 'portKeys'): # VDS switch - pg_obj = get_obj(self.content, [vim.dvs.DistributedVirtualPortgroup], network_devices[key]['name']) + pg_obj = find_obj(self.content, [vim.dvs.DistributedVirtualPortgroup], network_devices[key]['name']) if (nic.device.backing and (nic.device.backing.port.portgroupKey != pg_obj.key or @@ -747,57 +750,80 @@ class PyVmomiHelper(object): # Network settings adaptermaps = [] for network in self.params['networks']: + + guest_map = vim.vm.customization.AdapterMapping() + guest_map.adapter = vim.vm.customization.IPSettings() + if 'ip' in network and 'netmask' in network: - guest_map = vim.vm.customization.AdapterMapping() - guest_map.adapter = vim.vm.customization.IPSettings() + if 'type' in network and network['type'] != 'static': + self.module.fail_json(msg='Static IP information provided for network "%(name)s", but "type" is set to "%(type)s".' % network) guest_map.adapter.ip = vim.vm.customization.FixedIp() guest_map.adapter.ip.ipAddress = str(network['ip']) guest_map.adapter.subnetMask = str(network['netmask']) + elif 'type' in network and network['type'] == 'static': + self.module.fail_json(msg='Network "%(name)s" was set to type "%(type)s", but "ip" and "netmask" are missing.' % network) + elif 'type' in network and network['type'] == 'dhcp': + guest_map.adapter.ip = vim.vm.customization.DhcpIpGenerator() + else: + self.module.fail_json(msg='Network "%(name)s" was set to unknown type "%(type)s".' % network) - if 'gateway' in network: - guest_map.adapter.gateway = network['gateway'] + if 'gateway' in network: + guest_map.adapter.gateway = network['gateway'] - # On Windows, DNS domain and DNS servers can be set by network interface - # https://pubs.vmware.com/vi3/sdk/ReferenceGuide/vim.vm.customization.IPSettings.html - if 'domain' in network: - guest_map.adapter.dnsDomain = network['domain'] - elif self.params['customization'].get('domain'): - guest_map.adapter.dnsDomain = self.params['customization']['domain'] - if 'dns_servers' in network: - guest_map.adapter.dnsServerList = network['dns_servers'] - elif self.params['customization'].get('dns_servers'): - guest_map.adapter.dnsServerList = self.params['customization']['dns_servers'] + # On Windows, DNS domain and DNS servers can be set by network interface + # https://pubs.vmware.com/vi3/sdk/ReferenceGuide/vim.vm.customization.IPSettings.html + if 'domain' in network: + guest_map.adapter.dnsDomain = network['domain'] + elif 'domain' in self.params['customization']: + guest_map.adapter.dnsDomain = self.params['customization']['domain'] - adaptermaps.append(guest_map) + if 'dns_servers' in network: + guest_map.adapter.dnsServerList = network['dns_servers'] + elif 'dns_servers' in self.params['customization']: + guest_map.adapter.dnsServerList = self.params['customization']['dns_servers'] + + adaptermaps.append(guest_map) # Global DNS settings globalip = vim.vm.customization.GlobalIPSettings() if 'dns_servers' in self.params['customization']: - globalip.dnsServerList = self.params['customization'].get('dns_servers') + globalip.dnsServerList = self.params['customization']['dns_servers'] + # TODO: Maybe list the different domains from the interfaces here by default ? - if 'dns_suffix' in self.params['customization'] or 'domain' in self.params['customization']: - globalip.dnsSuffixList = self.params['customization'].get('dns_suffix', self.params['customization']['domain']) + if 'dns_suffix' in self.params['customization']: + globalip.dnsSuffixList = self.params['customization']['dns_suffix'] + elif 'domain' in self.params['customization']: + globalip.dnsSuffixList = self.params['customization']['domain'] if self.params['guest_id']: guest_id = self.params['guest_id'] else: guest_id = vm_obj.summary.config.guestId - # If I install a Windows use Sysprep + # For windows guest OS, use SysPrep # https://pubs.vmware.com/vi3/sdk/ReferenceGuide/vim.vm.customization.Sysprep.html#field_detail if 'win' in guest_id: ident = vim.vm.customization.Sysprep() ident.userData = vim.vm.customization.UserData() + + # Setting hostName, orgName and fullName is mandatory, so we set some default when missing ident.userData.computerName = vim.vm.customization.FixedName() - ident.userData.computerName.name = str(self.params['customization'].get('hostname', self.params['name'])) + ident.userData.computerName.name = str(self.params['customization'].get('hostname', self.params['name'].split('.')[0])) ident.userData.fullName = str(self.params['customization'].get('fullname', 'Administrator')) ident.userData.orgName = str(self.params['customization'].get('orgname', 'ACME')) + if 'productid' in self.params['customization']: + ident.userData.productId = str(self.params['customization']['productid']) + ident.guiUnattended = vim.vm.customization.GuiUnattended() - ident.guiUnattended.autoLogon = self.params['customization'].get('autologon', False) - ident.guiUnattended.autoLogonCount = self.params['customization'].get('autologoncount', 1) - ident.guiUnattended.timeZone = self.params['customization'].get('timezone', 85) + + if 'autologon' in self.params['customization']: + ident.guiUnattended.autoLogon = self.params['customization']['autologon'] + ident.guiUnattended.autoLogonCount = self.params['customization'].get('autologoncount', 1) + + if 'timezone' in self.params['customization']: + ident.guiUnattended.timeZone = self.params['customization']['timezone'] ident.identification = vim.vm.customization.Identification() @@ -805,38 +831,38 @@ class PyVmomiHelper(object): ident.guiUnattended.password = vim.vm.customization.Password() ident.guiUnattended.password.value = str(self.params['customization']['password']) ident.guiUnattended.password.plainText = True - else: - self.module.fail_json(msg="The 'customization' section requires a 'password' entry, which cannot be empty.") - - if 'productid' in self.params['customization']: - ident.userData.orgName = str(self.params['customization']['productid']) if 'joindomain' in self.params['customization']: if 'domainadmin' not in self.params['customization'] or 'domainadminpassword' not in self.params['customization']: self.module.fail_json(msg="'domainadmin' and 'domainadminpassword' entries are mandatory in 'customization' section to use " "joindomain feature") - ident.identification.domainAdmin = str(self.params['customization'].get('domainadmin')) - ident.identification.joinDomain = str(self.params['customization'].get('joindomain')) + ident.identification.domainAdmin = str(self.params['customization']['domainadmin']) + ident.identification.joinDomain = str(self.params['customization']['joindomain']) ident.identification.domainAdminPassword = vim.vm.customization.Password() - ident.identification.domainAdminPassword.value = str(self.params['customization'].get('domainadminpassword')) + ident.identification.domainAdminPassword.value = str(self.params['customization']['domainadminpassword']) ident.identification.domainAdminPassword.plainText = True elif 'joinworkgroup' in self.params['customization']: - ident.identification.joinWorkgroup = str(self.params['customization'].get('joinworkgroup')) + ident.identification.joinWorkgroup = str(self.params['customization']['joinworkgroup']) if 'runonce' in self.params['customization']: ident.guiRunOnce = vim.vm.customization.GuiRunOnce() ident.guiRunOnce.commandList = self.params['customization']['runonce'] + else: - # Else use LinuxPrep + # FIXME: We have no clue whether this non-Windows OS is actually Linux, hence it might fail ! + + # For Linux guest OS, use LinuxPrep # https://pubs.vmware.com/vi3/sdk/ReferenceGuide/vim.vm.customization.LinuxPrep.html ident = vim.vm.customization.LinuxPrep() + # TODO: Maybe add domain from interface if missing ? if 'domain' in self.params['customization']: - ident.domain = str(self.params['customization'].get('domain')) + ident.domain = str(self.params['customization']['domain']) + ident.hostName = vim.vm.customization.FixedName() - ident.hostName.name = str(self.params['customization'].get('hostname', self.params['name'])) + ident.hostName.name = str(self.params['customization'].get('hostname', self.params['name'].split('.')[0])) self.customspec = vim.vm.customization.Specification() self.customspec.nicSettingMap = adaptermaps @@ -941,7 +967,7 @@ class PyVmomiHelper(object): # VMWare doesn't allow to reduce disk sizes if kb < diskspec.device.capacityInKB: self.module.fail_json( - msg="Given disk size is lesser than found (%d < %d). Reducing disks is not allowed." % + msg="Given disk size is smaller than found (%d < %d). Reducing disks is not allowed." % (kb, diskspec.device.capacityInKB)) if kb != diskspec.device.capacityInKB or disk_modified: @@ -955,14 +981,16 @@ class PyVmomiHelper(object): if self.params['cluster']: cluster = self.cache.get_cluster(self.params['cluster']) if not cluster: - self.module.fail_json(msg="Failed to find a cluster named %(cluster)s" % self.params) + self.module.fail_json(msg='Failed to find cluster "%(cluster)s"' % self.params) hostsystems = [x for x in cluster.host] + if not hostsystems: + self.module.fail_json(msg='No hosts found in cluster "%(cluster)s. Maybe you lack the right privileges ?"' % self.params) # TODO: add a policy to select host hostsystem = hostsystems[0] else: hostsystem = self.cache.get_esx_host(self.params['esxi_hostname']) if not hostsystem: - self.module.fail_json(msg="Failed to find a host named %(esxi_hostname)s" % self.params) + self.module.fail_json(msg='Failed to find ESX host "%(esxi_hostname)s"' % self.params) return hostsystem @@ -991,11 +1019,10 @@ class PyVmomiHelper(object): elif 'datastore' in self.params['disk'][0]: datastore_name = self.params['disk'][0]['datastore'] - datastore = get_obj(self.content, [vim.Datastore], datastore_name) + datastore = find_obj(self.content, [vim.Datastore], datastore_name) else: - self.module.fail_json(msg="Either datastore or autoselect_datastore " - "should be provided to select datastore") - if not datastore and self.should_deploy_from_template(): + self.module.fail_json(msg="Either datastore or autoselect_datastore should be provided to select datastore") + if not datastore and self.params['template']: # use the template's existing DS disks = [x for x in vm_obj.config.hardware.device if isinstance(x, vim.vm.device.VirtualDisk)] datastore = disks[0].backing.datastore @@ -1017,7 +1044,13 @@ class PyVmomiHelper(object): if current_parent is None: return False - def select_resource_pool(self, host): + def select_resource_pool_by_name(self, resource_pool_name): + resource_pool = find_obj(self.content, [vim.ResourcePool], resource_pool_name) + if resource_pool is None: + self.module.fail_json(msg='Could not find resource_pool "%s"' % resource_pool_name) + return resource_pool + + def select_resource_pool_by_host(self, host): resource_pools = get_all_objs(self.content, [vim.ResourcePool]) for rp in resource_pools.items(): if not rp[0]: @@ -1060,9 +1093,9 @@ class PyVmomiHelper(object): # - multiple templates by the same name # - static IPs - #datacenters = get_all_objs(self.content, [vim.Datacenter]) - datacenter = get_obj(self.content, [vim.Datacenter], self.params['datacenter']) - if not datacenter: + # datacenters = get_all_objs(self.content, [vim.Datacenter]) + datacenter = find_obj(self.content, [vim.Datacenter], self.params['datacenter']) + if datacenter is None: self.module.fail_json(msg='No datacenter named %(datacenter)s was found' % self.params) destfolder = None @@ -1074,19 +1107,22 @@ class PyVmomiHelper(object): self.module.fail_json(msg='No folder matched the path: %(folder)s' % self.params) destfolder = f_obj - hostsystem = self.select_host() - - if self.should_deploy_from_template(): + if self.params['template']: # FIXME: need to search for this in the same way as guests to ensure accuracy - vm_obj = get_obj(self.content, [vim.VirtualMachine], self.params['template']) - if not vm_obj: + vm_obj = find_obj(self.content, [vim.VirtualMachine], self.params['template']) + if vm_obj is None: self.module.fail_json(msg="Could not find a template named %(template)s" % self.params) else: vm_obj = None + if self.params['resource_pool']: + resource_pool = self.select_resource_pool_by_name(self.params['resource_pool']) + + if resource_pool is None: + self.module.fail_json(msg='Unable to find resource pool "%(resource_pool)s"' % self.params) + # set the destination datastore for VM & disks (datastore, datastore_name) = self.select_datastore(vm_obj) - resource_pool = self.select_resource_pool(hostsystem) self.configspec = vim.vm.ConfigSpec(cpuHotAddEnabled=True, memoryHotAddEnabled=True) self.configspec.deviceChange = [] @@ -1095,18 +1131,30 @@ class PyVmomiHelper(object): self.configure_disks(vm_obj=vm_obj) self.configure_network(vm_obj=vm_obj) - if len(self.params['customization']) > 0 or len(self.params['networks']) > 0: + # Find if we need network customizations (find keys in dictionary that requires customizations) + network_changes = False + for nw in self.params['networks']: + for key in nw: + # We don't need customizations for these keys + if key not in ('device_type', 'mac', 'name', 'vlan'): + network_changes = True + break + + if len(self.params['customization']) > 0 or network_changes is True: self.customize_vm(vm_obj=vm_obj) try: - if self.should_deploy_from_template(): + if self.params['template']: # create the relocation spec relospec = vim.vm.RelocateSpec() - # Only provide specific host when using ESXi host directly + + # Only select specific host when ESXi hostname is provided if self.params['esxi_hostname']: - relospec.host = hostsystem + relospec.host = self.select_host() relospec.datastore = datastore - relospec.pool = resource_pool + + if self.params['resource_pool']: + relospec.pool = resource_pool if self.params['snapshot_src'] is not None and self.params['linked_clone']: relospec.diskMoveType = vim.vm.RelocateSpec.DiskMoveOptions.createNewChildDiskBacking @@ -1116,11 +1164,9 @@ class PyVmomiHelper(object): clonespec.customization = self.customspec if self.params['snapshot_src'] is not None: - snapshot = self.get_snapshots_by_name_recursively(snapshots=vm_obj.snapshot.rootSnapshotList, - snapname=self.params['snapshot_src']) + snapshot = self.get_snapshots_by_name_recursively(snapshots=vm_obj.snapshot.rootSnapshotList, snapname=self.params['snapshot_src']) if len(snapshot) != 1: - self.module.fail_json(msg='virtual machine "{0}" does not contain snapshot named "{1}"'.format( - self.params['template'], self.params['snapshot_src'])) + self.module.fail_json(msg='virtual machine "%(template)s" does not contain snapshot named "%(snapshot_src)s"' % self.params) clonespec.snapshot = snapshot[0].snapshot @@ -1189,15 +1235,19 @@ class PyVmomiHelper(object): self.configspec.annotation = str(self.params['annotation']) self.change_detected = True - relospec = vim.vm.RelocateSpec() - hostsystem = self.select_host() - relospec.pool = self.select_resource_pool(hostsystem) - change_applied = False - if relospec.pool != self.current_vm_obj.resourcePool: - task = self.current_vm_obj.RelocateVM_Task(spec=relospec) - self.wait_for_task(task) - change_applied = True + + relospec = vim.vm.RelocateSpec() + if self.params['resource_pool']: + relospec.pool = self.select_resource_pool_by_name(self.params['resource_pool']) + + if relospec.pool is None: + self.module.fail_json(msg='Unable to find resource pool "%(resource_pool)s"' % self.params) + + elif relospec.pool != self.current_vm_obj.resourcePool: + task = self.current_vm_obj.RelocateVM_Task(spec=relospec) + self.wait_for_task(task) + change_applied = True # Only send VMWare task if we see a modification if self.change_detected: @@ -1232,7 +1282,7 @@ class PyVmomiHelper(object): # https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.Task.html # https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.TaskInfo.html # https://github.com/virtdevninja/pyvmomi-community-samples/blob/master/samples/tools/tasks.py - while task.info.state not in ['success', 'error']: + while task.info.state not in ['error', 'success']: time.sleep(1) def wait_for_vm_ip(self, vm, poll=100, sleep=5): @@ -1251,61 +1301,20 @@ class PyVmomiHelper(object): return facts -def get_obj(content, vimtype, name): - """ - Return an object by name, if name is None the - first found object is returned - """ - obj = None - container = content.viewManager.CreateContainerView( - content.rootFolder, vimtype, True) - for c in container.view: - if name: - if c.name == name: - obj = c - break - else: - obj = c - break - - container.Destroy() - return obj - - def main(): module = AnsibleModule( argument_spec=dict( - hostname=dict( - type='str', - default=os.environ.get('VMWARE_HOST') - ), - username=dict( - type='str', - default=os.environ.get('VMWARE_USER') - ), - password=dict( - type='str', no_log=True, - default=os.environ.get('VMWARE_PASSWORD') - ), - state=dict( - required=False, - choices=[ - 'poweredon', - 'poweredoff', - 'present', - 'absent', - 'restarted', - 'suspended', - 'shutdownguest', - 'rebootguest' - ], - default='present'), + hostname=dict(type='str', default=os.environ.get('VMWARE_HOST')), + username=dict(type='str', default=os.environ.get('VMWARE_USER')), + password=dict(type='str', default=os.environ.get('VMWARE_PASSWORD'), no_log=True), + state=dict(type='str', default='present', + choices=['absent', 'poweredoff', 'poweredon', 'present', 'rebootguest', 'restarted', 'shutdownguest', 'suspended']), validate_certs=dict(type='bool', default=True), - template_src=dict(type='str', aliases=['template']), + template=dict(type='str', aliases=['template_src']), is_template=dict(type='bool', default=False), annotation=dict(type='str', aliases=['notes']), customvalues=dict(type='list', default=[]), - name=dict(required=True, type='str'), + name=dict(type='str', required=True), name_match=dict(type='str', default='first'), uuid=dict(type='str'), folder=dict(type='str', default='/vm'), @@ -1317,19 +1326,15 @@ def main(): esxi_hostname=dict(type='str'), cluster=dict(type='str'), wait_for_ip_address=dict(type='bool', default=False), - snapshot_src=dict(type='str', default=None), + snapshot_src=dict(type='str'), linked_clone=dict(type='bool', default=False), networks=dict(type='list', default=[]), resource_pool=dict(type='str'), - customization=dict(type='dict', no_log=True, default={}), + customization=dict(type='dict', default={}, no_log=True), ), supports_check_mode=True, mutually_exclusive=[ - ['esxi_hostname', 'cluster'], - ], - required_together=[ - ['state', 'force'], - ['template'], + ['cluster', 'esxi_hostname'], ], ) @@ -1341,10 +1346,9 @@ def main(): module.params['folder'] = module.params['folder'].rstrip('/') pyv = PyVmomiHelper(module) + # Check if the VM exists before continuing - vm = pyv.getvm(name=module.params['name'], - folder=module.params['folder'], - uuid=module.params['uuid']) + vm = pyv.getvm(name=module.params['name'], folder=module.params['folder'], uuid=module.params['uuid']) # VM already exists if vm: @@ -1372,9 +1376,6 @@ def main(): # Create it ... result = pyv.deploy_vm() - if 'failed' not in result: - result['failed'] = False - if result['failed']: module.fail_json(**result) else: diff --git a/test/sanity/pep8/legacy-files.txt b/test/sanity/pep8/legacy-files.txt index ab3eada139..e830f50e8a 100644 --- a/test/sanity/pep8/legacy-files.txt +++ b/test/sanity/pep8/legacy-files.txt @@ -196,7 +196,6 @@ lib/ansible/modules/cloud/vmware/vca_nat.py lib/ansible/modules/cloud/vmware/vca_vapp.py lib/ansible/modules/cloud/vmware/vmware_cluster.py lib/ansible/modules/cloud/vmware/vmware_dvswitch.py -lib/ansible/modules/cloud/vmware/vmware_guest.py lib/ansible/modules/cloud/vmware/vmware_local_user_manager.py lib/ansible/modules/cloud/vmware/vmware_migrate_vmk.py lib/ansible/modules/cloud/vmware/vmware_target_canonical_facts.py