Bacula

Last modified by Sebastian Marsching on 2022/06/21 21:12

While the information compiled on this page was originally intended for Bacula, most of it should also apply to Bareos.

There is a separate page with information about Bareos, and some of the information there might also apply to Bacula.

vchanger script

See http://article.gmane.org/gmane.comp.bacula.user/28990.

I modified the script slightly to fix some problems I experienced. You can find the modified version below:

#!/bin/bash
#
#  Bacula interface to virtual autochanger using removable disk drives
#
#  Based (somewhat) on the "disk-changer" script from bacula 1.39.26
#
#  Vchanger is a Bacula autochanger script that emulates a conventional
#  magazine-based tape library device using removable disk drives.
#  Partitions on the removable drives are used as virtual magazines,
#  where each "magazine" contains the same number of virtual slots. Each
#  "slot" holds one virtual tape, where a "tape" is a regular file that
#  Bacula treats as a "Device Type = File" volume.
#
#  This script will be invoked by Bacula using the Bacula Autochanger
#  Interface and will be passed the following arguments:
#
#  vchanger "changer-device" "command" "slot" "archive-device" "drive-index"
#                 $1            $2       $3          #4             #5
#
#  See the Bacula documentation for Autochanger Interface details
#
#  Copyright (C) 2006 Josh Fisher
#
#  Permission to use, copy, modify, distribute, and sell this software
#  and its documentation for any purpose is hereby granted without fee,
#  provided that the above copyright notice appears in all copies.  This
#  software is provided "as is" without express or implied warranty.
#
#  This software is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# $Id: vchanger,v 0.7.3 2006/11/05 11:31:47 jfisher Exp $

#
# log whats done
#
dbgfile="/var/lib/bacula/vchanger.log"
# to turn on logging, uncomment the following line
#touch $dbgfile
#

#
# Write to a log file
#    To log debugging info, create file /var/bacula/vchanger.log
#    with write permission for bacula-sd user. To stop logging,
#    delete file /var/bacula/vchanger.log
#
function debug()
{
   if test -e $dbgfile; then
       echo "`date +\"%Y%m%d-%H:%M:%S\"` $*" >> $dbgfile
   fi
}

#
# Return length of string $1
#
function strlen ()
{
   expr length $1
}

#
# Prepend zeros to $1 and return a string that is $2 characters long
#
function mklen ()
{
  o1=$1
  while [ `eval strlen ${o1}` -lt ${2} ]; do
     o1="0${o1}"
  done
  echo $o1
}

#
# Initialize autochanger's state directory if not already initialized
#
function init_statedir() {
   debug "Initializing $statedir"
  # Create state dir if needed
  if [ ! -d "${statedir}" ]; then
      mkdir "${statedir}"
     if [ $? -ne 0 ]; then
        echo "Could not create ${statedir}"
        exit 1
     fi
  fi
   chmod 770 "${statedir}"
  if [ $? -ne 0 ]; then
     echo "Could not chmod ${statedir}"
     exit 1
  fi
  # Create nextmag file to hold max magazine index used
  if [ ! -f "${statedir}/nextmag" ]; then
     echo 0 >"${statedir}/nextmag"
     if [ $? -ne 0 ]; then
        echo "Could not create ${statedir}/nextmag"
        exit 1
     fi
  fi
   chmod 660 "${statedir}/nextmag"
  if [ $? -ne 0 ]; then
     echo "Could not chmod ${statedir}/nextmag"
     exit 1
  fi
  # Check nextmag value
  nextmag=`cat "${statedir}/nextmag"`
  if [ $? -ne 0 -o "${nextmag}" == "" -o $nextmag -lt 0 -o $nextmag -gt 99 ]; then
     echo "${statedir}/nextmag has invalid value"
     return 1
  fi
  # Create 'loaded' files for each virtual drive that hold the slot
  # number currently loaded in that 'drive'
  i=0
  while [ $i -le $maxdrive ]; do
     if [ ! -f "${statedir}/loaded${i}" ]; then
        echo "0" 2>/dev/null >"${statedir}/loaded${i}"
        if [ $? -ne 0 ]; then
           echo "Could not create ${statedir}/loaded${i}"
           exit 1
        fi
         chmod 660 "${statedir}/loaded${i}"
        if [ $? -ne 0 ]; then
           echo "Could not chmod ${statedir}/loaded${i}"
           exit 1
        fi
     fi
     i=`expr ${i} + 1`
  done
}


