Subsections


Python Scripting

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 Configuration

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.

Bacula Events

A Bacula event is a point in the Bacula code where Bacula will call a subroutine (actually a method) that you have defined in the Python StartUp script. Events correspond to some significant event such as a Job Start, a Job End, Bacula needs a new Volume Name, ... When your script is called, it will have access to all the Bacula variables specific to the Job (attributes of the Job Object), and it can even call some of the Job methods (subroutines) or set new values in the Job attributes, such as the Priority. You will see below how the events are used.

Python Objects

There are four Python objects that you will need to work with:

The Bacula Object
The Bacula object is created by the Bacula daemon (the Director in the present case) when the daemon starts. It is available to the Python startup script, DirStartup.py, by importing the Bacula definitions with import bacula. The methods available with this object are described below.

The Bacula Events Class
You create this class in the startup script, and you pass it to the Bacula Object's set_events method. The purpose of the Bacula Events Class is to define what global or daemon events you want to monitor. When one of those events occurs, your Bacula Events Class will be called at the method corresponding to the event. There are currently three events, JobStart, JobEnd, and Exit, which are described in detail below.

The Job Object
When a Job starts, and assuming you have defined a JobStart method in your Bacula Events Class, Bacula will create a Job Object. This object will be passed to the JobStart event. The Job Object has a has good number of read-only members or attributes providing many details of the Job, and it also has a number of writable attributes that allow you to pass information into the Job. These attributes are described below.

The Job Events Class
You create this class in the JobStart method of your Bacula Events class, and it allows you to define which of the possible Job Object events you want to see. You must pass an instance of your Job Events class to the Job Object set_events() method. Normally, you will probably only have one Job Events Class, which will be instantiated for each Job. However, if you wish to see different events in different Jobs, you may have as many Job Events classes as you wish.

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.

JobStart
This Python method, if defined, will be called each time a Job is started. The method is passed the class instantiation object as the first argument, and the Bacula Job object as the second argument. The Bacula Job object has several built-in methods, and you can define which ones you want called. If you do not define this method, you will not be able to interact with Bacula jobs.

JobEnd
This Python method, if defined, will be called each time a Job terminates. The method is passed the class instantiation object as the first argument, and the Bacula Job object as the second argument.

Exit
This Python method, if defined, will be called when the Director terminates. The method is passed the class instantiation object as the first argument.

Access to the Bacula variables and methods is done with:

import bacula

The following are the read-only attributes provided by the bacula object.

Name
ConfigFile
WorkingDir
Version
string consisting of "Version Build-date"

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:

set_events
The set_events method takes a single argument, which is the instantiation of the Job Events class that contains the methods that you want called. The method names that will be called must correspond to the Bacula defined events. You may define additional methods but Bacula will not use them.
run
The run method takes a single string argument, which is the run command (same as in the Console) that you want to submit to start a new Job. The value returned by the run method is the JobId of the job that started, or -1 if there was an error.
write
The write method is used to be able to send print output to the Job Report. This will be described later.
cancel
The cancel method takes a single integer argument, which is a JobId. If JobId is found, it will be canceled.
DoesVolumeExist
The DoesVolumeExist method takes a single string argument, which is the Volume name, and returns 1 if the volume exists in the Catalog and 0 if the volume does not exist.

The following attributes are read/write within the Director for the job object.

Priority
Read or set the Job priority. Note, that setting a Job Priority is effective only before the Job actually starts.
Level
This attribute contains a string representing the Job level, e.g. Full, Differential, Incremental, ... if read. The level can also be set.

The following read-only attributes are available within the Director for the job object.

Type
This attribute contains a string representing the Job type, e.g. Backup, Restore, Verify, ...
JobId
This attribute contains an integer representing the JobId.
Client
This attribute contains a string with the name of the Client for this job.
NumVols
This attribute contains an integer with the number of Volumes in the Pool being used by the Job.
Pool
This attribute contains a string with the name of the Pool being used by the Job.
Storage
This attribute contains a string with the name of the Storage resource being used by the Job.
Catalog
This attribute contains a string with the name of the Catalog resource being used by the Job.
MediaType
This attribute contains a string with the name of the Media Type associated with the Storage resource being used by the Job.
Job
This attribute contains a string containing the name of the Job resource used by this job (not unique).
JobName
This attribute contains a string representing the full unique Job name.
JobStatus
This attribute contains a single character string representing the current Job status. The status may change during execution of the job. It may take on the following values:
C
Created, not yet running
R
Running
B
Blocked
T
Completed successfully
E
Terminated with errors
e
Non-fatal error
f
Fatal error
D
Verify found differences
A
Canceled by user
F
Waiting for Client
S
Waiting for Storage daemon
m
Waiting for new media
M
Waiting for media mount
s
Waiting for storage resource
j
Waiting for job resource
c
Waiting for client resource
d
Waiting on maximum jobs
t
Waiting on start time
p
Waiting on higher priority jobs

Priority
This attribute contains an integer with the priority assigned to the job.
CatalogRes
tuple consisting of (DBName, Address, User, Password, Socket, Port, Database Vendor) taken from the Catalog resource for the Job with the exception of Database Vendor, which is one of the following: MySQL, PostgreSQL, SQLite, Internal, depending on what database you configured.
VolumeName
After a Volume has been purged, this attribute will contain the name of that Volume. At other times, this value may have no meaning.

The following write-only attributes are available within the Director:

JobReport
Send line to the Job Report.
VolumeName
Set a new Volume name. Valid only during the NewVolume event.

Python Console Command

There is a new Console command named python. It takes a single argument restart. Example:

  python restart

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.

Debugging Python Scripts

In general, you debug your Python scripts by using print statements. You can also develop your script or important parts of it as a separate file using the Python interpreter to run it. Once you have it working correctly, you can then call the script from within the Bacula Python script (DirStartUp.py).

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.

Python Example

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