File: MaximumConcurrentJobPerLevel

package info (click to toggle)
bacula 15.0.3-5
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 29,780 kB
  • sloc: ansic: 194,276; cpp: 41,177; sh: 28,258; python: 6,669; makefile: 5,275; perl: 3,666; sql: 1,371; java: 345; xml: 196; awk: 51; sed: 25
file content (166 lines) | stat: -rwxr-xr-x 5,170 bytes parent folder | download | duplicates (3)
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;
}