#
# Initialize magazine if not already initialized
#
function init_magazine() {
   debug "Initializing magazine"
  # Get max magazine index that has been used
  nextmag=`cat "${statedir}/nextmag"`
  if [ $? -ne 0 -o "${nextmag}" == "" ]; then
     echo "Failed to read ${statedir}/nextmag"
     exit 1
  fi
  # Check magazine for existing index
  if [ -f "${mountpoint}/index" ]; then
     # retrieve existing magazine index
     mi=`cat "${mountpoint}/index"`
     if [ $? -ne 0 ]; then
        echo "Failed to read ${mountpoint}/index"
        exit 1
     fi
     # must be 1-99
     if [ $mi -lt 1 -o $mi -gt 99 ]; then
        echo "Magazine has invalid index ${mi}"
        exit 1
     fi
  else
     # new magazine, so assign it the next avail index
     mi=`expr ${nextmag} + 1`
     if [ $mi -lt 0 -o $mi -gt 99 ]; then
        echo "Max magazines exceeded"
        exit 1
     fi
     echo $mi 2>/dev/null >"${mountpoint}/index"
     if [ $? -ne 0 ]; then
        echo "Failed to write ${mountpoint}/index"
        exit 1
     fi
  fi
  # make sure max index used is up to date
  if [ $mi -gt $nextmag ]; then
     echo $mi 2>/dev/null >"${statedir}/nextmag"
     if [ $? -ne 0 ]; then
        echo "Failed to update ${statedir}/nextmag"
        exit 1
     fi
  fi
  # make magazine index 2 digits
  magindex=`eval mklen ${mi} 2`
  # setup slot files (ie. virtual tapes)
  i=1
  while [ $i -le $magslots ]; do
     s=`eval mklen ${i} 3`
     f="${mountpoint}/${volumenameprefix}m${magindex}s${s}"
     if [ ! -f "${f}" ]; then
         touch "${f}" 2>/dev/null >/dev/null
        if [ $? -ne 0 ]; then
           echo "Failed to create ${f}"
           exit 1
        fi
     fi
     i=`expr ${i} + 1`
  done
  return 0
}


#
# check parameter count on commandline
#
function check_parm_count() {
   pCount=$1
   pCountNeed=$2
   if test $pCount -lt $pCountNeed; then
       echo "usage: vchanger ctl-device command [slot archive-device drive-index]"
       echo "  Insufficient number of arguments arguments given."
       if test $pCount -lt 2; then
           echo "  Mimimum usage is first two arguments ..."
       else
           echo "  Command expected $pCountNeed arguments"
       fi
       exit 1
   fi
}


# Setup arguments
ctl=$1
cmd="$2"
slot=$3
device=$4
drive=$5

# Setup default config values
magslots=10
maxdrive=0
statedir="/var/bacula/vchanger"
mountpoint=
volumenameprefix=

# Pull in conf file
if [ -f $ctl ]; then
   . $ctl
else
  echo "Config file ${ctl} not found"
  exit 1
fi

# check for required config values
if [ "${mountpoint}" == "" ]; then
  echo "Required variable 'mountpoint' not defined in ${ctl}"
  exit 1
fi
if [ "${magslots}" == "" -o $magslots -lt 1 -o $magslots -gt 999 ]; then
  echo "Ivalid value for 'magslots' in ${ctl}"
  exit 1
fi
if [ "${maxdrive}" == "" -o $maxdrive -lt 0 -o $maxdrive -ge $magslots ]; then
  echo "Invalid value for 'maxdrive' in ${ctl}"
  exit 1
fi
if [ "${statedir}" == "" ]; then
  echo "Invalid value for 'statedir' in ${ctl}"
  exit 1
fi

# Initialize state directory for this autochanger
init_statedir

# Check for special cases where only 2 arguments are needed,
#  all others are a minimum of 5
#
case $2 in
    list)
        check_parm_count $# 2
        ;;
    slots)
        check_parm_count $# 2
        ;;
    *)
        check_parm_count $# 5
       if [ $drive -gt $maxdrive ]; then
          echo "Drive ($drive) out of range (0-${maxdrive})"
          exit 1
       fi
       if [ $slot -gt $magslots ]; then
          echo "Slot ($slot) out of range (1-$magslots)"
          exit 1
       fi
        ;;
esac

debug "Parms: $ctl $cmd $slot $device $drive"

