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