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