case $cmd in
   unload)
      debug "Doing vchanger -f $ctl unload $slot $device $drive"
     ld=`cat "${statedir}/loaded${drive}"`
     if [ $? -ne 0 ]; then
        echo "Failed to read ${statedir}/loaded${drive}"
        exit 1
     fi
     if [ $slot -eq $ld ]; then
        echo "0" >"${statedir}/loaded${drive}"
        if [ $? -ne 0 ]; then
           echo "Failed to write ${statedir}/loaded${drive}"
           exit 1
        fi
         unlink "${device}" 2>/dev/null >/dev/null
        exit 0
     fi
     if [ $ld -eq 0 ]; then
        echo "Drive ${drive} Is Empty"
     else
        echo "Storage Element ${slot} is Already Full"
     fi
     exit 1
      ;;

   load)
      debug "Doing vchanger $ctl load $slot $device $drive"
     ld=`cat "${statedir}/loaded${drive}"`
     if [ $? -ne 0 ]; then
        echo "Failed to read ${statedir}/loaded${drive}"
        exit 1
     fi
     if [ $ld -eq 0 ]; then
         unlink "${device}" 2>/dev/null >/dev/null
        # make sure slot is not loaded in another drive
        i=0
        while [ $i -le $maxdrive ]; do
           if [ $i -ne $drive ]; then
              ldi=`cat "${statedir}/loaded${i}"`
              if [ $ldi -eq $slot ]; then
                 echo "Storage Element ${slot} Empty (loaded in drive ${i})"
                 exit 1
              fi
           fi
           i=`expr ${i} + 1`
        done
         init_magazine
        if [ $? -ne 0 ]; then
           echo "Magazine Not Loaded"
           exit 1
        fi
        s=`eval mklen ${slot} 3`
         ln -s "${mountpoint}/${volumenameprefix}m${magindex}s${s}" "${device}"
        echo $slot >"${statedir}/loaded${drive}"
        exit 0
     else
        echo "Drive ${drive} Full (Storage element ${ld} loaded)"
        exit 1
     fi
      ;;

   list)
      debug "Doing vchanger -f $ctl -- to list volumes"
      init_magazine
     if [ $? -ne 0 ]; then
        echo "Magazine Not Loaded"
        exit 1
     fi
     i=1
     while [ $i -le $magslots ]; do
        s=`eval mklen ${i} 3`
        echo "${i}:${volumenameprefix}m${magindex}s${s}"
        i=`expr ${i} + 1`
     done
     exit 0
      ;;

   loaded)
      debug "Doing vchanger -f $ctl $drive -- to find what is loaded"
      cat "${statedir}/loaded${drive}"
     exit 0
      ;;

   slots)
      debug "Doing vchanger -f $ctl -- to get count of slots"
     echo $magslots
     exit 0
      ;;
esac

Labeling Volumes Automatically

For creating labels for new volumes automatically in an autochanger setup, you can use the label barcodes command in bconsole.

Recovering an error volume

A volume with an error status is not used when looking for available volumes. In order to make it available again, one can purge it, which will clear the error status:

Disaster Recovery for Windows Server 2012 R2 using Bacula

Disaster recovery for Windows systems has been a hot topic for years. The Bacula manual recommends creating a backup of the system state using "ntbackup" and (after restoring the files) using this backup for restoring the system state. However, ntbackup has not been a part of Windows since Windows Server 2008 and using the new "Windows Server Backup" (also known as "wbadmin") is not a great alternative, because its system-state backups are huge.

Bacula Systems offer a Bare Metal Recovery plugin as part of their Bacula Enterprise solution, however many people (like me) do not want to migrate to a commercial solution for their whole backup system.

Therefore I wondered, whether there might be a better solution for having disaster recovery with Bacula. My idea was that, using VSS, Bacula itself should be capable of creating an accurate backup of the system state and the main problem is not backup but restore. For my tests, I created a virtual machine running Windows Server 2012 R2 and set it up as an Active Directory domain controller. I used this setup because disaster recovery of a domain controller was one of my main points of worry. The idea is that, if you can restore a domain controller, all other things will be simple in comparison. I made a backup of the whole C: drive of this virtual machine using Bacula (having VSS enabled). Then I shutdown the virtual machine, created a new virtual machine with the same configuration and tried to restore the backup. This mimics the situation that I completely lost a system and now have to restore it on a new computer having the same hardware components. In my case having the same hardware is a pretty good assumption, because I like to run Windows Servers in VMs anyway.

