From 0d1c06814f0cd2a73fd25a39df57af51ccf1b720 Mon Sep 17 00:00:00 2001 From: Gregory DEPUILLE Date: Tue, 17 Jul 2018 17:13:30 +0200 Subject: [PATCH] vmware_vmotion: added storage vmotion capabilities. (#21421) This fix adds vmotion capabilities to vmware_vmotion across different datastores and different hostsystem. --- lib/ansible/module_utils/vmware.py | 5 +- .../modules/cloud/vmware/vmware_vmotion.py | 324 +++++++++++++----- 2 files changed, 242 insertions(+), 87 deletions(-) diff --git a/lib/ansible/module_utils/vmware.py b/lib/ansible/module_utils/vmware.py index 4d9ab68b24..886994e535 100644 --- a/lib/ansible/module_utils/vmware.py +++ b/lib/ansible/module_utils/vmware.py @@ -22,9 +22,8 @@ except ImportError: HAS_PYVMOMI = False from ansible.module_utils._text import to_text -from ansible.module_utils.six import integer_types, iteritems, string_types +from ansible.module_utils.six import integer_types, iteritems, string_types, raise_from from ansible.module_utils.basic import env_fallback -from ansible.module_utils import six class TaskError(Exception): @@ -57,7 +56,7 @@ def wait_for_task(task, max_backoff=64, timeout=3600): except AttributeError: pass finally: - six.raise_from(TaskError(error_msg), task.info.error) + raise_from(TaskError(error_msg), task.info.error) if task.info.state in [vim.TaskInfo.State.running, vim.TaskInfo.State.queued]: sleep_time = min(2 ** failure_counter + randint(1, 1000), max_backoff) time.sleep(sleep_time) diff --git a/lib/ansible/modules/cloud/vmware/vmware_vmotion.py b/lib/ansible/modules/cloud/vmware/vmware_vmotion.py index b0193fa8d0..a32fed3ee7 100644 --- a/lib/ansible/modules/cloud/vmware/vmware_vmotion.py +++ b/lib/ansible/modules/cloud/vmware/vmware_vmotion.py @@ -2,25 +2,33 @@ # -*- coding: utf-8 -*- # Copyright: (c) 2015, Bede Carroll + +# Copyright: (c) 2018, Abhijeet Kasurde +# Copyright: (c) 2018, Ansible Project +# # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} -DOCUMENTATION = ''' +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = r''' --- module: vmware_vmotion -short_description: Move a virtual machine using vMotion +short_description: Move a virtual machine using vMotion, and/or its vmdks using storage vMotion. description: - Using VMware vCenter, move a virtual machine using vMotion to a different - host. + host, and/or its vmdks to another datastore using storage vMotion. version_added: 2.2 author: - Bede Carroll (@bedecarroll) +- Olivier Boukili (@oboukili) notes: - Tested on vSphere 6.0 requirements: @@ -28,30 +36,58 @@ requirements: - pyVmomi options: vm_name: - description: - - Name of the VM to perform a vMotion on - required: True - aliases: ['vm'] + description: + - Name of the VM to perform a vMotion on. + - This is required parameter, if C(vm_uuid) is not set. + - Version 2.6 onwards, this parameter is not a required parameter, unlike the previous versions. + aliases: ['vm'] + vm_uuid: + description: + - UUID of the virtual machine to perform a vMotion operation on. + - This is a required parameter, if C(vm_name) is not set. + aliases: ['uuid'] + version_added: 2.7 destination_host: - description: - - Name of the end host the VM should be running on - required: True - aliases: ['destination'] + description: + - Name of the destination host the virtual machine should be running on. + - Version 2.6 onwards, this parameter is not a required parameter, unlike the previous versions. + aliases: ['destination'] + destination_datastore: + description: + - "Name of the destination datastore the virtual machine's vmdk should be moved on." + aliases: ['datastore'] + version_added: 2.7 extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' -# Example from Ansible playbook +- name: Perform vMotion of virtual machine + vmware_vmotion: + hostname: 'vcenter_hostname' + username: 'vcenter_username' + password: 'vcenter_password' + validate_certs: False + vm_name: 'vm_name_as_per_vcenter' + destination_host: 'destination_host_as_per_vcenter' - - name: Perform vMotion of VM - local_action: - module: vmware_vmotion - hostname: 'vcenter_hostname' - username: 'vcenter_username' - password: 'vcenter_password' - validate_certs: False - vm_name: 'vm_name_as_per_vcenter' - destination_host: 'destination_host_as_per_vcenter' +- name: Perform storage vMotion of of virtual machine + vmware_vmotion: + hostname: 'vcenter_hostname' + username: 'vcenter_username' + password: 'vcenter_password' + validate_certs: False + vm_name: 'vm_name_as_per_vcenter' + destination_datastore: 'destination_datastore_as_per_vcenter' + +- name: Perform storage vMotion and host vMotion of virtual machine + vmware_vmotion: + hostname: 'vcenter_hostname' + username: 'vcenter_username' + password: 'vcenter_password' + validate_certs: False + vm_name: 'vm_name_as_per_vcenter' + destination_host: 'destination_host_as_per_vcenter' + destination_datastore: 'destination_datastore_as_per_vcenter' ''' RETURN = ''' @@ -64,81 +100,201 @@ running_host: try: from pyVmomi import vim - HAS_PYVMOMI = True except ImportError: - HAS_PYVMOMI = False + pass +from ansible.module_utils._text import to_native from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.vmware import (connect_to_api, find_hostsystem_by_name, find_vm_by_name, - vmware_argument_spec, wait_for_task) +from ansible.module_utils.vmware import (PyVmomi, find_hostsystem_by_name, + find_vm_by_id, find_datastore_by_name, + vmware_argument_spec, wait_for_task, TaskError) -def migrate_vm(vm_object, host_object): - """ - Migrate virtual machine and return the task. - """ - relocate_spec = vim.vm.RelocateSpec(host=host_object) - task_object = vm_object.Relocate(relocate_spec) - return task_object +class VmotionManager(PyVmomi): + def __init__(self, module): + super(VmotionManager, self).__init__(module) + self.vm = None + self.vm_uuid = self.params.get('vm_uuid', None) + self.vm_name = self.params.get('vm_name', None) + result = dict() + + self.get_vm() + if self.vm is None: + self.module.fail_json(msg="Failed to find the virtual" + " machine with %s" % (self.vm_uuid or self.vm_name)) + + # Get Destination Host System if specified by user + dest_host_name = self.params.get('destination_host', None) + self.host_object = None + if dest_host_name is not None: + self.host_object = find_hostsystem_by_name(content=self.content, + hostname=dest_host_name) + + # Get Destination Datastore if specified by user + dest_datastore = self.params.get('destination_datastore', None) + self.datastore_object = None + if dest_datastore is not None: + self.datastore_object = find_datastore_by_name(content=self.content, + datastore_name=dest_datastore) + + # Atleast one of datastore, host system is required to migrate + if self.datastore_object is None and self.host_object is None: + self.module.fail_json(msg="Unable to find destination datastore" + " and destination host system.") + + # Check if datastore is required, this check is required if destination + # and source host system does not share same datastore. + host_datastore_required = [] + for vm_datastore in self.vm.datastore: + if self.host_object and vm_datastore not in self.host_object.datastore: + host_datastore_required.append(True) + else: + host_datastore_required.append(False) + + if any(host_datastore_required) and dest_datastore is None: + msg = "Destination host system does not share" \ + " datastore ['%s'] with source host system ['%s'] on which" \ + " virtual machine is located. Please specify destination_datastore" \ + " to rectify this problem." % ("', '".join([ds.name for ds in self.host_object.datastore]), + "', '".join([ds.name for ds in self.vm.datastore])) + + self.module.fail_json(msg=msg) + + storage_vmotion_needed = True + change_required = True + + if self.host_object and self.datastore_object: + # We have both host system and datastore object + if not self.datastore_object.summary.accessible: + # Datastore is not accessible + self.module.fail_json(msg='Destination datastore %s is' + ' not accessible.' % dest_datastore) + + if self.datastore_object not in self.host_object.datastore: + # Datastore is not associated with host system + self.module.fail_json(msg="Destination datastore %s provided" + " is not associated with destination" + " host system %s. Please specify" + " datastore value ['%s'] associated with" + " the given host system." % (dest_datastore, + dest_host_name, + "', '".join([ds.name for ds in self.host_object.datastore]))) + + if self.vm.runtime.host.name == dest_host_name and dest_datastore in [ds.name for ds in self.vm.datastore]: + change_required = False + + if self.host_object and self.datastore_object is None: + if self.vm.runtime.host.name == dest_host_name: + # VM is already located on same host + change_required = False + + storage_vmotion_needed = False + + elif self.datastore_object and self.host_object is None: + if self.datastore_object in self.vm.datastore: + # VM is already located on same datastore + change_required = False + + if not self.datastore_object.summary.accessible: + # Datastore is not accessible + self.module.fail_json(msg='Destination datastore %s is' + ' not accessible.' % dest_datastore) + + if module.check_mode: + result['running_host'] = module.params['destination_host'] + result['changed'] = True + module.exit_json(**result) + + if change_required: + # Migrate VM and get Task object back + task_object = self.migrate_vm() + # Wait for task to complete + try: + wait_for_task(task_object) + except TaskError as task_error: + self.module.fail_json(msg=to_native(task_error)) + # If task was a success the VM has moved, update running_host and complete module + if task_object.info.state == vim.TaskInfo.State.success: + # The storage layout is not automatically refreshed, so we trigger it to get coherent module return values + if storage_vmotion_needed: + self.vm.RefreshStorageInfo() + result['running_host'] = module.params['destination_host'] + result['changed'] = True + module.exit_json(**result) + else: + msg = 'Unable to migrate virtual machine due to an error, please check vCenter' + if task_object.info.error is not None: + msg += " : %s" % task_object.info.error + module.fail_json(msg=msg) + else: + try: + host = self.vm.summary.runtime.host + result['running_host'] = host.summary.config.name + except vim.fault.NoPermission: + result['running_host'] = 'NA' + result['changed'] = False + module.exit_json(**result) + + def migrate_vm(self): + """ + Migrate virtual machine and return the task. + """ + relocate_spec = vim.vm.RelocateSpec(host=self.host_object, + datastore=self.datastore_object) + task_object = self.vm.Relocate(relocate_spec) + return task_object + + def get_vm(self): + """ + Find unique virtual machine either by UUID or Name. + Returns: virtual machine object if found, else None. + + """ + vms = [] + if self.vm_uuid: + vm_obj = find_vm_by_id(self.content, vm_id=self.params['vm_uuid'], vm_id_type="uuid") + vms = [vm_obj] + + elif self.vm_name: + objects = self.get_managed_objects_properties(vim_type=vim.VirtualMachine, properties=['name']) + for temp_vm_object in objects: + if len(temp_vm_object.propSet) != 1: + continue + if temp_vm_object.obj.name == self.vm_name: + vms.append(temp_vm_object.obj) + break + + if len(vms) > 1: + self.module.fail_json(msg="Multiple virtual machines with same name %s found." + " Please specify vm_uuid instead of vm_name." % self.vm_name) + + self.vm = vms[0] def main(): - argument_spec = vmware_argument_spec() argument_spec.update( dict( - vm_name=dict(required=True, aliases=['vm'], type='str'), - destination_host=dict(required=True, aliases=['destination'], type='str'), + vm_name=dict(aliases=['vm']), + vm_uuid=dict(aliases=['uuid']), + destination_host=dict(aliases=['destination']), + destination_datastore=dict(aliases=['datastore']) ) ) - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - if not HAS_PYVMOMI: - module.fail_json(msg='pyVmomi is required for this module') + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_one_of=[ + ['destination_host', 'destination_datastore'], + ['vm_uuid', 'vm_name'], + ], + mutually_exclusive=[ + ['vm_uuid', 'vm_name'], + ], + ) - content = connect_to_api(module=module) - - vm_object = find_vm_by_name(content=content, vm_name=module.params['vm_name']) - host_object = find_hostsystem_by_name(content=content, hostname=module.params['destination_host']) - - # Setup result - result = { - 'changed': False - } - - # Check if we could find the VM or Host - if not vm_object: - module.fail_json(msg='Cannot find virtual machine') - if not host_object: - module.fail_json(msg='Cannot find host') - - # Make sure VM isn't already at the destination - if vm_object.runtime.host.name == module.params['destination_host']: - module.exit_json(**result) - - if not module.check_mode: - # Migrate VM and get Task object back - task_object = migrate_vm(vm_object=vm_object, host_object=host_object) - - # Wait for task to complete - wait_for_task(task_object) - - # If task was a success the VM has moved, update running_host and complete module - if task_object.info.state == vim.TaskInfo.State.success: - vm_object = find_vm_by_name(content=content, vm_name=module.params['vm_name']) - result['running_host'] = vm_object.runtime.host.name - result['changed'] = True - module.exit_json(**result) - else: - if task_object.info.error is None: - module.fail_json(msg='Unable to migrate VM due to an error, please check vCenter') - else: - module.fail_json(msg='Unable to migrate VM due to an error: %s' % task_object.info.error) - else: - # If we are in check mode return a result as if move was performed - result['running_host'] = module.params['destination_host'] - result['changed'] = True - module.exit_json(**result) + vmotion_manager = VmotionManager(module) if __name__ == '__main__':