1#!/usr/bin/env python2
2# -*- coding: utf-8 -*-
3# BAREOS® - Backup Archiving REcovery Open Sourced
4#
5# Copyright (C) 2014-2014 Bareos GmbH & Co. KG
6#
7# This program is Free Software; you can redistribute it and/or
8# modify it under the terms of version three of the GNU Affero General Public
9# License as published by the Free Software Foundation, which is
10# listed in the file LICENSE.
11#
12# This program is distributed in the hope that it will be useful, but
13# WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15# Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
20# 02110-1301, USA.
21#
22# Author: Stephan Duehr
23
24"""
25Python program for enabling/disabling/resetting CBT on a VMware VM
26"""
27
28from pyVim.connect import SmartConnect, Disconnect
29from pyVmomi import vim, vmodl
30
31import argparse
32import atexit
33import getpass
34import sys
35import os.path
36
37from collections import defaultdict
38
39
40def GetArgs():
41    """
42    Supports the command-line arguments listed below.
43    """
44
45    parser = argparse.ArgumentParser(
46        description='Process args for enabling/disabling/resetting CBT')
47    parser.add_argument('-s', '--host',
48                        required=True,
49                        action='store',
50                        help='Remote host to connect to')
51    parser.add_argument('-o', '--port',
52                        type=int,
53                        default=443,
54                        action='store',
55                        help='Port to connect on')
56    parser.add_argument('-u', '--user',
57                        required=True,
58                        action='store',
59                        help='User name to use when connecting to host')
60    parser.add_argument('-p', '--password',
61                        required=False,
62                        action='store',
63                        help='Password to use when connecting to host')
64    parser.add_argument('-d', '--datacenter',
65                        required=True,
66                        action='store',
67                        help='DataCenter Name')
68    parser.add_argument('-f', '--folder',
69                        required=False,
70                        action='store',
71                        help='Folder Name (must start with /, use / for root folder')
72    parser.add_argument('-v', '--vmname',
73                        required=False,
74                        action='append',
75                        help='Names of the Virtual Machines')
76    parser.add_argument('--vm-uuid',
77                        required=False,
78                        action='append',
79                        help='Instance UUIDs of the Virtual Machines')
80    parser.add_argument('--enablecbt',
81                        action='store_true',
82                        default=False,
83                        help='Enable CBT')
84    parser.add_argument('--disablecbt',
85                        action='store_true',
86                        default=False,
87                        help='Disable CBT')
88    parser.add_argument('--resetcbt',
89                        action='store_true',
90                        default=False,
91                        help='Reset CBT (disable, then enable)')
92    parser.add_argument('--info',
93                        action='store_true',
94                        default=False,
95                        help='Show information (CBT supported and enabled or disabled)')
96    parser.add_argument('--listall',
97                        action='store_true',
98                        default=False,
99                        help='List all VMs in the given datacenter with UUID and containing folder')
100    args = parser.parse_args()
101
102    if [args.enablecbt, args.disablecbt, args.resetcbt, args.info, args.listall].count(True) > 1:
103        parser.error("Only one of --enablecbt, --disablecbt, --resetcbt, --info, --listall allowed")
104
105    if args.folder and not args.folder.startswith('/'):
106        parser.error("Folder name must start with /")
107
108    return args
109
110
111def main():
112    """
113    Python program for enabling/disabling/resetting CBT on a VMware VM
114    """
115
116    # newer Python versions, eg. Debian 8/Python >= 2.7.9 and
117    # CentOS/RHEL since 7.4 by default do SSL cert verification,
118    # we then try to disable it here.
119    # see https://github.com/vmware/pyvmomi/issues/212
120    py_ver = sys.version_info[0:3]
121    if py_ver[0] == 2 and py_ver[1] == 7 and py_ver[2] >= 5:
122        import ssl
123        try:
124            ssl._create_default_https_context = ssl._create_unverified_context
125        except AttributeError:
126            pass
127
128    args = GetArgs()
129    if args.password:
130        password = args.password
131    else:
132        password = getpass.getpass(
133            prompt='Enter password for host %s and user %s: ' %
134            (args.host, args.user))
135
136    try:
137
138        si = None
139        try:
140            si = SmartConnect(host=args.host,
141                              user=args.user,
142                              pwd=password,
143                              port=int(args.port))
144        except IOError as e:
145            pass
146        if not si:
147            print ("Cannot connect to specified host using specified"
148                   "username and password")
149            sys.exit()
150
151        atexit.register(Disconnect, si)
152
153        content = si.content
154
155        dcftree = {}
156        dcView = content.viewManager.CreateContainerView(content.rootFolder,
157                                                         [vim.Datacenter],
158                                                         False)
159        dcList = dcView.view
160        dcView.Destroy()
161        for dc in dcList:
162            if dc.name == args.datacenter:
163                dcftree[dc.name] = {}
164                folder = ''
165                get_dcftree(dcftree[dc.name], folder, dc.vmFolder)
166
167        if args.listall:
168            print_dcftree(dcftree)
169            sys.exit(0)
170
171        vm = None
172
173        for vm in get_vm_list(args, dcftree):
174
175            print "INFO: VM %s CBT supported: %s" % (
176                vm.name, vm.capability.changeTrackingSupported)
177            print "INFO: VM %s CBT enabled: %s" % (
178                vm.name, vm.config.changeTrackingEnabled)
179
180            if args.enablecbt:
181                print "INFO: VM %s trying to enable CBT now" % (vm.name)
182                enable_cbt(si, vm)
183            if args.disablecbt:
184                print "INFO: VM %s trying to disable CBT now" % (vm.name)
185                disable_cbt(si, vm)
186            if args.resetcbt:
187                print "INFO: VM %s trying to reset CBT now" % (vm.name)
188                disable_cbt(si, vm)
189                enable_cbt(si, vm)
190
191    except vmodl.MethodFault as e:
192        print "Caught vmodl fault : " + e.msg
193    except Exception as e:
194        print "Caught unexpected Exception : " + str(e)
195        raise
196
197
198def get_vm_list(args, dcftree):
199    """
200    Check if VMs are specified by UUID or folder + VM names and
201    return list of VMs to work with
202    """
203    vm_list = []
204    if args.vmname:
205        for vmname in args.vmname:
206            folder_u = unicode(args.folder, 'utf8')
207            vmname_u = unicode(vmname, 'utf8')
208            vm_path = folder_u + '/' + vmname_u
209            if args.folder.endswith('/'):
210                vm_path = folder_u + vmname_u
211
212            if args.datacenter not in dcftree:
213                print "ERROR: Could not find datacenter %s" % (args.datacenter)
214                sys.exit(1)
215
216            if vm_path not in dcftree[args.datacenter]:
217                print "ERROR: Could not find VM %s in folder %s" % (
218                    vmname_u, folder_u)
219                sys.exit(1)
220
221            vm_list.append(dcftree[args.datacenter][vm_path])
222
223    elif args.vm_uuid:
224        vms_by_uuid = get_vms_by_uuid(dcftree)
225        for vm_uuid in args.vm_uuid:
226            if vm_uuid not in vms_by_uuid:
227                print "ERROR: Could not find VM with instance UUID %s" % (vm_uuid)
228                sys.exit(1)
229
230            vm_list.append(vms_by_uuid[vm_uuid])
231
232    else:
233        print "ERROR: No VMs given, neither by folder + name nor by UUID"
234        sys.exit(1)
235
236    return vm_list
237
238
239def enable_cbt(si, vm):
240    if not vm.capability.changeTrackingSupported:
241        print "ERROR: VM %s does not support CBT" % (vm.name)
242        return False
243
244    if vm.config.changeTrackingEnabled:
245        print "INFO: VM %s is already CBT enabled" % (vm.name)
246        return True
247
248    cspec = vim.vm.ConfigSpec()
249    cspec.changeTrackingEnabled = True
250    task = vm.ReconfigVM_Task(cspec)
251    WaitForTasks([task], si)
252    return create_and_remove_snapshot(si, vm)
253
254
255def disable_cbt(si, vm):
256    if not vm.capability.changeTrackingSupported:
257        print "ERROR: VM %s does not support CBT" % (vm.name)
258        return False
259
260    if not vm.config.changeTrackingEnabled:
261        print "INFO: VM %s is already CBT disabled" % (vm.name)
262        return True
263
264    cspec = vim.vm.ConfigSpec()
265    cspec.changeTrackingEnabled = False
266    task = vm.ReconfigVM_Task(cspec)
267    WaitForTasks([task], si)
268    return create_and_remove_snapshot(si, vm)
269
270
271def get_dcftree(dcf, folder, vm_folder):
272    for vm_or_folder in vm_folder.childEntity:
273        if isinstance(vm_or_folder, vim.VirtualMachine):
274            dcf[folder + '/' + vm_or_folder.name] = vm_or_folder
275        elif isinstance(vm_or_folder, vim.Folder):
276            get_dcftree(dcf, folder + '/' + vm_or_folder.name, vm_or_folder)
277        elif isinstance(vm_or_folder, vim.VirtualApp):
278            # vm_or_folder is a vApp in this case, contains a list a VMs
279            for vapp_vm in vm_or_folder.vm:
280                dcf[folder + '/' + vm_or_folder.name + '/' + vapp_vm.name] = vapp_vm
281        else:
282            print "NOTE: %s is neither Folder nor VirtualMachine nor vApp, ignoring." % vm_or_folder
283
284
285def create_vm_snapshot(si, vm):
286    """
287    creates a snapshot on the given vm
288    """
289    create_snap_task = None
290    create_snap_result = None
291    try:
292        create_snap_task = vm.CreateSnapshot_Task(
293            name='CBTtoolTmpSnap',
294            description='CBT tool temporary snapshot',
295            memory=False,
296            quiesce=False)
297    except vmodl.MethodFault as e:
298        print "Failed to create snapshot %s" % (e.msg)
299        return False
300
301    WaitForTasks([create_snap_task], si)
302    create_snap_result = create_snap_task.info.result
303    return create_snap_result
304
305
306def remove_vm_snapshot(si, create_snap_result):
307    """
308    removes a given snapshot
309    """
310    remove_snap_task = None
311    try:
312        remove_snap_task = create_snap_result.RemoveSnapshot_Task(
313            removeChildren=True)
314    except vmodl.MethodFault as e:
315        print "Failed to remove snapshot %s" % (e.msg)
316        return False
317
318    WaitForTasks([remove_snap_task], si)
319    return True
320
321
322def create_and_remove_snapshot(si, vm):
323    """
324    creates, then removes a snapshot,
325    also named stun-unstun cycle
326    """
327    print "INFO: VM %s trying to create and remove a snapshot to activate CBT" % (vm.name)
328    snapshot_result = create_vm_snapshot(si, vm)
329    if snapshot_result:
330        if remove_vm_snapshot(si, snapshot_result):
331            print "INFO: VM %s successfully created and removed snapshot" % (vm.name)
332            return True
333
334    return False
335
336
337def WaitForTasks(tasks, si):
338    """
339    Given the service instance si and tasks, it returns after all the
340    tasks are complete
341    """
342
343    pc = si.content.propertyCollector
344
345    taskList = [str(task) for task in tasks]
346
347    # Create filter
348    objSpecs = [vmodl.query.PropertyCollector.ObjectSpec(obj=task)
349                for task in tasks]
350    propSpec = vmodl.query.PropertyCollector.PropertySpec(type=vim.Task,
351                                                          pathSet=[], all=True)
352    filterSpec = vmodl.query.PropertyCollector.FilterSpec()
353    filterSpec.objectSet = objSpecs
354    filterSpec.propSet = [propSpec]
355    filter = pc.CreateFilter(filterSpec, True)
356
357    try:
358        version, state = None, None
359
360        # Loop looking for updates till the state moves to a completed state.
361        while len(taskList):
362            update = pc.WaitForUpdates(version)
363            for filterSet in update.filterSet:
364                for objSet in filterSet.objectSet:
365                    task = objSet.obj
366                    for change in objSet.changeSet:
367                        if change.name == 'info':
368                            state = change.val.state
369                        elif change.name == 'info.state':
370                            state = change.val
371                        else:
372                            continue
373
374                        if not str(task) in taskList:
375                            continue
376
377                        if state == vim.TaskInfo.State.success:
378                            # Remove task from taskList
379                            taskList.remove(str(task))
380                        elif state == vim.TaskInfo.State.error:
381                            raise task.info.error
382            # Move to next version
383            version = update.version
384    finally:
385        if filter:
386            filter.Destroy()
387
388    return True
389
390
391def print_dcftree(dcftree):
392    """
393    Print the Datacenter Folder Tree
394    """
395    for dc in dcftree:
396        dcftree_by_folder = defaultdict(dict)
397        for vm_path in dcftree[dc]:
398            dcftree_by_folder[os.path.dirname(vm_path)][os.path.basename(vm_path)] = \
399                dcftree[dc][vm_path].config.instanceUuid
400        print "DataCenter: %s" % dc
401        print "%-36s %-50s %s" % ('VM-Instance-UUID', 'VM-Name', 'VM-Folder and/or vApp')
402        for vm_folder in sorted(dcftree_by_folder):
403            for vm_name in sorted(dcftree_by_folder[vm_folder]):
404                print "%36s %-50s %s" % (dcftree_by_folder[vm_folder][vm_name], vm_name, vm_folder)
405
406
407def get_vms_by_uuid(dcftree):
408    """
409    Get the VMs in the Datacenter as dict with UUID as key
410    """
411    vms_by_uuid = {}
412    for dc in dcftree:
413        for vm_path in dcftree[dc]:
414            vms_by_uuid[dcftree[dc][vm_path].config.instanceUuid] = dcftree[dc][vm_path]
415
416    return vms_by_uuid
417
418
419# Start program
420if __name__ == "__main__":
421    main()
422
423# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
424