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