6 Bweb - A Bacula web interface
7 Bacula® - The Network Backup Solution
9 Copyright (C) 2000-2010 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.
41 use POSIX qw/strftime/;
42 use File::Basename qw/basename dirname/;
44 my $conf = new Bweb::Config(config_file => $Bweb::config_file);
46 my $bweb = new Bweb(info => $conf);
48 my $dbh = $bweb->{dbh};
49 my $debug = $bweb->{debug};
51 # Job table keep use Media or Job retention, so it's quite enought
53 # CREATE TABLE JobHistory (LIKE Job);
54 # INSERT INTO JobHistory
55 # (SELECT * FROM Job WHERE JobId NOT IN (SELECT JobId FROM JobHistory) );
56 my $jobt = $bweb->get_stat_table();
58 my $graph = CGI::param('graph') || 'job_size';
59 my $legend = CGI::param('legend') || 'on' ;
60 $legend = ($legend eq 'on')?1:0;
62 my $arg = $bweb->get_form(qw/width height limit offset age where jobid
63 jfilesets level status jjobnames jclients jclient_groups/);
65 my ($limitq, $label) = $bweb->get_limit(age => $arg->{age},
66 limit => $arg->{limit},
67 offset=> $arg->{offset},
68 order => "Job.StartTime ASC",
72 if ($arg->{status} and $arg->{status} ne 'Any') {
73 $statusq = " AND Job.JobStatus = '$arg->{status}' ";
77 if ($arg->{level} and $arg->{level} !~ 'All|Any') {
78 $levelq = " AND Job.Level = '$arg->{level}' ";
82 if ($arg->{jfilesets}) {
83 $filesetq = " AND FileSet.FileSet IN ($arg->{qfilesets}) ";
87 if ($arg->{jjobnames}) {
88 $jobnameq = " AND Job.Name IN ($arg->{jjobnames}) ";
90 $arg->{jjobnames} = 'all'; # skip warning
94 if ($arg->{jclients}) {
95 $clientq = " AND Client.Name IN ($arg->{jclients}) ";
97 $arg->{jclients} = 'all'; # skip warning
100 my $groupf=''; # from clause
101 my $groupq=''; # where clause
102 if ($arg->{jclient_groups}) {
103 $groupf = " JOIN client_group_member ON (Client.ClientId = client_group_member.clientid)
104 JOIN client_group USING (client_group_id)";
105 $groupq = " AND client_group_name IN ($arg->{jclient_groups}) ";
108 $bweb->can_do('r_view_stat');
109 my $filter = $bweb->get_client_filter();
111 my $gtype = CGI::param('gtype') || 'bars';
114 # in this mode, we generate an image and an imagemap
115 if ($gtype eq 'balloon') {
116 use Digest::MD5 qw(md5_hex);
119 my $b = new GBalloon(width=>$arg->{width},
120 height =>$arg->{height});
123 my %legend = (x_title => 'Time',
125 POSIX::strftime('%H:%M', gmtime($_[0]))
128 if ($graph eq 'job_time_size') {
129 $order = 'JobFiles,JobBytes';
131 $legend{y_title} = 'Nb files';
132 $legend{y_func} = sub { int(shift)};
133 $legend{z_title} = 'Size';
134 $legend{z_func} = \&Bweb::human_size;
136 $order = 'JobBytes,JobFiles';
138 $legend{y_title} = 'Size';
139 $legend{y_func} = \&Bweb::human_size;
140 $legend{z_title} = 'Nb files';
141 $legend{z_func} = sub { int(shift)};
144 $b->set_legend_axis(%legend);
146 my $all = $dbh->selectall_arrayref("
147 SELECT $bweb->{sql}->{JOB_DURATION} AS duration,
148 $order, JobId, Job.Name
150 FROM $jobt AS Job, Client $filter $groupf
151 WHERE Job.ClientId = Client.ClientId
161 foreach my $a (@$all) {
162 $b->add_point($a->[0], $a->[1], $a->[2],
163 "?action=job_zoom;jobid=$a->[3]",
164 "$a->[4] $legend{z_title} " . $legend{z_func}($a->[2]));
170 my $md5_rep = md5_hex(join(":", map { $arg->{$_} } sort keys %$arg));
172 # need to cleanup this path
173 open(FP, ">$conf->{fv_write_path}/$md5_rep.png");
174 print FP $GBalloon::gd->png;
177 print $b->get_imagemap("Job overview", "/bweb/fv/$md5_rep.png");
182 $bweb->send_content_type(-type => 'image/png');
188 use GD::Graph::colour qw(:colours);
190 if ($gtype eq 'lines') {
191 use GD::Graph::lines;
192 $graph = GD::Graph::lines->new ( $arg->{width}, $arg->{height} );
194 } elsif ($gtype eq 'bars') {
196 $graph = GD::Graph::bars->new ( $arg->{width}, $arg->{height} );
198 } elsif ($gtype eq 'linespoints') {
199 use GD::Graph::linespoints;
200 $graph = GD::Graph::linespoints->new ( $arg->{width}, $arg->{height} );
202 # this doesnt works at this time
203 # } elsif ($gtype eq 'bars3d') {
204 # use GD::Graph::bars3d;
205 # $graph = GD::Graph::bars3d->new ( $arg->{width}, $arg->{height} );
211 $graph->set('x_label' => 'Time',
212 'x_number_format' => sub { strftime('%D', localtime($_[0])) },
213 'x_tick_number' => 5*$arg->{width}/800,
215 dclrs => [ "lred", add_colour("#008e8e"),
216 add_colour("#afd8f8"), add_colour("#f6bd0f"),
217 add_colour("#8bba00"), add_colour("#ff8e46"),
218 add_colour("#d64646"),
219 add_colour("#8e468e"), add_colour("#588526"),
220 add_colour("#b3aa00"), add_colour("#008ed6"),
221 add_colour("#9d080d"), add_colour("#a186be"),
225 if ($conf->{graph_font} && -f $conf->{graph_font}) {
226 $graph->set_title_font([$conf->{graph_font}], 12);
227 $graph->set_legend_font([$conf->{graph_font}], 11);
241 foreach my $row (@$all_row) {
242 my $label = $row->[1] . "/" . $row->[2] ; # client/backup name
244 if ($arg->{level} && $arg->{level} eq 'All') { # can separate level
245 $label = $row->[4] . ': ' . $label; # if users ask for
248 $ret->{date}->[$i] = $row->[0];
249 $ret->{$label}->[$i] = $row->[3];
251 $last_date = $row->[0];
254 # insert a fake element
255 foreach my $elt ( keys %{$ret}) {
256 $ret->{$elt}->[$i] = undef;
259 $ret->{date}->[$i] = $last_date + 1;
261 my $date = $ret->{date} ;
264 return ($date, $ret);
276 foreach my $row (@$all_row) {
277 $ret->{date}->[$i] = $row->[0];
278 $ret->{nb}->[$i] = $row->[1];
285 if ($graph eq 'job_size') {
289 $bweb->{sql}->{STARTTIME_SEC} AS starttime,
290 Client.Name AS clientname,
292 Job.JobBytes AS jobbytes,
293 Job.Level AS joblevel
294 FROM $jobt AS Job, FileSet, Client $filter $groupf
295 WHERE Job.ClientId = Client.ClientId
296 AND Job.FileSetId = FileSet.FileSetId
307 print STDERR $query if ($debug);
309 my $obj = get_graph('title' => "Job Size : $arg->{jclients}/$arg->{jjobnames}",
312 'y_number_format' => \&Bweb::human_size,
315 my $all = $dbh->selectall_arrayref($query) ;
317 my ($d, $ret) = make_tab($all);
319 $obj->set_legend(keys %$ret);
321 print $obj->plot([$d, values %$ret])->png;
324 if ($graph eq 'job_file') {
328 $bweb->{sql}->{STARTTIME_SEC} AS starttime,
329 Client.Name AS clientname,
331 Job.JobFiles AS jobfiles,
332 Job.Level AS joblevel
333 FROM $jobt AS Job, FileSet, Client $filter $groupf
334 WHERE Job.ClientId = Client.ClientId
335 AND Job.FileSetId = FileSet.FileSetId
346 print STDERR $query if ($debug);
348 my $obj = get_graph('title' => "Job Files : $arg->{jclients}/$arg->{jjobnames}",
349 'y_label' => 'Number Files',
353 my $all = $dbh->selectall_arrayref($query) ;
355 my ($d, $ret) = make_tab($all);
357 $obj->set_legend(keys %$ret);
359 print $obj->plot([$d, values %$ret])->png;
362 # it works only with postgresql at this time
363 # we don't use $jobt because we use File, so job is in Job table
364 elsif ($graph eq 'file_histo' and $arg->{where}) {
366 my $dir = $dbh->quote(dirname($arg->{where}) . '/');
367 my $file = $dbh->quote(basename($arg->{where}));
370 SELECT $bweb->{sql}->{STARTTIME_SEC} AS starttime,
371 Client.Name AS client,
373 base64_decode_lstat(8,LStat) AS lstat,
374 Job.Level AS joblevel
376 FROM Job, FileSet, Filename, Path, File, Client $filter
377 WHERE Job.ClientId = Client.ClientId
378 AND Job.FileSetId = FileSet.FileSetId
380 AND File.JobId = Job.JobId
381 AND File.FilenameId = Filename.FilenameId
382 AND File.PathId = Path.PathId
384 AND Filename.Name = $file
393 print STDERR $query if ($debug);
395 my $all = $dbh->selectall_arrayref($query) ;
397 my $obj = get_graph('title' => "File size : $arg->{where}",
398 'y_label' => 'File size',
401 'y_number_format' => \&Bweb::human_size,
405 my ($d, $ret) = make_tab($all);
407 $obj->set_legend(keys %$ret);
409 print $obj->plot([$d, values %$ret])->png;
412 # it works only with postgresql at this time
413 # TODO: use brestore_missing_path
414 elsif ($graph eq 'rep_histo' and $arg->{where}) {
416 my $dir = $arg->{where};
417 $dir .= '/' if ($dir !~ m!/$!);
418 $dir = $dbh->quote($dir);
421 SELECT $bweb->{sql}->{STARTTIME_SEC} AS starttime,
422 Client.Name AS client,
424 brestore_pathvisibility.size AS size,
425 Job.Level AS joblevel
427 FROM Job, Client $filter, FileSet, Path, brestore_pathvisibility
428 WHERE Job.ClientId = Client.ClientId
429 AND Job.FileSetId = FileSet.FileSetId
431 AND Job.JobId = brestore_pathvisibility.JobId
432 AND Path.PathId = brestore_pathvisibility.PathId
442 print STDERR $query if ($debug);
444 my $all = $dbh->selectall_arrayref($query) ;
446 my $obj = get_graph('title' => "Directory size : $arg->{where}",
447 'y_label' => 'Directory size',
450 'y_number_format' => \&Bweb::human_size,
454 my ($d, $ret) = make_tab($all);
456 $obj->set_legend(keys %$ret);
458 print $obj->plot([$d, values %$ret])->png;
461 elsif ($graph eq 'job_rate') {
465 $bweb->{sql}->{STARTTIME_SEC} AS starttime,
466 Client.Name AS clientname,
469 ($bweb->{sql}->{JOB_DURATION} + 0.01) AS rate,
470 Job.Level AS joblevel
472 FROM $jobt AS Job, FileSet, Client $filter $groupf
473 WHERE Job.ClientId = Client.ClientId
474 AND Job.FileSetId = FileSet.FileSetId
485 print STDERR $query if ($debug);
487 my $obj = get_graph('title' => "Job Rate : $arg->{jclients}/$arg->{jjobnames}",
488 'y_label' => 'Rate b/s',
490 'y_number_format' => \&Bweb::human_size,
493 my $all = $dbh->selectall_arrayref($query) ;
495 my ($d, $ret) = make_tab($all);
497 $obj->set_legend(keys %$ret);
499 print $obj->plot([$d, values %$ret])->png;
504 elsif ($graph eq 'job_duration') {
508 $bweb->{sql}->{STARTTIME_SEC} AS starttime,
509 Client.Name AS clientname,
511 $bweb->{sql}->{JOB_DURATION} AS duration,
512 Job.Level AS joblevel
514 FROM $jobt AS Job, FileSet, Client $filter $groupf
515 WHERE Job.ClientId = Client.ClientId
516 AND Job.FileSetId = FileSet.FileSetId
527 print STDERR $query if ($debug);
529 my $obj = get_graph('title' => "Job Duration : $arg->{jclients}/$arg->{jjobnames}",
530 'y_label' => 'Duration',
532 'y_number_format' => \&Bweb::human_sec,
534 my $all = $dbh->selectall_arrayref($query) ;
536 my ($d, $ret) = make_tab($all);
538 $obj->set_legend(keys %$ret);
540 print $obj->plot([$d, values %$ret])->png;
543 # number of job per day/hour
544 } elsif ($graph =~ /^job_(count|sum|avg)_((p?)(day|hour|month))$/) {
548 my ($limit, $label) = $bweb->get_limit(age => $arg->{age},
549 limit => $arg->{limit},
550 offset=> $arg->{offset},
554 my @arg; # arg for plotting
556 if (!$per_t) { # much better aspect
559 push @arg, ("x_number_format" => undef,
564 if ($t eq 'sum' or $t eq 'avg') {
565 push @arg, ('y_number_format' => \&Bweb::human_size);
569 $stime = $bweb->{sql}->{"STARTTIME_$d"};
571 $stime = $bweb->{sql}->{STARTTIME_SEC};
578 FROM $jobt AS Job, FileSet, Client $filter $groupf
579 WHERE Job.ClientId = Client.ClientId
580 AND Job.FileSetId = FileSet.FileSetId
590 print STDERR $query if ($debug);
592 my $obj = get_graph('title' => "Job $t : $arg->{jclients}/$arg->{jjobnames}",
598 my $all = $dbh->selectall_arrayref($query) ;
599 # print STDERR Data::Dumper::Dumper($all);
600 my ($ret) = make_tab_sum($all);
602 print $obj->plot([$ret->{date}, $ret->{nb}])->png;