6 Bweb - A Bacula web interface
7 Bacula® - The Network Backup Solution
9 Copyright (C) 2000-2006 Free Software Foundation Europe e.V.
11 The main author of Bweb is Eric Bollengier.
12 The main author of Bacula is Kern Sibbald, with contributions from
13 many others, a complete list can be found in the file AUTHORS.
14 This program is Free Software; you can redistribute it and/or
15 modify it under the terms of version three of the GNU Affero General Public
16 License as published by the Free Software Foundation and included
19 This program is distributed in the hope that it will be useful, but
20 WITHOUT ANY WARRANTY; without even the implied warranty of
21 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
22 Affero General Public License for more details.
24 You should have received a copy of the GNU Affero General Public License
25 along with this program; if not, write to the Free Software
26 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
29 Bacula® is a registered trademark of Kern Sibbald.
30 The licensor of Bacula is the Free Software Foundation Europe
31 (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
32 Switzerland, email:ftf@fsfeurope.org.
45 use POSIX qw/strftime/;
46 use File::Basename qw/basename dirname/;
48 my $conf = new Bweb::Config(config_file => $Bweb::config_file);
50 my $bweb = new Bweb(info => $conf);
52 my $dbh = $bweb->{dbh};
53 my $debug = $bweb->{debug};
55 # Job table keep use Media or Job retention, so it's quite enought
57 # CREATE TABLE JobHistory (LIKE Job);
58 # INSERT INTO JobHistory
59 # (SELECT * FROM Job WHERE JobId NOT IN (SELECT JobId FROM JobHistory) );
60 my $jobt = $bweb->get_stat_table();
62 my $graph = CGI::param('graph') || 'job_size';
63 my $legend = CGI::param('legend') || 'on' ;
64 $legend = ($legend eq 'on')?1:0;
66 my $arg = $bweb->get_form(qw/width height limit offset age where jobid
67 jfilesets level status jjobnames jclients jclient_groups/);
69 my ($limitq, $label) = $bweb->get_limit(age => $arg->{age},
70 limit => $arg->{limit},
71 offset=> $arg->{offset},
72 order => "Job.StartTime ASC",
76 if ($arg->{status} and $arg->{status} ne 'Any') {
77 $statusq = " AND Job.JobStatus = '$arg->{status}' ";
81 if ($arg->{level} and $arg->{level} !~ 'All|Any') {
82 $levelq = " AND Job.Level = '$arg->{level}' ";
86 if ($arg->{jfilesets}) {
87 $filesetq = " AND FileSet.FileSet IN ($arg->{qfilesets}) ";
91 if ($arg->{jjobnames}) {
92 $jobnameq = " AND Job.Name IN ($arg->{jjobnames}) ";
94 $arg->{jjobnames} = 'all'; # skip warning
98 if ($arg->{jclients}) {
99 $clientq = " AND Client.Name IN ($arg->{jclients}) ";
101 $arg->{jclients} = 'all'; # skip warning
104 my $groupf=''; # from clause
105 my $groupq=''; # where clause
106 if ($arg->{jclient_groups}) {
107 $groupf = " JOIN client_group_member ON (Client.ClientId = client_group_member.clientid)
108 JOIN client_group USING (client_group_id)";
109 $groupq = " AND client_group_name IN ($arg->{jclient_groups}) ";
112 $bweb->can_do('r_view_stat');
113 my $filter = $bweb->get_client_filter();
115 my $gtype = CGI::param('gtype') || 'bars';
118 # in this mode, we generate an image and an imagemap
119 if ($gtype eq 'balloon') {
120 use Digest::MD5 qw(md5_hex);
123 my $b = new GBalloon(width=>$arg->{width},
124 height =>$arg->{height});
127 my %legend = (x_title => 'Time',
129 POSIX::strftime('%H:%M', gmtime($_[0]))
132 if ($graph eq 'job_time_size') {
133 $order = 'JobFiles,JobBytes';
135 $legend{y_title} = 'Nb files';
136 $legend{y_func} = sub { int(shift)};
137 $legend{z_title} = 'Size';
138 $legend{z_func} = \&Bweb::human_size;
140 $order = 'JobBytes,JobFiles';
142 $legend{y_title} = 'Size';
143 $legend{y_func} = \&Bweb::human_size;
144 $legend{z_title} = 'Nb files';
145 $legend{z_func} = sub { int(shift)};
148 $b->set_legend_axis(%legend);
150 my $all = $dbh->selectall_arrayref("
151 SELECT $bweb->{sql}->{SEC_TO_INT}( $bweb->{sql}->{UNIX_TIMESTAMP}(EndTime)
152 - $bweb->{sql}->{UNIX_TIMESTAMP}(StartTime))
153 AS duration, $order, JobId, Job.Name
155 FROM $jobt AS Job, Client $filter $groupf
156 WHERE Job.ClientId = Client.ClientId
166 foreach my $a (@$all) {
167 $b->add_point($a->[0], $a->[1], $a->[2],
168 "?action=job_zoom;jobid=$a->[3]",
169 "$a->[4] $legend{z_title} " . $legend{z_func}($a->[2]));
175 my $md5_rep = md5_hex(join(":", map { $arg->{$_} } sort keys %$arg));
177 # need to cleanup this path
178 open(FP, ">$conf->{fv_write_path}/$md5_rep.png");
179 print FP $GBalloon::gd->png;
182 print $b->get_imagemap("Job overview", "/bweb/fv/$md5_rep.png");
187 print CGI::header('image/png');
193 use GD::Graph::colour qw(:colours);
195 if ($gtype eq 'lines') {
196 use GD::Graph::lines;
197 $graph = GD::Graph::lines->new ( $arg->{width}, $arg->{height} );
199 } elsif ($gtype eq 'bars') {
201 $graph = GD::Graph::bars->new ( $arg->{width}, $arg->{height} );
203 } elsif ($gtype eq 'linespoints') {
204 use GD::Graph::linespoints;
205 $graph = GD::Graph::linespoints->new ( $arg->{width}, $arg->{height} );
207 # this doesnt works at this time
208 # } elsif ($gtype eq 'bars3d') {
209 # use GD::Graph::bars3d;
210 # $graph = GD::Graph::bars3d->new ( $arg->{width}, $arg->{height} );
216 $graph->set('x_label' => 'Time',
217 'x_number_format' => sub { strftime('%D', localtime($_[0])) },
218 'x_tick_number' => 5*$arg->{width}/800,
220 dclrs => [ "lred", add_colour("#008e8e"),
221 add_colour("#afd8f8"), add_colour("#f6bd0f"),
222 add_colour("#8bba00"), add_colour("#ff8e46"),
223 add_colour("#d64646"),
224 add_colour("#8e468e"), add_colour("#588526"),
225 add_colour("#b3aa00"), add_colour("#008ed6"),
226 add_colour("#9d080d"), add_colour("#a186be"),
230 if ($conf->{graph_font} && -f $conf->{graph_font}) {
231 $graph->set_title_font([$conf->{graph_font}], 12);
232 $graph->set_legend_font([$conf->{graph_font}], 11);
246 foreach my $row (@$all_row) {
247 my $label = $row->[1] . "/" . $row->[2] ; # client/backup name
249 if ($arg->{level} && $arg->{level} eq 'All') { # can separate level
250 $label = $row->[4] . ': ' . $label; # if users ask for
253 $ret->{date}->[$i] = $row->[0];
254 $ret->{$label}->[$i] = $row->[3];
256 $last_date = $row->[0];
259 # insert a fake element
260 foreach my $elt ( keys %{$ret}) {
261 $ret->{$elt}->[$i] = undef;
264 $ret->{date}->[$i] = $last_date + 1;
266 my $date = $ret->{date} ;
269 return ($date, $ret);
281 foreach my $row (@$all_row) {
282 $ret->{date}->[$i] = $row->[0];
283 $ret->{nb}->[$i] = $row->[1];
290 if ($graph eq 'job_size') {
294 UNIX_TIMESTAMP(Job.StartTime) AS starttime,
295 Client.Name AS clientname,
297 Job.JobBytes AS jobbytes,
298 Job.Level AS joblevel
299 FROM $jobt AS Job, FileSet, Client $filter $groupf
300 WHERE Job.ClientId = Client.ClientId
301 AND Job.FileSetId = FileSet.FileSetId
312 print STDERR $query if ($debug);
314 my $obj = get_graph('title' => "Job Size : $arg->{jclients}/$arg->{jjobnames}",
317 'y_number_format' => \&Bweb::human_size,
320 my $all = $dbh->selectall_arrayref($query) ;
322 my ($d, $ret) = make_tab($all);
324 $obj->set_legend(keys %$ret);
326 print $obj->plot([$d, values %$ret])->png;
329 if ($graph eq 'job_file') {
333 UNIX_TIMESTAMP(Job.StartTime) AS starttime,
334 Client.Name AS clientname,
336 Job.JobFiles AS jobfiles,
337 Job.Level AS joblevel
338 FROM $jobt AS Job, FileSet, Client $filter $groupf
339 WHERE Job.ClientId = Client.ClientId
340 AND Job.FileSetId = FileSet.FileSetId
351 print STDERR $query if ($debug);
353 my $obj = get_graph('title' => "Job Files : $arg->{jclients}/$arg->{jjobnames}",
354 'y_label' => 'Number Files',
358 my $all = $dbh->selectall_arrayref($query) ;
360 my ($d, $ret) = make_tab($all);
362 $obj->set_legend(keys %$ret);
364 print $obj->plot([$d, values %$ret])->png;
367 # it works only with postgresql at this time
368 # we don't use $jobt because we use File, so job is in Job table
369 elsif ($graph eq 'file_histo' and $arg->{where}) {
371 my $dir = $dbh->quote(dirname($arg->{where}) . '/');
372 my $file = $dbh->quote(basename($arg->{where}));
375 SELECT UNIX_TIMESTAMP(Job.StartTime) AS starttime,
376 Client.Name AS client,
378 base64_decode_lstat(8,LStat) AS lstat,
379 Job.Level AS joblevel
381 FROM Job, FileSet, Filename, Path, File, Client $filter
382 WHERE Job.ClientId = Client.ClientId
383 AND Job.FileSetId = FileSet.FileSetId
385 AND File.JobId = Job.JobId
386 AND File.FilenameId = Filename.FilenameId
387 AND File.PathId = Path.PathId
389 AND Filename.Name = $file
398 print STDERR $query if ($debug);
400 my $all = $dbh->selectall_arrayref($query) ;
402 my $obj = get_graph('title' => "File size : $arg->{where}",
403 'y_label' => 'File size',
406 'y_number_format' => \&Bweb::human_size,
410 my ($d, $ret) = make_tab($all);
412 $obj->set_legend(keys %$ret);
414 print $obj->plot([$d, values %$ret])->png;
417 # it works only with postgresql at this time
418 # TODO: use brestore_missing_path
419 elsif ($graph eq 'rep_histo' and $arg->{where}) {
421 my $dir = $arg->{where};
422 $dir .= '/' if ($dir !~ m!/$!);
423 $dir = $dbh->quote($dir);
426 SELECT UNIX_TIMESTAMP(Job.StartTime) AS starttime,
427 Client.Name AS client,
429 brestore_pathvisibility.size AS size,
430 Job.Level AS joblevel
432 FROM Job, Client $filter, FileSet, Path, brestore_pathvisibility
433 WHERE Job.ClientId = Client.ClientId
434 AND Job.FileSetId = FileSet.FileSetId
436 AND Job.JobId = brestore_pathvisibility.JobId
437 AND Path.PathId = brestore_pathvisibility.PathId
447 print STDERR $query if ($debug);
449 my $all = $dbh->selectall_arrayref($query) ;
451 my $obj = get_graph('title' => "Directory size : $arg->{where}",
452 'y_label' => 'Directory size',
455 'y_number_format' => \&Bweb::human_size,
459 my ($d, $ret) = make_tab($all);
461 $obj->set_legend(keys %$ret);
463 print $obj->plot([$d, values %$ret])->png;
466 elsif ($graph eq 'job_rate') {
470 UNIX_TIMESTAMP(Job.StartTime) AS starttime,
471 Client.Name AS clientname,
474 ($bweb->{sql}->{SEC_TO_INT}(
475 $bweb->{sql}->{UNIX_TIMESTAMP}(EndTime)
476 - $bweb->{sql}->{UNIX_TIMESTAMP}(StartTime)) + 0.01)
478 Job.Level AS joblevel
480 FROM $jobt AS Job, FileSet, Client $filter $groupf
481 WHERE Job.ClientId = Client.ClientId
482 AND Job.FileSetId = FileSet.FileSetId
493 print STDERR $query if ($debug);
495 my $obj = get_graph('title' => "Job Rate : $arg->{jclients}/$arg->{jjobnames}",
496 'y_label' => 'Rate b/s',
498 'y_number_format' => \&Bweb::human_size,
501 my $all = $dbh->selectall_arrayref($query) ;
503 my ($d, $ret) = make_tab($all);
505 $obj->set_legend(keys %$ret);
507 print $obj->plot([$d, values %$ret])->png;
512 elsif ($graph eq 'job_duration') {
516 UNIX_TIMESTAMP(Job.StartTime) AS starttime,
517 Client.Name AS clientname,
519 $bweb->{sql}->{SEC_TO_INT}( $bweb->{sql}->{UNIX_TIMESTAMP}(EndTime)
520 - $bweb->{sql}->{UNIX_TIMESTAMP}(StartTime))
522 Job.Level AS joblevel
524 FROM $jobt AS Job, FileSet, Client $filter $groupf
525 WHERE Job.ClientId = Client.ClientId
526 AND Job.FileSetId = FileSet.FileSetId
537 print STDERR $query if ($debug);
539 my $obj = get_graph('title' => "Job Duration : $arg->{jclients}/$arg->{jjobnames}",
540 'y_label' => 'Duration',
542 'y_number_format' => \&Bweb::human_sec,
544 my $all = $dbh->selectall_arrayref($query) ;
546 my ($d, $ret) = make_tab($all);
548 $obj->set_legend(keys %$ret);
550 print $obj->plot([$d, values %$ret])->png;
553 # number of job per day/hour
554 } elsif ($graph =~ /^job_(count|sum|avg)_((p?)(day|hour|month))$/) {
558 my ($limit, $label) = $bweb->get_limit(age => $arg->{age},
559 limit => $arg->{limit},
560 offset=> $arg->{offset},
564 my @arg; # arg for plotting
566 if (!$per_t) { # much better aspect
569 push @arg, ("x_number_format" => undef,
574 if ($t eq 'sum' or $t eq 'avg') {
575 push @arg, ('y_number_format' => \&Bweb::human_size);
578 my $stime = $bweb->{sql}->{"STARTTIME_$d"};
582 " . ($per_t?"":"UNIX_TIMESTAMP") . "($stime) AS A,
584 FROM $jobt AS Job, FileSet, Client $filter $groupf
585 WHERE Job.ClientId = Client.ClientId
586 AND Job.FileSetId = FileSet.FileSetId
597 print STDERR $query if ($debug);
599 my $obj = get_graph('title' => "Job $t : $arg->{jclients}/$arg->{jjobnames}",
605 my $all = $dbh->selectall_arrayref($query) ;
606 # print STDERR Data::Dumper::Dumper($all);
607 my ($ret) = make_tab_sum($all);
609 print $obj->plot([$ret->{date}, $ret->{nb}])->png;