1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
|
#!/usr/bin/perl -w
# Copyright (C) 2000-2023 Bacula Systems SA
# License: BSD 2-Clause; see file LICENSE-FOSS
use strict;
my $VERSION = 1.1;
################################################################
# Installation
################################################################
#
# - Install the perl extension JSON (perl-JSON or libjson-perl)
#
# - Copy the script into /opt/bacula/scripts
# - Configure the variables at the top of the script (bconsole, limits)
# - Use the following runscript
# Job {
# RunScript {
# RunsWhen = Queued
# Command = "/opt/bacula/scripts/MaximumConcurrentJobPerLevel '%c' %l"
# Abort Job On Error = no
# RunsOnClient = no
# }
# ...
# }
#
# Can be executed manually, and the VERBOSE=1 environnement variable
# might help to diagnose problems.
#
# We use a file per client and level to avoid concurrency issues
# the location of the file is controlled by the $working variable.
################################################################
# Arguments
my $client = shift or usage();
my $level = shift or usage();
my $verbose = $ENV{VERBOSE} || 0;
################################################################
# Custom
my $bconsole = "/opt/bacula/bin/bconsole -u10";
my $working = "/opt/bacula/working/mcjpl";
my $conflict_time = 5;
my %MaximumConcurrentJob = (
'Full' => 1,
'Differential' => 1,
'Incremental' => 1
);
################################################################
# The Job intend to use a separate file-daemon for each of our clusters. The
# schedule calls for Full, Incremental, and Differential backups to
# occasionally run simultaneously but I want to make sure that a slot is always
# open for one job of each level to run against the cluster.
# The behavior might be summarized by:
# Maximum Concurrent Full Jobs = 1
# Maximum Concurrent Differential Jobs = 1
# Maximum Concurrent Incremental Jobs = 1
sub usage
{
print "ERROR: Incorrect usage: $0 client level\n";
exit -1;
}
use File::Temp;
# The JSON package must be installed libjson-perl or perl-JSON
eval "use JSON;";
if ($@) {
print "ERROR: Perl JSON module not found. Job control disabled.\n$@";
exit -1;
}
# We store some information in our $working directory
if (! -d $working) {
mkdir($working);
}
my $l;
# Get the list of running jobs for the same level and the same client
if ($level =~ /^([FDI])/) {
$l = $1;
} else {
print "Level $level not handled by Job control procedure\n";
exit -1;
}
# We escape the client name to avoid issues with unexpected characters
my $client_esc = $client;
$client_esc =~ s/[^a-z0-9.-_]/_/gi;
# The file in our working directory is used to avoid concurrent conflicts
# If the same level for the given client was authorized few seconds ago,
# we can delay our test to the next loop.
my @attrs = stat("$working/${client_esc}_${l}");
if (@attrs) {
# attrs[9] is the mtime
if ($attrs[9] > scalar(time() - $conflict_time)) {
print "Job started recently with the same level, testing the next time\n";
exit 1;
}
}
# We put our bconsole commands output into a temp file
my ($fh, $filename) = File::Temp::tempfile();
if (!open(FP, "|$bconsole> $filename")) {
print "ERROR: Unable to execute bconsole. Job control disabled.\n$!";
unlink($filename);
exit -1;
}
print FP ".api 2 api_opts=j\n";
print FP ".status dir running client=\"$client\"\nquit\n";
close(FP);
unlink($filename); # The file is still open via tempfile()
my $running;
while (my $line = <$fh>) {
if ($verbose) {
print "DEBUG: $line";
}
# {"running":[{"jobid":3,"level":"F","type":"B","status":"a","status_desc":"SD despooling Attributes","comment":"","jobbytes":0,"jobfiles":0,"job":"BackupClient1.2023-03-01_13.46.46_03","name":"BackupClient1","clientname":"zog8-fd","fileset":"Full Set","storage":"File1","rstorage":"","schedtime_epoch":1677674805,"schedtime":"2023-03-01 13:46:45","starttime_epoch":1677674808,"starttime":"2023-03-01 13:46:48","priority":10,"errors":0}],"error":0,"errmsg":""}
if ($line =~ /^\{/) {
$running = $line;
last;
}
}
if (!$running) {
print "ERROR: Unable to get running job list. Job control disabled.\n";
exit -1;
}
# We have a JSON string that we can decode and analyze. All parameters
# can be used in our decision to run or not
my $json = JSON::decode_json($running);
if (!$json || !$json->{running}) {
print "ERROR: Unable to decode JSON output from Director. Job control disabled.\n";
exit -1;
}
# In this example, we filter the job list by level and by status
my @jobs = grep {
$_->{level} eq $l && $_->{status} eq 'R'
} @{ $json->{running} };
# @jobs contains the list of running jobs with the given level
my $nb = scalar(@jobs);
print "Found $nb Job(s) running at level $level for $client\n";
# We do a simple check on the number of jobs running for the client at a certain level
if ($nb < $MaximumConcurrentJob{$level}) {
if (open(FP, ">$working/${client_esc}_${l}")) {
close(FP);
}
# OK, let it go!
exit 0;
} else {
# Need to wait for the next time
exit 1;
}
|