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