From 87f2893481f127a39f1c9ccd4ba7a5863d4f9d1b Mon Sep 17 00:00:00 2001 From: Abhijeet Kasurde Date: Fri, 19 Jan 2018 09:50:20 +0530 Subject: [PATCH] VMware: Get virtual machine object using property (#33525) This fixes get_vm method to use propertyCollector which can efficiently find the virtual machine from given VMware infrastructure using only name. * VMware: Refactor vmware_guest module * Add nested paths of datacenter * Add tchernomax suggestions Signed-off-by: Abhijeet Kasurde --- lib/ansible/module_utils/vmware.py | 168 ++++++++++++++++-- .../modules/cloud/vmware/vmware_guest.py | 47 ++--- 2 files changed, 176 insertions(+), 39 deletions(-) diff --git a/lib/ansible/module_utils/vmware.py b/lib/ansible/module_utils/vmware.py index e331e91c003..8cd2147a8df 100644 --- a/lib/ansible/module_utils/vmware.py +++ b/lib/ansible/module_utils/vmware.py @@ -801,6 +801,9 @@ def set_vm_power_state(content, vm, state, force): class PyVmomi(object): def __init__(self, module): + """ + Constructor + """ if not HAS_PYVMOMI: module.fail_json(msg='PyVmomi Python module required. Install using "pip install PyVmomi"') @@ -828,41 +831,174 @@ class PyVmomi(object): elif api_type == 'HostAgent': return False + def get_managed_objects_properties(self, vim_type, properties=None): + """ + Function to look up a Managed Object Reference in vCenter / ESXi Environment + :param vim_type: Type of vim object e.g, for datacenter - vim.Datacenter + :param properties: List of properties related to vim object e.g. Name + :return: local content object + """ + # Get Root Folder + root_folder = self.content.rootFolder + + if properties is None: + properties = ['name'] + + # Create Container View with default root folder + mor = self.content.viewManager.CreateContainerView(root_folder, [vim_type], True) + + # Create Traversal spec + traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( + name="traversal_spec", + path='view', + skip=False, + type=vim.view.ContainerView + ) + + # Create Property Spec + property_spec = vmodl.query.PropertyCollector.PropertySpec( + type=vim_type, # Type of object to retrieved + all=False, + pathSet=properties + ) + + # Create Object Spec + object_spec = vmodl.query.PropertyCollector.ObjectSpec( + obj=mor, + skip=True, + selectSet=[traversal_spec] + ) + + # Create Filter Spec + filter_spec = vmodl.query.PropertyCollector.FilterSpec( + objectSet=[object_spec], + propSet=[property_spec], + reportMissingObjectsInResults=False + ) + + return self.content.propertyCollector.RetrieveContents([filter_spec]) + # Virtual Machine related functions def get_vm(self): - vm = None - match_first = (self.params['name_match'] == 'first') + """ + Function to find unique virtual machine either by UUID or Name. + Returns: virtual machine object if found, else None. + + """ + vm_obj = None + user_desired_path = None if self.params['uuid']: - vm = find_vm_by_id(self.content, vm_id=self.params['uuid'], vm_id_type="uuid") - elif self.params['folder'] and self.params['name']: - vm = find_vm_by_id(self.content, vm_id=self.params['name'], vm_id_type="inventory_path", - folder=self.params['folder'], match_first=match_first) + vm_obj = find_vm_by_id(self.content, vm_id=self.params['uuid'], vm_id_type="uuid") - if vm: - self.current_vm_obj = vm + elif self.params['name']: + objects = self.get_managed_objects_properties(vim_type=vim.VirtualMachine, properties=['name']) + vms = [] - return vm + for temp_vm_object in objects: + if len(temp_vm_object.propSet) != 1: + continue + for temp_vm_object_property in temp_vm_object.propSet: + if temp_vm_object_property.val == self.params['name']: + vms.append(temp_vm_object.obj) + break + + # get_managed_objects_properties may return multiple virtual machine, + # following code tries to find user desired one depending upon the folder specified. + if len(vms) > 1: + # We have found multiple virtual machines, decide depending upon folder value + if self.params['folder'] is None: + self.module.fail_json(msg="Multiple virtual machines with same name [%s] found, " + "Folder value is a required parameter to find uniqueness " + "of the virtual machine" % self.params['name'], + details="Please see documentation of the vmware_guest module " + "for folder parameter.") + + # Get folder path where virtual machine is located + # User provided folder where user thinks virtual machine is present + user_folder = self.params['folder'] + # User defined datacenter + user_defined_dc = self.params['datacenter'] + # User defined datacenter's object + datacenter_obj = find_datacenter_by_name(self.content, self.params['datacenter']) + # Get Path for Datacenter + dcpath = compile_folder_path_for_object(vobj=datacenter_obj) + + # Nested folder does not return trailing / + if not dcpath.endswith('/'): + dcpath += '/' + + if user_folder in [None, '', '/']: + # User provided blank value or + # User provided only root value, we fail + self.module.fail_json(msg="vmware_guest found multiple virtual machines with same " + "name [%s], please specify folder path other than blank " + "or '/'" % self.params['name']) + elif user_folder.startswith('/vm/'): + # User provided nested folder under VMware default vm folder i.e. folder = /vm/india/finance + user_desired_path = "%s%s%s" % (dcpath, user_defined_dc, user_folder) + else: + # User defined datacenter is not nested i.e. dcpath = '/' , or + # User defined datacenter is nested i.e. dcpath = '/F0/DC0' or + # User provided folder starts with / and datacenter i.e. folder = /ha-datacenter/ or + # User defined folder starts with datacenter without '/' i.e. + # folder = DC0/vm/india/finance or + # folder = DC0/vm + user_desired_path = user_folder + + for vm in vms: + # Check if user has provided same path as virtual machine + actual_vm_folder_path = self.get_vm_path(content=self.content, vm_name=vm) + if not actual_vm_folder_path.startswith("%s%s" % (dcpath, user_defined_dc)): + continue + if user_desired_path in actual_vm_folder_path: + vm_obj = vm + break + elif vms: + # Unique virtual machine found. + vm_obj = vms[0] + + if vm_obj: + self.current_vm_obj = vm_obj + + return vm_obj def gather_facts(self, vm): + """ + Function to gather facts of virtual machine. + Args: + vm: Name of virtual machine. + + Returns: Facts dictionary of the given virtual machine. + + """ return gather_vm_facts(self.content, vm) @staticmethod - def get_vm_path(content, vm): - foldername = None - folder = vm.parent + def get_vm_path(content, vm_name): + """ + Function to find the path of virtual machine. + Args: + content: VMware content object + vm_name: virtual machine managed object + + Returns: Folder of virtual machine if exists, else None + + """ + folder_name = None + folder = vm_name.parent if folder: - foldername = folder.name + folder_name = folder.name fp = folder.parent # climb back up the tree to find our path, stop before the root folder while fp is not None and fp.name is not None and fp != content.rootFolder: - foldername = fp.name + '/' + foldername + folder_name = fp.name + '/' + folder_name try: fp = fp.parent except: break - foldername = '/' + foldername - return foldername + folder_name = '/' + folder_name + return folder_name # Cluster related functions def find_cluster_by_name(self, cluster_name, datacenter_name=None): diff --git a/lib/ansible/modules/cloud/vmware/vmware_guest.py b/lib/ansible/modules/cloud/vmware/vmware_guest.py index 5376cc2b424..1e923cd223e 100644 --- a/lib/ansible/modules/cloud/vmware/vmware_guest.py +++ b/lib/ansible/modules/cloud/vmware/vmware_guest.py @@ -78,7 +78,6 @@ options: - ' folder: /folder1/datacenter1/vm' - ' folder: folder1/datacenter1/vm' - ' folder: /folder1/datacenter1/vm/folder2' - default: /vm hardware: description: - Manage some VM hardware attributes. @@ -1433,16 +1432,19 @@ class PyVmomiHelper(PyVmomi): # - multiple templates by the same name # - static IPs - # datacenters = get_all_objs(self.content, [vim.Datacenter]) + self.folder = self.params.get('folder', None) + if self.folder is None: + self.module.fail_json(msg="Folder is required parameter while deploying new virtual machine") + + # Prepend / if it was missing from the folder path, also strip trailing slashes + if not self.folder.startswith('/'): + self.folder = '/%(folder)s' % self.params + self.folder = self.folder.rstrip('/') + datacenter = self.cache.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) - # Prepend / if it was missing from the folder path, also strip trailing slashes - if not self.params['folder'].startswith('/'): - self.params['folder'] = '/%(folder)s' % self.params - self.params['folder'] = self.params['folder'].rstrip('/') - dcpath = compile_folder_path_for_object(datacenter) # Nested folder does not have trailing / @@ -1450,15 +1452,15 @@ class PyVmomiHelper(PyVmomi): dcpath += '/' # Check for full path first in case it was already supplied - if (self.params['folder'].startswith(dcpath + self.params['datacenter'] + '/vm') or - self.params['folder'].startswith(dcpath + '/' + self.params['datacenter'] + '/vm')): - fullpath = self.params['folder'] - elif self.params['folder'].startswith('/vm/') or self.params['folder'] == '/vm': - fullpath = "%s%s%s" % (dcpath, self.params['datacenter'], self.params['folder']) - elif self.params['folder'].startswith('/'): - fullpath = "%s%s/vm%s" % (dcpath, self.params['datacenter'], self.params['folder']) + if (self.folder.startswith(dcpath + self.params['datacenter'] + '/vm') or + self.folder.startswith(dcpath + '/' + self.params['datacenter'] + '/vm')): + fullpath = self.folder + elif self.folder.startswith('/vm/') or self.folder == '/vm': + fullpath = "%s%s%s" % (dcpath, self.params['datacenter'], self.folder) + elif self.folder.startswith('/'): + fullpath = "%s%s/vm%s" % (dcpath, self.params['datacenter'], self.folder) else: - fullpath = "%s%s/vm/%s" % (dcpath, self.params['datacenter'], self.params['folder']) + fullpath = "%s%s/vm/%s" % (dcpath, self.params['datacenter'], self.folder) f_obj = self.content.searchIndex.FindByInventoryPath(fullpath) @@ -1468,10 +1470,10 @@ class PyVmomiHelper(PyVmomi): details = { 'datacenter': datacenter.name, 'datacenter_path': dcpath, - 'folder': self.params['folder'], + 'folder': self.folder, 'full_search_path': fullpath, } - self.module.fail_json(msg='No folder %s matched in the search path : %s' % (self.params['folder'], fullpath), + self.module.fail_json(msg='No folder %s matched in the search path : %s' % (self.folder, fullpath), details=details) destfolder = f_obj @@ -1751,10 +1753,10 @@ def main(): is_template=dict(type='bool', default=False), annotation=dict(type='str', aliases=['notes']), customvalues=dict(type='list', default=[]), - name=dict(type='str', required=True), + name=dict(type='str'), name_match=dict(type='str', choices=['first', 'last'], default='first'), uuid=dict(type='str'), - folder=dict(type='str', default='/vm'), + folder=dict(type='str'), guest_id=dict(type='str'), disk=dict(type='list', default=[]), cdrom=dict(type='dict', default={}), @@ -1776,14 +1778,13 @@ def main(): mutually_exclusive=[ ['cluster', 'esxi_hostname'], ], + required_one_of=[ + ['name', 'uuid'], + ], ) result = {'failed': False, 'changed': False} - # FindByInventoryPath() does not require an absolute path - # so we should leave the input folder path unmodified - module.params['folder'] = module.params['folder'].rstrip('/') - pyv = PyVmomiHelper(module) # Check if the VM exists before continuing