1 ################################################################
6 Bweb - A Bacula web interface
7 Bacula® - The Network Backup Solution
9 Copyright (C) 2000-2009 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.
15 This program is Free Software; you can redistribute it and/or
16 modify it under the terms of version two of the GNU General Public
17 License as published by the Free Software Foundation plus additions
18 that are listed in the file LICENSE.
20 This program is distributed in the hope that it will be useful, but
21 WITHOUT ANY WARRANTY; without even the implied warranty of
22 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
23 General Public License for more details.
25 You should have received a copy of the GNU General Public License
26 along with this program; if not, write to the Free Software
27 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
30 Bacula® is a registered trademark of Kern Sibbald.
31 The licensor of Bacula is the Free Software Foundation Europe
32 (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zurich,
33 Switzerland, email:ftf@fsfeurope.org.
45 Bweb::Gui - Base package for all Bweb object
49 This package define base fonction like new, display, etc..
54 our $template_dir='/usr/share/bweb/tpl';
58 new - creation a of new Bweb object
62 This function take an hash of argument and place them
65 IE : $obj = new Obj(name => 'test', age => '10');
67 $obj->{name} eq 'test' and $obj->{age} eq 10
73 my ($class, %arg) = @_;
78 map { $self->{lc($_)} = $arg{$_} } keys %arg ;
85 my ($self, $what) = @_;
89 print "<pre>" . Data::Dumper::Dumper($what) . "</pre>";
91 print "<pre>$what</pre>";
98 my ($self, $what) = @_;
100 my $old = $self->{debug};
103 $self->{debug} = $old;
108 error - display an error to the user
112 this function set $self->{error} with arg, display a message with
113 error.tpl and return 0
118 return $self->error("Can't use this file");
125 my ($self, $what) = @_;
126 $self->{error} = $what;
127 $self->display($self, 'error.tpl');
133 display - display an html page with HTML::Template
137 this function is use to render all html codes. it takes an
138 ref hash as arg in which all param are usable in template.
140 it will use user template_dir then global template_dir
141 to search the template file.
143 hash keys are not sensitive. See HTML::Template for more
144 explanations about the hash ref. (it's can be quiet hard to understand)
148 $ref = { name => 'me', age => 26 };
149 $self->display($ref, "people.tpl");
155 my ($self, $hash, $tpl) = @_ ;
156 my $dir = $self->{template_dir} || $template_dir;
157 my $lang = $self->{lang} || 'en';
158 my $template = HTML::Template->new(filename => $tpl,
159 path =>["$dir/$lang",
162 die_on_bad_params => 0,
163 case_sensitive => 0);
165 foreach my $var (qw/limit offset/) {
167 unless ($hash->{$var}) {
168 my $value = CGI::param($var) || '';
170 if ($value =~ /^(\d+)$/) {
171 $template->param($var, $1) ;
176 $template->param('thisurl', CGI::url(-relative => 1, -query=>1));
177 $template->param('loginname', CGI::remote_user());
179 $template->param($hash);
180 print $template->output();
184 ################################################################
186 package Bweb::Config;
188 use base q/Bweb::Gui/;
192 Bweb::Config - read, write, display, modify configuration
196 this package is used for manage configuration
200 $conf = new Bweb::Config(config_file => '/path/to/conf');
211 =head1 PACKAGE VARIABLE
213 %k_re - hash of all acceptable option.
217 this variable permit to check all option with a regexp.
221 our %k_re = ( dbi => qr/^(dbi:(Pg|mysql):(?:\w+=[\w\d\.-]+;?)+)$/i,
222 user => qr/^([\w\d\.-]+)$/i,
223 password => qr/^(.*)$/,
224 fv_write_path => qr!^([/\w\d\.-]*)$!,
225 template_dir => qr!^([/\w\d\.-]+)$!,
226 debug => qr/^(on)?$/,
227 lang => qr/^(\w\w)?$/,
228 email_media => qr/^([\w\d\.-]+@[\d\w\.-]+)$/,
229 graph_font => qr!^([/\w\d\.-]+.ttf)?$!,
230 bconsole => qr!^(.+)?$!,
231 syslog_file => qr!^(.+)?$!,
232 log_dir => qr!^(.+)?$!,
233 wiki_url => qr!(.*)$!,
234 stat_job_table => qr!^(\w*)$!,
235 display_log_time => qr!^(on)?$!,
236 enable_security => qr/^(on)?$/,
237 enable_security_acl => qr/^(on)?$/,
238 default_age => qr/^((?:\d+(?:[ywdhms]\s*?)?)+)\s*$/,
243 load - load config_file
247 this function load the specified config_file.
255 unless (open(FP, $self->{config_file}))
257 return $self->error("can't load config_file $self->{config_file} : $!");
259 my $f=''; my $tmpbuffer;
260 while(read FP,$tmpbuffer,4096)
268 no strict; # I have no idea of the contents of the file
275 return $self->error("If you update from an old bweb install, your must reload this page and if it's fail again, you have to configure bweb again...") ;
279 $self->{default_age} = '7d';
281 foreach my $k (keys %$VAR1) {
282 $self->{$k} = $VAR1->{$k};
290 load_old - load old configuration format
298 unless (open(FP, $self->{config_file}))
300 return $self->error("$self->{config_file} : $!");
303 while (my $line = <FP>)
306 my ($k, $v) = split(/\s*=\s*/, $line, 2);
318 save - save the current configuration to config_file
326 if ($self->{ach_list}) {
327 # shortcut for display_begin
328 $self->{achs} = [ map {{ name => $_ }}
329 keys %{$self->{ach_list}}
333 unless (open(FP, ">$self->{config_file}"))
335 return $self->error("$self->{config_file} : $!\n" .
336 "You must add this to your config file\n"
337 . Data::Dumper::Dumper($self));
340 print FP Data::Dumper::Dumper($self);
348 edit, view, modify - html form ouput
356 $self->display($self, "config_edit.tpl");
362 $self->display($self, "config_view.tpl");
370 # we need to reset checkbox first
372 $self->{display_log_time} = 0;
373 $self->{enable_security} = 0;
374 $self->{enable_security_acl} = 0;
376 foreach my $k (CGI::param())
378 next unless (exists $k_re{$k}) ;
379 my $val = CGI::param($k);
380 if ($val =~ $k_re{$k}) {
383 $self->{error} .= "bad parameter : $k = [$val]";
389 if ($self->{error}) { # an error as occured
390 $self->display($self, 'error.tpl');
398 ################################################################
400 package Bweb::Client;
402 use base q/Bweb::Gui/;
406 Bweb::Client - Bacula FD
410 this package is use to do all Client operations like, parse status etc...
414 $client = new Bweb::Client(name => 'zog-fd');
415 $client->status(); # do a 'status client=zog-fd'
421 display_running_job - Html display of a running job
425 this function is used to display information about a current job
429 sub display_running_job
431 my ($self, $bweb, $jobid, $infos) = @_ ;
432 my $status = $self->status($bweb->{info});
435 if ($status->{$jobid}) {
436 $status = $status->{$jobid};
437 $status->{last_jobbytes} = $infos->{jobbytes};
438 $status->{last_jobfiles} = $infos->{jobfiles};
439 $status->{corr_jobbytes} = $infos->{corr_jobbytes};
440 $status->{corr_jobfiles} = $infos->{corr_jobfiles};
441 $status->{jobbytes}=$status->{Bytes};
442 $status->{jobbytes} =~ s![^\d]!!g;
443 $status->{jobfiles}=$status->{'Files Examined'};
444 $status->{jobfiles} =~ s/,//g;
445 $bweb->display($status, "client_job_status.tpl");
448 for my $id (keys %$status) {
449 $bweb->display($status->{$id}, "client_job_status.tpl");
456 $client = new Bweb::Client(name => 'plume-fd');
458 $client->status($bweb);
462 dirty hack to parse "status client=xxx-fd"
466 JobId 105 Job Full_plume.2006-06-06_17.22.23 is running.
467 Backup Job started: 06-jun-06 17:22
468 Files=8,971 Bytes=194,484,132 Bytes/sec=7,480,158
469 Files Examined=10,697
470 Processing file: /home/eric/.openoffice.org2/user/config/standard.sod
476 JobName => Full_plume.2006-06-06_17.22.23,
479 Bytes => 194,484,132,
489 my ($self, $conf) = @_ ;
491 if (defined $self->{cur_jobs}) {
492 return $self->{cur_jobs} ;
496 my $b = new Bconsole(pref => $conf);
497 my $ret = $b->send_cmd("st client=$self->{name}");
501 for my $r (split(/\n/, $ret)) {
503 $r =~ s/(^\s+|\s+$)//g;
504 if ($r =~ /JobId (\d+) Job (\S+)/) {
506 $arg->{$jobid} = { @param, JobId => $jobid } ;
510 @param = ( JobName => $2 );
512 } elsif ($r =~ /=.+=/) {
513 push @param, split(/\s+|\s*=\s*/, $r) ;
515 } elsif ($r =~ /=/) { # one per line
516 push @param, split(/\s*=\s*/, $r) ;
518 } elsif ($r =~ /:/) { # one per line
519 push @param, split(/\s*:\s*/, $r, 2) ;
523 if ($jobid and @param) {
524 $arg->{$jobid} = { @param,
526 Client => $self->{name},
530 $self->{cur_jobs} = $arg ;
536 ################################################################
538 package Bweb::Autochanger;
540 use base q/Bweb::Gui/;
544 Bweb::Autochanger - Object to manage Autochanger
548 this package will parse the mtx output and manage drives.
552 $auto = new Bweb::Autochanger(precmd => 'sudo');
554 $auto = new Bweb::Autochanger(precmd => 'ssh root@robot');
558 $auto->slot_is_full(10);
559 $auto->transfer(10, 11);
565 my ($class, %arg) = @_;
568 name => '', # autochanger name
569 label => {}, # where are volume { label1 => 40, label2 => drive0 }
570 drive => [], # drive use [ 'media1', 'empty', ..]
571 slot => [], # slot use [ undef, 'empty', 'empty', ..] no slot 0
572 io => [], # io slot number list [ 41, 42, 43...]
573 info => {slot => 0, # informations (slot, drive, io)
577 mtxcmd => '/usr/sbin/mtx',
579 device => '/dev/changer',
580 precmd => '', # ssh command
581 bweb => undef, # link to bacula web object (use for display)
584 map { $self->{lc($_)} = $arg{$_} } keys %arg ;
591 status - parse the output of mtx status
595 this function will launch mtx status and parse the output. it will
596 give a perlish view of the autochanger content.
598 it uses ssh if the autochanger is on a other host.
605 my @out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} status` ;
607 # TODO : reset all infos
608 $self->{info}->{drive} = 0;
609 $self->{info}->{slot} = 0;
610 $self->{info}->{io} = 0;
612 #my @out = `cat /home/eric/travail/brestore/plume/mtx` ;
615 # Storage Changer /dev/changer:2 Drives, 45 Slots ( 5 Import/Export )
616 #Data Transfer Element 0:Full (Storage Element 1 Loaded):VolumeTag = 000000
617 #Data Transfer Element 1:Empty
618 # Storage Element 1:Empty
619 # Storage Element 2:Full :VolumeTag=000002
620 # Storage Element 3:Empty
621 # Storage Element 4:Full :VolumeTag=000004
622 # Storage Element 5:Full :VolumeTag=000001
623 # Storage Element 6:Full :VolumeTag=000003
624 # Storage Element 7:Empty
625 # Storage Element 41 IMPORT/EXPORT:Empty
626 # Storage Element 41 IMPORT/EXPORT:Full :VolumeTag=000002
631 # Storage Element 7:Empty
632 # Storage Element 2:Full :VolumeTag=000002
633 if ($l =~ /Storage Element (\d+):(Empty|Full)(\s+:VolumeTag=([\w\d.-]+))?/){
636 $self->set_empty_slot($1);
638 $self->set_slot($1, $4);
641 } elsif ($l =~ /Data Transfer.+(\d+):(Full|Empty)(\s+.Storage Element (\d+) Loaded.(:VolumeTag = ([\w\d.-]+))?)?/) {
644 $self->set_empty_drive($1);
646 $self->set_drive($1, $4, $6);
649 } elsif ($l =~ /Storage Element (\d+).+IMPORT\/EXPORT:(Empty|Full)( :VolumeTag=([\d\w.-]+))?/)
652 $self->set_empty_io($1);
654 $self->set_io($1, $4);
657 # Storage Changer /dev/changer:2 Drives, 30 Slots ( 1 Import/Export )
659 } elsif ($l =~ /Storage Changer .+:(\d+) Drives, (\d+) Slots/) {
660 $self->{info}->{drive} = $1;
661 $self->{info}->{slot} = $2;
662 if ($l =~ /(\d+)\s+Import/) {
663 $self->{info}->{io} = $1 ;
665 $self->{info}->{io} = 0;
670 $self->debug($self) ;
675 my ($self, $slot) = @_;
678 if ($self->{slot}->[$slot] eq 'loaded') {
682 my $label = $self->{slot}->[$slot] ;
684 return $self->is_media_loaded($label);
689 my ($self, $drive, $slot) = @_;
691 return 0 if (not defined $drive or $self->{drive}->[$drive] eq 'empty') ;
692 return 0 if ($self->slot_is_full($slot)) ;
694 my $out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} unload $slot $drive 2>&1`;
697 my $content = $self->get_slot($slot);
698 print "content = $content<br/> $drive => $slot<br/>";
699 $self->set_empty_drive($drive);
700 $self->set_slot($slot, $content);
703 $self->{error} = $out;
708 # TODO: load/unload have to use mtx script from bacula
711 my ($self, $drive, $slot) = @_;
713 return 0 if (not defined $drive or $self->{drive}->[$drive] ne 'empty') ;
714 return 0 unless ($self->slot_is_full($slot)) ;
716 print "Loading drive $drive with slot $slot<br/>\n";
717 my $out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} load $slot $drive 2>&1`;
720 my $content = $self->get_slot($slot);
721 print "content = $content<br/> $slot => $drive<br/>";
722 $self->set_drive($drive, $slot, $content);
725 $self->{error} = $out;
733 my ($self, $media) = @_;
735 unless ($self->{label}->{$media}) {
739 if ($self->{label}->{$media} =~ /drive\d+/) {
749 return (defined $self->{info}->{io} and $self->{info}->{io} > 0);
754 my ($self, $slot, $tag) = @_;
755 $self->{slot}->[$slot] = $tag || 'full';
756 push @{ $self->{io} }, $slot;
759 $self->{label}->{$tag} = $slot;
765 my ($self, $slot) = @_;
767 push @{ $self->{io} }, $slot;
769 unless ($self->{slot}->[$slot]) { # can be loaded (parse before)
770 $self->{slot}->[$slot] = 'empty';
776 my ($self, $slot) = @_;
777 return $self->{slot}->[$slot];
782 my ($self, $slot, $tag) = @_;
783 $self->{slot}->[$slot] = $tag || 'full';
786 $self->{label}->{$tag} = $slot;
792 my ($self, $slot) = @_;
794 unless ($self->{slot}->[$slot]) { # can be loaded (parse before)
795 $self->{slot}->[$slot] = 'empty';
801 my ($self, $drive) = @_;
802 $self->{drive}->[$drive] = 'empty';
807 my ($self, $drive, $slot, $tag) = @_;
808 $self->{drive}->[$drive] = $tag || $slot;
809 $self->{drive_slot}->[$drive] = $slot;
811 $self->{slot}->[$slot] = $tag || 'loaded';
814 $self->{label}->{$tag} = "drive$drive";
820 my ($self, $slot) = @_;
822 # slot don't exists => full
823 if (not defined $self->{slot}->[$slot]) {
827 if ($self->{slot}->[$slot] eq 'empty') {
830 return 1; # vol, full, loaded
833 sub slot_get_first_free
836 for (my $slot=1; $slot < $self->{info}->{slot}; $slot++) {
837 return $slot unless ($self->slot_is_full($slot));
841 sub io_get_first_free
845 foreach my $slot (@{ $self->{io} }) {
846 return $slot unless ($self->slot_is_full($slot));
853 my ($self, $media) = @_;
855 return $self->{label}->{$media} ;
860 my ($self, $media) = @_;
862 return defined $self->{label}->{$media} ;
867 my ($self, $slot) = @_;
869 unless ($self->slot_is_full($slot)) {
870 print "Autochanger $self->{name} slot $slot is empty<br>\n";
875 if ($self->is_slot_loaded($slot)) {
878 print "Autochanger $self->{name} $slot is currently in use<br>\n";
882 # autochanger must have I/O
883 unless ($self->have_io()) {
884 print "Autochanger $self->{name} don't have I/O, you can take media yourself<br>\n";
888 my $dst = $self->io_get_first_free();
891 print "Autochanger $self->{name} mailbox is full, you must empty I/O first<br>\n";
895 $self->transfer($slot, $dst);
900 my ($self, $src, $dst) = @_ ;
901 if ($self->{debug}) {
902 print "<pre>$self->{precmd} $self->{mtxcmd} -f $self->{device} transfer $src $dst</pre>\n";
904 my $out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} transfer $src $dst 2>&1`;
907 my $content = $self->get_slot($src);
908 $self->{slot}->[$src] = 'empty';
909 $self->set_slot($dst, $content);
912 $self->{error} = $out;
919 my ($self, $index) = @_;
920 return $self->{drive_name}->[$index];
923 # TODO : do a tapeinfo request to get informations
933 print "<table><tr>\n";
934 for my $slot (@{$self->{io}})
936 if ($self->is_slot_loaded($slot)) {
937 print "<td></td><td>Slot $slot is currently loaded</td></tr>\n";
941 if ($self->slot_is_full($slot))
943 my $free = $self->slot_get_first_free() ;
944 print "</tr><tr><td>move slot $slot to $free :</td>";
947 if ($self->transfer($slot, $free)) {
948 print "<td><img src='/bweb/T.png' alt='ok'></td>\n";
950 print "<td><img src='/bweb/E.png' alt='ok' title='$self->{error}'></td>\n";
954 $self->{error} = "<td><img src='/bweb/E.png' alt='ok' title='E : Can t find free slot'></td>\n";
958 print "</tr></table>\n";
961 # TODO : this is with mtx status output,
962 # we can do an other function from bacula view (with StorageId)
966 my $bweb = $self->{bweb};
968 # $self->{label} => ('vol1', 'vol2', 'vol3', ..);
969 my $media_list = $bweb->dbh_join( keys %{ $self->{label} });
972 SELECT Media.VolumeName AS volumename,
973 Media.VolStatus AS volstatus,
974 Media.LastWritten AS lastwritten,
975 Media.VolBytes AS volbytes,
976 Media.MediaType AS mediatype,
978 Media.InChanger AS inchanger,
980 $bweb->{sql}->{FROM_UNIXTIME}(
981 $bweb->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
982 + $bweb->{sql}->{TO_SEC}(Media.VolRetention)
985 INNER JOIN Pool USING (PoolId)
987 WHERE Media.VolumeName IN ($media_list)
990 my $all = $bweb->dbh_selectall_hashref($query, 'volumename') ;
992 # TODO : verify slot and bacula slot
996 for (my $slot=1; $slot <= $self->{info}->{slot} ; $slot++) {
998 if ($self->slot_is_full($slot)) {
1000 my $vol = $self->{slot}->[$slot];
1001 if (defined $all->{$vol}) { # TODO : autochanger without barcodes
1003 my $bslot = $all->{$vol}->{slot} ;
1004 my $inchanger = $all->{$vol}->{inchanger};
1006 # if bacula slot or inchanger flag is bad, we display a message
1007 if ($bslot != $slot or !$inchanger) {
1008 push @to_update, $slot;
1011 $all->{$vol}->{realslot} = $slot;
1013 push @{ $param }, $all->{$vol};
1015 } else { # empty or no label
1016 push @{ $param }, {realslot => $slot,
1017 volstatus => 'Unknown',
1018 volumename => $self->{slot}->[$slot]} ;
1021 push @{ $param }, {realslot => $slot, volumename => 'empty'} ;
1025 my $i=0; my $drives = [] ;
1026 foreach my $d (@{ $self->{drive} }) {
1027 $drives->[$i] = { index => $i,
1028 load => $self->{drive}->[$i],
1029 name => $self->{drive_name}->[$i],
1034 $bweb->display({ Name => $self->{name},
1035 nb_drive => $self->{info}->{drive},
1036 nb_io => $self->{info}->{io},
1039 Update => scalar(@to_update) },
1046 ################################################################
1048 package Bweb::Sched;
1049 use base q/Bweb::Gui/;
1053 Bweb::Sched() - Bweb package that parse show schedule ouput
1055 new Bweb::Sched(format => '%Y-%m-%d', year => 2008);
1059 my $b = $bweb->get_bconsole();
1060 my $s = $b->send_cmd("show schedule");
1061 my $sched = new Bweb::Sched(begin => '2007-01-01', end => '2007-01-02 12:00');
1062 $sched->parse_scheds(split(/\r?\n/, $s));
1073 'level' => 'Differential',
1080 my ($class, @arg) = @_;
1081 my $self = $class->SUPER::new(@arg);
1083 # we compare the current schedule date with begin and end
1084 # in a float form ex: 20071212.1243 > 20070101
1085 if ($self->{begin} and $self->{end}) {
1086 ($self->{fbegin}, $self->{fend}) = ($self->{begin}, $self->{end});
1087 $self->{fbegin} =~ s/(-|:)//g; $self->{fbegin} =~ s/ /./;
1088 $self->{fend} =~ s/(-|:)//g; $self->{fend} =~ s/ /./;
1091 bless($self,$class);
1093 if ($self->{bconsole}) {
1094 my $sel = $self->{name}?"=\"$self->{name}\"":'';
1095 my $b = $self->{bconsole};
1096 my $out = $b->send_cmd("show schedule$sel");
1097 $self->parse_scheds(split(/\r?\n/, $out));
1098 undef $self->{bconsole}; # useless now
1104 # cleanup and add a schedule
1107 my ($self, $name, $info) = @_;
1108 # bacula uses dates that start from 0, we start from 1
1109 foreach (@{$info->{mday}},@{$info->{month}}) { $_++ };
1112 $info->{event} = [ $self->get_events($info, $self->{format}) ];
1114 foreach my $i (qw/hour mday month wday wom woy mins/) {
1118 push @{$self->{schedules}->{$name}}, $info;
1121 # return the name of all schedules
1124 my ($self, $name) = @_;
1126 return keys %{ $self->{schedules} };
1129 # return an array of all schedule
1132 my ($self, $sched) = @_;
1133 return $self->{schedules}->{$sched};
1136 # return an ref array of all events
1137 # [ '2007-12-19 04:05', '2007-12-19 04:05' ]
1140 my ($self, $sched) = @_;
1141 return $sched->{event};
1144 # return the pool name
1147 my ($self, $sched) = @_;
1148 return $sched->{pool} || '';
1151 # return the level name (Incremental, Differential, Full)
1154 my ($self, $sched) = @_;
1155 return $sched->{level};
1158 # parse bacula sched bitmap
1161 my ($self, @output) = @_;
1168 foreach my $ligne (@output) {
1169 if ($ligne =~ /Schedule: name=(.+)/) {
1170 if ($name and $elt) {
1171 $elt->{level} = $run;
1172 $self->add_sched($name, $elt);
1177 elsif ($ligne =~ /Run Level=(.+)/) {
1178 if ($name and $elt) {
1179 $elt->{level} = $run;
1180 $self->add_sched($name, $elt);
1185 elsif ($ligne =~ /(hour|mday|month|mins)=(.+)/) {
1186 # All theses lines have the same format
1188 my ($k,$v) = ($1,$2);
1189 # we get all values (0 1 4 9)
1190 $elt->{$k}=[split (/\s/,$v)];
1192 # we make a bitmap for this kind of data (0 0 1 0 0 0 1) for a quick access
1193 elsif ($ligne =~ /(wday|wom|woy)=(.+)/) {
1194 my ($k,$v) = ($1,$2);
1195 foreach my $e (split (/\s/,$v)) {
1199 elsif ($ligne =~ /Pool: name=(.+?) PoolType/) {
1204 if ($name and $elt) {
1205 $elt->{level} = $run;
1206 $self->add_sched($name, $elt);
1210 use Date::Calc qw(:all);
1212 # read bacula schedule bitmap and get $format date string
1216 my ($self, $s,$format) = @_;
1217 my $year = $self->{year} || ((localtime)[5] + 1900);
1218 $format = $format || '%u-%02u-%02u %02u:%02u';
1220 foreach my $m (@{$s->{month}}) # mois de l'annee
1222 foreach my $md (@{$s->{mday}}) # jour du mois
1224 # print " m=$m md=$md\n";
1225 # we check if this day exists (31 fev)
1226 next if (!check_date($year,$m,$md));
1227 # print " check_date ok\n";
1229 my $w = ($md-1)/7; # we use the same thing than bacula
1230 next if (! $s->{wom}->[$w]);
1231 # print " wom ok\n";
1233 # on recupere le jour de la semaine
1234 my $wd = Day_of_Week($year,$m,$md);
1236 my ($w1,undef) = Week_of_Year($year,$m,$wd);
1237 next if (! $s->{woy}->[$w1-1]); # bacula 0-51
1238 # print " woy ok\n";
1240 $wd = 0 if ($wd == 7) ; # sunday is 0 on bacula
1241 next if (! $s->{wday}->[$wd]);
1242 # print " wday ok\n";
1244 foreach my $h (@{$s->{hour}}) # hour of the day
1246 foreach my $min (@{$s->{mins}}) # minute
1248 if ($self->{fbegin}) {
1250 my $d = sprintf('%d%0.2d%0.2d.%0.2d%0.2d',
1251 $year,$m,$md,$h,$min);
1252 next if ($d < $self->{fbegin} or $d > $self->{fend});
1254 push @ret, sprintf($format, $year,$m,$md,$h,$min);
1263 ################################################################
1267 use base q/Bweb::Gui/;
1271 Bweb - main Bweb package
1275 this package is use to compute and display informations
1280 use POSIX qw/strftime/;
1282 our $config_file= '/etc/bacula/bweb.conf';
1284 if ($ENV{BWEBCONF} && -f $ENV{BWEBCONF}) {
1285 $config_file = $ENV{BWEBCONF};
1292 %sql_func - hash to make query mysql/postgresql compliant
1298 UNIX_TIMESTAMP => '',
1299 FROM_UNIXTIME => '',
1300 TO_SEC => " interval '1 second' * ",
1301 SEC_TO_INT => "SEC_TO_INT",
1304 STARTTIME_SEC => " date_trunc('sec', Job.StartTime) ",
1305 STARTTIME_DAY => " date_trunc('day', Job.StartTime) ",
1306 STARTTIME_HOUR => " date_trunc('hour', Job.StartTime) ",
1307 STARTTIME_MONTH => " date_trunc('month', Job.StartTime) ",
1308 STARTTIME_WEEK => " date_trunc('week', Job.StartTime) ",
1309 STARTTIME_PHOUR=> " date_part('hour', Job.StartTime) ",
1310 STARTTIME_PDAY => " date_part('day', Job.StartTime) ",
1311 STARTTIME_PMONTH => " date_part('month', Job.StartTime) ",
1312 STARTTIME_PWEEK => " date_part('week', Job.StartTime) ",
1313 DB_SIZE => " SELECT pg_database_size(current_database()) ",
1314 CAT_POOL_TYPE => " MediaType || '_' || Pool.Name ",
1318 UNIX_TIMESTAMP => 'UNIX_TIMESTAMP',
1319 FROM_UNIXTIME => 'FROM_UNIXTIME',
1322 SEC_TO_TIME => 'SEC_TO_TIME',
1323 MATCH => " REGEXP ",
1324 STARTTIME_SEC => " DATE_FORMAT(Job.StartTime, '%Y-%m-%d %T') ",
1325 STARTTIME_DAY => " DATE_FORMAT(Job.StartTime, '%Y-%m-%d') ",
1326 STARTTIME_HOUR => " DATE_FORMAT(Job.StartTime, '%Y-%m-%d %H') ",
1327 STARTTIME_MONTH => " DATE_FORMAT(Job.StartTime, '%Y-%m') ",
1328 STARTTIME_WEEK => " DATE_FORMAT(Job.StartTime, '%Y-%v') ",
1329 STARTTIME_PHOUR=> " DATE_FORMAT(Job.StartTime, '%H') ",
1330 STARTTIME_PDAY => " DATE_FORMAT(Job.StartTime, '%d') ",
1331 STARTTIME_PMONTH => " DATE_FORMAT(Job.StartTime, '%m') ",
1332 STARTTIME_PWEEK => " DATE_FORMAT(Job.StartTime, '%v') ",
1333 # with mysql < 5, you have to play with the ugly SHOW command
1334 #DB_SIZE => " SELECT 0 ",
1335 # works only with mysql 5
1336 DB_SIZE => " SELECT sum(DATA_LENGTH) FROM INFORMATION_SCHEMA.TABLES ",
1337 CAT_POOL_TYPE => " CONCAT(MediaType,'_',Pool.Name) ",
1338 CONCAT_SEP => " SEPARATOR '' ",
1345 return $self->{info}->{dbi} =~ /dbi:mysql/i;
1352 $self->{dbh}->disconnect();
1357 sub dbh_selectall_arrayref
1359 my ($self, $query) = @_;
1360 $self->connect_db();
1361 $self->debug($query);
1362 return $self->{dbh}->selectall_arrayref($query);
1367 my ($self, @what) = @_;
1368 return join(',', $self->dbh_quote(@what)) ;
1373 my ($self, @what) = @_;
1375 $self->connect_db();
1377 return map { $self->{dbh}->quote($_) } @what;
1379 return $self->{dbh}->quote($what[0]) ;
1385 my ($self, $query) = @_ ;
1386 $self->connect_db();
1387 $self->debug($query);
1388 return $self->{dbh}->do($query);
1391 sub dbh_selectall_hashref
1393 my ($self, $query, $join) = @_;
1395 $self->connect_db();
1396 $self->debug($query);
1397 return $self->{dbh}->selectall_hashref($query, $join) ;
1400 sub dbh_selectrow_hashref
1402 my ($self, $query) = @_;
1404 $self->connect_db();
1405 $self->debug($query);
1406 return $self->{dbh}->selectrow_hashref($query) ;
1411 my ($self, @what) = @_;
1412 if ($self->dbh_is_mysql()) {
1413 return 'CONCAT(' . join(',', @what) . ')' ;
1415 return join(' || ', @what);
1421 my ($self, $query) = @_;
1422 $self->debug($query, up => 1);
1423 return $self->{dbh}->prepare($query);
1429 my @unit = qw(B KB MB GB TB);
1430 my $val = shift || 0;
1432 my $format = '%i %s';
1433 while ($val / 1024 > 1) {
1437 $format = ($i>0)?'%0.1f %s':'%i %s';
1438 return sprintf($format, $val, $unit[$i]);
1445 if ($val =~ /^(?:\d+(?:[ywdhms]\s*)?)+\s*$/) {
1460 # display Day, Hour, Year
1466 $val /= 60; # sec -> min
1468 if ($val / 60 <= 1) {
1472 $val /= 60; # min -> hour
1473 if ($val / 24 <= 1) {
1474 return "$val hours";
1477 $val /= 24; # hour -> day
1478 if ($val / 365 < 2) {
1482 $val /= 365 ; # day -> year
1484 return "$val years";
1490 my $val = shift || 0;
1492 if ($val eq '1' or $val eq "yes") {
1494 } elsif ($val eq '2' or $val eq "archived") {
1502 sub from_human_enabled
1504 my $val = shift || 0;
1506 if ($val eq '1' or $val eq "yes") {
1508 } elsif ($val eq '2' or $val eq "archived") {
1515 # get Day, Hour, Year
1521 unless ($val =~ /^\s*(\d+)\s*(\w)\w*\s*$/) {
1525 my %times = ( m => 60,
1531 my $mult = $times{$2} || 0;
1536 # get long term statistic table
1540 my $ret = $self->{info}->{stat_job_table} || 'JobHisto';
1541 if ($ret !~ m/^job$/i) {
1542 $ret = "(SELECT * FROM Job UNION SELECT * FROM $ret)";
1551 unless ($self->{dbh}) {
1553 $self->{dbh} = DBI->connect($self->{info}->{dbi},
1554 $self->{info}->{user},
1555 $self->{info}->{password});
1557 $self->error("Can't connect to your database:\n$DBI::errstr\n")
1558 unless ($self->{dbh});
1560 $self->{dbh}->{FetchHashKeyName} = 'NAME_lc';
1562 if ($self->dbh_is_mysql()) {
1563 $self->{dbh}->do("SET group_concat_max_len=1000000");
1565 $self->{dbh}->do("SET datestyle TO 'ISO, YMD'");
1572 my ($class, %arg) = @_;
1574 dbh => undef, # connect_db();
1576 dbi => '', # DBI:Pg:database=bacula;host=127.0.0.1
1582 map { $self->{lc($_)} = $arg{$_} } keys %arg ;
1584 if ($self->{info}->{dbi} =~ /DBI:(\w+):/i) {
1585 $self->{sql} = $sql_func{$1};
1588 $self->{loginname} = CGI::remote_user();
1589 $self->{debug} = $self->{info}->{debug};
1590 $self->{lang} = $self->{info}->{lang};
1591 $self->{template_dir} = $self->{info}->{template_dir};
1599 if ($self->{info}->{enable_security}) {
1600 $self->get_roles(); # get lang
1602 $self->display($self->{info}, "begin.tpl");
1608 $self->display($self->{info}, "end.tpl");
1614 my $arg = $self->get_form("qclient");
1615 my $f1 = $self->get_client_group_filter();
1616 my $f2 = $self->get_client_filter();
1618 # client_group_name | here
1619 #-------------------+-----
1624 SELECT client_group_name, max(here) AS here FROM (
1625 SELECT client_group_name, 1 AS here
1627 JOIN client_group_member USING (client_group_id)
1628 JOIN Client USING (ClientId) $f2
1629 WHERE Name = $arg->{qclient}
1631 SELECT client_group_name, 0
1632 FROM client_group $f1
1634 GROUP by client_group_name";
1636 my $all = $self->dbh_selectall_hashref($query, 'client_group_name');
1638 $self->display({ client_group => [ values %$all ], %$arg }, "client_edit.tpl");
1644 my $where=''; # by default
1646 my $arg = $self->get_form("client", "qre_client",
1647 "jclient_groups", "qnotingroup");
1649 if ($arg->{qre_client}) {
1650 $where = "WHERE Name $self->{sql}->{MATCH} $arg->{qre_client} ";
1651 } elsif ($arg->{client}) {
1652 $where = "WHERE Name = '$arg->{client}' ";
1653 } elsif ($arg->{jclient_groups}) {
1654 # $filter could already contains client_group_member
1656 JOIN client_group_member USING (ClientId)
1657 JOIN client_group USING (client_group_id)
1658 WHERE client_group_name IN ($arg->{jclient_groups}) ";
1659 } elsif ($arg->{qnotingroup}) {
1662 (SELECT 1 FROM client_group_member
1663 WHERE Client.ClientId = client_group_member.ClientId
1669 SELECT Name AS name,
1671 AutoPrune AS autoprune,
1672 FileRetention AS fileretention,
1673 JobRetention AS jobretention
1674 FROM Client " . $self->get_client_filter() .
1677 my $all = $self->dbh_selectall_hashref($query, 'name') ;
1679 my $dsp = { ID => $cur_id++,
1680 clients => [ values %$all] };
1682 $self->display($dsp, "client_list.tpl") ;
1687 my ($self, %arg) = @_;
1692 if ($arg{since} and $arg{age}) {
1693 my $now = "$self->{sql}->{UNIX_TIMESTAMP}(TIMESTAMP '$arg{since}')";
1695 AND $self->{sql}->{UNIX_TIMESTAMP}(StartTime) > $now
1696 AND $self->{sql}->{UNIX_TIMESTAMP}(EndTime) < ($now + $self->{sql}->{TO_SEC}($arg{age}))";
1697 $label .= "since $arg{since} and during " . human_sec($arg{age});
1699 } elsif ($arg{age}) {
1701 "AND $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
1703 ( $self->{sql}->{UNIX_TIMESTAMP}(NOW())
1705 $self->{sql}->{TO_SEC}($arg{age})
1708 $label = "last " . human_sec($arg{age});
1711 if ($arg{groupby}) {
1712 $limit .= " GROUP BY $arg{groupby} ";
1716 $limit .= " ORDER BY $arg{order} ";
1720 $limit .= " LIMIT $arg{limit} ";
1721 $label .= " limited to $arg{limit}";
1725 $limit .= " OFFSET $arg{offset} ";
1726 $label .= " with $arg{offset} offset ";
1730 $label = 'no filter';
1733 return ($limit, $label);
1738 $bweb->get_form(...) - Get useful stuff
1742 This function get and check parameters against regexp.
1744 If word begin with 'q', the return will be quoted or join quoted
1745 if it's end with 's'.
1750 $bweb->get_form('jobid', 'qclient', 'qpools') ;
1753 qclient => 'plume-fd',
1754 qpools => "'plume-fd', 'test-fd', '...'",
1761 my ($self, @what) = @_;
1762 my %what = map { $_ => 1 } @what;
1776 age => $self->{info}->{default_age},
1786 my %opt_ss =( # string with space
1790 my %opt_s = ( # default to ''
1812 my %opt_p = ( # option with path
1819 my %opt_r = (regexwhere => 1);
1820 my %opt_d = ( # option with date
1824 my %opt_t = (when => 2, # option with time
1825 begin => 1, # 1 hh:min are optionnal
1826 end => 1, # 2 hh:min are required
1829 foreach my $i (@what) {
1830 if (exists $opt_i{$i}) {# integer param
1831 my $value = CGI::param($i) || $opt_i{$i} ;
1832 if ($value =~ /^(\d+)$/) {
1834 } elsif ($i eq 'age' && # can have unit
1835 $value =~ /^(?:\d+(?:[ywdhms]\s*)?)+\s*$/) # 2y1h2m34s
1837 $ret{$i} = human_sec_unit($value);
1839 } elsif ($opt_s{$i}) { # simple string param
1840 my $value = CGI::param($i) || '';
1841 if ($value =~ /^([\w\d\.-]+)$/) {
1844 } elsif ($opt_ss{$i}) { # simple string param (with space)
1845 my $value = CGI::param($i) || '';
1846 if ($value =~ /^([\w\d\.\-\s]+)$/) {
1849 } elsif ($i =~ /^j(\w+)s$/) { # quote join args "'arg1', 'arg2'"
1850 my @value = grep { ! /^\s*$/ } CGI::param($1) ;
1852 $ret{$i} = $self->dbh_join(@value) ;
1855 } elsif ($i =~ /^q(\w+[^s])$/) { # 'arg1'
1856 my $value = CGI::param($1) ;
1858 $ret{$i} = $self->dbh_quote($value);
1861 } elsif ($i =~ /^q(\w+)s$/) { #[ 'arg1', 'arg2']
1862 $ret{$i} = [ map { { name => $self->dbh_quote($_) } }
1863 grep { ! /^\s*$/ } CGI::param($1) ];
1864 } elsif (exists $opt_p{$i}) {
1865 my $value = CGI::param($i) || '';
1866 if ($value =~ /^([\w\d\.\/\s:\@\-]+)$/) {
1869 } elsif (exists $opt_r{$i}) {
1870 my $value = CGI::param($i) || '';
1871 if ($value =~ /^([^'"']+)$/) {
1874 } elsif (exists $opt_d{$i}) {
1875 my $value = CGI::param($i) || '';
1876 if ($value =~ /^\s*(\d+\s+\w+)$/) {
1879 } elsif (exists $opt_t{$i}) { # 1: hh:min optionnal, 2: hh:min required
1880 my $when = CGI::param($i) || '';
1881 if ($when =~ /(\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?)/) {
1882 if ($opt_t{$i} == 1 or defined $2) {
1889 if ($what{storage_cmd}) {
1890 if (!grep {/^\Q$ret{storage_cmd}\E$/} ('mount', 'umount', 'release','status')) {
1891 delete $ret{storage_cmd};
1896 foreach my $s (CGI::param('slot')) {
1897 if ($s =~ /^(\d+)$/) {
1898 push @{$ret{slots}}, $s;
1904 my $age = $ret{age} || human_sec_unit($opt_i{age});
1905 my $since = CGI::param('since') || strftime('%F %T', localtime(time - $age));
1906 if ($since =~ /^(\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?)$/) {
1912 my $lang = CGI::param('lang') || 'en';
1913 if ($lang =~ /^(\w\w)$/) {
1918 if ($what{db_clients}) {
1920 if ($what{filter}) {
1921 # get security filter only if asked
1922 $filter = $self->get_client_filter();
1926 SELECT Client.Name as clientname
1930 my $clients = $self->dbh_selectall_hashref($query, 'clientname');
1931 $ret{db_clients} = [sort {$a->{clientname} cmp $b->{clientname} }
1935 if ($what{db_client_groups}) {
1937 if ($what{filter}) {
1938 # get security filter only if asked
1939 $filter = $self->get_client_group_filter();
1943 SELECT client_group_name AS name, comment AS comment
1944 FROM client_group $filter
1946 my $grps = $self->dbh_selectall_hashref($query, 'name');
1947 $ret{db_client_groups} = [sort {$a->{name} cmp $b->{name} }
1951 if ($what{db_usernames}) {
1953 SELECT username, comment
1956 my $users = $self->dbh_selectall_hashref($query, 'username');
1957 $ret{db_usernames} = [sort {$a->{username} cmp $b->{username} }
1961 if ($what{db_roles}) {
1963 SELECT rolename, comment
1966 my $r = $self->dbh_selectall_hashref($query, 'rolename');
1967 $ret{db_roles} = [sort {$a->{rolename} cmp $b->{rolename} }
1971 if ($what{db_mediatypes}) {
1973 SELECT MediaType as mediatype
1976 my $media = $self->dbh_selectall_hashref($query, 'mediatype');
1977 $ret{db_mediatypes} = [sort {$a->{mediatype} cmp $b->{mediatype} }
1981 if ($what{db_locations}) {
1983 SELECT Location as location, Cost as cost
1986 my $loc = $self->dbh_selectall_hashref($query, 'location');
1987 $ret{db_locations} = [ sort { $a->{location}
1993 if ($what{db_pools}) {
1994 my $query = "SELECT Name as name FROM Pool";
1996 my $all = $self->dbh_selectall_hashref($query, 'name') ;
1997 $ret{db_pools} = [ sort { $a->{name} cmp $b->{name} } values %$all ];
2000 if ($what{db_filesets}) {
2002 SELECT FileSet.FileSet AS fileset
2005 my $filesets = $self->dbh_selectall_hashref($query, 'fileset');
2007 $ret{db_filesets} = [sort {lc($a->{fileset}) cmp lc($b->{fileset}) }
2008 values %$filesets] ;
2011 if ($what{db_jobnames}) {
2013 if ($what{filter}) {
2014 $filter = " JOIN Client USING (ClientId) " . $self->get_client_filter();
2017 SELECT DISTINCT Job.Name AS jobname
2020 my $jobnames = $self->dbh_selectall_hashref($query, 'jobname');
2022 $ret{db_jobnames} = [sort {lc($a->{jobname}) cmp lc($b->{jobname}) }
2023 values %$jobnames] ;
2026 if ($what{db_devices}) {
2028 SELECT Device.Name AS name
2031 my $devices = $self->dbh_selectall_hashref($query, 'name');
2033 $ret{db_devices} = [sort {lc($a->{name}) cmp lc($b->{name}) }
2043 $self->can_do('r_view_stat');
2044 my $fields = $self->get_form(qw/age level status clients filesets
2045 graph gtype type filter db_clients
2046 limit db_filesets width height
2047 qclients qfilesets qjobnames db_jobnames/);
2049 my $url = CGI::url(-full => 0,
2052 $url =~ s/^.+?\?//; # http://path/to/bweb.pl?arg => arg
2054 # this organisation is to keep user choice between 2 click
2055 # TODO : fileset and client selection doesn't work
2062 if ($fields->{gtype} and $fields->{gtype} eq 'balloon') {
2063 system("./bgraph.pl");
2067 sub get_selected_media_location
2071 my $media = $self->get_form('jmedias');
2073 unless ($media->{jmedias}) {
2078 SELECT Media.VolumeName AS volumename, Location.Location AS location
2079 FROM Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2080 WHERE Media.VolumeName IN ($media->{jmedias})
2083 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2085 # { 'vol1' => { [volumename => 'vol1', location => 'ici'],
2094 my ($self, $in) = @_ ;
2095 $self->can_do('r_media_mgnt');
2096 my $media = $self->get_selected_media_location();
2102 my $elt = $self->get_form('db_locations');
2104 $self->display({ ID => $cur_id++,
2105 enabled => human_enabled($in),
2106 %$elt, # db_locations
2108 sort { $a->{volumename} cmp $b->{volumename} } values %$media
2117 $self->can_do('r_media_mgnt');
2119 my $elt = $self->get_form(qw/db_pools db_mediatypes db_locations/) ;
2121 $self->display($elt, "help_extern.tpl");
2124 sub help_extern_compute
2127 $self->can_do('r_media_mgnt');
2129 my $number = CGI::param('limit') || '' ;
2130 unless ($number =~ /^(\d+)$/) {
2131 return $self->error("Bad arg number : $number ");
2134 my ($sql, undef) = $self->get_param('pools',
2135 'locations', 'mediatypes');
2138 SELECT Media.VolumeName AS volumename,
2139 Media.VolStatus AS volstatus,
2140 Media.LastWritten AS lastwritten,
2141 Media.MediaType AS mediatype,
2142 Media.VolMounts AS volmounts,
2144 Media.Recycle AS recycle,
2145 $self->{sql}->{FROM_UNIXTIME}(
2146 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2147 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2150 INNER JOIN Pool ON (Pool.PoolId = Media.PoolId)
2151 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2153 WHERE Media.InChanger = 1
2154 AND Media.VolStatus IN ('Disabled', 'Error', 'Full', 'Used')
2156 ORDER BY expire DESC, recycle, Media.VolMounts DESC
2160 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2162 $self->display({ Media => [ values %$all ] },
2163 "help_extern_compute.tpl");
2169 $self->can_do('r_media_mgnt');
2171 my $param = $self->get_form(qw/db_locations db_pools db_mediatypes/) ;
2172 $self->display($param, "help_intern.tpl");
2175 sub help_intern_compute
2178 $self->can_do('r_media_mgnt');
2180 my $number = CGI::param('limit') || '' ;
2181 unless ($number =~ /^(\d+)$/) {
2182 return $self->error("Bad arg number : $number ");
2185 my ($sql, undef) = $self->get_param('pools', 'locations', 'mediatypes');
2187 if (CGI::param('expired')) {
2188 # we take only expired volumes or purged/recycle ones
2191 ( ($self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2192 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2195 Media.VolStatus IN ('Purged', 'Recycle')
2202 SELECT Media.VolumeName AS volumename,
2203 Media.VolStatus AS volstatus,
2204 Media.LastWritten AS lastwritten,
2205 Media.MediaType AS mediatype,
2206 Media.VolMounts AS volmounts,
2208 $self->{sql}->{FROM_UNIXTIME}(
2209 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2210 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2213 INNER JOIN Pool ON (Pool.PoolId = Media.PoolId)
2214 LEFT JOIN Location ON (Location.LocationId = Media.LocationId)
2216 WHERE Media.InChanger <> 1
2217 AND Media.VolStatus IN ('Purged', 'Full', 'Append', 'Recycle')
2218 AND Media.Recycle = 1
2220 ORDER BY Media.VolUseDuration DESC, Media.VolMounts ASC, expire ASC
2224 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2226 $self->display({ Media => [ values %$all ] },
2227 "help_intern_compute.tpl");
2233 my ($self, %arg) = @_ ;
2235 my ($limit, $label) = $self->get_limit(%arg);
2239 (SELECT count(Pool.PoolId) FROM Pool) AS nb_pool,
2240 (SELECT count(Media.MediaId) FROM Media) AS nb_media,
2241 (SELECT count(Job.JobId) FROM Job) AS nb_job,
2242 (SELECT sum(VolBytes) FROM Media) AS nb_bytes,
2243 ($self->{sql}->{DB_SIZE}) AS db_size,
2244 (SELECT count(Job.JobId)
2246 WHERE Job.JobStatus IN ('E','e','f','A')
2249 (SELECT count(Client.ClientId) FROM Client) AS nb_client
2252 my $row = $self->dbh_selectrow_hashref($query) ;
2254 $row->{nb_bytes} = human_size($row->{nb_bytes});
2256 $row->{db_size} = human_size($row->{db_size});
2257 $row->{label} = $label;
2258 $row->{age} = $arg{age};
2260 $self->display($row, "general.tpl");
2265 my ($self, @what) = @_ ;
2266 my %elt = map { $_ => 1 } @what;
2271 if ($elt{clients}) {
2272 my @clients = grep { ! /^\s*$/ } CGI::param('client');
2274 $ret{clients} = \@clients;
2275 my $str = $self->dbh_join(@clients);
2276 $limit .= "AND Client.Name IN ($str) ";
2280 if ($elt{client_groups}) {
2281 my @clients = grep { ! /^\s*$/ } CGI::param('client_group');
2283 $ret{client_groups} = \@clients;
2284 my $str = $self->dbh_join(@clients);
2285 $limit .= "AND client_group_name IN ($str) ";
2289 if ($elt{filesets}) {
2290 my @filesets = grep { ! /^\s*$/ } CGI::param('fileset');
2292 $ret{filesets} = \@filesets;
2293 my $str = $self->dbh_join(@filesets);
2294 $limit .= "AND FileSet.FileSet IN ($str) ";
2298 if ($elt{mediatypes}) {
2299 my @media = grep { ! /^\s*$/ } CGI::param('mediatype');
2301 $ret{mediatypes} = \@media;
2302 my $str = $self->dbh_join(@media);
2303 $limit .= "AND Media.MediaType IN ($str) ";
2308 my $client = CGI::param('client');
2310 $ret{client} = $client;
2311 $client = $self->dbh_quote($client);
2312 $limit .= "AND Client.Name = $client ";
2317 my $level = CGI::param('level') || '';
2318 if ($level =~ /^(\w)$/) {
2320 $limit .= "AND Job.Level = '$1' ";
2325 my $jobid = CGI::param('jobid') || '';
2327 if ($jobid =~ /^(\d+)$/) {
2329 $limit .= "AND Job.JobId = '$1' ";
2334 my $status = CGI::param('status') || '';
2335 if ($status =~ /^(\w)$/) {
2338 $limit .= "AND Job.JobStatus IN ('E','e','f','A') ";
2339 } elsif ($1 eq 'W') {
2340 $limit .= "AND Job.JobStatus IN ('T', 'W') OR Job.JobErrors > 0 ";
2342 $limit .= "AND Job.JobStatus = '$1' ";
2347 if ($elt{volstatus}) {
2348 my $status = CGI::param('volstatus') || '';
2349 if ($status =~ /^(\w+)$/) {
2351 $limit .= "AND Media.VolStatus = '$1' ";
2355 if ($elt{locations}) {
2356 my @location = grep { ! /^\s*$/ } CGI::param('location') ;
2358 $ret{locations} = \@location;
2359 my $str = $self->dbh_join(@location);
2360 $limit .= "AND Location.Location IN ($str) ";
2365 my @pool = grep { ! /^\s*$/ } CGI::param('pool') ;
2367 $ret{pools} = \@pool;
2368 my $str = $self->dbh_join(@pool);
2369 $limit .= "AND Pool.Name IN ($str) ";
2373 if ($elt{location}) {
2374 my $location = CGI::param('location') || '';
2376 $ret{location} = $location;
2377 $location = $self->dbh_quote($location);
2378 $limit .= "AND Location.Location = $location ";
2383 my $pool = CGI::param('pool') || '';
2386 $pool = $self->dbh_quote($pool);
2387 $limit .= "AND Pool.Name = $pool ";
2391 if ($elt{jobtype}) {
2392 my $jobtype = CGI::param('jobtype') || '';
2393 if ($jobtype =~ /^(\w)$/) {
2395 $limit .= "AND Job.Type = '$1' ";
2399 return ($limit, %ret);
2410 my ($self, %arg) = @_ ;
2411 return if $self->cant_do('r_view_job');
2413 $arg{order} = ' Job.JobId DESC ';
2415 my ($limit, $label) = $self->get_limit(%arg);
2416 my ($where, undef) = $self->get_param('clients',
2425 if (CGI::param('client_group')) {
2427 JOIN client_group_member USING (ClientId)
2428 JOIN client_group USING (client_group_id)
2431 my $filter = $self->get_client_filter();
2434 SELECT Job.JobId AS jobid,
2435 Client.Name AS client,
2436 FileSet.FileSet AS fileset,
2437 Job.Name AS jobname,
2439 StartTime AS starttime,
2441 Pool.Name AS poolname,
2442 JobFiles AS jobfiles,
2443 JobBytes AS jobbytes,
2444 JobStatus AS jobstatus,
2446 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
2447 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime))
2450 JobErrors AS joberrors
2452 FROM Client $filter $cgq,
2453 Job LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId)
2454 LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId)
2455 WHERE Client.ClientId=Job.ClientId
2456 AND Job.JobStatus NOT IN ('R', 'C')
2461 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
2463 $self->display({ Filter => $label,
2467 sort { $a->{jobid} <=> $b->{jobid} }
2474 # display job informations
2475 sub display_job_zoom
2477 my ($self, $jobid) = @_ ;
2478 $self->can_do('r_view_job');
2480 $jobid = $self->dbh_quote($jobid);
2482 # get security filter
2483 my $filter = $self->get_client_filter();
2486 SELECT DISTINCT Job.JobId AS jobid,
2487 Client.Name AS client,
2488 Job.Name AS jobname,
2489 FileSet.FileSet AS fileset,
2491 Pool.Name AS poolname,
2492 StartTime AS starttime,
2493 JobFiles AS jobfiles,
2494 JobBytes AS jobbytes,
2495 JobStatus AS jobstatus,
2496 JobErrors AS joberrors,
2498 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
2499 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime)) AS duration
2501 FROM Client $filter,
2502 Job LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId)
2503 LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId)
2504 WHERE Client.ClientId=Job.ClientId
2505 AND Job.JobId = $jobid
2508 my $row = $self->dbh_selectrow_hashref($query) ;
2510 # display all volumes associate with this job
2512 SELECT Media.VolumeName as volumename
2513 FROM Job,Media,JobMedia
2514 WHERE Job.JobId = $jobid
2515 AND JobMedia.JobId=Job.JobId
2516 AND JobMedia.MediaId=Media.MediaId
2519 my $all = $self->dbh_selectall_hashref($query, 'volumename');
2521 $row->{volumes} = [ values %$all ] ;
2522 $row->{wiki_url} = $self->{info}->{wiki_url};
2524 $self->display($row, "display_job_zoom.tpl");
2527 sub display_job_group
2529 my ($self, %arg) = @_;
2530 $self->can_do('r_view_job');
2532 my ($limit, $label) = $self->get_limit(groupby => 'client_group_name', %arg);
2534 my ($where, undef) = $self->get_param('client_groups',
2537 my $filter = $self->get_client_group_filter();
2540 SELECT client_group_name AS client_group_name,
2541 COALESCE(jobok.jobfiles,0) + COALESCE(joberr.jobfiles,0) AS jobfiles,
2542 COALESCE(jobok.jobbytes,0) + COALESCE(joberr.jobbytes,0) AS jobbytes,
2543 COALESCE(jobok.joberrors,0) + COALESCE(joberr.joberrors,0) AS joberrors,
2544 COALESCE(jobok.nbjobs,0) AS nbjobok,
2545 COALESCE(joberr.nbjobs,0) AS nbjoberr,
2546 COALESCE(jobok.duration, '0:0:0') AS duration
2548 FROM client_group $filter LEFT JOIN (
2549 SELECT client_group_name AS client_group_name, COUNT(1) AS nbjobs,
2550 SUM(JobFiles) AS jobfiles, SUM(JobBytes) AS jobbytes,
2551 SUM(JobErrors) AS joberrors,
2552 SUM($self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
2553 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime)))
2556 FROM Job JOIN client_group_member ON (Job.ClientId = client_group_member.ClientId)
2557 JOIN client_group USING (client_group_id)
2559 WHERE Type IN ('B', 'R') AND JobStatus IN ('T', 'W')
2562 ) AS jobok USING (client_group_name) LEFT JOIN
2565 SELECT client_group_name AS client_group_name, COUNT(1) AS nbjobs,
2566 SUM(JobFiles) AS jobfiles, SUM(JobBytes) AS jobbytes,
2567 SUM(JobErrors) AS joberrors
2568 FROM Job JOIN client_group_member ON (Job.ClientId = client_group_member.ClientId)
2569 JOIN client_group USING (client_group_id)
2571 WHERE Type IN ('B', 'R') AND JobStatus IN ('f','E', 'A')
2574 ) AS joberr USING (client_group_name)
2578 my $all = $self->dbh_selectall_hashref($query, 'client_group_name');
2580 my $rep = { groups => [ values %$all ], age => $arg{age}, filter => $label };
2583 $self->display($rep, "display_job_group.tpl");
2588 my ($self, %arg) = @_ ;
2589 $self->can_do('r_view_media');
2591 my ($limit, $label) = $self->get_limit(%arg);
2592 my ($where, %elt) = $self->get_param('pools',
2597 my $arg = $self->get_form('jmedias', 'qre_media', 'expired');
2599 if ($arg->{jmedias}) {
2600 $where = "AND Media.VolumeName IN ($arg->{jmedias}) $where";
2602 if ($arg->{qre_media}) {
2603 $where = "AND Media.VolumeName $self->{sql}->{MATCH} $arg->{qre_media} $where";
2605 if ($arg->{expired}) {
2607 AND VolStatus = ('Full', 'Used')
2608 AND ( $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2609 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2610 ) < NOW() " . $where ;
2614 SELECT Media.VolumeName AS volumename,
2615 Media.VolBytes AS volbytes,
2616 Media.VolStatus AS volstatus,
2617 Media.MediaType AS mediatype,
2618 Media.InChanger AS online,
2619 Media.LastWritten AS lastwritten,
2620 Location.Location AS location,
2621 (volbytes*100/COALESCE(media_avg_size.size,-1)) AS volusage,
2622 Pool.Name AS poolname,
2623 $self->{sql}->{FROM_UNIXTIME}(
2624 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2625 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2628 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2629 LEFT JOIN (SELECT avg(Media.VolBytes) AS size,
2630 Media.MediaType AS MediaType
2632 WHERE Media.VolStatus = 'Full'
2633 GROUP BY Media.MediaType
2634 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
2636 WHERE Media.PoolId=Pool.PoolId
2641 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2643 $self->display({ ID => $cur_id++,
2645 Location => $elt{location},
2646 Media => [ values %$all ],
2648 "display_media.tpl");
2651 sub display_allmedia
2655 my $pool = $self->get_form('db_pools');
2657 foreach my $name (@{ $pool->{db_pools} }) {
2658 CGI::param('pool', $name->{name});
2659 $self->display_media();
2663 sub display_media_zoom
2667 my $media = $self->get_form('jmedias');
2669 unless ($media->{jmedias}) {
2670 return $self->error("Can't get media selection");
2674 SELECT InChanger AS online,
2675 Media.Enabled AS enabled,
2676 VolBytes AS nb_bytes,
2677 VolumeName AS volumename,
2678 VolStatus AS volstatus,
2679 VolMounts AS nb_mounts,
2680 Media.VolUseDuration AS voluseduration,
2681 Media.MaxVolJobs AS maxvoljobs,
2682 Media.MaxVolFiles AS maxvolfiles,
2683 Media.MaxVolBytes AS maxvolbytes,
2684 VolErrors AS nb_errors,
2685 Pool.Name AS poolname,
2686 Location.Location AS location,
2687 Media.Recycle AS recycle,
2688 Media.VolRetention AS volretention,
2689 Media.LastWritten AS lastwritten,
2690 Media.VolReadTime/1000000 AS volreadtime,
2691 Media.VolWriteTime/1000000 AS volwritetime,
2692 Media.RecycleCount AS recyclecount,
2693 Media.Comment AS comment,
2694 $self->{sql}->{FROM_UNIXTIME}(
2695 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2696 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2699 Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2700 WHERE Pool.PoolId = Media.PoolId
2701 AND VolumeName IN ($media->{jmedias})
2704 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2706 foreach my $media (values %$all) {
2707 my $mq = $self->dbh_quote($media->{volumename});
2710 SELECT DISTINCT Job.JobId AS jobid,
2712 Job.StartTime AS starttime,
2715 Job.JobFiles AS files,
2716 Job.JobBytes AS bytes,
2717 Job.jobstatus AS status
2718 FROM Media,JobMedia,Job
2719 WHERE Media.VolumeName=$mq
2720 AND Media.MediaId=JobMedia.MediaId
2721 AND JobMedia.JobId=Job.JobId
2724 my $jobs = $self->dbh_selectall_hashref($query, 'jobid') ;
2727 SELECT LocationLog.Date AS date,
2728 Location.Location AS location,
2729 LocationLog.Comment AS comment
2730 FROM Media,LocationLog INNER JOIN Location ON (LocationLog.LocationId = Location.LocationId)
2731 WHERE Media.MediaId = LocationLog.MediaId
2732 AND Media.VolumeName = $mq
2736 my $log = $self->dbh_selectall_arrayref($query) ;
2738 $logtxt = join("\n", map { ($_->[0] . ' ' . $_->[1] . ' ' . $_->[2])} @$log ) ;
2741 $self->display({ jobs => [ values %$jobs ],
2742 LocationLog => $logtxt,
2744 "display_media_zoom.tpl");
2751 $self->can_do('r_location_mgnt');
2753 my $loc = $self->get_form('qlocation');
2754 unless ($loc->{qlocation}) {
2755 return $self->error("Can't get location");
2759 SELECT Location.Location AS location,
2760 Location.Cost AS cost,
2761 Location.Enabled AS enabled
2763 WHERE Location.Location = $loc->{qlocation}
2766 my $row = $self->dbh_selectrow_hashref($query);
2767 $row->{enabled} = human_enabled($row->{enabled});
2768 $self->display({ ID => $cur_id++,
2769 %$row }, "location_edit.tpl") ;
2775 $self->can_do('r_location_mgnt');
2777 my $arg = $self->get_form(qw/qlocation qnewlocation cost enabled/) ;
2778 unless ($arg->{qlocation}) {
2779 return $self->error("Can't get location");
2781 unless ($arg->{qnewlocation}) {
2782 return $self->error("Can't get new location name");
2784 unless ($arg->{cost}) {
2785 return $self->error("Can't get new cost");
2788 my $enabled = from_human_enabled($arg->{enabled});
2791 UPDATE Location SET Cost = $arg->{cost},
2792 Location = $arg->{qnewlocation},
2794 WHERE Location.Location = $arg->{qlocation}
2797 $self->dbh_do($query);
2799 $self->location_display();
2805 $self->can_do('r_location_mgnt');
2807 my $arg = $self->get_form(qw/qlocation/) ;
2809 unless ($arg->{qlocation}) {
2810 return $self->error("Can't get location");
2814 SELECT count(Media.MediaId) AS nb
2815 FROM Media INNER JOIN Location USING (LocationID)
2816 WHERE Location = $arg->{qlocation}
2819 my $res = $self->dbh_selectrow_hashref($query);
2822 return $self->error("Sorry, the location must be empty");
2826 DELETE FROM Location WHERE Location = $arg->{qlocation}
2829 $self->dbh_do($query);
2831 $self->location_display();
2837 $self->can_do('r_location_mgnt');
2839 my $arg = $self->get_form(qw/qlocation cost/) ;
2841 unless ($arg->{qlocation}) {
2842 $self->display({}, "location_add.tpl");
2845 unless ($arg->{cost}) {
2846 return $self->error("Can't get new cost");
2849 my $enabled = CGI::param('enabled') || '';
2850 $enabled = from_human_enabled($enabled);
2853 INSERT INTO Location (Location, Cost, Enabled)
2854 VALUES ($arg->{qlocation}, $arg->{cost}, $enabled)
2857 $self->dbh_do($query);
2859 $self->location_display();
2862 sub location_display
2867 SELECT Location.Location AS location,
2868 Location.Cost AS cost,
2869 Location.Enabled AS enabled,
2870 (SELECT count(Media.MediaId)
2872 WHERE Media.LocationId = Location.LocationId
2877 my $location = $self->dbh_selectall_hashref($query, 'location');
2879 $self->display({ ID => $cur_id++,
2880 Locations => [ values %$location ] },
2881 "display_location.tpl");
2888 my $media = $self->get_selected_media_location();
2893 my $arg = $self->get_form('db_locations', 'qnewlocation');
2895 $self->display({ email => $self->{info}->{email_media},
2897 media => [ values %$media ],
2899 "update_location.tpl");
2902 ###########################################################
2907 my $arg = $self->get_form(qw/jclient_groups qclient/);
2909 unless ($arg->{qclient}) {
2910 return $self->error("Can't get client name");
2913 $self->can_do('r_group_mgnt');
2915 my $f1 = $self->get_client_filter();
2916 my $f2 = $self->get_client_group_filter();
2918 $self->{dbh}->begin_work();
2921 DELETE FROM client_group_member
2925 WHERE Client.Name = $arg->{qclient})
2927 $self->dbh_do($query);
2929 if ($arg->{jclient_groups}) {
2931 INSERT INTO client_group_member (client_group_id, ClientId)
2932 (SELECT client_group_id, (SELECT ClientId
2934 WHERE Name = $arg->{qclient})
2935 FROM client_group $f2 WHERE client_group_name IN ($arg->{jclient_groups})
2938 $self->dbh_do($query);
2941 $self->{dbh}->commit() or $self->error($self->{dbh}->errstr);
2943 $self->display_clients();
2949 my $grp = $self->get_form(qw/qclient_group db_clients/);
2951 unless ($grp->{qclient_group}) {
2952 $self->can_do('r_group_mgnt');
2953 $self->display({ ID => $cur_id++,
2954 client_group => "''",
2956 }, "groups_edit.tpl");
2960 unless ($self->cant_do('r_group_mgnt')) {
2961 $self->can_do('r_view_group');
2966 FROM Client JOIN client_group_member using (ClientId)
2967 JOIN client_group using (client_group_id)
2968 WHERE client_group_name = $grp->{qclient_group}
2971 my $row = $self->dbh_selectall_hashref($query, "name");
2973 $self->display({ ID => $cur_id++,
2974 client_group => $grp->{qclient_group},
2976 client_group_member => [ values %$row]},
2983 $self->can_do('r_group_mgnt');
2985 my $arg = $self->get_form(qw/qclient_group jclients qnewgroup qcomment/);
2986 if (!$arg->{qcomment}) {
2987 $arg->{qcomment} = "''";
2990 if (!$arg->{qclient_group} and $arg->{qnewgroup}) {
2992 INSERT INTO client_group (client_group_name, comment)
2993 VALUES ($arg->{qnewgroup}, $arg->{qcomment})
2995 $self->dbh_do($query);
2996 $arg->{qclient_group} = $arg->{qnewgroup};
2999 unless ($arg->{qclient_group}) {
3000 return $self->error("Can't get groups");
3003 $self->{dbh}->begin_work();
3006 DELETE FROM client_group_member
3007 WHERE client_group_id IN
3008 (SELECT client_group_id
3010 WHERE client_group_name = $arg->{qclient_group})
3012 $self->dbh_do($query);
3014 if ($arg->{jclients}) {
3016 INSERT INTO client_group_member (ClientId, client_group_id)
3018 (SELECT client_group_id
3020 WHERE client_group_name = $arg->{qclient_group})
3021 FROM Client WHERE Name IN ($arg->{jclients})
3024 $self->dbh_do($query);
3026 if ($arg->{qclient_group} ne $arg->{qnewgroup}) {
3029 SET client_group_name = $arg->{qnewgroup}, comment = $arg->{qcomment}
3030 WHERE client_group_name = $arg->{qclient_group}
3033 $self->dbh_do($query);
3036 $self->{dbh}->commit() or $self->error($self->{dbh}->errstr);
3038 $self->display_groups();
3044 $self->can_do('r_group_mgnt');
3046 my $arg = $self->get_form(qw/qclient_group/);
3048 unless ($arg->{qclient_group}) {
3049 return $self->error("Can't get groups");
3052 $self->{dbh}->begin_work();
3055 DELETE FROM client_group_member
3056 WHERE client_group_id IN
3057 (SELECT client_group_id
3059 WHERE client_group_name = $arg->{qclient_group})");
3062 DELETE FROM bweb_client_group_acl
3063 WHERE client_group_id IN
3064 (SELECT client_group_id
3066 WHERE client_group_name = $arg->{qclient_group})");
3069 DELETE FROM client_group
3070 WHERE client_group_name = $arg->{qclient_group}");
3072 $self->{dbh}->commit();
3073 $self->display_groups();
3081 if ($self->cant_do('r_group_mgnt')) {
3082 $arg = $self->get_form(qw/db_client_groups filter/) ;
3084 $arg = $self->get_form(qw/db_client_groups/) ;
3087 if ($self->{dbh}->errstr) {
3088 return $self->error("Can't use groups with bweb, read INSTALL to enable them");
3093 $self->display({ ID => $cur_id++,
3095 "display_groups.tpl");
3098 ###########################################################
3103 if (not $self->{info}->{enable_security}) {
3106 if (!$self->{loginname}) {
3107 $self->error("Can't get your login name");
3108 $self->display_end();
3111 # admin is a special user that can do everything
3112 if ($self->{loginname} eq 'admin') {
3116 if (defined $self->{security}) {
3119 $self->{security} = {};
3120 my $u = $self->dbh_quote($self->{loginname});
3123 SELECT use_acl, rolename, tpl
3125 JOIN bweb_role_member USING (userid)
3126 JOIN bweb_role USING (roleid)
3129 my $rows = $self->dbh_selectall_arrayref($query);
3130 # do cache with this role
3131 if (!$rows or !scalar(@$rows)) {
3132 $self->error("Can't get $self->{loginname}'s roles");
3133 $self->display_end();
3136 foreach my $r (@$rows) {
3137 $self->{security}->{$r->[1]}=1;
3139 $self->{security}->{use_acl} = $rows->[0]->[0];
3140 if ($rows->[0]->[2] =~ /^(\w\w)$/) {
3148 my ($self, $client) = @_;
3150 my $filter = $self->get_client_filter();
3154 my $cont = $self->dbh_selectrow_hashref("
3157 WHERE Name = '$client'
3159 return defined $cont;
3164 my ($self, $action) = @_;
3165 # is security enabled in configuration ?
3166 if (not $self->{info}->{enable_security}) {
3169 # admin is a special user that can do everything
3170 if ($self->{loginname} eq 'admin') {
3174 if (!$self->{loginname}) {
3175 $self->{error} = "Can't do $action, your are not logged. " .
3176 "Check security with your administrator";
3179 if (!$self->get_roles()) {
3182 if (!$self->{security}->{$action}) {
3184 "$self->{loginname} sorry, but this action ($action) " .
3185 "is not permited. " .
3186 "Check security with your administrator";
3192 # make like an assert (program die)
3195 my ($self, $action) = @_;
3196 if ($self->cant_do($action)) {
3197 $self->error($self->{error});
3198 $self->display_end();
3208 if (!$self->{info}->{enable_security} or
3209 !$self->{info}->{enable_security_acl})
3214 if ($self->get_roles()) {
3215 return $self->{security}->{use_acl};
3221 # JOIN Client USING (ClientId) " . $b->get_client_filter() . "
3222 sub get_client_filter
3224 my ($self, $login) = @_;
3227 $u = $self->dbh_quote($login);
3228 } elsif ($self->use_filter()) {
3229 $u = $self->dbh_quote($self->{loginname});
3234 JOIN (SELECT ClientId FROM client_group_member
3235 JOIN client_group USING (client_group_id)
3236 JOIN bweb_client_group_acl USING (client_group_id)
3237 JOIN bweb_user USING (userid)
3238 WHERE bweb_user.username = $u
3239 ) AS filter USING (ClientId)";
3242 #JOIN client_group USING (client_group_id)" . $b->get_client_group_filter()
3243 sub get_client_group_filter
3245 my ($self, $login) = @_;
3248 $u = $self->dbh_quote($login);
3249 } elsif ($self->use_filter()) {
3250 $u = $self->dbh_quote($self->{loginname});
3255 JOIN (SELECT client_group_id
3256 FROM bweb_client_group_acl
3257 JOIN bweb_user USING (userid)
3258 WHERE bweb_user.username = $u
3259 ) AS filter USING (client_group_id)";
3262 # role and username have to be quoted before
3263 # role and username can be a quoted list
3266 my ($self, $role, $username) = @_;
3267 $self->can_do("r_user_mgnt");
3269 my $nb = $self->dbh_do("
3270 DELETE FROM bweb_role_member
3271 WHERE roleid = (SELECT roleid FROM bweb_role
3272 WHERE rolename IN ($role))
3273 AND userid = (SELECT userid FROM bweb_user
3274 WHERE username IN ($username))");
3278 # role and username have to be quoted before
3279 # role and username can be a quoted list
3282 my ($self, $role, $username) = @_;
3283 $self->can_do("r_user_mgnt");
3285 my $nb = $self->dbh_do("
3286 INSERT INTO bweb_role_member (roleid, userid)
3287 SELECT roleid, userid FROM bweb_role, bweb_user
3288 WHERE rolename IN ($role)
3289 AND username IN ($username)
3294 # role and username have to be quoted before
3295 # role and username can be a quoted list
3298 my ($self, $copy, $user) = @_;
3299 $self->can_do("r_user_mgnt");
3301 my $nb = $self->dbh_do("
3302 INSERT INTO bweb_role_member (roleid, userid)
3303 SELECT roleid, a.userid
3304 FROM bweb_user AS a, bweb_role_member
3305 JOIN bweb_user USING (userid)
3306 WHERE bweb_user.username = $copy
3307 AND a.username = $user");
3311 # username can be a join quoted list of usernames
3314 my ($self, $username) = @_;
3315 $self->can_do("r_user_mgnt");
3318 DELETE FROM bweb_role_member
3322 WHERE username in ($username))");
3324 DELETE FROM bweb_client_group_acl
3328 WHERE username IN ($username))");
3335 $self->can_do("r_user_mgnt");
3337 my $arg = $self->get_form(qw/jusernames/);
3339 unless ($arg->{jusernames}) {
3340 return $self->error("Can't get user");
3343 $self->{dbh}->begin_work();
3345 $self->revoke_all($arg->{jusernames});
3347 DELETE FROM bweb_user WHERE username IN ($arg->{jusernames})");
3349 $self->{dbh}->commit();
3351 $self->display_users();
3357 $self->can_do("r_user_mgnt");
3359 # we don't quote username directly to check that it is conform
3360 my $arg = $self->get_form(qw/username qpasswd qcomment jrolenames qcreate
3361 lang qcopy_username jclient_groups/) ;
3363 if (not $arg->{qcreate}) {
3364 $arg = $self->get_form(qw/db_roles db_usernames db_client_groups/);
3365 $self->display($arg, "display_user.tpl");
3369 my $u = $self->dbh_quote($arg->{username});
3371 $arg->{use_acl}=(CGI::param('use_acl')?'true':'false');
3373 if (!$arg->{qpasswd}) {
3374 $arg->{qpasswd} = "''";
3376 if (!$arg->{qcomment}) {
3377 $arg->{qcomment} = "''";
3380 # will fail if user already exists
3381 # UPDATE with mysql dbi does not return if update is ok
3384 SET passwd=$arg->{qpasswd}, comment=$arg->{qcomment},
3385 use_acl=$arg->{use_acl}, tpl='$arg->{lang}'
3386 WHERE username = $u")
3387 # and (! $self->dbh_is_mysql() )
3390 INSERT INTO bweb_user (username, passwd, use_acl, comment, tpl)
3391 VALUES ($u, $arg->{qpasswd}, $arg->{use_acl},
3392 $arg->{qcomment}, '$arg->{lang}')");
3394 $self->{dbh}->begin_work();
3396 $self->revoke_all($u);
3398 if ($arg->{qcopy_username}) {
3399 $self->grant_like($arg->{qcopy_username}, $u);
3401 $self->grant($arg->{jrolenames}, $u);
3404 if ($arg->{jclient_groups}) {
3406 INSERT INTO bweb_client_group_acl (client_group_id, userid)
3407 SELECT client_group_id, userid
3408 FROM client_group, bweb_user
3409 WHERE client_group_name IN ($arg->{jclient_groups})
3414 $self->{dbh}->commit();
3416 $self->display_users();
3419 # TODO: we miss a matrix with all user/roles
3423 $self->can_do("r_user_mgnt");
3425 my $arg = $self->get_form(qw/db_usernames/) ;
3427 if ($self->{dbh}->errstr) {
3428 return $self->error("Can't use users with bweb, read INSTALL to enable them");
3431 $self->display({ ID => $cur_id++,
3433 "display_users.tpl");
3439 $self->can_do("r_user_mgnt");
3441 my $arg = $self->get_form('username');
3442 my $user = $self->dbh_quote($arg->{username});
3444 my $userp = $self->dbh_selectrow_hashref("
3445 SELECT username, passwd, comment, use_acl, tpl
3447 WHERE username = $user
3450 return $self->error("Can't find $user in catalog");
3452 my $filter = $self->get_client_group_filter($arg->{username});
3453 my $scg = $self->dbh_selectall_hashref("
3454 SELECT client_group_name AS name
3455 FROM client_group $filter
3459 #------------+--------
3464 my $role = $self->dbh_selectall_hashref("
3465 SELECT rolename, max(here) AS userid FROM (
3466 SELECT rolename, 1 AS here
3468 JOIN bweb_role_member USING (userid)
3469 JOIN bweb_role USING (roleid)
3470 WHERE username = $user
3475 GROUP by rolename", 'rolename');
3477 $arg = $self->get_form(qw/db_usernames db_client_groups/);
3480 db_usernames => $arg->{db_usernames},
3481 username => $userp->{username},
3482 comment => $userp->{comment},
3483 passwd => $userp->{passwd},
3484 lang => $userp->{tpl},
3485 use_acl => $userp->{use_acl},
3486 db_client_groups => $arg->{db_client_groups},
3487 client_group => [ values %$scg ],
3488 db_roles => [ values %$role],
3489 }, "display_user.tpl");
3493 ###########################################################
3495 sub get_media_max_size
3497 my ($self, $type) = @_;
3499 "SELECT avg(VolBytes) AS size
3501 WHERE Media.VolStatus = 'Full'
3502 AND Media.MediaType = '$type'
3505 my $res = $self->selectrow_hashref($query);
3508 return $res->{size};
3518 my $media = $self->get_form('qmedia');
3520 unless ($media->{qmedia}) {
3521 return $self->error("Can't get media");
3525 SELECT Media.Slot AS slot,
3526 PoolMedia.Name AS poolname,
3527 Media.VolStatus AS volstatus,
3528 Media.InChanger AS inchanger,
3529 Location.Location AS location,
3530 Media.VolumeName AS volumename,
3531 Media.MaxVolBytes AS maxvolbytes,
3532 Media.MaxVolJobs AS maxvoljobs,
3533 Media.MaxVolFiles AS maxvolfiles,
3534 Media.VolUseDuration AS voluseduration,
3535 Media.VolRetention AS volretention,
3536 Media.Comment AS comment,
3537 PoolRecycle.Name AS poolrecycle,
3538 Media.Enabled AS enabled
3540 FROM Media INNER JOIN Pool AS PoolMedia ON (Media.PoolId = PoolMedia.PoolId)
3541 LEFT JOIN Pool AS PoolRecycle ON (Media.RecyclePoolId = PoolRecycle.PoolId)
3542 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
3544 WHERE Media.VolumeName = $media->{qmedia}
3547 my $row = $self->dbh_selectrow_hashref($query);
3548 $row->{volretention} = human_sec($row->{volretention});
3549 $row->{voluseduration} = human_sec($row->{voluseduration});
3550 $row->{enabled} = human_enabled($row->{enabled});
3552 my $elt = $self->get_form(qw/db_pools db_locations/);
3557 }, "update_media.tpl");
3563 $self->can_do('r_media_mgnt');
3565 my $arg = $self->get_form('jmedias', 'qnewlocation') ;
3567 unless ($arg->{jmedias}) {
3568 return $self->error("Can't get selected media");
3571 unless ($arg->{qnewlocation}) {
3572 return $self->error("Can't get new location");
3577 SET LocationId = (SELECT LocationId
3579 WHERE Location = $arg->{qnewlocation})
3580 WHERE Media.VolumeName IN ($arg->{jmedias})
3583 my $nb = $self->dbh_do($query);
3585 print "$nb media updated, you may have to update your autochanger.";
3587 $self->display_media();
3593 $self->can_do('r_media_mgnt');
3595 my $media = $self->get_selected_media_location();
3597 return $self->error("Can't get media selection");
3599 my $newloc = CGI::param('newlocation');
3601 my $user = CGI::param('user') || 'unknown';
3602 my $comm = CGI::param('comment') || '';
3603 $comm = $self->dbh_quote("$user: $comm");
3605 my $arg = $self->get_form('enabled');
3606 my $en = from_human_enabled($arg->{enabled});
3607 my $b = $self->get_bconsole();
3610 foreach my $vol (keys %$media) {
3612 INSERT INTO LocationLog (Date,Comment,MediaId,LocationId,NewEnabled,NewVolStatus)
3613 SELECT NOW(), $comm, Media.MediaId, Location.LocationId, $en, VolStatus
3614 FROM Media, Location
3615 WHERE Media.VolumeName = '$vol'
3616 AND Location.Location = '$media->{$vol}->{location}'
3618 $self->dbh_do($query);
3619 $self->debug($query);
3620 $b->send_cmd("update volume=\"$vol\" enabled=$en");
3625 $q->param('action', 'update_location');
3626 my $url = $q->url(-full => 1, -query=>1);
3628 $self->display({ email => $self->{info}->{email_media},
3630 newlocation => $newloc,
3631 # [ { volumename => 'vol1' }, { volumename => 'vol2'},..]
3632 media => [ values %$media ],
3634 "change_location.tpl");
3638 sub display_client_stats
3640 my ($self, %arg) = @_ ;
3641 $self->can_do('r_view_stat');
3643 my $client = $self->dbh_quote($arg{clientname});
3644 # get security filter
3645 my $filter = $self->get_client_filter();
3647 my ($limit, $label) = $self->get_limit(%arg);
3650 count(Job.JobId) AS nb_jobs,
3651 sum(Job.JobBytes) AS nb_bytes,
3652 sum(Job.JobErrors) AS nb_err,
3653 sum(Job.JobFiles) AS nb_files,
3654 Client.Name AS clientname
3655 FROM Job JOIN Client USING (ClientId) $filter
3657 Client.Name = $client
3659 GROUP BY Client.Name
3662 my $row = $self->dbh_selectrow_hashref($query);
3664 $row->{ID} = $cur_id++;
3665 $row->{label} = $label;
3666 $row->{grapharg} = "client";
3667 $row->{age} = $arg{age};
3669 $self->display($row, "display_client_stats.tpl");
3673 sub _display_group_stats
3675 my ($self, %arg) = @_ ;
3677 my $carg = $self->get_form(qw/qclient_group/);
3679 unless ($carg->{qclient_group}) {
3680 return $self->error("Can't get group");
3682 my $jobt = $self->get_stat_table();
3683 my ($limit, $label) = $self->get_limit(%arg);
3687 count(Job.JobId) AS nb_jobs,
3688 sum(Job.JobBytes) AS nb_bytes,
3689 sum(Job.JobErrors) AS nb_err,
3690 sum(Job.JobFiles) AS nb_files,
3691 client_group.client_group_name AS clientname
3693 JOIN Client USING (ClientId)
3694 JOIN client_group_member ON (Client.ClientId = client_group_member.ClientId)
3695 JOIN client_group USING (client_group_id)
3697 client_group.client_group_name = $carg->{qclient_group}
3699 GROUP BY client_group.client_group_name
3702 my $row = $self->dbh_selectrow_hashref($query);
3704 $row->{ID} = $cur_id++;
3705 $row->{label} = $label;
3706 $row->{grapharg} = "client_group";
3708 $self->display($row, "display_client_stats.tpl");
3711 # [ name, num, value, joberrors, nb_job ] =>
3713 # [ { name => 'ALL',
3714 # events => [ { num => 1, label => '2007-01',
3715 # value => 'T', title => 10 },
3716 # { num => 2, label => '2007-02',
3717 # value => 'R', title => 11 },
3720 # { name => 'Other',
3724 sub make_overview_tab
3726 my ($self, $q) = @_;
3727 my $ret = $self->dbh_selectall_arrayref($q);
3731 for my $elt (@$ret) {
3732 if ($cur_name and $cur_name ne $elt->[0]) { # order by name, num
3733 push @items, { name => $cur_name, events => $events};
3736 $cur_name = $elt->[0];
3738 { num => $elt->[1], status => $elt->[2],
3739 joberrors => $elt->[3], title => "$elt->[4] jobs"};
3741 push @items, { name => $cur_name, events => $events};
3745 sub get_time_overview
3747 my ($self, $arg) = @_; # want since et age from get_form();
3748 my $type = $arg->{type} || 'day';
3749 if ($type =~ /^(day|week|hour|month)$/) {
3755 my $jobt = $self->get_stat_table();
3756 my $stime1 = $self->{sql}->{"STARTTIME_P" . $type}; # get 1,2,3
3757 $stime1 =~ s/Job.StartTime/date/;
3758 my $stime2 = $self->{sql}->{"STARTTIME_" . $type}; # get 2007-01-03, 2007-01-23
3760 my ($limit, $label) = $self->get_limit('since' => $arg->{since},
3761 'age' => $arg->{age});
3762 return ($stime1, $stime2, $limit, $label, $jobt);
3765 # lu ma me je ve sa di
3766 # groupe1 v v x w v v v overview
3767 # |-- s1 v v v v v v v overview_zoom
3768 # |-- s2 v v x v v v v
3769 # `-- s3 v v v w v v v
3770 sub display_overview_zoom
3773 $self->can_do('r_view_stat');
3775 my $arg = $self->get_form(qw/jclient_groups age since type level/);
3777 if (!$arg->{jclient_groups}) {
3778 return $self->error("Can't get client_group selection");
3780 my ($filter2, undef) = $self->get_param(qw/level jobtype/);
3781 my ($stime1, $stime2, $limit, $label, $jobt) = $self->get_time_overview($arg);
3783 my $filter = $self->get_client_filter();
3785 SELECT name, $stime1 AS num,
3786 JobStatus AS value, joberrors, nb_job
3788 SELECT $stime2 AS date,
3789 Client.Name AS name,
3790 MAX(severity) AS severity,
3792 SUM(JobErrors) AS joberrors
3794 JOIN client_group_member USING (ClientId)
3795 JOIN client_group USING (client_group_id)
3796 JOIN Client USING (ClientId) $filter
3797 JOIN Status USING (JobStatus)
3798 WHERE client_group_name IN ($arg->{jclient_groups})
3799 AND JobStatus IN ('T', 'W', 'f', 'A', 'e', 'E')
3801 GROUP BY Client.Name, date
3802 ) AS sub JOIN Status USING (severity)
3805 my $items = $self->make_overview_tab($q);
3806 $self->display({label => $label,
3807 action => "job;since=$arg->{since};level=$arg->{level};type=$arg->{type};age=$arg->{age};client=",
3808 items => $items}, "overview.tpl");
3811 sub display_overview
3814 $self->can_do('r_view_stat');
3816 my $arg = $self->get_form(qw/jclient_groups age since type level/);
3817 my ($filter2, undef) = $self->get_param(qw/client_groups level jobtype/);
3818 my $filter3 = $self->get_client_group_filter();
3819 my ($stime1, $stime2, $filter1, $label, $jobt) = $self->get_time_overview($arg);
3822 SELECT name, $stime1 AS num,
3823 JobStatus AS value, joberrors, nb_job
3825 SELECT $stime2 AS date,
3826 client_group_name AS name,
3827 MAX(severity) AS severity,
3829 SUM(JobErrors) AS joberrors
3831 JOIN client_group_member USING (ClientId)
3832 JOIN client_group USING (client_group_id) $filter3
3833 JOIN Status USING (JobStatus)
3834 WHERE JobStatus IN ('T', 'W', 'f', 'A', 'e', 'E')
3836 GROUP BY client_group_name, date
3837 ) AS sub JOIN Status USING (severity)
3840 my $items = $self->make_overview_tab($q);
3841 $self->display({label=>$label,
3842 action => "overview_zoom;since=$arg->{since};level=$arg->{level};type=$arg->{type};age=$arg->{age};client_group=",
3843 items => $items}, "overview.tpl");
3847 # poolname can be undef
3850 my ($self, $poolname) = @_ ;
3851 $self->can_do('r_view_media');
3856 my $arg = $self->get_form('jmediatypes', 'qmediatypes');
3857 if ($arg->{jmediatypes}) {
3858 $whereW = "WHERE MediaType IN ($arg->{jmediatypes}) ";
3859 $whereA = "AND MediaType IN ($arg->{jmediatypes}) ";
3862 # TODO : afficher les tailles et les dates
3865 SELECT subq.volmax AS volmax,
3866 subq.volnum AS volnum,
3867 subq.voltotal AS voltotal,
3869 Pool.Recycle AS recycle,
3870 Pool.VolRetention AS volretention,
3871 Pool.VolUseDuration AS voluseduration,
3872 Pool.MaxVolJobs AS maxvoljobs,
3873 Pool.MaxVolFiles AS maxvolfiles,
3874 Pool.MaxVolBytes AS maxvolbytes,
3875 subq.PoolId AS PoolId,
3876 subq.MediaType AS mediatype,
3877 $self->{sql}->{CAT_POOL_TYPE} AS uniq
3880 SELECT COALESCE(media_avg_size.volavg,0) * count(Media.MediaId) AS volmax,
3881 count(Media.MediaId) AS volnum,
3882 sum(Media.VolBytes) AS voltotal,
3883 Media.PoolId AS PoolId,
3884 Media.MediaType AS MediaType
3886 LEFT JOIN (SELECT avg(Media.VolBytes) AS volavg,
3887 Media.MediaType AS MediaType
3889 WHERE Media.VolStatus = 'Full'
3890 GROUP BY Media.MediaType
3891 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
3892 GROUP BY Media.MediaType, Media.PoolId, media_avg_size.volavg
3894 LEFT JOIN Pool ON (Pool.PoolId = subq.PoolId)
3898 my $all = $self->dbh_selectall_hashref($query, 'uniq') ;
3901 SELECT Pool.Name AS name,
3902 sum(VolBytes) AS size
3903 FROM Media JOIN Pool ON (Media.PoolId = Pool.PoolId)
3904 WHERE Media.VolStatus IN ('Recycled', 'Purged')
3908 my $empty = $self->dbh_selectall_hashref($query, 'name');
3910 foreach my $p (values %$all) {
3911 if ($p->{volmax} > 0) { # mysql returns 0.0000
3912 # we remove Recycled/Purged media from pool usage
3913 if (defined $empty->{$p->{name}}) {
3914 $p->{voltotal} -= $empty->{$p->{name}}->{size};
3916 $p->{poolusage} = sprintf('%.2f', $p->{voltotal} * 100/ $p->{volmax}) ;
3918 $p->{poolusage} = 0;
3922 SELECT VolStatus AS volstatus, count(MediaId) AS nb
3924 WHERE PoolId=$p->{poolid}
3925 AND Media.MediaType = '$p->{mediatype}'
3929 my $content = $self->dbh_selectall_hashref($query, 'volstatus');
3930 foreach my $t (values %$content) {
3931 $p->{"nb_" . $t->{volstatus}} = $t->{nb} ;
3936 $self->display({ ID => $cur_id++,
3937 MediaType => $arg->{qmediatypes}, # [ { name => type1 } , { name => type2 } ]
3938 Pools => [ values %$all ]},
3939 "display_pool.tpl");
3942 # With this function, we get an estimation of next jobfiles/jobbytes count
3943 sub get_estimate_query
3945 my ($self, $mode, $job, $level) = @_;
3946 # get security filter
3947 my $filter = $self->get_client_filter();
3951 if ($self->dbh_is_mysql()) { # mysql doesn't have statistics functions
3953 SELECT jobname AS jobname,
3954 0.1 AS corr_jobbytes, AVG(jobbytes) AS jobbytes,
3955 COUNT(1) AS nb_jobbytes ";
3957 # postgresql have functions that permit to handle lineal regression
3959 # REGR_SLOPE(Y,X) = get x
3960 # REGR_INTERCEPT(Y,X) = get b
3961 # and we need y when x=now()
3962 # CORR gives the correlation
3963 # (TODO: display progress bar only if CORR > 0.8)
3964 my $now = scalar(time);
3966 SELECT temp.jobname AS jobname,
3967 CORR(jobbytes,jobtdate) AS corr_jobbytes,
3968 ($now*REGR_SLOPE(jobbytes,jobtdate)
3969 + REGR_INTERCEPT(jobbytes,jobtdate)) AS jobbytes,
3970 COUNT(1) AS nb_jobbytes ";
3972 # if it's a differential, we need to compare since the last full
3974 # F D D D F D D D F I I I I D I I I
3976 # | # # # # # # | # #
3977 # | # # # # # # # # | # # # # # # # # #
3978 # +----------------- +-------------------
3980 if ($level eq 'D') {
3982 AND Job.StartTime > (
3985 WHERE Job.Name = '$job'
3987 AND Job.JobStatus IN ('T', 'W')
3988 ORDER BY Job.StartTime DESC LIMIT 1
3995 SELECT Job.Name AS jobname,
3996 JobBytes AS jobbytes,
3997 JobTDate AS jobtdate
3998 FROM Job INNER JOIN Client USING (ClientId) $filter
3999 WHERE Job.Name = '$job'
4000 AND Job.Level = '$level'
4001 AND Job.JobStatus IN ('T', 'W')
4003 ORDER BY StartTime DESC
4005 ) AS temp GROUP BY temp.jobname
4008 if ($mode eq 'jobfiles') {
4009 $query =~ s/jobbytes/jobfiles/g;
4010 $query =~ s/JobBytes/JobFiles/g;
4015 sub display_running_job
4018 return if $self->cant_do('r_view_running_job');
4020 my $arg = $self->get_form('jobid');
4022 return $self->error("Can't get jobid") unless ($arg->{jobid});
4024 # get security filter
4025 my $filter = $self->get_client_filter();
4028 SELECT Client.Name AS name, Job.Name AS jobname,
4029 Job.Level AS level, Type AS type, JobStatus AS jobstatus
4030 FROM Job INNER JOIN Client USING (ClientId) $filter
4031 WHERE Job.JobId = $arg->{jobid}
4034 my $row = $self->dbh_selectrow_hashref($query);
4037 $arg->{client} = $row->{name};
4039 return $self->error("Can't get client");
4042 my $status = $row->{jobstatus};
4044 if ($status =~ /[TfAaEWD]/) {
4045 $self->display_job_zoom($arg->{jobid});
4046 $self->get_job_log();
4050 if ($row->{type} eq 'B') {
4051 # for jobfiles, we use only last Full backup. status client= returns
4052 # all files that have been checked
4053 my $query1 = $self->get_estimate_query('jobfiles', $row->{jobname}, 'F');
4054 my $query2 = $self->get_estimate_query('jobbytes',
4055 $row->{jobname}, $row->{level});
4057 # LEFT JOIN because we always have a previous Full
4059 SELECT corr_jobbytes, jobbytes, corr_jobfiles, jobfiles
4060 FROM ($query1) AS A LEFT JOIN ($query2) AS B USING (jobname)
4062 $row = $self->dbh_selectrow_hashref($query);
4065 $row->{jobbytes} = $row->{jobfiles} = 0;
4068 if ($status =~ /[RBSmMsjlL]/) {
4069 my $cli = new Bweb::Client(name => $arg->{client});
4070 $cli->display_running_job($self, $arg->{jobid}, $row);
4072 if ($arg->{jobid}) {
4073 $self->get_job_log();
4077 sub display_running_jobs
4079 my ($self, $display_action) = @_;
4080 return if $self->cant_do('r_view_running_job');
4082 # get security filter
4083 my $filter = $self->get_client_filter();
4086 SELECT Job.JobId AS jobid,
4087 Job.Name AS jobname,
4089 Job.StartTime AS starttime,
4090 Job.JobFiles AS jobfiles,
4091 Job.JobBytes AS jobbytes,
4092 Job.JobStatus AS jobstatus,
4093 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(NOW())
4094 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime))
4096 Client.Name AS clientname
4097 FROM Job INNER JOIN Client USING (ClientId) $filter
4099 JobStatus IN ('C','R','B','e','D','F','S','m','M','s','j','c','d','t','p')
4101 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
4103 $self->display({ ID => $cur_id++,
4104 display_action => $display_action,
4105 Jobs => [ values %$all ]},
4106 "running_job.tpl") ;
4109 sub display_group_stats
4112 my $arg = $self->get_form('age', 'since');
4113 return if $self->cant_do('r_view_stat');
4115 my $filter = $self->get_client_group_filter();
4117 my $jobt = $self->get_stat_table();
4119 my ($limit, $label) = $self->get_limit(%$arg);
4120 my ($where, undef) = $self->get_param('client_groups', 'level');
4123 SELECT client_group_name AS name, nb_byte, nb_file, nb_job, nb_err, nb_resto
4126 SELECT sum(JobBytes) AS nb_byte,
4127 sum(JobFiles) AS nb_file,
4128 count(1) AS nb_job, client_group_name
4129 FROM $jobt AS Job JOIN client_group_member USING (ClientId)
4130 JOIN client_group USING (client_group_id) $filter
4131 WHERE JobStatus IN ('T', 'W') AND Type IN ('M', 'B', 'g')
4133 GROUP BY client_group_name ORDER BY client_group_name
4137 SELECT count(1) AS nb_err, client_group_name
4138 FROM $jobt AS Job JOIN client_group_member USING (ClientId)
4139 JOIN client_group USING (client_group_id)
4140 WHERE JobStatus IN ('E','e','f','A') AND Type = 'B'
4142 GROUP BY client_group_name ORDER BY client_group_name
4144 ) AS T3 USING (client_group_name) LEFT JOIN (
4146 SELECT count(1) AS nb_resto, client_group_name
4147 FROM $jobt AS Job JOIN client_group_member USING (ClientId)
4148 JOIN client_group USING (client_group_id)
4149 WHERE JobStatus IN ('T','W') AND Type = 'R'
4151 GROUP BY client_group_name ORDER BY client_group_name
4153 ) AS T2 USING (client_group_name)
4155 $self->debug($query);
4156 my $all = $self->dbh_selectall_hashref($query, 'name') ;
4159 $self->display({ ID => $cur_id++,
4161 Stats => [ values %$all ]},
4162 "display_stats.tpl") ;
4165 # return the autochanger list to update
4169 $self->can_do('r_media_mgnt');
4172 my $arg = $self->get_form('jmedias');
4174 unless ($arg->{jmedias}) {
4175 return $self->error("Can't get media selection");
4179 SELECT Media.VolumeName AS volumename,
4180 Storage.Name AS storage,
4181 Location.Location AS location,
4183 FROM Media INNER JOIN Storage ON (Media.StorageId = Storage.StorageId)
4184 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
4185 WHERE Media.VolumeName IN ($arg->{jmedias})
4186 AND Media.InChanger = 1
4189 my $all = $self->dbh_selectall_hashref($query, 'volumename');
4191 foreach my $vol (values %$all) {
4192 my $a = $self->ach_get($vol->{location});
4194 $ret{$vol->{location}} = 1;
4196 unless ($a->{have_status}) {
4198 $a->{have_status} = 1;
4201 print "eject $vol->{volumename} from $vol->{storage} : ";
4202 if ($a->send_to_io($vol->{slot})) {
4203 print "<img src='/bweb/T.png' alt='ok'><br/>";
4205 print "<img src='/bweb/E.png' alt='err'><br/>";
4215 my ($to, $subject, $content) = (CGI::param('email'),
4216 CGI::param('subject'),
4217 CGI::param('content'));
4218 $to =~ s/[^\w\d\.\@<>,]//;
4219 $subject =~ s/[^\w\d\.\[\]]/ /;
4221 open(MAIL, "|mail -s '$subject' '$to'") ;
4222 print MAIL $content;
4232 my $arg = $self->get_form('jobid', 'client');
4234 print CGI::header('text/brestore');
4235 print "jobid=$arg->{jobid}\n" if ($arg->{jobid});
4236 print "client=$arg->{client}\n" if ($arg->{client});
4237 print "\n\nYou have to assign this mime type with /usr/bin/brestore.pl\n";
4241 # TODO : move this to Bweb::Autochanger ?
4242 # TODO : make this internal to not eject tape ?
4248 my ($self, $name) = @_;
4251 return $self->error("Can't get your autochanger name ach");
4254 unless ($self->{info}->{ach_list}) {
4255 return $self->error("Could not find any autochanger");
4258 my $a = $self->{info}->{ach_list}->{$name};
4261 $self->error("Can't get your autochanger $name from your ach_list");
4266 $a->{debug} = $self->{debug};
4273 my ($self, $ach) = @_;
4274 $self->can_do('r_configure');
4276 $self->{info}->{ach_list}->{$ach->{name}} = $ach;
4278 $self->{info}->save();
4286 $self->can_do('r_configure');
4288 my $arg = $self->get_form('ach');
4290 or !$self->{info}->{ach_list}
4291 or !$self->{info}->{ach_list}->{$arg->{ach}})
4293 return $self->error("Can't get autochanger name");
4296 my $ach = $self->{info}->{ach_list}->{$arg->{ach}};
4300 [ map { { name => $_, index => $i++ } } @{$ach->{drive_name}} ] ;
4302 my $b = $self->get_bconsole();
4304 my @storages = $b->list_storage() ;
4306 $ach->{devices} = [ map { { name => $_ } } @storages ];
4308 $self->display($ach, "ach_add.tpl");
4309 delete $ach->{drives};
4310 delete $ach->{devices};
4317 $self->can_do('r_configure');
4319 my $arg = $self->get_form('ach');
4322 or !$self->{info}->{ach_list}
4323 or !$self->{info}->{ach_list}->{$arg->{ach}})
4325 return $self->error("Can't get autochanger name");
4328 delete $self->{info}->{ach_list}->{$arg->{ach}} ;
4330 $self->{info}->save();
4331 $self->{info}->view();
4337 $self->can_do('r_configure');
4339 my $arg = $self->get_form('ach', 'mtxcmd', 'device', 'precmd');
4341 my $b = $self->get_bconsole();
4342 my @storages = $b->list_storage() ;
4344 unless ($arg->{ach}) {
4345 $arg->{devices} = [ map { { name => $_ } } @storages ];
4346 return $self->display($arg, "ach_add.tpl");
4350 foreach my $drive (CGI::param('drives'))
4352 unless (grep(/^$drive$/,@storages)) {
4353 return $self->error("Can't find $drive in storage list");
4356 my $index = CGI::param("index_$drive");
4357 unless (defined $index and $index =~ /^(\d+)$/) {
4358 return $self->error("Can't get $drive index");
4361 $drives[$index] = $drive;
4365 return $self->error("Can't get drives from Autochanger");
4368 my $a = new Bweb::Autochanger(name => $arg->{ach},
4369 precmd => $arg->{precmd},
4370 drive_name => \@drives,
4371 device => $arg->{device},
4372 mtxcmd => $arg->{mtxcmd});
4374 $self->ach_register($a) ;
4376 $self->{info}->view();
4382 $self->can_do('r_delete_job');
4384 my $arg = $self->get_form('jobid');
4386 if ($arg->{jobid}) {
4387 my $b = $self->get_bconsole();
4388 my $ret = $b->send_cmd("delete jobid=\"$arg->{jobid}\"");
4392 title => "Delete a job ",
4393 name => "delete jobid=$arg->{jobid}",
4402 $self->can_do('r_media_mgnt');
4404 my $arg = $self->get_form(qw/media volstatus inchanger pool
4405 slot volretention voluseduration
4406 maxvoljobs maxvolfiles maxvolbytes
4407 qcomment poolrecycle enabled
4410 unless ($arg->{media}) {
4411 return $self->error("Can't find media selection");
4414 my $update = "update volume=$arg->{media} ";
4416 if ($arg->{volstatus}) {
4417 $update .= " volstatus=$arg->{volstatus} ";
4420 if ($arg->{inchanger}) {
4421 $update .= " inchanger=yes " ;
4423 $update .= " slot=$arg->{slot} ";
4426 $update .= " slot=0 inchanger=no ";
4429 if ($arg->{enabled}) {
4430 $update .= " enabled=$arg->{enabled} ";
4434 $update .= " pool=$arg->{pool} " ;
4437 if (defined $arg->{volretention}) {
4438 $update .= " volretention=\"$arg->{volretention}\" " ;
4441 if (defined $arg->{voluseduration}) {
4442 $update .= " voluse=\"$arg->{voluseduration}\" " ;
4445 if (defined $arg->{maxvoljobs}) {
4446 $update .= " maxvoljobs=$arg->{maxvoljobs} " ;
4449 if (defined $arg->{maxvolfiles}) {
4450 $update .= " maxvolfiles=$arg->{maxvolfiles} " ;
4453 if (defined $arg->{maxvolbytes}) {
4454 $update .= " maxvolbytes=$arg->{maxvolbytes} " ;
4457 if (defined $arg->{poolrecycle}) {
4458 $update .= " recyclepool=\"$arg->{poolrecycle}\" " ;
4461 my $b = $self->get_bconsole();
4464 content => $b->send_cmd($update),
4465 title => "Update a volume ",
4473 my $media = $self->dbh_quote($arg->{media});
4475 my $loc = CGI::param('location') || '';
4477 $loc = $self->dbh_quote($loc); # is checked by db
4478 push @q, "LocationId=(SELECT LocationId FROM Location WHERE Location=$loc)";
4480 if (!$arg->{qcomment}) {
4481 $arg->{qcomment} = "''";
4483 push @q, "Comment=$arg->{qcomment}";
4488 SET " . join (',', @q) . "
4489 WHERE Media.VolumeName = $media
4491 $self->dbh_do($query);
4493 $self->update_media();
4499 $self->can_do('r_autochanger_mgnt');
4501 my $ach = CGI::param('ach') ;
4502 $ach = $self->ach_get($ach);
4504 return $self->error("Bad autochanger name");
4508 title => "Scanning autochanger content ",
4509 name => "update slots",
4513 my $b = new Bconsole(pref => $self->{info},timeout => 60,log_stdout => 1);
4514 $b->update_slots($ach->{name});
4524 $self->can_do('r_view_log');
4526 my $arg = $self->get_form('jobid', 'limit', 'offset');
4527 unless ($arg->{jobid}) {
4528 return $self->error("Can't get jobid");
4531 if ($arg->{limit} == 100) {
4532 $arg->{limit} = 1000;
4534 # get security filter
4535 my $filter = $self->get_client_filter();
4538 SELECT Job.Name as name, Client.Name as clientname
4539 FROM Job INNER JOIN Client USING (ClientId) $filter
4540 WHERE JobId = $arg->{jobid}
4543 my $row = $self->dbh_selectrow_hashref($query);
4546 return $self->error("Can't find $arg->{jobid} in catalog");
4549 # display only Error and Warning messages
4551 if (CGI::param('error')) {
4552 $filter = " AND LogText $self->{sql}->{MATCH} 'Error|Warning|ERR=' ";
4556 if (CGI::param('time') || $self->{info}->{display_log_time}) {
4557 $logtext = $self->dbh_strcat('Time', " ' ' ", 'LogText');
4559 $logtext = 'LogText';
4563 SELECT count(1) AS nbline,
4564 GROUP_CONCAT($logtext $self->{sql}->{CONCAT_SEP}) AS logtxt, id
4566 SELECT 1 AS id, Time, LogText
4568 WHERE ( Log.JobId = $arg->{jobid}
4570 AND Time >= (SELECT StartTime FROM Job WHERE JobId=$arg->{jobid})
4571 AND Time <= (SELECT COALESCE(EndTime,NOW()) FROM Job WHERE JobId=$arg->{jobid})
4575 OFFSET $arg->{offset}
4581 my $log = $self->dbh_selectrow_hashref($query);
4583 return $self->error("Can't get log for jobid $arg->{jobid}");
4585 $log->{logtxt} =~ s/\0//g;
4586 $self->display({ lines=> $log->{logtxt},
4587 nbline => $log->{nbline},
4588 jobid => $arg->{jobid},
4589 name => $row->{name},
4590 client => $row->{clientname},
4591 offset => $arg->{offset},
4592 limit => $arg->{limit},
4593 }, 'display_log.tpl');
4596 sub cancel_future_job
4599 $self->can_do('r_cancel_job');
4601 my $arg = $self->get_form(qw/job pool level client when/);
4603 if ( !$arg->{job} or !$arg->{pool} or !$arg->{level}
4604 or !$arg->{client} or !$arg->{when})
4606 return $self->error("Can't get enough information to mark this job as canceled");
4609 $arg->{level} =~ s/^(.).+/$1/; # we keep the first letter
4610 my $jobtable = $self->{info}->{stat_job_table} || 'JobHisto';
4612 if ($jobtable =~ /^Job$/i) {
4613 return $self->error("Can add records only in history table");
4615 my $jname = "$arg->{job}.$arg->{when}";
4618 my $found = $self->dbh_selectrow_hashref("
4623 AND Name = '$arg->{job}'
4626 return $self->error("$jname is already in history table");
4630 INSERT INTO $jobtable
4631 (JobId, Name, Job, Type, Level, JobStatus, SchedTime, StartTime, EndTime,
4632 RealEndTime, ClientId, PoolId)
4634 (0, '$arg->{job}', '$jname', 'B', '$arg->{level}', 'A',
4635 '$arg->{when}', '$arg->{when}', '$arg->{when}', '$arg->{when}',
4636 (SELECT ClientId FROM Client WHERE Name = '$arg->{client}'),
4637 (SELECT PoolId FROM Pool WHERE Name = '$arg->{pool}')
4640 $self->display({ Filter => "Dummy record for $jname",
4644 client => $arg->{client},
4645 jobname => $arg->{job},
4646 pool => $arg->{pool},
4647 level => $arg->{level},
4648 starttime => $arg->{when},
4649 duration => '00:00:00',
4662 $self->can_do('r_media_mgnt');
4663 my $arg = $self->get_form('storage', 'pool', 'nb', 'media', 'offset');
4664 my $b = $self->get_bconsole();
4666 if (!$arg->{storage} || !$arg->{pool} || not defined $arg->{nb} || !$arg->{media} || !$arg->{offset}) {
4667 CGI::param(offset => 0);
4668 $arg = $self->get_form('db_pools');
4669 $arg->{storage} = [ map { { name => $_ } }$b->list_storage()];
4670 $self->display($arg, 'add_media.tpl');
4674 $b->send("add pool=\"$arg->{pool}\" storage=\"$arg->{storage}\"\n");
4675 if ($arg->{nb} > 0) {
4676 $arg->{offset} = $arg->{offset}?$arg->{offset}:1;
4677 $b->send("$arg->{nb}\n");
4678 $b->send("$arg->{media}\n");
4679 $b->send("$arg->{offset}\n");
4683 $b->send("$arg->{media}\n");
4686 $b->expect_it('-re','^[*]');
4688 CGI::param('media', '');
4689 CGI::param('re_media', $arg->{media});
4690 $self->display_media();
4696 $self->can_do('r_autochanger_mgnt');
4698 my $arg = $self->get_form('ach', 'slots', 'drive', 'pool');
4700 unless ($arg->{ach}) {
4701 return $self->error("Can't find autochanger name");
4704 my $a = $self->ach_get($arg->{ach});
4706 return $self->error("Can't find autochanger name in configuration");
4709 my $storage = $a->get_drive_name($arg->{drive});
4711 return $self->error("Can't get your drive name");
4717 if ($arg->{slots}) {
4718 $slots = join(",", @{ $arg->{slots} });
4719 $slots_sql = " AND Slot IN ($slots) ";
4720 $t += 60*scalar( @{ $arg->{slots} }) ;
4722 my $pool = $arg->{pool} || 'Scratch';
4723 my $b = new Bconsole(pref => $self->{info}, timeout => $t,log_stdout => 1);
4724 print "<h1>This command can take long time, be patient...</h1>";
4726 $b->label_barcodes(storage => $storage,
4727 drive => $arg->{drive},
4735 SET LocationId = (SELECT LocationId
4737 WHERE Location = '$arg->{ach}')
4739 WHERE (LocationId = 0 OR LocationId IS NULL)
4748 $self->can_do('r_purge');
4750 my @volume = CGI::param('media');
4753 return $self->error("Can't get media selection");
4756 my $b = new Bconsole(pref => $self->{info}, timeout => 60);
4758 foreach my $v (@volume) {
4760 content => $b->purge_volume($v),
4761 title => "Purge media",
4762 name => "purge volume=$v",
4772 $self->can_do('r_prune');
4774 my @volume = CGI::param('media');
4776 return $self->error("Can't get media selection");
4779 my $b = new Bconsole(pref => $self->{info}, timeout => 60);
4781 foreach my $v (@volume) {
4783 content => $b->prune_volume($v),
4784 title => "Prune volume",
4785 name => "prune volume=$v",
4795 $self->can_do('r_cancel_job');
4797 my $arg = $self->get_form('jobid');
4798 unless ($arg->{jobid}) {
4799 return $self->error("Can't get jobid");
4802 my $b = $self->get_bconsole();
4804 content => $b->cancel($arg->{jobid}),
4805 title => "Cancel job",
4806 name => "cancel jobid=$arg->{jobid}",
4813 # Warning, we display current fileset
4816 my $arg = $self->get_form('fileset');
4818 if ($arg->{fileset}) {
4819 my $b = $self->get_bconsole();
4820 my $ret = $b->get_fileset($arg->{fileset});
4821 $self->display({ fileset => $arg->{fileset},
4823 }, "fileset_view.tpl");
4825 $self->error("Can't get fileset name");
4829 sub director_show_sched
4832 $self->can_do('r_view_job');
4833 my $arg = $self->get_form('days');
4835 my $b = $self->get_bconsole();
4836 my $ret = $b->director_get_sched( $arg->{days} );
4841 }, "scheduled_job.tpl");
4844 sub enable_disable_job
4846 my ($self, $what) = @_ ;
4847 $self->can_do('r_run_job');
4849 my $arg = $self->get_form('job');
4851 return $self->error("Can't find job name");
4854 my $b = $self->get_bconsole();
4864 content => $b->send_cmd("$cmd job=\"$arg->{job}\""),
4865 title => "$cmd $arg->{job}",
4866 name => "$cmd job=\"$arg->{job}\"",
4874 return new Bconsole(pref => $self->{info});
4880 $self->can_do('r_storage_mgnt');
4881 my $arg = $self->get_form(qw/storage storage_cmd drive slot/);
4882 my $b = $self->get_bconsole();
4884 if ($arg->{storage} and $arg->{storage_cmd}) {
4885 my $cmd = "$arg->{storage_cmd} storage=\"$arg->{storage}\" drive=$arg->{drive} slot=$arg->{slot}";
4886 my $ret = $b->send_cmd($cmd);
4890 title => "Storage ",
4895 my $storages= [ map { { name => $_ } } $b->list_storage()];
4896 $self->display({ storage => $storages}, "cmd_storage.tpl");
4903 $self->can_do('r_run_job');
4905 my $b = $self->get_bconsole();
4907 my $joblist = [ map { { name => $_ } } $b->list_backup() ];
4909 $self->display({ Jobs => $joblist }, "run_job.tpl");
4914 my ($self, $ouput) = @_;
4917 $self->debug($ouput);
4918 foreach my $l (split(/\r?\n/, $ouput)) {
4920 if ($l =~ /(\w+): name=([\w\d\.\s-]+?)(\s+\w+=.+)?$/) {
4926 if (my @l = $l =~ /(\w+)=([\w\d*]+)/g) {
4932 foreach my $k (keys %arg) {
4933 $lowcase{lc($k)} = $arg{$k} ;
4935 $self->debug(\%lowcase);
4942 $self->can_do('r_run_job');
4944 my $b = $self->get_bconsole();
4945 my $arg = $self->get_form(qw/pool level client fileset storage media job/);
4948 return $self->error("Can't get job name");
4951 # we take informations from director, and we overwrite with user wish
4952 my $info = $b->send_cmd("show job=\"$arg->{job}\"");
4953 my $attr = $self->run_parse_job($info);
4955 if (!$arg->{pool} and $arg->{media}) {
4956 my $r = $self->dbh_selectrow_hashref("
4957 SELECT Pool.Name AS name
4958 FROM Media JOIN Pool USING (PoolId)
4959 WHERE Media.VolumeName = '$arg->{media}'
4960 AND Pool.Name != 'Scratch'
4963 $arg->{pool} = $r->{name};
4967 my %job_opt = (%$attr, %$arg);
4969 my $jobs = [ map {{ name => $_ }} $b->list_job() ];
4971 my $pools = [ map { { name => $_ } } $b->list_pool() ];
4972 my $clients = [ map { { name => $_ } }$b->list_client()];
4973 my $filesets= [ map { { name => $_ } }$b->list_fileset() ];
4974 my $storages= [ map { { name => $_ } }$b->list_storage()];
4979 clients => $clients,
4980 filesets => $filesets,
4981 storages => $storages,
4983 }, "run_job_mod.tpl");
4989 $self->can_do('r_run_job');
4991 my $b = $self->get_bconsole();
4993 my $jobs = [ map {{ name => $_ }} $b->list_job() ];
5003 $self->can_do('r_run_job');
5005 my $b = $self->get_bconsole();
5007 # TODO: check input (don't use pool, level)
5009 my $arg = $self->get_form(qw/pool level client priority when
5010 fileset job storage/);
5012 return $self->error("Can't get your job name");
5015 my $jobid = $b->run(job => $arg->{job},
5016 client => $arg->{client},
5017 priority => $arg->{priority},
5018 level => $arg->{level},
5019 storage => $arg->{storage},
5020 pool => $arg->{pool},
5021 fileset => $arg->{fileset},
5022 when => $arg->{when},
5027 print "<br>You can follow job (jobid=$jobid) execution <a href='?action=dsp_cur_job;client=$arg->{client};jobid=$jobid'> here </a><script type='text/javascript' language='JavaScript'>setTimeout(function() { window.location='?action=dsp_cur_job;client=$arg->{client};jobid=$jobid'},2000);</script>";
5030 sub display_next_job
5034 my $arg = $self->get_form(qw/job begin end/);
5036 return $self->error("Can't get job name");
5039 my $b = $self->get_bconsole();
5041 my $job = $b->send_cmd("show job=\"$arg->{job}\"");
5042 my $attr = $self->run_parse_job($job);
5044 if (!$attr->{schedule}) {
5045 return $self->error("Can't get $arg->{job} schedule");
5047 my $jpool=$attr->{pool} || '';
5049 my $sched = new Bweb::Sched(bconsole => $b, name => $attr->{schedule},
5050 begin => $arg->{begin}, end => $arg->{end});
5052 my $ss = $sched->get_scheds($attr->{schedule});
5055 foreach my $s (@$ss) {
5056 my $level = $sched->get_level($s);
5057 my $pool = $sched->get_pool($s) || $jpool;
5058 my $evt = $sched->get_event($s);
5059 push @ret, map { "$_ : $pool ($level)\n" } @$evt;
5062 print "<b>$arg->{job}:</b><pre>", sort @ret, "</pre><br>";
5065 # permit to verify for higher level backup
5066 # we attempt a Increment, we made a Full, that ok
5067 # TODO: Pool may have change
5068 sub get_higher_level
5070 my ($self, $level) = @_;
5071 if ($level eq 'F') {
5073 } elsif ($level eq 'D') {
5075 } elsif ($level eq 'I') {
5076 return "'F', 'D', 'I'";
5081 # check jobs against their schedule
5084 my ($self, $sched, $schedname, $job, $job_pool, $client, $type) = @_;
5085 return undef if (!$self->can_view_client($client));
5087 my $sch = $sched->get_scheds($schedname);
5088 return undef if (!$sch);
5091 foreach my $s (@$sch) {
5093 if ($type eq 'B') { # we take the pool only for backup job
5094 $pool = $sched->get_pool($s) || $job_pool;
5096 my $level = $sched->get_level($s);
5097 my ($l) = ($level =~ m/^(.)/); # we keep the first letter
5098 $l = $self->get_higher_level($l);
5099 my $evts = $sched->get_event($s);
5100 my $end = $sched->{end}; # this backup must have start before the next one
5101 foreach my $evt (reverse @$evts) {
5102 my $all = $self->dbh_selectrow_hashref("
5105 JOIN Client USING (ClientId) LEFT JOIN Pool USING (PoolId)
5106 WHERE Job.StartTime >= '$evt'
5107 AND Job.StartTime < '$end'
5108 AND Job.Name = '$job'
5109 AND Job.Type = '$type'
5110 AND Job.JobStatus IN ('T', 'W')
5111 AND Job.Level IN ($l)
5112 " . ($pool?" AND Pool.Name = '$pool' ":'') . "
5113 AND Client.Name = '$client'
5119 push @{$self->{tmp}}, {date => $evt, level => $level,
5120 type => 'Backup', name => $job,
5121 pool => $pool, volume => $pool,
5129 sub display_missing_job
5132 my $arg = $self->get_form(qw/begin end age/);
5134 if (!$arg->{begin}) { # TODO: change this
5135 $arg->{begin} = strftime('%F %T', localtime(time - $arg->{age}));
5138 $arg->{end} = strftime('%F %T', localtime(time));
5140 $self->{tmp} = []; # check_job use this for result
5142 my $bconsole = $self->get_bconsole();
5144 my $sched = new Bweb::Sched(bconsole => $bconsole,
5145 begin => $arg->{begin},
5146 end => $arg->{end});
5148 my $job = $bconsole->send_cmd("show job");
5149 my ($jname, $jsched, $jclient, $jpool, $jtype);
5150 foreach my $j (split(/\r?\n/, $job)) {
5151 if ($j =~ /Job: name=([\w\d\-]+?) JobType=(\d+)/i) {
5152 if ($jname and $jsched) {
5153 $self->check_job($sched, $jsched, $jname,
5154 $jpool, $jclient, $jtype);
5158 $jclient = $jpool = $jsched = undef;
5159 } elsif ($j =~ /Client: name=(.+?) address=/i) {
5161 } elsif ($j =~ /Pool: name=([\w\d\-]+) PoolType=/i) {
5163 } elsif ($j =~ /Schedule: name=([\w\d\-]+)/i) {
5169 title => "Missing Jobs (from $arg->{begin} to $arg->{end})",
5170 list => $self->{tmp},
5171 wiki_url => $self->{info}->{wiki_url},
5173 }, "scheduled_job.tpl");
5175 delete $self->{tmp};