You may be asking what Python is and why a scripting language is needed in Bacula. The answer to the first question is that Python is an Object Oriented scripting language with features similar to those found in Perl, but the syntax of the language is much cleaner and simpler. The answer to why have scripting in Bacula is to give the user more control over the whole backup process. Probably the simplest example is when Bacula needs a new Volume name, with a scripting language such as Python, you can generate any name you want, based on the current state of Bacula.
Python must be enabled during the configuration process by adding a
--with-python, and possibly specifying an alternate directory if your Python is not installed in a standard system location. If you are using RPMs you will need the python-devel package installed.
When Python is configured, it becomes an integral part of Bacula and runs in Bacula's address space, so even though it is an interpreted language, it is very efficient.
When the Director starts, it looks to see if you have a Scripts Directory Directive defined (normal default /etc/bacula/scripts, if so, it looks in that directory for a file named DirStartUp.py. If it is found, Bacula will pass this file to Python for execution. The Scripts Directory is a new directive that you add to the Director resource of your bacula-dir.conf file.
Note: Bacula does not install Python scripts by default because these scripts are for you to program. This means that with a default installation with Python enabled, Bacula will print the following error message:
09-Jun 15:14 bacula-dir: ERROR in pythonlib.c:131 Could not import Python script /etc/bacula/scripts/DirStartUp. Python disabled.
The source code directory examples/python contains sample scripts for DirStartUp.py, SDStartUp.py, and FDStartUp.py that you might want to use as a starting point. Normally, your scripts directory (at least where you store the Python scripts) should be writable by Bacula, because Python will attempt to write a compiled version of the scripts (e.g. DirStartUp.pyc) back to that directory.
When starting with the sample scripts, you can delete any part that you will not need, but you should keep all the Bacula Event and Job Event definitions. If you do not want a particular event, simply replace the existing code with a noop = 1.
There are four Python objects that you will need to work with:
The first thing the startup script must do is to define what global Bacula events (daemon events), it wants to see. This is done by creating a Bacula Events class, instantiating it, then passing it to the set_events method. There are three possible events.
Access to the Bacula variables and methods is done with:
The following are the read-only attributes provided by the bacula object.
A simple definition of the Bacula Events Class might be the following:
import sys, bacula class BaculaEvents: def JobStart(self, job): ...
Then to instantiate the class and pass it to Bacula, you would do:
bacula.set_events(BaculaEvents()) # register Bacula Events wanted
And at that point, each time a Job is started, your BaculaEvents JobStart method will be called.
Now to actually do anything with a Job, you must define which Job events you want to see, and this is done by defining a JobEvents class containing the methods you want called. Each method name corresponds to one of the Job Events that Bacula will generate.
A simple Job Events class might look like the following:
class JobEvents: def NewVolume(self, job): ...
Here, your JobEvents class method NewVolume will be called each time the Job needs a new Volume name. To actually register the events defined in your class with the Job, you must instantiate the JobEvents class and set it in the Job set_events variable. Note, this is a bit different from how you registered the Bacula events. The registration process must be done in the Bacula JobStart event (your method). So, you would modify Bacula Events (not the Job events) as follows:
import sys, bacula class BaculaEvents: def JobStart(self, job): events = JobEvents() # create instance of Job class job.set_events(events) # register Job events desired ...
When a job event is triggered, the appropriate event definition is called in the JobEvents class. This is the means by which your Python script or code gets control. Once it has control, it may read job attributes, or set them. See below for a list of read-only attributes, and those that are writable.
In addition, the Bacula job object in the Director has a number of methods (subroutines) that can be called. They are:
The following attributes are read/write within the Director for the job object.
The following read-only attributes are available within the Director for the job object.
The following write-only attributes are available within the Director:
There is a new Console command named python. It takes a single argument restart. Example:
This command restarts the Python interpreter in the Director. This can be useful when you are modifying the DirStartUp script, because normally Python will cache it, and thus the script will be read one time.
If you are having problems loading DirStartUp.py, you will probably not get any error messages because Bacula can only print Python error messages after the Python interpreter is started. However, you may be able to see the error messages by starting Bacula in a shell window with the -d1 option on the command line. That should cause the Python error messages to be printed in the shell window.
If you are getting error messages such as the following when loading DirStartUp.py:
Traceback (most recent call last): File "/etc/bacula/scripts/DirStartUp.py", line 6, in ? import time, sys, bacula ImportError: /usr/lib/python2.3/lib-dynload/timemodule.so: undefined symbol: PyInt_FromLong bacula-dir: pythonlib.c:134 Python Import error.
It is because the DirStartUp script is calling a dynamically loaded module (timemodule.so in the above case) that then tries to use Python functions exported from the Python interpreter (in this case PyInt_FromLong). The way Bacula is currently linked with Python does not permit this. The solution to the problem is to put such functions (in this case the import of time into a separate Python script, which will do your calculations and return the values you want. Then call (not import) this script from the Bacula DirStartUp.py script, and it all should work as you expect.
An example script for the Director startup file is provided in examples/python/DirStartup.py as follows:
# # Bacula Python interface script for the Director # # You must import both sys and bacula import sys, bacula # This is the list of Bacula daemon events that you # can receive. class BaculaEvents(object): def __init__(self): # Called here when a new Bacula Events class is # is created. Normally not used noop = 1 def JobStart(self, job): """ Called here when a new job is started. If you want to do anything with the Job, you must register events you want to receive. """ events = JobEvents() # create instance of Job class events.job = job # save Bacula's job pointer job.set_events(events) # register events desired sys.stderr = events # send error output to Bacula sys.stdout = events # send stdout to Bacula jobid = job.JobId; client = job.Client numvols = job.NumVols job.JobReport="Python Dir JobStart: JobId=%d Client=%s NumVols=%d\n" % (jobid,client,numvols) # Bacula Job is going to terminate def JobEnd(self, job): jobid = job.JobId client = job.Client job.JobReport="Python Dir JobEnd output: JobId=%d Client=%s.\n" % (jobid, client) # Called here when the Bacula daemon is going to exit def Exit(self, job): print "Daemon exiting." bacula.set_events(BaculaEvents()) # register daemon events desired """ These are the Job events that you can receive. """ class JobEvents(object): def __init__(self): # Called here when you instantiate the Job. Not # normally used noop = 1 def JobInit(self, job): # Called when the job is first scheduled noop = 1 def JobRun(self, job): # Called just before running the job after initializing # This is the point to change most Job parameters. # It is equivalent to the JobRunBefore point. noop = 1 def NewVolume(self, job): # Called when Bacula wants a new Volume name. The Volume # name returned, if any, must be stored in job.VolumeName jobid = job.JobId client = job.Client numvol = job.NumVols; print job.CatalogRes job.JobReport = "JobId=%d Client=%s NumVols=%d" % (jobid, client, numvol) job.JobReport="Python before New Volume set for Job.\n" Vol = "TestA-%d" % numvol job.JobReport = "Exists=%d TestA-%d" % (job.DoesVolumeExist(Vol), numvol) job.VolumeName="TestA-%d" % numvol job.JobReport="Python after New Volume set for Job.\n" return 1 def VolumePurged(self, job): # Called when a Volume is purged. The Volume name can be referenced # with job.VolumeName noop = 1
Kern Sibbald 2017-08-28