The following guide lists the steps that I used for restoring the system. Although I tested it with Windows Server 2012 R2, I think the same steps will apply for disaster recovery of a Windows Server 2008, Windows Server 2008 R2 or Windows Server 2012 system.

Important Note: The steps described here worked for me in a particular situation. For example I restored from a full backup (without any differential or incremental backups in between). If you want to use differential or incremental backups, I think that at least you will want to enable the Accurate option on the job. For a different system configuration, these steps might not work. So you should not rely on the steps described here for disaster recovery, but instead test whether these steps work for your situation. It is much more comfortable to test a restore when you know that the original system is fine than when the restore just has to work. I can sleep better at night when i know that I tested restore procedures for critical systems.

Step-by-Step Guide

We have to build a Windows 7 x64 image using WinBuilder / multiPE (x86 might also work). The German computer magazine PC-Welt provides a nice download bundle that contains everything needed (apart from the Windows system itself of course). It is important to include the Visual C++ runtime (with 32-bit compatibility support). This option named "Force x86 SideBySide For 64-Bit Operation Systems" can be found in WinBuilder under "VC++ 2008". A PE-disk based on Windows XP might not work, because Windows XP did not support Junction Points (although the support for them has been in NTFS for decades), so Bacula might not be able to restore them. If we have a NIC or disk controller that is not natively supported by Windows, we have to include the corresponding drivers in the PE disk because otherwise we will not be able to restore the backup later.

First we have to boot from the operating-system installation-disk and install the OS the same way we would for a new system. However, to save some time, we can use the "Server Core" installation because we only do this to create partitions and to restore the boot loader. We stop the installation process at the time of the first reboot.

Now we boot from the PE-disk created with WinBuilder. We assign the C: drive letter to the new system partition on the hard-disk (using Disk Management), so that Bacula will restore the files to the right partition.

We format the C: drive, so that the files that were just created by the installer are deleted.

Now we have to download and install the Bacula File Daemon. It makes sense to use the same version of the Bacula FD that was used to create the Backup. For a 64-bit PE-disk we use the 64-bit version of Bacula. We use an installation path that for sure does not exist in the image to be restored (e.g. C:\BaculaJustForRestore). If we used the default path, we would get a collission with the Bacula files that are part of the restore. We remove the checkmarks on the "Install as service" and "Start after install" options because we will take care of starting Bacula manually once we have adjusted the configuration.

After the installation has finished, we open the bacula-fd.conf file and make sure that the configuration is correct: Basically, we want the same settings here that we had on the original system. In particular, the client name, director name and password should match. We also have to make sure that the paths for the working and PID directories point to the correct (non-standard) directory.

Finally, we have to ensure that the IP address of the system is set correctly. If the IP address does not match the address in the Bacula director configuration, we can either change the director configuration or manually configure the IP address of the system.

Now we start the Bacula FD by opening a command prompt, changing to the installation directory and running bacula-fd.exe with the right command-line parameters. Example:

c:
cd BaculaJustForRestore
bacula-fd /run -c C:\BaculaJustForRestore\bacula-fd.conf

Now we are ready to start the restore in the Bacula console. We have to select the whole C: drive (but no other drives - we can restore them later). In the restore options we set an empty prefix ("/") for the target location ("Where:"), so that the files are restored to their original location on the C: drive.

When the restore has completed, we shut the computer down and remove the PE-disk. That’s it. The next time we boot the computer the restored system should boot. After that, we can delete the temporary Bacula installation and restore the files on other disks (if there are any).

If the computer does not boot the system in the last step, the first thing I would try would be to boot from OS installation disk and use startrep.exe in repair mode. However, this is not a part of this guide, because for me it worked right away.

Making a system state backup using Windows Server Backup

If you want to make a system state backup using Windows Server Backup each time that you make a backup with Bacula, you might find the following script useful, that you can add as a run before job script.

@echo off
if "%1" == "" (
 echo Backup job level has to be passed as first parameter.
 exit /b 1
)
if /i %1 == full (
rem  wbadmin delete systemstatebackup -backupTarget:p: -keepVersions:0 -quiet
 if exist p:\WindowsImageBackup rd /s /q p:\WindowsImageBackup
  wbadmin start systemstatebackup -backupTarget:p: -quiet
) else (
 echo Backup Level is %1, skipping system state backup.
)