1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3 4# Copyright: (c) 2015, Bede Carroll <bc+github () bedecarroll.com> 5 6# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com> 7# Copyright: (c) 2018, Ansible Project 8# 9# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 10 11from __future__ import absolute_import, division, print_function 12__metaclass__ = type 13 14 15ANSIBLE_METADATA = { 16 'metadata_version': '1.1', 17 'status': ['preview'], 18 'supported_by': 'community' 19} 20 21DOCUMENTATION = r''' 22--- 23module: vmware_vmotion 24short_description: Move a virtual machine using vMotion, and/or its vmdks using storage vMotion. 25description: 26 - Using VMware vCenter, move a virtual machine using vMotion to a different 27 host, and/or its vmdks to another datastore using storage vMotion. 28version_added: 2.2 29author: 30- Bede Carroll (@bedecarroll) 31- Olivier Boukili (@oboukili) 32notes: 33 - Tested on vSphere 6.0 34requirements: 35 - "python >= 2.6" 36 - pyVmomi 37options: 38 vm_name: 39 description: 40 - Name of the VM to perform a vMotion on. 41 - This is required parameter, if C(vm_uuid) is not set. 42 - Version 2.6 onwards, this parameter is not a required parameter, unlike the previous versions. 43 aliases: ['vm'] 44 type: str 45 vm_uuid: 46 description: 47 - UUID of the virtual machine to perform a vMotion operation on. 48 - This is a required parameter, if C(vm_name) or C(moid) is not set. 49 aliases: ['uuid'] 50 version_added: 2.7 51 type: str 52 moid: 53 description: 54 - Managed Object ID of the instance to manage if known, this is a unique identifier only within a single vCenter instance. 55 - This is required if C(vm_name) or C(vm_uuid) is not supplied. 56 version_added: '2.9' 57 type: str 58 use_instance_uuid: 59 description: 60 - Whether to use the VMware instance UUID rather than the BIOS UUID. 61 default: no 62 type: bool 63 version_added: '2.8' 64 destination_host: 65 description: 66 - Name of the destination host the virtual machine should be running on. 67 - Version 2.6 onwards, this parameter is not a required parameter, unlike the previous versions. 68 aliases: ['destination'] 69 type: str 70 destination_datastore: 71 description: 72 - "Name of the destination datastore the virtual machine's vmdk should be moved on." 73 aliases: ['datastore'] 74 version_added: 2.7 75 type: str 76extends_documentation_fragment: vmware.documentation 77''' 78 79EXAMPLES = ''' 80- name: Perform vMotion of virtual machine 81 vmware_vmotion: 82 hostname: '{{ vcenter_hostname }}' 83 username: '{{ vcenter_username }}' 84 password: '{{ vcenter_password }}' 85 validate_certs: no 86 vm_name: 'vm_name_as_per_vcenter' 87 destination_host: 'destination_host_as_per_vcenter' 88 delegate_to: localhost 89 90- name: Perform vMotion of virtual machine 91 vmware_vmotion: 92 hostname: '{{ vcenter_hostname }}' 93 username: '{{ vcenter_username }}' 94 password: '{{ vcenter_password }}' 95 validate_certs: no 96 moid: vm-42 97 destination_host: 'destination_host_as_per_vcenter' 98 delegate_to: localhost 99 100- name: Perform storage vMotion of of virtual machine 101 vmware_vmotion: 102 hostname: '{{ vcenter_hostname }}' 103 username: '{{ vcenter_username }}' 104 password: '{{ vcenter_password }}' 105 validate_certs: no 106 vm_name: 'vm_name_as_per_vcenter' 107 destination_datastore: 'destination_datastore_as_per_vcenter' 108 delegate_to: localhost 109 110- name: Perform storage vMotion and host vMotion of virtual machine 111 vmware_vmotion: 112 hostname: '{{ vcenter_hostname }}' 113 username: '{{ vcenter_username }}' 114 password: '{{ vcenter_password }}' 115 validate_certs: no 116 vm_name: 'vm_name_as_per_vcenter' 117 destination_host: 'destination_host_as_per_vcenter' 118 destination_datastore: 'destination_datastore_as_per_vcenter' 119 delegate_to: localhost 120''' 121 122RETURN = ''' 123running_host: 124 description: List the host the virtual machine is registered to 125 returned: changed or success 126 type: str 127 sample: 'host1.example.com' 128''' 129 130try: 131 from pyVmomi import vim, VmomiSupport 132except ImportError: 133 pass 134 135from ansible.module_utils._text import to_native 136from ansible.module_utils.basic import AnsibleModule 137from ansible.module_utils.vmware import (PyVmomi, find_hostsystem_by_name, 138 find_vm_by_id, find_datastore_by_name, 139 vmware_argument_spec, wait_for_task, TaskError) 140 141 142class VmotionManager(PyVmomi): 143 def __init__(self, module): 144 super(VmotionManager, self).__init__(module) 145 self.vm = None 146 self.vm_uuid = self.params.get('vm_uuid', None) 147 self.use_instance_uuid = self.params.get('use_instance_uuid', False) 148 self.vm_name = self.params.get('vm_name', None) 149 self.moid = self.params.get('moid') or None 150 result = dict() 151 152 self.get_vm() 153 if self.vm is None: 154 vm_id = self.vm_uuid or self.vm_name or self.moid 155 self.module.fail_json(msg="Failed to find the virtual machine with %s" % vm_id) 156 157 # Get Destination Host System if specified by user 158 dest_host_name = self.params.get('destination_host', None) 159 self.host_object = None 160 if dest_host_name is not None: 161 self.host_object = find_hostsystem_by_name(content=self.content, 162 hostname=dest_host_name) 163 164 # Get Destination Datastore if specified by user 165 dest_datastore = self.params.get('destination_datastore', None) 166 self.datastore_object = None 167 if dest_datastore is not None: 168 self.datastore_object = find_datastore_by_name(content=self.content, 169 datastore_name=dest_datastore) 170 171 # At-least one of datastore, host system is required to migrate 172 if self.datastore_object is None and self.host_object is None: 173 self.module.fail_json(msg="Unable to find destination datastore" 174 " and destination host system.") 175 176 # Check if datastore is required, this check is required if destination 177 # and source host system does not share same datastore. 178 host_datastore_required = [] 179 for vm_datastore in self.vm.datastore: 180 if self.host_object and vm_datastore not in self.host_object.datastore: 181 host_datastore_required.append(True) 182 else: 183 host_datastore_required.append(False) 184 185 if any(host_datastore_required) and dest_datastore is None: 186 msg = "Destination host system does not share" \ 187 " datastore ['%s'] with source host system ['%s'] on which" \ 188 " virtual machine is located. Please specify destination_datastore" \ 189 " to rectify this problem." % ("', '".join([ds.name for ds in self.host_object.datastore]), 190 "', '".join([ds.name for ds in self.vm.datastore])) 191 192 self.module.fail_json(msg=msg) 193 194 storage_vmotion_needed = True 195 change_required = True 196 197 if self.host_object and self.datastore_object: 198 # We have both host system and datastore object 199 if not self.datastore_object.summary.accessible: 200 # Datastore is not accessible 201 self.module.fail_json(msg='Destination datastore %s is' 202 ' not accessible.' % dest_datastore) 203 204 if self.datastore_object not in self.host_object.datastore: 205 # Datastore is not associated with host system 206 self.module.fail_json(msg="Destination datastore %s provided" 207 " is not associated with destination" 208 " host system %s. Please specify" 209 " datastore value ['%s'] associated with" 210 " the given host system." % (dest_datastore, 211 dest_host_name, 212 "', '".join([ds.name for ds in self.host_object.datastore]))) 213 214 if self.vm.runtime.host.name == dest_host_name and dest_datastore in [ds.name for ds in self.vm.datastore]: 215 change_required = False 216 217 if self.host_object and self.datastore_object is None: 218 if self.vm.runtime.host.name == dest_host_name: 219 # VM is already located on same host 220 change_required = False 221 222 storage_vmotion_needed = False 223 224 elif self.datastore_object and self.host_object is None: 225 if self.datastore_object in self.vm.datastore: 226 # VM is already located on same datastore 227 change_required = False 228 229 if not self.datastore_object.summary.accessible: 230 # Datastore is not accessible 231 self.module.fail_json(msg='Destination datastore %s is' 232 ' not accessible.' % dest_datastore) 233 234 if module.check_mode: 235 result['running_host'] = module.params['destination_host'] 236 result['changed'] = True 237 module.exit_json(**result) 238 239 if change_required: 240 # Migrate VM and get Task object back 241 task_object = self.migrate_vm() 242 # Wait for task to complete 243 try: 244 wait_for_task(task_object) 245 except TaskError as task_error: 246 self.module.fail_json(msg=to_native(task_error)) 247 # If task was a success the VM has moved, update running_host and complete module 248 if task_object.info.state == vim.TaskInfo.State.success: 249 # The storage layout is not automatically refreshed, so we trigger it to get coherent module return values 250 if storage_vmotion_needed: 251 self.vm.RefreshStorageInfo() 252 result['running_host'] = module.params['destination_host'] 253 result['changed'] = True 254 module.exit_json(**result) 255 else: 256 msg = 'Unable to migrate virtual machine due to an error, please check vCenter' 257 if task_object.info.error is not None: 258 msg += " : %s" % task_object.info.error 259 module.fail_json(msg=msg) 260 else: 261 try: 262 host = self.vm.summary.runtime.host 263 result['running_host'] = host.summary.config.name 264 except vim.fault.NoPermission: 265 result['running_host'] = 'NA' 266 result['changed'] = False 267 module.exit_json(**result) 268 269 def migrate_vm(self): 270 """ 271 Migrate virtual machine and return the task. 272 """ 273 relocate_spec = vim.vm.RelocateSpec(host=self.host_object, 274 datastore=self.datastore_object) 275 task_object = self.vm.Relocate(relocate_spec) 276 return task_object 277 278 def get_vm(self): 279 """ 280 Find unique virtual machine either by UUID or Name. 281 Returns: virtual machine object if found, else None. 282 283 """ 284 vms = [] 285 if self.vm_uuid: 286 if not self.use_instance_uuid: 287 vm_obj = find_vm_by_id(self.content, vm_id=self.params['vm_uuid'], vm_id_type="uuid") 288 elif self.use_instance_uuid: 289 vm_obj = find_vm_by_id(self.content, vm_id=self.params['vm_uuid'], vm_id_type="instance_uuid") 290 vms = [vm_obj] 291 elif self.vm_name: 292 objects = self.get_managed_objects_properties(vim_type=vim.VirtualMachine, properties=['name']) 293 for temp_vm_object in objects: 294 if len(temp_vm_object.propSet) != 1: 295 continue 296 if temp_vm_object.obj.name == self.vm_name: 297 vms.append(temp_vm_object.obj) 298 break 299 elif self.moid: 300 vm_obj = VmomiSupport.templateOf('VirtualMachine')(self.moid, self.si._stub) 301 if vm_obj: 302 vms.append(vm_obj) 303 304 if len(vms) > 1: 305 self.module.fail_json(msg="Multiple virtual machines with same name %s found." 306 " Please specify vm_uuid instead of vm_name." % self.vm_name) 307 308 self.vm = vms[0] 309 310 311def main(): 312 argument_spec = vmware_argument_spec() 313 argument_spec.update( 314 dict( 315 vm_name=dict(aliases=['vm']), 316 vm_uuid=dict(aliases=['uuid']), 317 moid=dict(type='str'), 318 use_instance_uuid=dict(type='bool', default=False), 319 destination_host=dict(aliases=['destination']), 320 destination_datastore=dict(aliases=['datastore']) 321 ) 322 ) 323 324 module = AnsibleModule( 325 argument_spec=argument_spec, 326 supports_check_mode=True, 327 required_one_of=[ 328 ['destination_host', 'destination_datastore'], 329 ['vm_uuid', 'vm_name', 'moid'], 330 ], 331 mutually_exclusive=[ 332 ['vm_uuid', 'vm_name', 'moid'], 333 ], 334 ) 335 336 vmotion_manager = VmotionManager(module) 337 338 339if __name__ == '__main__': 340 main() 341