1#!/usr/bin/env python2 2# -*- coding: utf-8 -*- 3# BAREOS® - Backup Archiving REcovery Open Sourced 4# 5# Copyright (C) 2014-2017 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# Renout Gerrits, Oct 2017, added thumbprint 24# 25# Bareos python class for VMware related backup and restore 26# 27 28import bareosfd 29from bareos_fd_consts import bJobMessageType, bFileType, bRCs, bIOPS 30from bareos_fd_consts import bEventType, bVariable, bCFs 31import BareosFdPluginBaseclass 32import json 33import os 34import time 35import tempfile 36import subprocess 37import shlex 38import signal 39import ssl 40import socket 41import hashlib 42 43from pyVim.connect import SmartConnect, Disconnect 44from pyVmomi import vim 45from pyVmomi import vmodl 46 47# if OrderedDict is not available from collection (eg. SLES11), 48# the additional package python-ordereddict must be used 49try: 50 from collections import OrderedDict 51except ImportError: 52 from ordereddict import OrderedDict 53 54# Job replace code as defined in src/include/baconfig.h like this: 55# #define REPLACE_ALWAYS 'a' 56# #define REPLACE_IFNEWER 'w' 57# #define REPLACE_NEVER 'n' 58# #define REPLACE_IFOLDER 'o' 59# In python, we get this in restorepkt.replace as integer. 60# This may be added to bareos_fd_consts in the future: 61bReplace = dict(ALWAYS=ord("a"), IFNEWER=ord("w"), NEVER=ord("n"), IFOLDER=ord("o")) 62 63 64class BareosFdPluginVMware(BareosFdPluginBaseclass.BareosFdPluginBaseclass): 65 """ 66 VMware related backup and restore BAREOS fd-plugin methods 67 """ 68 69 def __init__(self, context, plugindef): 70 bareosfd.DebugMessage( 71 context, 72 100, 73 "Constructor called in module %s with plugindef=%s\n" 74 % (__name__, plugindef), 75 ) 76 super(BareosFdPluginVMware, self).__init__(context, plugindef) 77 self.events = [] 78 self.events.append(bEventType["bEventStartBackupJob"]) 79 self.events.append(bEventType["bEventStartRestoreJob"]) 80 bareosfd.RegisterEvents(context, self.events) 81 self.mandatory_options_default = ["vcserver", "vcuser", "vcpass"] 82 self.mandatory_options_vmname = ["dc", "folder", "vmname"] 83 self.utf8_options = ["vmname", "folder"] 84 85 self.vadp = BareosVADPWrapper() 86 self.vadp.plugin = self 87 88 def parse_plugin_definition(self, context, plugindef): 89 """ 90 Parses the plugin arguments 91 """ 92 bareosfd.DebugMessage( 93 context, 94 100, 95 "parse_plugin_definition() was called in module %s\n" % (__name__), 96 ) 97 super(BareosFdPluginVMware, self).parse_plugin_definition(context, plugindef) 98 self.vadp.options = self.options 99 100 return bRCs["bRC_OK"] 101 102 def check_options(self, context, mandatory_options=None): 103 """ 104 Check Plugin options 105 Note: this is called by parent class parse_plugin_definition(), 106 to handle plugin options entered at restore, the real check 107 here is done by check_plugin_options() which is called from 108 start_backup_job() and start_restore_job() 109 """ 110 return bRCs["bRC_OK"] 111 112 def check_plugin_options(self, context, mandatory_options=None): 113 """ 114 Check Plugin options 115 """ 116 bareosfd.DebugMessage( 117 context, 100, "BareosFdPluginVMware:check_plugin_options() called\n" 118 ) 119 120 if mandatory_options is None: 121 # not passed, use default 122 mandatory_options = self.mandatory_options_default 123 if "uuid" not in self.options: 124 mandatory_options += self.mandatory_options_vmname 125 126 for option in mandatory_options: 127 if option not in self.options: 128 bareosfd.DebugMessage( 129 context, 100, "Option '%s' not defined.\n" % (option) 130 ) 131 bareosfd.JobMessage( 132 context, 133 bJobMessageType["M_FATAL"], 134 "Option '%s' not defined.\n" % option, 135 ) 136 137 return bRCs["bRC_Error"] 138 139 if option in self.utf8_options: 140 # make sure to convert to utf8 141 bareosfd.DebugMessage( 142 context, 143 100, 144 "Type of option %s is %s\n" % (option, type(self.options[option])), 145 ) 146 bareosfd.DebugMessage( 147 context, 148 100, 149 "Converting Option %s=%s to utf8\n" 150 % (option, self.options[option]), 151 ) 152 self.options[option] = unicode(self.options[option], "utf8") 153 bareosfd.DebugMessage( 154 context, 155 100, 156 "Type of option %s is %s\n" % (option, type(self.options[option])), 157 ) 158 159 bareosfd.DebugMessage( 160 context, 161 100, 162 "Using Option %s=%s\n" % (option, self.options[option].encode("utf-8")), 163 ) 164 165 if "vcthumbprint" not in self.options: 166 # if vcthumbprint is not given in options, retrieve it 167 if not self.vadp.retrieve_vcthumbprint(context): 168 return bRCs["bRC_Error"] 169 return bRCs["bRC_OK"] 170 171 def start_backup_job(self, context): 172 """ 173 Start of Backup Job. Called just before backup job really start. 174 Overload this to arrange whatever you have to do at this time. 175 """ 176 bareosfd.DebugMessage( 177 context, 100, "BareosFdPluginVMware:start_backup_job() called\n" 178 ) 179 180 check_option_bRC = self.check_plugin_options(context) 181 if check_option_bRC != bRCs["bRC_OK"]: 182 return check_option_bRC 183 184 if not self.vadp.connect_vmware(context): 185 return bRCs["bRC_Error"] 186 187 return self.vadp.prepare_vm_backup(context) 188 189 def start_backup_file(self, context, savepkt): 190 """ 191 Defines the file to backup and creates the savepkt. 192 """ 193 bareosfd.DebugMessage( 194 context, 100, "BareosFdPluginVMware:start_backup_file() called\n" 195 ) 196 197 if not self.vadp.files_to_backup: 198 self.vadp.disk_device_to_backup = self.vadp.disk_devices.pop(0) 199 self.vadp.files_to_backup = [] 200 if "uuid" in self.options: 201 self.vadp.files_to_backup.append( 202 "/VMS/%s/%s" 203 % ( 204 self.options["uuid"], 205 self.vadp.disk_device_to_backup["fileNameRoot"], 206 ) 207 ) 208 else: 209 self.vadp.files_to_backup.append( 210 "/VMS/%s%s/%s/%s" 211 % ( 212 self.options["dc"], 213 self.options["folder"].rstrip("/"), 214 self.options["vmname"], 215 self.vadp.disk_device_to_backup["fileNameRoot"], 216 ) 217 ) 218 219 self.vadp.files_to_backup.insert( 220 0, self.vadp.files_to_backup[0] + "_cbt.json" 221 ) 222 223 self.vadp.file_to_backup = self.vadp.files_to_backup.pop(0) 224 225 bareosfd.DebugMessage( 226 context, 100, "file: %s\n" % (self.vadp.file_to_backup.encode("utf-8")) 227 ) 228 if self.vadp.file_to_backup.endswith("_cbt.json"): 229 if not self.vadp.get_vm_disk_cbt(context): 230 return bRCs["bRC_Error"] 231 232 # create a stat packet for a restore object 233 statp = bareosfd.StatPacket() 234 savepkt.statp = statp 235 # see src/filed/backup.c how this is done in c 236 savepkt.type = bFileType["FT_RESTORE_FIRST"] 237 # set the fname of the restore object to the vmdk name 238 # by stripping of the _cbt.json suffix 239 v_fname = self.vadp.file_to_backup[: -len("_cbt.json")].encode("utf-8") 240 if chr(self.level) != "F": 241 # add level and timestamp to fname in savepkt 242 savepkt.fname = "%s+%s+%s" % ( 243 v_fname, 244 chr(self.level), 245 repr(self.vadp.create_snap_tstamp), 246 ) 247 else: 248 savepkt.fname = v_fname 249 250 savepkt.object_name = savepkt.fname 251 savepkt.object = bytearray(self.vadp.changed_disk_areas_json) 252 savepkt.object_len = len(savepkt.object) 253 savepkt.object_index = int(time.time()) 254 255 else: 256 # start bareos_vadp_dumper 257 self.vadp.start_dumper(context, "dump") 258 259 # create a regular stat packet 260 statp = bareosfd.StatPacket() 261 savepkt.statp = statp 262 savepkt.fname = self.vadp.file_to_backup.encode("utf-8") 263 if chr(self.level) != "F": 264 # add level and timestamp to fname in savepkt 265 savepkt.fname = "%s+%s+%s" % ( 266 self.vadp.file_to_backup.encode("utf-8"), 267 chr(self.level), 268 repr(self.vadp.create_snap_tstamp), 269 ) 270 savepkt.type = bFileType["FT_REG"] 271 272 bareosfd.JobMessage( 273 context, 274 bJobMessageType["M_INFO"], 275 "Starting backup of %s\n" % self.vadp.file_to_backup.encode("utf-8"), 276 ) 277 return bRCs["bRC_OK"] 278 279 def start_restore_job(self, context): 280 """ 281 Start of Restore Job. Called , if you have Restore objects. 282 Overload this to handle restore objects, if applicable 283 """ 284 bareosfd.DebugMessage( 285 context, 100, "BareosFdPluginVMware:start_restore_job() called\n" 286 ) 287 288 check_option_bRC = self.check_plugin_options(context) 289 if check_option_bRC != bRCs["bRC_OK"]: 290 return check_option_bRC 291 292 if not self.vadp.connect_vmware(context): 293 return bRCs["bRC_Error"] 294 295 return self.vadp.prepare_vm_restore(context) 296 297 def start_restore_file(self, context, cmd): 298 bareosfd.DebugMessage( 299 context, 300 100, 301 "BareosFdPluginVMware:start_restore_file() called with %s\n" % (cmd), 302 ) 303 return bRCs["bRC_OK"] 304 305 def create_file(self, context, restorepkt): 306 """ 307 For the time being, assumes that the virtual disk already 308 exists with the same name that has been backed up. 309 This should work for restoring the same VM. 310 For restoring to a new different VM, additional steps 311 must be taken, because the disk path will be different. 312 """ 313 bareosfd.DebugMessage( 314 context, 315 100, 316 "BareosFdPluginVMware:create_file() called with %s\n" % (restorepkt), 317 ) 318 319 tmp_path = "/var/tmp/bareos-vmware-plugin" 320 if restorepkt.where != "": 321 objectname = "/" + os.path.relpath(restorepkt.ofname, restorepkt.where) 322 else: 323 objectname = restorepkt.ofname 324 325 json_filename = tmp_path + objectname + "_cbt.json" 326 # for now, restore requires no snapshot to exist so disk to 327 # be written must be the the root-disk, even if a manual snapshot 328 # existed when the backup was run. So the diskPath in JSON will 329 # be set to diskPathRoot 330 cbt_data = self.vadp.restore_objects_by_objectname[objectname]["data"] 331 cbt_data["DiskParams"]["diskPath"] = cbt_data["DiskParams"]["diskPathRoot"] 332 self.vadp.writeStringToFile(context, json_filename, json.dumps(cbt_data)) 333 self.cbt_json_local_file_path = json_filename 334 335 if self.options.get("localvmdk") == "yes": 336 self.vadp.restore_vmdk_file = ( 337 restorepkt.where + "/" + cbt_data["DiskParams"]["diskPathRoot"] 338 ) 339 # check if this is the "Full" part of restore, for inc/diff the 340 # the restorepkt.ofname has trailing "+I+..." or "+D+..." 341 if os.path.basename(self.vadp.restore_vmdk_file) == os.path.basename( 342 restorepkt.ofname 343 ): 344 if os.path.exists(self.vadp.restore_vmdk_file): 345 if restorepkt.replace in (bReplace["IFNEWER"], bReplace["IFOLDER"]): 346 bareosfd.JobMessage( 347 context, 348 bJobMessageType["M_FATAL"], 349 "This Plugin only implements Replace Mode 'Always' or 'Never'\n", 350 ) 351 self.vadp.cleanup_tmp_files(context) 352 return bRCs["bRC_Error"] 353 354 if restorepkt.replace == bReplace["NEVER"]: 355 bareosfd.JobMessage( 356 context, 357 bJobMessageType["M_FATAL"], 358 "File %s exist, but Replace Mode is 'Never'\n" 359 % (self.vadp.restore_vmdk_file.encode("utf-8")), 360 ) 361 self.vadp.cleanup_tmp_files(context) 362 return bRCs["bRC_Error"] 363 364 # Replace Mode is ALWAYS if we get here 365 try: 366 os.unlink(self.vadp.restore_vmdk_file) 367 except OSError as e: 368 bareosfd.JobMessage( 369 context, 370 bJobMessageType["M_FATAL"], 371 "Error deleting File %s exist: %s\n" 372 % (self.vadp.restore_vmdk_file.encode("utf-8"), e.strerror), 373 ) 374 self.vadp.cleanup_tmp_files(context) 375 return bRCs["bRC_Error"] 376 377 if not self.vadp.start_dumper(context, "restore"): 378 return bRCs["bRC_ERROR"] 379 380 if restorepkt.type == bFileType["FT_REG"]: 381 restorepkt.create_status = bCFs["CF_EXTRACT"] 382 return bRCs["bRC_OK"] 383 384 def check_file(self, context, fname): 385 bareosfd.DebugMessage( 386 context, 387 100, 388 "BareosFdPluginVMware:check_file() called with fname %s\n" 389 % (fname.encode("utf-8")), 390 ) 391 return bRCs["bRC_Seen"] 392 393 def plugin_io(self, context, IOP): 394 bareosfd.DebugMessage( 395 context, 396 200, 397 ( 398 "BareosFdPluginVMware:plugin_io() called with function %s" 399 " self.FNAME is set to %s\n" 400 ) 401 % (IOP.func, self.FNAME), 402 ) 403 404 self.vadp.keepalive() 405 406 if IOP.func == bIOPS["IO_OPEN"]: 407 self.FNAME = IOP.fname 408 bareosfd.DebugMessage( 409 context, 100, "self.FNAME was set to %s from IOP.fname\n" % (self.FNAME) 410 ) 411 try: 412 if IOP.flags & (os.O_CREAT | os.O_WRONLY): 413 bareosfd.DebugMessage( 414 context, 415 100, 416 "Open file %s for writing with %s\n" % (self.FNAME, IOP), 417 ) 418 419 # this is a restore 420 # create_file() should have started 421 # bareos_vadp_dumper, check: 422 # if self.vadp.dumper_process: 423 if self.vadp.check_dumper(context): 424 bareosfd.DebugMessage( 425 context, 426 100, 427 ("plugin_io: bareos_vadp_dumper running with" " PID %s\n") 428 % (self.vadp.dumper_process.pid), 429 ) 430 else: 431 bareosfd.JobMessage( 432 context, 433 bJobMessageType["M_FATAL"], 434 "plugin_io: bareos_vadp_dumper not running\n", 435 ) 436 return bRCs["bRC_Error"] 437 438 else: 439 bareosfd.DebugMessage( 440 context, 441 100, 442 "plugin_io: trying to open %s for reading\n" % (self.FNAME), 443 ) 444 # this is a backup 445 # start_backup_file() should have started 446 # bareos_vadp_dumper, check: 447 if self.vadp.dumper_process: 448 bareosfd.DebugMessage( 449 context, 450 100, 451 ("plugin_io: bareos_vadp_dumper running with" " PID %s\n") 452 % (self.vadp.dumper_process.pid), 453 ) 454 else: 455 bareosfd.JobMessage( 456 context, 457 bJobMessageType["M_FATAL"], 458 "plugin_io: bareos_vadp_dumper not running\n", 459 ) 460 return bRCs["bRC_Error"] 461 462 except OSError as os_error: 463 IOP.status = -1 464 bareosfd.DebugMessage( 465 context, 466 100, 467 "plugin_io: failed to open %s: %s\n" 468 % (self.FNAME, os_error.strerror), 469 ) 470 bareosfd.JobMessage( 471 context, 472 bJobMessageType["M_FATAL"], 473 "plugin_io: failed to open %s: %s\n" 474 % (self.FNAME, os_error.strerror), 475 ) 476 return bRCs["bRC_Error"] 477 478 return bRCs["bRC_OK"] 479 480 elif IOP.func == bIOPS["IO_CLOSE"]: 481 if self.jobType == "B": 482 # Backup Job 483 bareosfd.DebugMessage( 484 context, 485 100, 486 ( 487 "plugin_io: calling end_dumper() to wait for" 488 " PID %s to terminate\n" 489 ) 490 % (self.vadp.dumper_process.pid), 491 ) 492 bareos_vadp_dumper_returncode = self.vadp.end_dumper(context) 493 if bareos_vadp_dumper_returncode != 0: 494 bareosfd.JobMessage( 495 context, 496 bJobMessageType["M_FATAL"], 497 ( 498 "plugin_io[IO_CLOSE]: bareos_vadp_dumper returncode:" 499 " %s error output:\n%s\n" 500 ) 501 % (bareos_vadp_dumper_returncode, self.vadp.get_dumper_err()), 502 ) 503 return bRCs["bRC_Error"] 504 505 elif self.jobType == "R": 506 # Restore Job 507 bareosfd.DebugMessage( 508 context, 509 100, 510 "Closing Pipe to bareos_vadp_dumper for %s\n" % (self.FNAME), 511 ) 512 if self.vadp.dumper_process: 513 self.vadp.dumper_process.stdin.close() 514 bareosfd.DebugMessage( 515 context, 516 100, 517 ( 518 "plugin_io: calling end_dumper() to wait for" 519 " PID %s to terminate\n" 520 ) 521 % (self.vadp.dumper_process.pid), 522 ) 523 bareos_vadp_dumper_returncode = self.vadp.end_dumper(context) 524 if bareos_vadp_dumper_returncode != 0: 525 bareosfd.JobMessage( 526 context, 527 bJobMessageType["M_FATAL"], 528 ("plugin_io[IO_CLOSE]: bareos_vadp_dumper returncode:" " %s\n") 529 % (bareos_vadp_dumper_returncode), 530 ) 531 return bRCs["bRC_Error"] 532 533 else: 534 bareosfd.JobMessage( 535 context, 536 bJobMessageType["M_FATAL"], 537 "plugin_io[IO_CLOSE]: unknown Job Type %s\n" % (self.jobType), 538 ) 539 return bRCs["bRC_Error"] 540 541 return bRCs["bRC_OK"] 542 543 elif IOP.func == bIOPS["IO_SEEK"]: 544 return bRCs["bRC_OK"] 545 546 elif IOP.func == bIOPS["IO_READ"]: 547 IOP.buf = bytearray(IOP.count) 548 IOP.status = self.vadp.dumper_process.stdout.readinto(IOP.buf) 549 IOP.io_errno = 0 550 551 return bRCs["bRC_OK"] 552 553 elif IOP.func == bIOPS["IO_WRITE"]: 554 try: 555 self.vadp.dumper_process.stdin.write(IOP.buf) 556 IOP.status = IOP.count 557 IOP.io_errno = 0 558 except IOError as e: 559 bareosfd.DebugMessage( 560 context, 100, "plugin_io[IO_WRITE]: IOError: %s\n" % (e) 561 ) 562 self.vadp.end_dumper(context) 563 IOP.status = 0 564 IOP.io_errno = e.errno 565 return bRCs["bRC_Error"] 566 return bRCs["bRC_OK"] 567 568 def handle_plugin_event(self, context, event): 569 if event in self.events: 570 self.jobType = chr(bareosfd.GetValue(context, bVariable["bVarType"])) 571 bareosfd.DebugMessage(context, 100, "jobType: %s\n" % (self.jobType)) 572 573 if event == bEventType["bEventJobEnd"]: 574 bareosfd.DebugMessage( 575 context, 100, "handle_plugin_event() called with bEventJobEnd\n" 576 ) 577 bareosfd.DebugMessage( 578 context, 579 100, 580 "Disconnecting from VSphere API on host %s with user %s\n" 581 % (self.options["vcserver"], self.options["vcuser"]), 582 ) 583 584 self.vadp.cleanup() 585 586 elif event == bEventType["bEventEndBackupJob"]: 587 bareosfd.DebugMessage( 588 context, 100, "handle_plugin_event() called with bEventEndBackupJob\n" 589 ) 590 bareosfd.DebugMessage(context, 100, "removing Snapshot\n") 591 self.vadp.remove_vm_snapshot(context) 592 593 elif event == bEventType["bEventEndFileSet"]: 594 bareosfd.DebugMessage( 595 context, 100, "handle_plugin_event() called with bEventEndFileSet\n" 596 ) 597 598 elif event == bEventType["bEventStartBackupJob"]: 599 bareosfd.DebugMessage( 600 context, 100, "handle_plugin_event() called with bEventStartBackupJob\n" 601 ) 602 603 return self.start_backup_job(context) 604 605 elif event == bEventType["bEventStartRestoreJob"]: 606 bareosfd.DebugMessage( 607 context, 608 100, 609 "handle_plugin_event() called with bEventStartRestoreJob\n", 610 ) 611 612 return self.start_restore_job(context) 613 614 else: 615 bareosfd.DebugMessage( 616 context, 100, "handle_plugin_event() called with event %s\n" % (event) 617 ) 618 619 return bRCs["bRC_OK"] 620 621 def end_backup_file(self, context): 622 bareosfd.DebugMessage( 623 context, 100, "BareosFdPluginVMware:end_backup_file() called\n" 624 ) 625 if self.vadp.disk_devices or self.vadp.files_to_backup: 626 bareosfd.DebugMessage( 627 context, 100, "end_backup_file(): returning bRC_More\n" 628 ) 629 return bRCs["bRC_More"] 630 631 bareosfd.DebugMessage(context, 100, "end_backup_file(): returning bRC_OK\n") 632 return bRCs["bRC_OK"] 633 634 def restore_object_data(self, context, ROP): 635 """ 636 Note: 637 This is called in two cases: 638 - on diff/inc backup (should be called only once) 639 - on restore (for every job id being restored) 640 But at the point in time called, it is not possible 641 to distinguish which of them it is, because job type 642 is "I" until the bEventStartBackupJob event 643 """ 644 bareosfd.DebugMessage( 645 context, 646 100, 647 "BareosFdPluginVMware:restore_object_data() called with ROP:%s\n" % (ROP), 648 ) 649 bareosfd.DebugMessage( 650 context, 651 100, 652 "ROP.object_name(%s): %s\n" % (type(ROP.object_name), ROP.object_name), 653 ) 654 bareosfd.DebugMessage( 655 context, 656 100, 657 "ROP.plugin_name(%s): %s\n" % (type(ROP.plugin_name), ROP.plugin_name), 658 ) 659 bareosfd.DebugMessage( 660 context, 661 100, 662 "ROP.object_len(%s): %s\n" % (type(ROP.object_len), ROP.object_len), 663 ) 664 bareosfd.DebugMessage( 665 context, 666 100, 667 "ROP.object_full_len(%s): %s\n" 668 % (type(ROP.object_full_len), ROP.object_full_len), 669 ) 670 bareosfd.DebugMessage( 671 context, 100, "ROP.object(%s): %s\n" % (type(ROP.object), ROP.object) 672 ) 673 ro_data = self.vadp.json2cbt(str(ROP.object)) 674 ro_filename = ro_data["DiskParams"]["diskPathRoot"] 675 # self.vadp.restore_objects_by_diskpath is used on backup 676 # in get_vm_disk_cbt() 677 # self.vadp.restore_objects_by_objectname is used on restore 678 # by create_file() 679 self.vadp.restore_objects_by_diskpath[ro_filename] = [] 680 self.vadp.restore_objects_by_diskpath[ro_filename].append( 681 {"json": ROP.object, "data": ro_data} 682 ) 683 self.vadp.restore_objects_by_objectname[ 684 ROP.object_name 685 ] = self.vadp.restore_objects_by_diskpath[ro_filename][-1] 686 return bRCs["bRC_OK"] 687 688 689class BareosVADPWrapper(object): 690 """ 691 VADP specific class. 692 """ 693 694 def __init__(self): 695 self.si = None 696 self.si_last_keepalive = None 697 self.vm = None 698 self.create_snap_task = None 699 self.create_snap_result = None 700 self.file_to_backup = None 701 self.files_to_backup = None 702 self.disk_devices = None 703 self.disk_device_to_backup = None 704 self.cbt_json_local_file_path = None 705 self.dumper_process = None 706 self.dumper_stderr_log = None 707 self.changed_disk_areas_json = None 708 self.restore_objects_by_diskpath = {} 709 self.restore_objects_by_objectname = {} 710 self.options = None 711 self.skip_disk_modes = ["independent_nonpersistent", "independent_persistent"] 712 self.restore_vmdk_file = None 713 self.plugin = None 714 715 def connect_vmware(self, context): 716 # this prevents from repeating on second call 717 if self.si: 718 bareosfd.DebugMessage( 719 context, 720 100, 721 "connect_vmware(): connection to server %s already exists\n" 722 % (self.options["vcserver"]), 723 ) 724 return True 725 726 bareosfd.DebugMessage( 727 context, 728 100, 729 "connect_vmware(): connecting server %s\n" % (self.options["vcserver"]), 730 ) 731 try: 732 self.si = SmartConnect( 733 host=self.options["vcserver"], 734 user=self.options["vcuser"], 735 pwd=self.options["vcpass"], 736 port=443, 737 ) 738 self.si_last_keepalive = int(time.time()) 739 740 except IOError: 741 pass 742 if not self.si: 743 bareosfd.JobMessage( 744 context, 745 bJobMessageType["M_FATAL"], 746 "Cannot connect to host %s with user %s and password\n" 747 % (self.options["vcserver"], self.options["vcuser"]), 748 ) 749 return False 750 751 bareosfd.DebugMessage( 752 context, 753 100, 754 ("Successfully connected to VSphere API on host %s with" " user %s\n") 755 % (self.options["vcserver"], self.options["vcuser"]), 756 ) 757 758 return True 759 760 def cleanup(self): 761 Disconnect(self.si) 762 # the Disconnect Method does not close the tcp connection 763 # is that so intentionally? 764 # However, explicitly closing it works like this: 765 self.si._stub.DropConnections() 766 self.si = None 767 self.log = None 768 769 def keepalive(self): 770 # FIXME: prevent from vSphere API timeout, needed until pyvmomi fixes 771 # https://github.com/vmware/pyvmomi/issues/239 772 # otherwise idle timeout occurs after 20 minutes (1200s), 773 # so call CurrentTime() every 15min (900s) to keep alive 774 if int(time.time()) - self.si_last_keepalive > 900: 775 self.si.CurrentTime() 776 self.si_last_keepalive = int(time.time()) 777 778 def prepare_vm_backup(self, context): 779 """ 780 prepare VM backup: 781 - get vm details 782 - take snapshot 783 - get disk devices 784 """ 785 if "uuid" in self.options: 786 vmname = self.options["uuid"] 787 if not self.get_vm_details_by_uuid(context): 788 bareosfd.DebugMessage( 789 context, 790 100, 791 "Error getting details for VM with UUID %s\n" % (vmname), 792 ) 793 bareosfd.JobMessage( 794 context, 795 bJobMessageType["M_FATAL"], 796 "Error getting details for VM with UUID %s\n" % (vmname), 797 ) 798 return bRCs["bRC_Error"] 799 else: 800 vmname = self.options["vmname"] 801 if not self.get_vm_details_dc_folder_vmname(context): 802 bareosfd.DebugMessage( 803 context, 804 100, 805 "Error getting details for VM %s\n" % (vmname.encode("utf-8")), 806 ) 807 bareosfd.JobMessage( 808 context, 809 bJobMessageType["M_FATAL"], 810 "Error getting details for VM %s\n" % (vmname.encode("utf-8")), 811 ) 812 return bRCs["bRC_Error"] 813 814 bareosfd.DebugMessage( 815 context, 816 100, 817 "Successfully got details for VM %s\n" % (vmname.encode("utf-8")), 818 ) 819 820 # check if the VM supports CBT and that CBT is enabled 821 if not self.vm.capability.changeTrackingSupported: 822 bareosfd.JobMessage( 823 context, 824 bJobMessageType["M_FATAL"], 825 "Error VM %s does not support CBT\n" % (vmname.encode("utf-8")), 826 ) 827 return bRCs["bRC_Error"] 828 829 if not self.vm.config.changeTrackingEnabled: 830 bareosfd.JobMessage( 831 context, 832 bJobMessageType["M_FATAL"], 833 "Error VM %s is not CBT enabled\n" % (vmname.encode("utf-8")), 834 ) 835 return bRCs["bRC_Error"] 836 837 bareosfd.DebugMessage( 838 context, 100, "Creating Snapshot on VM %s\n" % (vmname.encode("utf-8")) 839 ) 840 841 if not self.create_vm_snapshot(context): 842 bareosfd.JobMessage( 843 context, 844 bJobMessageType["M_FATAL"], 845 "Error creating snapshot on VM %s\n" % (vmname.encode("utf-8")), 846 ) 847 return bRCs["bRC_Error"] 848 849 bareosfd.DebugMessage( 850 context, 851 100, 852 "Successfully created snapshot on VM %s\n" % (vmname.encode("utf-8")), 853 ) 854 855 bareosfd.DebugMessage( 856 context, 857 100, 858 "Getting Disk Devices on VM %s from snapshot\n" % (vmname.encode("utf-8")), 859 ) 860 self.get_vm_snap_disk_devices(context) 861 if not self.disk_devices: 862 bareosfd.JobMessage( 863 context, 864 bJobMessageType["M_FATAL"], 865 "Error getting Disk Devices on VM %s from snapshot\n" 866 % (vmname.encode("utf-8")), 867 ) 868 return bRCs["bRC_Error"] 869 870 return bRCs["bRC_OK"] 871 872 def prepare_vm_restore(self, context): 873 """ 874 prepare VM restore: 875 - get vm details 876 - ensure vm is powered off 877 - get disk devices 878 """ 879 880 if self.options.get("localvmdk") == "yes": 881 bareosfd.DebugMessage( 882 context, 883 100, 884 "prepare_vm_restore(): restore to local vmdk, skipping checks\n", 885 ) 886 return bRCs["bRC_OK"] 887 888 if "uuid" in self.options: 889 vmname = self.options["uuid"] 890 if not self.get_vm_details_by_uuid(context): 891 bareosfd.DebugMessage( 892 context, 893 100, 894 "Error getting details for VM %s\n" % (vmname.encode("utf-8")), 895 ) 896 return bRCs["bRC_Error"] 897 else: 898 vmname = self.options["vmname"] 899 if not self.get_vm_details_dc_folder_vmname(context): 900 bareosfd.DebugMessage( 901 context, 902 100, 903 "Error getting details for VM %s\n" % (vmname.encode("utf-8")), 904 ) 905 return bRCs["bRC_Error"] 906 907 bareosfd.DebugMessage( 908 context, 909 100, 910 "Successfully got details for VM %s\n" % (vmname.encode("utf-8")), 911 ) 912 913 vm_power_state = self.vm.summary.runtime.powerState 914 if vm_power_state != "poweredOff": 915 bareosfd.JobMessage( 916 context, 917 bJobMessageType["M_FATAL"], 918 "Error VM %s must be poweredOff for restore, but is %s\n" 919 % (vmname.encode("utf-8"), vm_power_state), 920 ) 921 return bRCs["bRC_Error"] 922 923 if self.vm.snapshot is not None: 924 bareosfd.JobMessage( 925 context, 926 bJobMessageType["M_FATAL"], 927 "Error VM %s must not have any snapshots before restore\n" 928 % (vmname.encode("utf-8")), 929 ) 930 return bRCs["bRC_Error"] 931 932 bareosfd.DebugMessage( 933 context, 100, "Getting Disk Devices on VM %s\n" % (vmname.encode("utf-8")) 934 ) 935 self.get_vm_disk_devices(context) 936 if not self.disk_devices: 937 bareosfd.JobMessage( 938 context, 939 bJobMessageType["M_FATAL"], 940 "Error getting Disk Devices on VM %s\n" % (vmname.encode("utf-8")), 941 ) 942 return bRCs["bRC_Error"] 943 944 # make sure backed up disks match VM disks 945 if not self.check_vm_disks_match(context): 946 return bRCs["bRC_Error"] 947 948 return bRCs["bRC_OK"] 949 950 def get_vm_details_dc_folder_vmname(self, context): 951 """ 952 Get details of VM given by plugin options dc, folder, vmname 953 and save result in self.vm 954 Returns True on success, False otherwise 955 """ 956 content = self.si.content 957 dcView = content.viewManager.CreateContainerView( 958 content.rootFolder, [vim.Datacenter], False 959 ) 960 vmListWithFolder = {} 961 dcList = dcView.view 962 dcView.Destroy() 963 for dc in dcList: 964 if dc.name == self.options["dc"]: 965 folder = "" 966 self._get_dcftree(vmListWithFolder, folder, dc.vmFolder) 967 968 if self.options["folder"].endswith("/"): 969 vm_path = "%s%s" % (self.options["folder"], self.options["vmname"]) 970 else: 971 vm_path = "%s/%s" % (self.options["folder"], self.options["vmname"]) 972 973 if vm_path not in vmListWithFolder: 974 bareosfd.JobMessage( 975 context, 976 bJobMessageType["M_FATAL"], 977 "No VM with Folder/Name %s found in DC %s\n" 978 % (vm_path.encode("utf-8"), self.options["dc"]), 979 ) 980 return False 981 982 self.vm = vmListWithFolder[vm_path] 983 return True 984 985 def get_vm_details_by_uuid(self, context): 986 """ 987 Get details of VM given by plugin options uuid 988 and save result in self.vm 989 Returns True on success, False otherwise 990 """ 991 search_index = self.si.content.searchIndex 992 self.vm = search_index.FindByUuid(None, self.options["uuid"], True, True) 993 if self.vm is None: 994 return False 995 else: 996 return True 997 998 def _get_dcftree(self, dcf, folder, vm_folder): 999 """ 1000 Recursive function to get VMs with folder names 1001 """ 1002 for vm_or_folder in vm_folder.childEntity: 1003 if isinstance(vm_or_folder, vim.VirtualMachine): 1004 dcf[folder + "/" + vm_or_folder.name] = vm_or_folder 1005 elif isinstance(vm_or_folder, vim.Folder): 1006 self._get_dcftree(dcf, folder + "/" + vm_or_folder.name, vm_or_folder) 1007 elif isinstance(vm_or_folder, vim.VirtualApp): 1008 # vm_or_folder is a vApp in this case, contains a list a VMs 1009 for vapp_vm in vm_or_folder.vm: 1010 dcf[folder + "/" + vm_or_folder.name + "/" + vapp_vm.name] = vapp_vm 1011 1012 def create_vm_snapshot(self, context): 1013 """ 1014 Creates a snapshot 1015 """ 1016 try: 1017 self.create_snap_task = self.vm.CreateSnapshot_Task( 1018 name="BareosTmpSnap_jobId_%s" % (self.plugin.jobId), 1019 description="Bareos Tmp Snap jobId %s jobName %s" 1020 % (self.plugin.jobId, self.plugin.jobName), 1021 memory=False, 1022 quiesce=True, 1023 ) 1024 except vmodl.MethodFault as e: 1025 bareosfd.JobMessage( 1026 context, 1027 bJobMessageType["M_FATAL"], 1028 "Failed to create snapshot %s\n" % (e.msg), 1029 ) 1030 return False 1031 1032 self.vmomi_WaitForTasks([self.create_snap_task]) 1033 self.create_snap_result = self.create_snap_task.info.result 1034 self.create_snap_tstamp = time.time() 1035 return True 1036 1037 def remove_vm_snapshot(self, context): 1038 """ 1039 Removes the snapshot taken before 1040 """ 1041 1042 if not self.create_snap_result: 1043 bareosfd.JobMessage( 1044 context, 1045 bJobMessageType["M_WARNING"], 1046 "No snapshot was taken, skipping snapshot removal\n", 1047 ) 1048 return False 1049 1050 try: 1051 rmsnap_task = self.create_snap_result.RemoveSnapshot_Task( 1052 removeChildren=True 1053 ) 1054 except vmodl.MethodFault as e: 1055 bareosfd.JobMessage( 1056 context, 1057 bJobMessageType["M_WARNING"], 1058 "Failed to remove snapshot %s\n" % (e.msg), 1059 ) 1060 return False 1061 1062 self.vmomi_WaitForTasks([rmsnap_task]) 1063 return True 1064 1065 def get_vm_snap_disk_devices(self, context): 1066 """ 1067 Get the disk devices from the created snapshot 1068 Assumption: Snapshot successfully created 1069 """ 1070 self.get_disk_devices(context, self.create_snap_result.config.hardware.device) 1071 1072 def get_vm_disk_devices(self, context): 1073 """ 1074 Get the disk devices from vm 1075 """ 1076 self.get_disk_devices(context, self.vm.config.hardware.device) 1077 1078 def get_disk_devices(self, context, devicespec): 1079 """ 1080 Get disk devices from a devicespec 1081 """ 1082 self.disk_devices = [] 1083 for hw_device in devicespec: 1084 if type(hw_device) == vim.vm.device.VirtualDisk: 1085 if hw_device.backing.diskMode in self.skip_disk_modes: 1086 bareosfd.JobMessage( 1087 context, 1088 bJobMessageType["M_INFO"], 1089 "Skipping Disk %s because mode is %s\n" 1090 % ( 1091 self.get_vm_disk_root_filename(hw_device.backing).encode( 1092 "utf-8" 1093 ), 1094 hw_device.backing.diskMode, 1095 ), 1096 ) 1097 continue 1098 1099 self.disk_devices.append( 1100 { 1101 "deviceKey": hw_device.key, 1102 "fileName": hw_device.backing.fileName, 1103 "fileNameRoot": self.get_vm_disk_root_filename( 1104 hw_device.backing 1105 ), 1106 "changeId": hw_device.backing.changeId, 1107 } 1108 ) 1109 1110 def get_vm_disk_root_filename(self, disk_device_backing): 1111 """ 1112 Get the disk name from the ende of the parents chain 1113 When snapshots exist, the original disk filename is 1114 needed. If no snapshots exist, the disk has no parent 1115 and the filename is the same. 1116 """ 1117 actual_backing = disk_device_backing 1118 while actual_backing.parent: 1119 actual_backing = actual_backing.parent 1120 return actual_backing.fileName 1121 1122 def get_vm_disk_cbt(self, context): 1123 """ 1124 Get CBT Information 1125 """ 1126 cbt_changeId = "*" 1127 if ( 1128 self.disk_device_to_backup["fileNameRoot"] 1129 in self.restore_objects_by_diskpath 1130 ): 1131 if ( 1132 len( 1133 self.restore_objects_by_diskpath[ 1134 self.disk_device_to_backup["fileNameRoot"] 1135 ] 1136 ) 1137 > 1 1138 ): 1139 bareosfd.JobMessage( 1140 context, 1141 bJobMessageType["M_FATAL"], 1142 "ERROR: more then one CBT info for Diff/Inc exists\n", 1143 ) 1144 return False 1145 1146 cbt_changeId = self.restore_objects_by_diskpath[ 1147 self.disk_device_to_backup["fileNameRoot"] 1148 ][0]["data"]["DiskParams"]["changeId"] 1149 bareosfd.DebugMessage( 1150 context, 1151 100, 1152 "get_vm_disk_cbt(): using changeId %s from restore object\n" 1153 % (cbt_changeId), 1154 ) 1155 self.changed_disk_areas = self.vm.QueryChangedDiskAreas( 1156 snapshot=self.create_snap_result, 1157 deviceKey=self.disk_device_to_backup["deviceKey"], 1158 startOffset=0, 1159 changeId=cbt_changeId, 1160 ) 1161 self.cbt2json(context) 1162 return True 1163 1164 def check_vm_disks_match(self, context): 1165 """ 1166 Check if the backed up disks match selecte VM disks 1167 """ 1168 backed_up_disks = set(self.restore_objects_by_diskpath.keys()) 1169 vm_disks = set([disk_dev["fileNameRoot"] for disk_dev in self.disk_devices]) 1170 1171 if backed_up_disks == vm_disks: 1172 bareosfd.DebugMessage( 1173 context, 1174 100, 1175 "check_vm_disks_match(): OK, VM disks match backed up disks\n", 1176 ) 1177 return True 1178 1179 bareosfd.JobMessage( 1180 context, 1181 bJobMessageType["M_WARNING"], 1182 "VM Disks: %s\n" % (", ".join(vm_disks).encode("utf-8")), 1183 ) 1184 bareosfd.JobMessage( 1185 context, 1186 bJobMessageType["M_WARNING"], 1187 "Backed up Disks: %s\n" % (", ".join(backed_up_disks).encode("utf-8")), 1188 ) 1189 bareosfd.JobMessage( 1190 context, 1191 bJobMessageType["M_FATAL"], 1192 "ERROR: VM disks not matching backed up disks\n", 1193 ) 1194 return False 1195 1196 def cbt2json(self, context): 1197 """ 1198 Convert CBT data into json serializable structure and 1199 return it as json string 1200 """ 1201 1202 # the order of keys in JSON data must be preserved for 1203 # bareos_vadp_dumper to work properly, this is done 1204 # by using the OrderedDict type 1205 cbt_data = OrderedDict() 1206 cbt_data["ConnParams"] = {} 1207 cbt_data["ConnParams"]["VmMoRef"] = "moref=" + self.vm._moId 1208 cbt_data["ConnParams"]["VsphereHostName"] = self.options["vcserver"] 1209 cbt_data["ConnParams"]["VsphereUsername"] = self.options["vcuser"] 1210 cbt_data["ConnParams"]["VspherePassword"] = self.options["vcpass"] 1211 cbt_data["ConnParams"]["VsphereThumbPrint"] = ":".join( 1212 [ 1213 self.options["vcthumbprint"][i : i + 2] 1214 for i in range(0, len(self.options["vcthumbprint"]), 2) 1215 ] 1216 ) 1217 cbt_data["ConnParams"]["VsphereSnapshotMoRef"] = self.create_snap_result._moId 1218 1219 # disk params for bareos_vadp_dumper 1220 cbt_data["DiskParams"] = {} 1221 cbt_data["DiskParams"]["diskPath"] = self.disk_device_to_backup["fileName"] 1222 cbt_data["DiskParams"]["diskPathRoot"] = self.disk_device_to_backup[ 1223 "fileNameRoot" 1224 ] 1225 cbt_data["DiskParams"]["changeId"] = self.disk_device_to_backup["changeId"] 1226 1227 # cbt data for bareos_vadp_dumper 1228 cbt_data["DiskChangeInfo"] = {} 1229 cbt_data["DiskChangeInfo"]["startOffset"] = self.changed_disk_areas.startOffset 1230 cbt_data["DiskChangeInfo"]["length"] = self.changed_disk_areas.length 1231 cbt_data["DiskChangeInfo"]["changedArea"] = [] 1232 for extent in self.changed_disk_areas.changedArea: 1233 cbt_data["DiskChangeInfo"]["changedArea"].append( 1234 {"start": extent.start, "length": extent.length} 1235 ) 1236 1237 self.changed_disk_areas_json = json.dumps(cbt_data) 1238 self.writeStringToFile( 1239 context, 1240 "/var/tmp" + self.file_to_backup.encode("utf-8"), 1241 self.changed_disk_areas_json, 1242 ) 1243 1244 def json2cbt(self, cbt_json_string): 1245 """ 1246 Convert JSON string from restore object to ordered dict 1247 to preserve the key order required for bareos_vadp_dumper 1248 to work properly 1249 return OrderedDict 1250 """ 1251 1252 # the order of keys in JSON data must be preserved for 1253 # bareos_vadp_dumper to work properly 1254 cbt_data = OrderedDict() 1255 cbt_keys_ordered = ["ConnParams", "DiskParams", "DiskChangeInfo"] 1256 cbt_data_tmp = json.loads(cbt_json_string) 1257 for cbt_key in cbt_keys_ordered: 1258 cbt_data[cbt_key] = cbt_data_tmp[cbt_key] 1259 1260 return cbt_data 1261 1262 def dumpJSONfile(self, context, filename, data): 1263 """ 1264 Write a Python data structure in JSON format to the given file. 1265 Note: obsolete, no longer used because order of keys in JSON 1266 string must be preserved 1267 """ 1268 bareosfd.DebugMessage( 1269 context, 100, "dumpJSONfile(): writing JSON data to file %s\n" % (filename) 1270 ) 1271 try: 1272 out = open(filename, "w") 1273 json.dump(data, out) 1274 out.close() 1275 bareosfd.DebugMessage( 1276 context, 1277 100, 1278 "dumpJSONfile(): successfully wrote JSON data to file %s\n" 1279 % (filename), 1280 ) 1281 1282 except IOError as io_error: 1283 bareosfd.JobMessage( 1284 context, 1285 bJobMessageType["M_FATAL"], 1286 ( 1287 "dumpJSONFile(): failed to write JSON data to file %s," 1288 " reason: %s\n" 1289 ) 1290 % (filename.encode("utf-8"), io_error.strerror), 1291 ) 1292 1293 def writeStringToFile(self, context, filename, data_string): 1294 """ 1295 Write a String to the given file. 1296 """ 1297 bareosfd.DebugMessage( 1298 context, 1299 100, 1300 "writeStringToFile(): writing String to file %s\n" % (filename), 1301 ) 1302 # ensure the directory for writing the file exists 1303 self.mkdir(os.path.dirname(filename)) 1304 try: 1305 out = open(filename, "w") 1306 out.write(data_string) 1307 out.close() 1308 bareosfd.DebugMessage( 1309 context, 1310 100, 1311 "saveStringTofile(): successfully wrote String to file %s\n" 1312 % (filename), 1313 ) 1314 1315 except IOError as io_error: 1316 bareosfd.JobMessage( 1317 context, 1318 bJobMessageType["M_FATAL"], 1319 ( 1320 "writeStingToFile(): failed to write String to file %s," 1321 " reason: %s\n" 1322 ) 1323 % (filename, io_error.strerror), 1324 ) 1325 1326 # the following path must be passed to bareos_vadp_dumper as parameter 1327 self.cbt_json_local_file_path = filename 1328 1329 def vmomi_WaitForTasks(self, tasks): 1330 """ 1331 Given the service instance si and tasks, it returns after all the 1332 tasks are complete 1333 """ 1334 1335 si = self.si 1336 pc = si.content.propertyCollector 1337 1338 taskList = [str(task) for task in tasks] 1339 1340 # Create filter 1341 objSpecs = [ 1342 vmodl.query.PropertyCollector.ObjectSpec(obj=task) for task in tasks 1343 ] 1344 propSpec = vmodl.query.PropertyCollector.PropertySpec( 1345 type=vim.Task, pathSet=[], all=True 1346 ) 1347 filterSpec = vmodl.query.PropertyCollector.FilterSpec() 1348 filterSpec.objectSet = objSpecs 1349 filterSpec.propSet = [propSpec] 1350 pcfilter = pc.CreateFilter(filterSpec, True) 1351 1352 try: 1353 version, state = None, None 1354 1355 # Loop looking for updates till the state moves to a completed 1356 # state. 1357 while len(taskList): 1358 update = pc.WaitForUpdates(version) 1359 for filterSet in update.filterSet: 1360 for objSet in filterSet.objectSet: 1361 task = objSet.obj 1362 for change in objSet.changeSet: 1363 if change.name == "info": 1364 state = change.val.state 1365 elif change.name == "info.state": 1366 state = change.val 1367 else: 1368 continue 1369 1370 if not str(task) in taskList: 1371 continue 1372 1373 if state == vim.TaskInfo.State.success: 1374 # Remove task from taskList 1375 taskList.remove(str(task)) 1376 elif state == vim.TaskInfo.State.error: 1377 raise task.info.error 1378 # Move to next version 1379 version = update.version 1380 finally: 1381 if pcfilter: 1382 pcfilter.Destroy() 1383 1384 def start_dumper(self, context, cmd): 1385 """ 1386 Start bareos_vadp_dumper 1387 Parameters 1388 - cmd: must be "dump" or "restore" 1389 """ 1390 bareos_vadp_dumper_bin = "bareos_vadp_dumper_wrapper.sh" 1391 1392 # options for bareos_vadp_dumper: 1393 # -S: Cleanup on Start 1394 # -D: Cleanup on Disconnect 1395 # -M: Save metadata of VMDK on dump action 1396 # -R: Restore metadata of VMDK on restore action 1397 # -l: Write to a local VMDK 1398 # -C: Create local VMDK 1399 # -d: Specify local VMDK name 1400 # -f: Specify forced transport method 1401 bareos_vadp_dumper_opts = {} 1402 bareos_vadp_dumper_opts["dump"] = "-S -D -M" 1403 if "transport" in self.options: 1404 bareos_vadp_dumper_opts["dump"] += " -f %s" % self.options["transport"] 1405 if self.restore_vmdk_file: 1406 if os.path.exists(self.restore_vmdk_file): 1407 # restore of diff/inc, local vmdk exists already, 1408 # handling of replace options is done in create_file() 1409 bareos_vadp_dumper_opts["restore"] = "-l -R -d " 1410 else: 1411 # restore of full, must pass -C to create local vmdk 1412 # and make sure the target directory exists 1413 self.mkdir(os.path.dirname(self.restore_vmdk_file)) 1414 bareos_vadp_dumper_opts["restore"] = "-l -C -R -d " 1415 bareos_vadp_dumper_opts["restore"] += ( 1416 '"' + self.restore_vmdk_file.encode("utf-8") + '"' 1417 ) 1418 else: 1419 bareos_vadp_dumper_opts["restore"] = "-S -D -R" 1420 if "transport" in self.options: 1421 bareos_vadp_dumper_opts["restore"] += ( 1422 " -f %s" % self.options["transport"] 1423 ) 1424 1425 bareosfd.DebugMessage( 1426 context, 1427 100, 1428 "start_dumper(): dumper options: %s\n" 1429 % (repr(bareos_vadp_dumper_opts[cmd])), 1430 ) 1431 1432 bareos_vadp_dumper_command = '%s %s %s "%s"' % ( 1433 bareos_vadp_dumper_bin, 1434 bareos_vadp_dumper_opts[cmd], 1435 cmd, 1436 self.cbt_json_local_file_path, 1437 ) 1438 1439 bareosfd.DebugMessage( 1440 context, 1441 100, 1442 "start_dumper(): dumper command: %s\n" % (repr(bareos_vadp_dumper_command)), 1443 ) 1444 1445 bareos_vadp_dumper_command_args = shlex.split(bareos_vadp_dumper_command) 1446 bareosfd.DebugMessage( 1447 context, 1448 100, 1449 "start_dumper(): bareos_vadp_dumper_command_args: %s\n" 1450 % (repr(bareos_vadp_dumper_command_args)), 1451 ) 1452 log_path = "/var/log/bareos" 1453 stderr_log_fd = tempfile.NamedTemporaryFile(dir=log_path, delete=False) 1454 1455 bareos_vadp_dumper_process = None 1456 bareos_vadp_dumper_logfile = None 1457 try: 1458 if cmd == "dump": 1459 # backup 1460 bareos_vadp_dumper_process = subprocess.Popen( 1461 bareos_vadp_dumper_command_args, 1462 bufsize=-1, 1463 stdin=open("/dev/null"), 1464 stdout=subprocess.PIPE, 1465 stderr=stderr_log_fd, 1466 close_fds=True, 1467 ) 1468 else: 1469 # restore 1470 bareos_vadp_dumper_process = subprocess.Popen( 1471 bareos_vadp_dumper_command_args, 1472 bufsize=-1, 1473 stdin=subprocess.PIPE, 1474 stdout=open("/dev/null"), 1475 stderr=stderr_log_fd, 1476 close_fds=True, 1477 ) 1478 1479 # rename the stderr log file to one containing the PID 1480 bareos_vadp_dumper_logfile = "%s/bareos_vadp_dumper.%s.log" % ( 1481 log_path, 1482 bareos_vadp_dumper_process.pid, 1483 ) 1484 os.rename(stderr_log_fd.name, bareos_vadp_dumper_logfile) 1485 bareosfd.DebugMessage( 1486 context, 1487 100, 1488 "start_dumper(): started %s, log stderr to %s\n" 1489 % (repr(bareos_vadp_dumper_command), bareos_vadp_dumper_logfile), 1490 ) 1491 1492 except: 1493 # kill children if they arent done 1494 if bareos_vadp_dumper_process: 1495 bareosfd.JobMessage( 1496 context, 1497 bJobMessageType["M_WARNING"], 1498 "Failed to start %s\n" % (repr(bareos_vadp_dumper_command)), 1499 ) 1500 if ( 1501 bareos_vadp_dumper_process is not None 1502 and bareos_vadp_dumper_process.returncode is None 1503 ): 1504 bareosfd.JobMessage( 1505 context, 1506 bJobMessageType["M_WARNING"], 1507 "Killing probably stuck %s PID %s with signal 9\n" 1508 % ( 1509 repr(bareos_vadp_dumper_command), 1510 bareos_vadp_dumper_process.pid, 1511 ), 1512 ) 1513 os.kill(bareos_vadp_dumper_process.pid, 9) 1514 try: 1515 if bareos_vadp_dumper_process is not None: 1516 bareosfd.DebugMessage( 1517 context, 1518 100, 1519 "Waiting for command %s PID %s to terminate\n" 1520 % ( 1521 repr(bareos_vadp_dumper_command), 1522 bareos_vadp_dumper_process.pid, 1523 ), 1524 ) 1525 os.waitpid(bareos_vadp_dumper_process.pid, 0) 1526 bareosfd.DebugMessage( 1527 context, 1528 100, 1529 "Command %s PID %s terminated\n" 1530 % ( 1531 repr(bareos_vadp_dumper_command), 1532 bareos_vadp_dumper_process.pid, 1533 ), 1534 ) 1535 1536 except: 1537 pass 1538 raise 1539 else: 1540 raise 1541 1542 # bareos_vadp_dumper should be running now, set the process object 1543 # for further processing 1544 self.dumper_process = bareos_vadp_dumper_process 1545 self.dumper_stderr_log = bareos_vadp_dumper_logfile 1546 1547 # check if dumper is running to catch any error that occured 1548 # immediately after starting it 1549 if not self.check_dumper(context): 1550 return False 1551 1552 return True 1553 1554 def end_dumper(self, context): 1555 """ 1556 Wait for bareos_vadp_dumper to terminate 1557 """ 1558 bareos_vadp_dumper_returncode = None 1559 # Wait up to 120s for bareos_vadp_dumper to terminate, 1560 # if still running, send SIGTERM and wait up to 60s. 1561 # This handles cancelled jobs properly and prevents 1562 # from infinite loop if something unexpected goes wrong. 1563 timeout = 120 1564 start_time = int(time.time()) 1565 sent_sigterm = False 1566 while self.dumper_process.poll() is None: 1567 if int(time.time()) - start_time > timeout: 1568 bareosfd.DebugMessage( 1569 context, 1570 100, 1571 "Timeout wait for bareos_vadp_dumper PID %s to terminate\n" 1572 % (self.dumper_process.pid), 1573 ) 1574 if not sent_sigterm: 1575 bareosfd.DebugMessage( 1576 context, 1577 100, 1578 "sending SIGTERM to bareos_vadp_dumper PID %s\n" 1579 % (self.dumper_process.pid), 1580 ) 1581 os.kill(self.dumper_process.pid, signal.SIGTERM) 1582 sent_sigterm = True 1583 timeout = 60 1584 start_time = int(time.time()) 1585 continue 1586 else: 1587 bareosfd.DebugMessage( 1588 context, 1589 100, 1590 "Giving up to wait for bareos_vadp_dumper PID %s to terminate\n" 1591 % (self.dumper_process.pid), 1592 ) 1593 break 1594 1595 bareosfd.DebugMessage( 1596 context, 1597 100, 1598 "Waiting for bareos_vadp_dumper PID %s to terminate\n" 1599 % (self.dumper_process.pid), 1600 ) 1601 time.sleep(1) 1602 1603 bareos_vadp_dumper_returncode = self.dumper_process.returncode 1604 bareosfd.DebugMessage( 1605 context, 1606 100, 1607 "end_dumper() bareos_vadp_dumper returncode: %s\n" 1608 % (bareos_vadp_dumper_returncode), 1609 ) 1610 if bareos_vadp_dumper_returncode != 0: 1611 self.check_dumper(context) 1612 else: 1613 self.dumper_process = None 1614 1615 self.cleanup_tmp_files(context) 1616 1617 return bareos_vadp_dumper_returncode 1618 1619 def get_dumper_err(self): 1620 """ 1621 Read vadp_dumper stderr output file and return its content 1622 """ 1623 dumper_log_file = open(self.dumper_stderr_log, "r") 1624 err_msg = dumper_log_file.read() 1625 dumper_log_file.close() 1626 return err_msg 1627 1628 def check_dumper(self, context): 1629 """ 1630 Check if vadp_dumper has unexpectedly terminated, if so 1631 generate fatal job message 1632 """ 1633 bareosfd.DebugMessage( 1634 context, 100, "BareosFdPluginVMware:check_dumper() called\n" 1635 ) 1636 1637 if self.dumper_process.poll() is not None: 1638 bareos_vadp_dumper_returncode = self.dumper_process.returncode 1639 bareosfd.JobMessage( 1640 context, 1641 bJobMessageType["M_FATAL"], 1642 ( 1643 "check_dumper(): bareos_vadp_dumper returncode:" 1644 " %s error output:\n%s\n" 1645 ) 1646 % (bareos_vadp_dumper_returncode, self.get_dumper_err()), 1647 ) 1648 return False 1649 1650 bareosfd.DebugMessage( 1651 context, 1652 100, 1653 "BareosFdPluginVMware:check_dumper() dumper seems to be running\n", 1654 ) 1655 1656 return True 1657 1658 def cleanup_tmp_files(self, context): 1659 """ 1660 Cleanup temporary files 1661 """ 1662 1663 # delete temporary json file 1664 if not self.cbt_json_local_file_path: 1665 # not set, nothing to do 1666 return True 1667 1668 bareosfd.DebugMessage( 1669 context, 1670 100, 1671 "end_dumper() deleting temporary file %s\n" 1672 % (self.cbt_json_local_file_path), 1673 ) 1674 try: 1675 os.unlink(self.cbt_json_local_file_path) 1676 except OSError as e: 1677 bareosfd.JobMessage( 1678 context, 1679 bJobMessageType["M_WARNING"], 1680 "Could not delete %s: %s\n" 1681 % (self.cbt_json_local_file_path.encode("utf-8"), e.strerror), 1682 ) 1683 1684 self.cbt_json_local_file_path = None 1685 1686 return True 1687 1688 def retrieve_vcthumbprint(self, context): 1689 """ 1690 Retrieve the SSL Cert thumbprint from VC Server 1691 """ 1692 success = True 1693 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 1694 sock.settimeout(1) 1695 wrappedSocket = ssl.wrap_socket(sock) 1696 bareosfd.DebugMessage( 1697 context, 1698 100, 1699 "retrieve_vcthumbprint() Retrieving SSL ThumbPrint from %s\n" 1700 % (self.options["vcserver"]), 1701 ) 1702 try: 1703 wrappedSocket.connect((self.options["vcserver"], 443)) 1704 except: 1705 bareosfd.JobMessage( 1706 context, 1707 bJobMessageType["M_WARNING"], 1708 "Could not retrieve SSL Cert from %s\n" % (self.options["vcserver"]), 1709 ) 1710 success = False 1711 else: 1712 der_cert_bin = wrappedSocket.getpeercert(True) 1713 thumb_sha1 = hashlib.sha1(der_cert_bin).hexdigest() 1714 self.options["vcthumbprint"] = thumb_sha1.upper() 1715 1716 wrappedSocket.close() 1717 return success 1718 1719 # helper functions ############ 1720 1721 def mkdir(self, directory_name): 1722 """ 1723 Utility Function for creating directories, 1724 works like mkdir -p 1725 """ 1726 try: 1727 os.stat(directory_name) 1728 except OSError: 1729 os.makedirs(directory_name) 1730 1731 1732# vim: tabstop=4 shiftwidth=4 softtabstop=4 expandtab 1733