@ -74,8 +74,21 @@ options:
version_added : ' 2.3 '
version_added : ' 2.3 '
folder :
folder :
description :
description :
- Destination folder , absolute path to find an existing guest or create the new guest .
- Destination folder , absolute or relative path to find an existing guest or create the new guest .
default : /
- The folder should include the datacenter . ESX ' s datacenter is ha-datacenter
- ' Examples: '
- ' folder: /ha-datacenter/vm '
- ' folder: ha-datacenter/vm '
- ' folder: /datacenter1/vm '
- ' folder: datacenter1/vm '
- ' folder: /datacenter1/vm/folder1 '
- ' folder: datacenter1/vm/folder1 '
- ' folder: /folder1/datacenter1/vm '
- ' folder: folder1/datacenter1/vm '
- ' folder: /folder1/datacenter1/vm/folder2 '
- ' folder: vm/folder2 '
- ' fodler: folder2 '
default : / vm
hardware :
hardware :
description :
description :
- Manage some VM hardware attributes .
- Manage some VM hardware attributes .
@ -303,9 +316,8 @@ import time
from ansible . module_utils . basic import AnsibleModule
from ansible . module_utils . basic import AnsibleModule
from ansible . module_utils . pycompat24 import get_exception
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 connect_to_api , find_obj , gather_vm_facts , get_all_objs
from ansible . module_utils . vmware import connect_to_api , find_obj , gather_vm_facts , get_all_objs
from ansible . module_utils . vmware import serialize_spec
try :
try :
import json
import json
@ -424,30 +436,77 @@ class PyVmomiDeviceHelper(object):
class PyVmomiCache ( object ) :
class PyVmomiCache ( object ) :
""" This class caches references to objects which are requested multiples times but not modified """
""" This class caches references to objects which are requested multiples times but not modified """
def __init__ ( self , content ):
def __init__ ( self , content , dc_name = None ):
self . content = content
self . content = content
self . dc_name = dc_name
self . networks = { }
self . networks = { }
self . clusters = { }
self . clusters = { }
self . esx_hosts = { }
self . esx_hosts = { }
self . parent_datacenters = { }
def find_obj ( self , content , types , name , confine_to_datacenter = True ) :
""" Wrapper around find_obj to set datacenter context """
result = find_obj ( content , types , name )
if result and confine_to_datacenter :
if self . get_parent_datacenter ( result ) . name != self . dc_name :
objects = self . get_all_objs ( content , types , confine_to_datacenter = True )
for obj in objects :
if obj . name == name :
return obj
return result
def get_all_objs ( self , content , types , confine_to_datacenter = True ) :
""" Wrapper around get_all_objs to set datacenter context """
objects = get_all_objs ( content , [ vim . Datastore ] )
if confine_to_datacenter :
if hasattr ( objects , ' items ' ) :
# resource pools come back as a dictionary
for k , v in objects . items ( ) :
parent_dc = self . get_parent_datacenter ( k )
if parent_dc . name != self . dc_name :
objects . pop ( k , None )
else :
# everything else should be a list
objects = [ x for x in objects if self . get_parent_datacenter ( x ) . name == self . dc_name ]
return objects
def get_network ( self , network ) :
def get_network ( self , network ) :
if network not in self . networks :
if network not in self . networks :
self . networks [ network ] = find_obj ( self . content , [ vim . Network ] , network )
self . networks [ network ] = self . find_obj ( self . content , [ vim . Network ] , network )
return self . networks [ network ]
return self . networks [ network ]
def get_cluster ( self , cluster ) :
def get_cluster ( self , cluster ) :
if cluster not in self . clusters :
if cluster not in self . clusters :
self . clusters [ cluster ] = find_obj ( self . content , [ vim . ClusterComputeResource ] , cluster )
self . clusters [ cluster ] = self . find_obj ( self . content , [ vim . ClusterComputeResource ] , cluster )
return self . clusters [ cluster ]
return self . clusters [ cluster ]
def get_esx_host ( self , host ) :
def get_esx_host ( self , host ) :
if host not in self . esx_hosts :
if host not in self . esx_hosts :
self . esx_hosts [ host ] = find_obj ( self . content , [ vim . HostSystem ] , host )
self . esx_hosts [ host ] = self . find_obj ( self . content , [ vim . HostSystem ] , host )
return self . esx_hosts [ host ]
return self . esx_hosts [ host ]
def get_parent_datacenter ( self , obj ) :
""" Walk the parent tree to find the objects datacenter """
if isinstance ( obj , vim . Datacenter ) :
return obj
if obj in self . parent_datacenters :
return self . parent_datacenters [ obj ]
datacenter = None
while True :
if not hasattr ( obj , ' parent ' ) :
break
obj = obj . parent
if isinstance ( obj , vim . Datacenter ) :
datacenter = obj
break
self . parent_datacenters [ obj ] = datacenter
return datacenter
class PyVmomiHelper ( object ) :
class PyVmomiHelper ( object ) :
def __init__ ( self , module ) :
def __init__ ( self , module ) :
@ -463,7 +522,7 @@ class PyVmomiHelper(object):
self . change_detected = False
self . change_detected = False
self . customspec = None
self . customspec = None
self . current_vm_obj = None
self . current_vm_obj = None
self . cache = PyVmomiCache ( self . content )
self . cache = PyVmomiCache ( self . content , dc_name = self . params [ ' datacenter ' ] )
def getvm ( self , name = None , uuid = None , folder = None ) :
def getvm ( self , name = None , uuid = None , folder = None ) :
@ -476,19 +535,21 @@ class PyVmomiHelper(object):
if uuid :
if uuid :
vm = self . content . searchIndex . FindByUuid ( uuid = uuid , vmSearch = True )
vm = self . content . searchIndex . FindByUuid ( uuid = uuid , vmSearch = True )
elif folder :
elif folder :
# Build the absolute folder path to pass into the search method
# searchpaths do not need to be absolute
if not self . params [ ' folder ' ] . startswith ( ' / ' ) :
searchpath = self . params [ ' folder ' ]
self . module . fail_json ( msg = " Folder %(folder)s needs to be an absolute path, starting with ' / ' . " % self . params )
searchpath = ' %(datacenter)s %(folder)s ' % self . params
# get all objects for this path ...
# get all objects for this path ...
f_obj = self . content . searchIndex . FindByInventoryPath ( searchpath )
f_obj = self . content . searchIndex . FindByInventoryPath ( searchpath )
if f_obj :
if f_obj :
if isinstance ( f_obj , vim . Datacenter ) :
if isinstance ( f_obj , vim . Datacenter ) :
f_obj = f_obj . vmFolder
f_obj = f_obj . vmFolder
for c_obj in f_obj . childEntity :
for c_obj in f_obj . childEntity :
if not isinstance ( c_obj , vim . VirtualMachine ) :
if not isinstance ( c_obj , vim . VirtualMachine ) :
continue
continue
if c_obj . name == name :
if c_obj . name == name :
vm = c_obj
vm = c_obj
if self . params [ ' name_match ' ] == ' first ' :
if self . params [ ' name_match ' ] == ' first ' :
@ -650,7 +711,7 @@ class PyVmomiHelper(object):
self . module . fail_json ( msg = " Network ' %(name)s ' does not exists " % network )
self . module . fail_json ( msg = " Network ' %(name)s ' does not exists " % network )
elif ' vlan ' in network :
elif ' vlan ' in network :
dvps = get_all_objs ( self . content , [ vim . dvs . DistributedVirtualPortgroup ] )
dvps = self . cache . get_all_objs ( self . content , [ vim . dvs . DistributedVirtualPortgroup ] )
for dvp in dvps :
for dvp in dvps :
if hasattr ( dvp . config . defaultPortConfig , ' vlan ' ) and dvp . config . defaultPortConfig . vlan . vlanId == network [ ' vlan ' ] :
if hasattr ( dvp . config . defaultPortConfig , ' vlan ' ) and dvp . config . defaultPortConfig . vlan . vlanId == network [ ' vlan ' ] :
network [ ' name ' ] = dvp . config . name
network [ ' name ' ] = dvp . config . name
@ -994,13 +1055,32 @@ class PyVmomiHelper(object):
return hostsystem
return hostsystem
def autoselect_datastore ( self ) :
datastore = None
datastore_name = None
datastores = self . cache . get_all_objs ( self . content , [ vim . Datastore ] )
if datastores is None or len ( datastores ) == 0 :
self . module . fail_json ( msg = " Unable to find a datastore list when autoselecting " )
datastore_freespace = 0
for ds in datastores :
if ds . summary . freeSpace > datastore_freespace :
datastore = ds
datastore_name = datastore . name
datastore_freespace = ds . summary . freeSpace
return datastore
def select_datastore ( self , vm_obj = None ) :
def select_datastore ( self , vm_obj = None ) :
datastore = None
datastore = None
datastore_name = None
datastore_name = None
if len ( self . params [ ' disk ' ] ) != 0 :
if len ( self . params [ ' disk ' ] ) != 0 :
# TODO: really use the datastore for newly created disks
# TODO: really use the datastore for newly created disks
if ' autoselect_datastore ' in self . params [ ' disk ' ] [ 0 ] and self . params [ ' disk ' ] [ 0 ] [ ' autoselect_datastore ' ] :
if ' autoselect_datastore ' in self . params [ ' disk ' ] [ 0 ] and self . params [ ' disk ' ] [ 0 ] [ ' autoselect_datastore ' ] :
datastores = get_all_objs ( self . content , [ vim . Datastore ] )
datastores = self . cache . get_all_objs ( self . content , [ vim . Datastore ] )
datastores = [ x for x in datastores if self . cache . get_parent_datacenter ( x ) . name == self . params [ ' datacenter ' ] ]
if datastores is None or len ( datastores ) == 0 :
if datastores is None or len ( datastores ) == 0 :
self . module . fail_json ( msg = " Unable to find a datastore list when autoselecting " )
self . module . fail_json ( msg = " Unable to find a datastore list when autoselecting " )
@ -1019,14 +1099,23 @@ class PyVmomiHelper(object):
elif ' datastore ' in self . params [ ' disk ' ] [ 0 ] :
elif ' datastore ' in self . params [ ' disk ' ] [ 0 ] :
datastore_name = self . params [ ' disk ' ] [ 0 ] [ ' datastore ' ]
datastore_name = self . params [ ' disk ' ] [ 0 ] [ ' datastore ' ]
datastore = find_obj ( self . content , [ vim . Datastore ] , datastore_name )
datastore = self . cache . find_obj ( self . content , [ vim . Datastore ] , datastore_name )
else :
else :
self . module . fail_json ( msg = " Either datastore or autoselect_datastore should be provided to select datastore " )
self . module . fail_json ( msg = " Either datastore or autoselect_datastore should be provided to select datastore " )
if not datastore and self . params [ ' template ' ] :
if not datastore and self . params [ ' template ' ] :
# use the template's existing DS
# use the template's existing DS
disks = [ x for x in vm_obj . config . hardware . device if isinstance ( x , vim . vm . device . VirtualDisk ) ]
disks = [ x for x in vm_obj . config . hardware . device if isinstance ( x , vim . vm . device . VirtualDisk ) ]
datastore = disks [ 0 ] . backing . datastore
if disks :
datastore_name = datastore . name
datastore = disks [ 0 ] . backing . datastore
datastore_name = datastore . name
# validation
if datastore :
dc = self . cache . get_parent_datacenter ( datastore )
if dc . name != self . params [ ' datacenter ' ] :
datastore = self . autoselect_datastore ( )
datastore_name = datastore . name
if not datastore :
if not datastore :
self . module . fail_json ( msg = " Failed to find a matching datastore " )
self . module . fail_json ( msg = " Failed to find a matching datastore " )
@ -1045,13 +1134,13 @@ class PyVmomiHelper(object):
return False
return False
def select_resource_pool_by_name ( self , resource_pool_name ) :
def select_resource_pool_by_name ( self , resource_pool_name ) :
resource_pool = find_obj ( self . content , [ vim . ResourcePool ] , resource_pool_name )
resource_pool = self . cache . find_obj ( self . content , [ vim . ResourcePool ] , resource_pool_name )
if resource_pool is None :
if resource_pool is None :
self . module . fail_json ( msg = ' Could not find resource_pool " %s " ' % resource_pool_name )
self . module . fail_json ( msg = ' Could not find resource_pool " %s " ' % resource_pool_name )
return resource_pool
return resource_pool
def select_resource_pool_by_host ( self , host ) :
def select_resource_pool_by_host ( self , host ) :
resource_pools = get_all_objs ( self . content , [ vim . ResourcePool ] )
resource_pools = self . cache . get_all_objs ( self . content , [ vim . ResourcePool ] )
for rp in resource_pools . items ( ) :
for rp in resource_pools . items ( ) :
if not rp [ 0 ] :
if not rp [ 0 ] :
continue
continue
@ -1082,6 +1171,39 @@ class PyVmomiHelper(object):
self . module . fail_json ( msg = " hardware.scsi attribute should be ' paravirtual ' or ' lsilogic ' " )
self . module . fail_json ( msg = " hardware.scsi attribute should be ' paravirtual ' or ' lsilogic ' " )
return disk_controller_type
return disk_controller_type
def find_folder ( self , searchpath ) :
""" Walk inventory objects one position of the searchpath at a time """
# split the searchpath so we can iterate through it
paths = [ x . replace ( ' / ' , ' ' ) for x in searchpath . split ( ' / ' ) ]
paths_total = len ( paths ) - 1
position = 0
# recursive walk while looking for next element in searchpath
root = self . content . rootFolder
while root and position < = paths_total :
change = False
if hasattr ( root , ' childEntity ' ) :
for child in root . childEntity :
if child . name == paths [ position ] :
root = child
position + = 1
change = True
break
elif isinstance ( root , vim . Datacenter ) :
if hasattr ( root , ' vmFolder ' ) :
if root . vmFolder . name == paths [ position ] :
root = root . vmFolder
position + = 1
change = True
else :
root = None
if not change :
root = None
return root
def deploy_vm ( self ) :
def deploy_vm ( self ) :
# https://github.com/vmware/pyvmomi-community-samples/blob/master/samples/clone_vm.py
# https://github.com/vmware/pyvmomi-community-samples/blob/master/samples/clone_vm.py
# https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.vm.CloneSpec.html
# https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.vm.CloneSpec.html
@ -1094,15 +1216,21 @@ class PyVmomiHelper(object):
# - static IPs
# - static IPs
# datacenters = get_all_objs(self.content, [vim.Datacenter])
# datacenters = get_all_objs(self.content, [vim.Datacenter])
datacenter = find_obj ( self . content , [ vim . Datacenter ] , self . params [ ' datacenter ' ] )
datacenter = self . cache . find_obj ( self . content , [ vim . Datacenter ] , self . params [ ' datacenter ' ] )
if datacenter is None :
if datacenter is None :
self . module . fail_json ( msg = ' No datacenter named %(datacenter)s was found ' % self . params )
self . module . fail_json ( msg = ' No datacenter named %(datacenter)s was found ' % self . params )
destfolder = None
destfolder = None
if not self . params [ ' folder ' ] . startswith ( ' / ' ) :
self . module . fail_json ( msg = " Folder %(folder)s needs to be an absolute path, starting with ' / ' . " % self . params )
f_obj = self . content . searchIndex . FindByInventoryPath ( ' / %(datacenter)s %(folder)s ' % self . params )
# make an attempt with findinventorybypath, although this works poorly
# when datacenters are nested under folders
f_obj = self . content . searchIndex . FindByInventoryPath ( ' %(folder)s ' % self . params )
# retry by walking down the object tree
if f_obj is None :
f_obj = self . find_folder ( self . params [ ' folder ' ] )
# abort if no strategy was successful
if f_obj is None :
if f_obj is None :
self . module . fail_json ( msg = ' No folder matched the path: %(folder)s ' % self . params )
self . module . fail_json ( msg = ' No folder matched the path: %(folder)s ' % self . params )
destfolder = f_obj
destfolder = f_obj
@ -1115,8 +1243,13 @@ class PyVmomiHelper(object):
else :
else :
vm_obj = None
vm_obj = None
if self . params [ ' resource_pool ' ] :
# need a resource pool if cloning from template
resource_pool = self . select_resource_pool_by_name ( self . params [ ' resource_pool ' ] )
if self . params [ ' resource_pool ' ] or self . params [ ' template ' ] :
if self . params [ ' esxi_hostname ' ] :
host = self . select_host ( )
resource_pool = self . select_resource_pool_by_host ( host )
else :
resource_pool = self . select_resource_pool_by_name ( self . params [ ' resource_pool ' ] )
if resource_pool is None :
if resource_pool is None :
self . module . fail_json ( msg = ' Unable to find resource pool " %(resource_pool)s " ' % self . params )
self . module . fail_json ( msg = ' Unable to find resource pool " %(resource_pool)s " ' % self . params )
@ -1143,6 +1276,8 @@ class PyVmomiHelper(object):
if len ( self . params [ ' customization ' ] ) > 0 or network_changes is True :
if len ( self . params [ ' customization ' ] ) > 0 or network_changes is True :
self . customize_vm ( vm_obj = vm_obj )
self . customize_vm ( vm_obj = vm_obj )
clonespec = None
clone_method = None
try :
try :
if self . params [ ' template ' ] :
if self . params [ ' template ' ] :
# create the relocation spec
# create the relocation spec
@ -1153,8 +1288,9 @@ class PyVmomiHelper(object):
relospec . host = self . select_host ( )
relospec . host = self . select_host ( )
relospec . datastore = datastore
relospec . datastore = datastore
if self . params [ ' resource_pool ' ] :
# https://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/vim.vm.RelocateSpec.html
relospec . pool = resource_pool
# > pool: For a clone operation from a template to a virtual machine, this argument is required.
relospec . pool = resource_pool
if self . params [ ' snapshot_src ' ] is not None and self . params [ ' linked_clone ' ] :
if self . params [ ' snapshot_src ' ] is not None and self . params [ ' linked_clone ' ] :
relospec . diskMoveType = vim . vm . RelocateSpec . DiskMoveOptions . createNewChildDiskBacking
relospec . diskMoveType = vim . vm . RelocateSpec . DiskMoveOptions . createNewChildDiskBacking
@ -1171,6 +1307,7 @@ class PyVmomiHelper(object):
clonespec . snapshot = snapshot [ 0 ] . snapshot
clonespec . snapshot = snapshot [ 0 ] . snapshot
clonespec . config = self . configspec
clonespec . config = self . configspec
clone_method = ' Clone '
task = vm_obj . Clone ( folder = destfolder , name = self . params [ ' name ' ] , spec = clonespec )
task = vm_obj . Clone ( folder = destfolder , name = self . params [ ' name ' ] , spec = clonespec )
self . change_detected = True
self . change_detected = True
else :
else :
@ -1181,6 +1318,7 @@ class PyVmomiHelper(object):
suspendDirectory = None ,
suspendDirectory = None ,
vmPathName = " [ " + datastore_name + " ] " + self . params [ " name " ] )
vmPathName = " [ " + datastore_name + " ] " + self . params [ " name " ] )
clone_method = ' CreateVM_Task '
task = destfolder . CreateVM_Task ( config = self . configspec , pool = resource_pool )
task = destfolder . CreateVM_Task ( config = self . configspec , pool = resource_pool )
self . change_detected = True
self . change_detected = True
self . wait_for_task ( task )
self . wait_for_task ( task )
@ -1191,7 +1329,20 @@ class PyVmomiHelper(object):
if task . info . state == ' error ' :
if task . info . state == ' error ' :
# https://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=2021361
# https://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=2021361
# https://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=2173
# https://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=2173
return { ' changed ' : self . change_detected , ' failed ' : True , ' msg ' : task . info . error . msg }
# provide these to the user for debugging
clonespec_json = serialize_spec ( clonespec )
configspec_json = serialize_spec ( self . configspec )
kwargs = {
' changed ' : self . change_detected ,
' failed ' : True ,
' msg ' : task . info . error . msg ,
' clonespec ' : clonespec_json ,
' configspec ' : configspec_json ,
' clone_method ' : clone_method
}
return kwargs
else :
else :
# set annotation
# set annotation
vm = task . info . result
vm = task . info . result
@ -1340,9 +1491,8 @@ def main():
result = { ' failed ' : False , ' changed ' : False }
result = { ' failed ' : False , ' changed ' : False }
# Prepend /vm if it was missing from the folder path, also strip trailing slashes
# FindByInventoryPath() does not require an absolute path
if not module . params [ ' folder ' ] . startswith ( ' /vm ' ) and module . params [ ' folder ' ] . startswith ( ' / ' ) :
# so we should leave the input folder path unmodified
module . params [ ' folder ' ] = ' /vm %(folder)s ' % module . params
module . params [ ' folder ' ] = module . params [ ' folder ' ] . rstrip ( ' / ' )
module . params [ ' folder ' ] = module . params [ ' folder ' ] . rstrip ( ' / ' )
pyv = PyVmomiHelper ( module )
pyv = PyVmomiHelper ( module )