1#!/usr/bin/env python
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: Maik Aussendorf
23#
24# Baseclass for Bareos python plugins
25# Functions taken and adapted from bareos-fd.py
26
27import bareosfd
28from bareosfd import *
29import os
30import stat
31import time
32
33
34class BareosFdPluginBaseclass(object):
35
36    """ Bareos python plugin base class """
37
38    def __init__(self, plugindef, mandatory_options=None):
39        bareosfd.DebugMessage(
40            100,
41            "Constructor called in module %s with plugindef=%s\n"
42            % (__name__, plugindef),
43        )
44        events = []
45        events.append(bEventJobEnd)
46        events.append(bEventEndBackupJob)
47        events.append(bEventEndFileSet)
48        events.append(bEventHandleBackupFile)
49        events.append(bEventStartBackupJob)
50        events.append(bEventStartRestoreJob)
51        bareosfd.RegisterEvents(events)
52        # get some static Bareos values
53        self.fdname = bareosfd.GetValue(bVarFDName)
54        self.jobId = bareosfd.GetValue(bVarJobId)
55        self.client = bareosfd.GetValue(bVarClient)
56        self.since = bareosfd.GetValue(bVarSinceTime)
57        self.level = bareosfd.GetValue(bVarLevel)
58        self.jobName = bareosfd.GetValue(bVarJobName)
59        # jobName is of format myName.2020-05-12_11.35.27_05
60        # short Name is everything left of the third point seen from the right
61        self.shortName = self.jobName.rsplit(".", 3)[0]
62        self.workingdir = bareosfd.GetValue(bVarWorkingDir)
63        self.startTime = int(time.time())
64        self.FNAME = "undef"
65        self.filetype = "undef"
66        self.file = None
67        bareosfd.DebugMessage(
68            100, "FDName = %s - BareosFdPluginBaseclass\n" % (self.fdname)
69        )
70        bareosfd.DebugMessage(
71            100, "WorkingDir: %s JobId: %s\n" % (self.workingdir, self.jobId)
72        )
73        self.mandatory_options = mandatory_options
74
75    def __str__(self):
76        return "<%s:fdname=%s jobId=%s client=%s since=%d level=%c jobName=%s workingDir=%s>" % (
77            self.__class__,
78            self.fdname,
79            self.jobId,
80            self.client,
81            self.since,
82            self.level,
83            self.jobName,
84            self.workingdir,
85        )
86
87    def parse_plugin_definition(self, plugindef):
88        bareosfd.DebugMessage(100, 'plugin def parser called with "%s"\n' % (plugindef))
89        # Parse plugin options into a dict
90        if not hasattr(self, "options"):
91            self.options = dict()
92        plugin_options = plugindef.split(":")
93        while plugin_options:
94            current_option = plugin_options.pop(0)
95            key, sep, val = current_option.partition("=")
96            # See if the last character is a escape of the value string
97            while val[-1:] == "\\":
98                val = val[:-1] + ":" + plugin_options.pop(0)
99            # Replace all '\\'
100            val = val.replace("\\", "")
101            bareosfd.DebugMessage(100, 'key:val = "%s:%s"\n' % (key, val))
102            if val == "":
103                continue
104            else:
105                if key not in self.options:
106                    self.options[key] = val
107        # after we've parsed all arguments, we check the options for mandatory settings
108        return self.check_options(self.mandatory_options)
109
110    def check_options(self, mandatory_options=None):
111        """
112        Check Plugin options
113        Here we just verify that eventual mandatory options are set.
114        If you have more to veriy, just overwrite ths method in your class
115        """
116        if mandatory_options is None:
117            return bRC_OK
118        for option in mandatory_options:
119            if option not in self.options:
120                bareosfd.DebugMessage(
121                    100, "Mandatory option '%s' not defined.\n" % option
122                )
123                bareosfd.JobMessage(
124                    M_FATAL,
125                    "Mandatory option '%s' not defined.\n" % (option),
126                )
127                return bRC_Error
128            bareosfd.DebugMessage(
129                100, "Using Option %s=%s\n" % (option, self.options[option])
130            )
131        return bRC_OK
132
133    def plugin_io_open(self, IOP):
134        self.FNAME = IOP.fname
135        bareosfd.DebugMessage(250, "io_open: self.FNAME is set to %s\n" % (self.FNAME))
136        if os.path.isdir(self.FNAME):
137            bareosfd.DebugMessage(100, "%s is a directory\n" % (self.FNAME))
138            self.fileType = "FT_DIR"
139            bareosfd.DebugMessage(
140                100,
141                "Did not open file %s of type %s\n" % (self.FNAME, self.fileType),
142            )
143            return bRC_OK
144        elif os.path.islink(self.FNAME):
145            self.fileType = "FT_LNK"
146            bareosfd.DebugMessage(
147                100,
148                "Did not open file %s of type %s\n" % (self.FNAME, self.fileType),
149            )
150            return bRC_OK
151        elif os.path.exists(self.FNAME) and stat.S_ISFIFO(os.stat(self.FNAME).st_mode):
152            self.fileType = "FT_FIFO"
153            bareosfd.DebugMessage(
154                100,
155                "Did not open file %s of type %s\n" % (self.FNAME, self.fileType),
156            )
157            return bRC_OK
158        else:
159            self.fileType = "FT_REG"
160            bareosfd.DebugMessage(
161                150,
162                "file %s has type %s - trying to open it\n"
163                % (self.FNAME, self.fileType),
164            )
165        try:
166            if IOP.flags & (os.O_CREAT | os.O_WRONLY):
167                bareosfd.DebugMessage(
168                    100,
169                    "Open file %s for writing with %s\n" % (self.FNAME, IOP),
170                )
171                dirname = os.path.dirname(self.FNAME)
172                if not os.path.exists(dirname):
173                    bareosfd.DebugMessage(
174                        100,
175                        "Directory %s does not exist, creating it now\n" % (dirname),
176                    )
177                    os.makedirs(dirname)
178                self.file = open(self.FNAME, "wb")
179            else:
180                bareosfd.DebugMessage(
181                    100,
182                    "Open file %s for reading with %s\n" % (self.FNAME, IOP),
183                )
184                self.file = open(self.FNAME, "rb")
185        except:
186            IOP.status = -1
187            return bRC_Error
188        return bRC_OK
189
190    def plugin_io_close(self, IOP):
191        bareosfd.DebugMessage(100, "Closing file " + "\n")
192        if self.fileType == "FT_REG":
193            self.file.close()
194        return bRC_OK
195
196    def plugin_io_seek(self, IOP):
197        return bRC_OK
198
199    def plugin_io_read(self, IOP):
200        if self.fileType == "FT_REG":
201            bareosfd.DebugMessage(
202                200, "Reading %d from file %s\n" % (IOP.count, self.FNAME)
203            )
204            IOP.buf = bytearray(IOP.count)
205            try:
206                IOP.status = self.file.readinto(IOP.buf)
207                IOP.io_errno = 0
208            except Exception as e:
209                bareosfd.JobMessage(
210                    M_ERROR,
211                    'Could net read %d bytes from file %s. "%s"'
212                    % (IOP.count, file_to_backup, e.message),
213                )
214                IOP.io_errno = e.errno
215                return bRC_Error
216        else:
217            bareosfd.DebugMessage(
218                100,
219                "Did not read from file %s of type %s\n" % (self.FNAME, self.fileType),
220            )
221            IOP.buf = bytearray()
222            IOP.status = 0
223            IOP.io_errno = 0
224        return bRC_OK
225
226    def plugin_io_write(self, IOP):
227        bareosfd.DebugMessage(200, "Writing buffer to file %s\n" % (self.FNAME))
228        try:
229            self.file.write(IOP.buf)
230        except Exception as e:
231            bareosfd.JobMessage(
232                M_ERROR,
233                'Could net write to file %s. "%s"' % (file_to_backup, e.message),
234            )
235            IOP.io_errno = e.errno
236            IOP.status = 0
237            return bRC_Error
238        IOP.status = IOP.count
239        IOP.io_errno = 0
240        return bRC_OK
241
242    def plugin_io(self, IOP):
243        """
244        Basic IO operations. Some tweaks here: IOP.fname is only set on file-open
245        We need to capture it on open and keep it for the remaining procedures
246        Now (since 2020) separated into sub-methods to ease overloading in derived classes
247        """
248        bareosfd.DebugMessage(
249            250,
250            "plugin_io called with function %s filename %s\n" % (IOP.func, IOP.fname),
251        )
252        bareosfd.DebugMessage(250, "self.FNAME is set to %s\n" % (self.FNAME))
253        if IOP.func == IO_OPEN:
254            return self.plugin_io_open(IOP)
255        elif IOP.func == IO_CLOSE:
256            return self.plugin_io_close(IOP)
257        elif IOP.func == IO_SEEK:
258            return self.plugin_io_seek(IOP)
259        elif IOP.func == IO_READ:
260            return self.plugin_io_read(IOP)
261        elif IOP.func == IO_WRITE:
262            return self.plugin_io_write(IOP)
263
264    def handle_plugin_event(self, event):
265        if event == bEventJobEnd:
266            bareosfd.DebugMessage(100, "handle_plugin_event called with bEventJobEnd\n")
267            return self.end_job()
268
269        elif event == bEventEndBackupJob:
270            bareosfd.DebugMessage(
271                100, "handle_plugin_event called with bEventEndBackupJob\n"
272            )
273            return self.end_backup_job()
274
275        elif event == bEventEndFileSet:
276            bareosfd.DebugMessage(
277                100, "handle_plugin_event called with bEventEndFileSet\n"
278            )
279            return self.end_fileset()
280
281        elif event == bEventStartBackupJob:
282            bareosfd.DebugMessage(
283                100, "handle_plugin_event() called with bEventStartBackupJob\n"
284            )
285            return self.start_backup_job()
286
287        elif event == bEventStartRestoreJob:
288            bareosfd.DebugMessage(
289                100,
290                "handle_plugin_event() called with bEventStartRestoreJob\n",
291            )
292            return self.start_restore_job()
293
294        else:
295            bareosfd.DebugMessage(
296                100, "handle_plugin_event called with event %s\n" % (event)
297            )
298        return bRC_OK
299
300    def start_backup_job(self):
301        """
302        Start of Backup Job. Called just before backup job really start.
303        Overload this to arrange whatever you have to do at this time.
304        """
305        return bRC_OK
306
307    def end_job(self):
308        """
309        Called if job ends regularyly (not for cancelled jobs)
310        Overload this to arrange whatever you have to do at this time.
311        """
312        return bRC_OK
313
314    def end_backup_job(self):
315        """
316        Called if backup job ends, before ClientAfterJob
317        Overload this to arrange whatever you have to do at this time.
318        """
319        return bRC_OK
320
321    def start_backup_file(self, savepkt):
322        """
323        Base method, we do not add anything, overload this method with your
324        implementation to add files to backup fileset
325        """
326        bareosfd.DebugMessage(100, "start_backup called\n")
327        return bRC_Skip
328
329    def end_backup_file(self):
330        bareosfd.DebugMessage(100, "end_backup_file() entry point in Python called\n")
331        return bRC_OK
332
333    def end_fileset(self):
334        bareosfd.DebugMessage(100, "end_fileset() entry point in Python called\n")
335        return bRC_OK
336
337    def start_restore_job(self):
338        """
339        Start of Restore Job. Called , if you have Restore objects.
340        Overload this to handle restore objects, if applicable
341        """
342        return bRC_OK
343
344    def start_restore_file(self, cmd):
345        bareosfd.DebugMessage(
346            100,
347            "start_restore_file() entry point in Python called with %s\n" % (cmd),
348        )
349        return bRC_OK
350
351    def end_restore_file(self):
352        bareosfd.DebugMessage(100, "end_restore_file() entry point in Python called\n")
353        return bRC_OK
354
355    def restore_object_data(self, ROP):
356        bareosfd.DebugMessage(100, "restore_object_data called with " + str(ROP) + "\n")
357        return bRC_OK
358
359    def create_file(self, restorepkt):
360        """
361        Creates the file to be restored and directory structure, if needed.
362        Adapt this in your derived class, if you need modifications for
363        virtual files or similar
364        """
365        bareosfd.DebugMessage(
366            100,
367            "create_file() entry point in Python called with %s\n" % (restorepkt),
368        )
369        # We leave file creation up to the core for the default case
370        restorepkt.create_status = CF_CORE
371        return bRC_OK
372
373    def set_file_attributes(self, restorepkt):
374        bareosfd.DebugMessage(
375            100,
376            "set_file_attributes() entry point in Python called with %s\n"
377            % (str(restorepkt)),
378        )
379        return bRC_OK
380
381    def check_file(self, fname):
382        bareosfd.DebugMessage(
383            100,
384            "check_file() entry point in Python called with %s\n" % (fname),
385        )
386        return bRC_OK
387
388    def get_acl(self, acl):
389        bareosfd.DebugMessage(
390            100, "get_acl() entry point in Python called with %s\n" % (acl)
391        )
392        return bRC_OK
393
394    def set_acl(self, acl):
395        bareosfd.DebugMessage(
396            100, "set_acl() entry point in Python called with %s\n" % (acl)
397        )
398        return bRC_OK
399
400    def get_xattr(self, xattr):
401        bareosfd.DebugMessage(
402            100, "get_xattr() entry point in Python called with %s\n" % (xattr)
403        )
404        return bRC_OK
405
406    def set_xattr(self, xattr):
407        bareosfd.DebugMessage(
408            100, "set_xattr() entry point in Python called with %s\n" % (xattr)
409        )
410        return bRC_OK
411
412    def handle_backup_file(self, savepkt):
413        bareosfd.DebugMessage(
414            100,
415            "handle_backup_file() entry point in Python called with %s\n" % (savepkt),
416        )
417        return bRC_OK
418
419
420# vim: ts=4 tabstop=4 expandtab shiftwidth=4 softtabstop=4
421