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