1 ################################################################
6 Bweb - A Bacula web interface
7 Bacula® - The Network Backup Solution
9 Copyright (C) 2000-2006 Free Software Foundation Europe e.V.
11 The main author of Bweb is Eric Bollengier.
12 The main author of Bacula is Kern Sibbald, with contributions from
13 many others, a complete list can be found in the file AUTHORS.
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 John Walker.
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 error - display an error to the user
102 this function set $self->{error} with arg, display a message with
103 error.tpl and return 0
108 return $self->error("Can't use this file");
115 my ($self, $what) = @_;
116 $self->{error} = $what;
117 $self->display($self, 'error.tpl');
123 display - display an html page with HTML::Template
127 this function is use to render all html codes. it takes an
128 ref hash as arg in which all param are usable in template.
130 it will use global template_dir to search the template file.
132 hash keys are not sensitive. See HTML::Template for more
133 explanations about the hash ref. (it's can be quiet hard to understand)
137 $ref = { name => 'me', age => 26 };
138 $self->display($ref, "people.tpl");
144 my ($self, $hash, $tpl) = @_ ;
146 my $template = HTML::Template->new(filename => $tpl,
147 path =>[$template_dir],
148 die_on_bad_params => 0,
149 case_sensitive => 0);
151 foreach my $var (qw/limit offset/) {
153 unless ($hash->{$var}) {
154 my $value = CGI::param($var) || '';
156 if ($value =~ /^(\d+)$/) {
157 $template->param($var, $1) ;
162 $template->param('thisurl', CGI::url(-relative => 1, -query=>1));
163 $template->param('loginname', CGI::remote_user());
165 $template->param($hash);
166 print $template->output();
170 ################################################################
172 package Bweb::Config;
174 use base q/Bweb::Gui/;
178 Bweb::Config - read, write, display, modify configuration
182 this package is used for manage configuration
186 $conf = new Bweb::Config(config_file => '/path/to/conf');
197 =head1 PACKAGE VARIABLE
199 %k_re - hash of all acceptable option.
203 this variable permit to check all option with a regexp.
207 our %k_re = ( dbi => qr/^(dbi:(Pg|mysql):(?:\w+=[\w\d\.-]+;?)+)$/i,
208 user => qr/^([\w\d\.-]+)$/i,
209 password => qr/^(.*)$/i,
210 fv_write_path => qr!^([/\w\d\.-]*)$!,
211 template_dir => qr!^([/\w\d\.-]+)$!,
212 debug => qr/^(on)?$/,
213 email_media => qr/^([\w\d\.-]+@[\d\w\.-]+)$/,
214 graph_font => qr!^([/\w\d\.-]+.ttf)$!,
215 bconsole => qr!^(.+)?$!,
216 syslog_file => qr!^(.+)?$!,
217 log_dir => qr!^(.+)?$!,
222 load - load config_file
226 this function load the specified config_file.
234 unless (open(FP, $self->{config_file}))
236 return $self->error("can't load config_file $self->{config_file} : $!");
238 my $f=''; my $tmpbuffer;
239 while(read FP,$tmpbuffer,4096)
247 no strict; # I have no idea of the contents of the file
254 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...") ;
257 foreach my $k (keys %$VAR1) {
258 $self->{$k} = $VAR1->{$k};
266 load_old - load old configuration format
274 unless (open(FP, $self->{config_file}))
276 return $self->error("$self->{config_file} : $!");
279 while (my $line = <FP>)
282 my ($k, $v) = split(/\s*=\s*/, $line, 2);
294 save - save the current configuration to config_file
302 if ($self->{ach_list}) {
303 # shortcut for display_begin
304 $self->{achs} = [ map {{ name => $_ }}
305 keys %{$self->{ach_list}}
309 unless (open(FP, ">$self->{config_file}"))
311 return $self->error("$self->{config_file} : $!\n" .
312 "You must add this to your config file\n"
313 . Data::Dumper::Dumper($self));
316 print FP Data::Dumper::Dumper($self);
324 edit, view, modify - html form ouput
332 $self->display($self, "config_edit.tpl");
338 $self->display($self, "config_view.tpl");
348 foreach my $k (CGI::param())
350 next unless (exists $k_re{$k}) ;
351 my $val = CGI::param($k);
352 if ($val =~ $k_re{$k}) {
355 $self->{error} .= "bad parameter : $k = [$val]";
361 if ($self->{error}) { # an error as occured
362 $self->display($self, 'error.tpl');
370 ################################################################
372 package Bweb::Client;
374 use base q/Bweb::Gui/;
378 Bweb::Client - Bacula FD
382 this package is use to do all Client operations like, parse status etc...
386 $client = new Bweb::Client(name => 'zog-fd');
387 $client->status(); # do a 'status client=zog-fd'
393 display_running_job - Html display of a running job
397 this function is used to display information about a current job
401 sub display_running_job
403 my ($self, $conf, $jobid) = @_ ;
405 my $status = $self->status($conf);
408 if ($status->{$jobid}) {
409 $self->display($status->{$jobid}, "client_job_status.tpl");
412 for my $id (keys %$status) {
413 $self->display($status->{$id}, "client_job_status.tpl");
420 $client = new Bweb::Client(name => 'plume-fd');
422 $client->status($bweb);
426 dirty hack to parse "status client=xxx-fd"
430 JobId 105 Job Full_plume.2006-06-06_17.22.23 is running.
431 Backup Job started: 06-jun-06 17:22
432 Files=8,971 Bytes=194,484,132 Bytes/sec=7,480,158
433 Files Examined=10,697
434 Processing file: /home/eric/.openoffice.org2/user/config/standard.sod
440 JobName => Full_plume.2006-06-06_17.22.23,
443 Bytes => 194,484,132,
453 my ($self, $conf) = @_ ;
455 if (defined $self->{cur_jobs}) {
456 return $self->{cur_jobs} ;
460 my $b = new Bconsole(pref => $conf);
461 my $ret = $b->send_cmd("st client=$self->{name}");
465 for my $r (split(/\n/, $ret)) {
467 $r =~ s/(^\s+|\s+$)//g;
468 if ($r =~ /JobId (\d+) Job (\S+)/) {
470 $arg->{$jobid} = { @param, JobId => $jobid } ;
474 @param = ( JobName => $2 );
476 } elsif ($r =~ /=.+=/) {
477 push @param, split(/\s+|\s*=\s*/, $r) ;
479 } elsif ($r =~ /=/) { # one per line
480 push @param, split(/\s*=\s*/, $r) ;
482 } elsif ($r =~ /:/) { # one per line
483 push @param, split(/\s*:\s*/, $r, 2) ;
487 if ($jobid and @param) {
488 $arg->{$jobid} = { @param,
490 Client => $self->{name},
494 $self->{cur_jobs} = $arg ;
500 ################################################################
502 package Bweb::Autochanger;
504 use base q/Bweb::Gui/;
508 Bweb::Autochanger - Object to manage Autochanger
512 this package will parse the mtx output and manage drives.
516 $auto = new Bweb::Autochanger(precmd => 'sudo');
518 $auto = new Bweb::Autochanger(precmd => 'ssh root@robot');
522 $auto->slot_is_full(10);
523 $auto->transfer(10, 11);
529 my ($class, %arg) = @_;
532 name => '', # autochanger name
533 label => {}, # where are volume { label1 => 40, label2 => drive0 }
534 drive => [], # drive use [ 'media1', 'empty', ..]
535 slot => [], # slot use [ undef, 'empty', 'empty', ..] no slot 0
536 io => [], # io slot number list [ 41, 42, 43...]
537 info => {slot => 0, # informations (slot, drive, io)
541 mtxcmd => '/usr/sbin/mtx',
543 device => '/dev/changer',
544 precmd => '', # ssh command
545 bweb => undef, # link to bacula web object (use for display)
548 map { $self->{lc($_)} = $arg{$_} } keys %arg ;
555 status - parse the output of mtx status
559 this function will launch mtx status and parse the output. it will
560 give a perlish view of the autochanger content.
562 it uses ssh if the autochanger is on a other host.
569 my @out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} status` ;
571 # TODO : reset all infos
572 $self->{info}->{drive} = 0;
573 $self->{info}->{slot} = 0;
574 $self->{info}->{io} = 0;
576 #my @out = `cat /home/eric/travail/brestore/plume/mtx` ;
579 # Storage Changer /dev/changer:2 Drives, 45 Slots ( 5 Import/Export )
580 #Data Transfer Element 0:Full (Storage Element 1 Loaded):VolumeTag = 000000
581 #Data Transfer Element 1:Empty
582 # Storage Element 1:Empty
583 # Storage Element 2:Full :VolumeTag=000002
584 # Storage Element 3:Empty
585 # Storage Element 4:Full :VolumeTag=000004
586 # Storage Element 5:Full :VolumeTag=000001
587 # Storage Element 6:Full :VolumeTag=000003
588 # Storage Element 7:Empty
589 # Storage Element 41 IMPORT/EXPORT:Empty
590 # Storage Element 41 IMPORT/EXPORT:Full :VolumeTag=000002
595 # Storage Element 7:Empty
596 # Storage Element 2:Full :VolumeTag=000002
597 if ($l =~ /Storage Element (\d+):(Empty|Full)(\s+:VolumeTag=([\w\d]+))?/){
600 $self->set_empty_slot($1);
602 $self->set_slot($1, $4);
605 } elsif ($l =~ /Data Transfer.+(\d+):(Full|Empty)(\s+.Storage Element (\d+) Loaded.(:VolumeTag = ([\w\d]+))?)?/) {
608 $self->set_empty_drive($1);
610 $self->set_drive($1, $4, $6);
613 } elsif ($l =~ /Storage Element (\d+).+IMPORT\/EXPORT:(Empty|Full)( :VolumeTag=([\d\w]+))?/)
616 $self->set_empty_io($1);
618 $self->set_io($1, $4);
621 # Storage Changer /dev/changer:2 Drives, 30 Slots ( 1 Import/Export )
623 } elsif ($l =~ /Storage Changer .+:(\d+) Drives, (\d+) Slots/) {
624 $self->{info}->{drive} = $1;
625 $self->{info}->{slot} = $2;
626 if ($l =~ /(\d+)\s+Import/) {
627 $self->{info}->{io} = $1 ;
629 $self->{info}->{io} = 0;
634 $self->debug($self) ;
639 my ($self, $slot) = @_;
642 if ($self->{slot}->[$slot] eq 'loaded') {
646 my $label = $self->{slot}->[$slot] ;
648 return $self->is_media_loaded($label);
653 my ($self, $drive, $slot) = @_;
655 return 0 if (not defined $drive or $self->{drive}->[$drive] eq 'empty') ;
656 return 0 if ($self->slot_is_full($slot)) ;
658 my $out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} unload $slot $drive 2>&1`;
661 my $content = $self->get_slot($slot);
662 print "content = $content<br/> $drive => $slot<br/>";
663 $self->set_empty_drive($drive);
664 $self->set_slot($slot, $content);
667 $self->{error} = $out;
672 # TODO: load/unload have to use mtx script from bacula
675 my ($self, $drive, $slot) = @_;
677 return 0 if (not defined $drive or $self->{drive}->[$drive] ne 'empty') ;
678 return 0 unless ($self->slot_is_full($slot)) ;
680 print "Loading drive $drive with slot $slot<br/>\n";
681 my $out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} load $slot $drive 2>&1`;
684 my $content = $self->get_slot($slot);
685 print "content = $content<br/> $slot => $drive<br/>";
686 $self->set_drive($drive, $slot, $content);
689 $self->{error} = $out;
697 my ($self, $media) = @_;
699 unless ($self->{label}->{$media}) {
703 if ($self->{label}->{$media} =~ /drive\d+/) {
713 return (defined $self->{info}->{io} and $self->{info}->{io} > 0);
718 my ($self, $slot, $tag) = @_;
719 $self->{slot}->[$slot] = $tag || 'full';
720 push @{ $self->{io} }, $slot;
723 $self->{label}->{$tag} = $slot;
729 my ($self, $slot) = @_;
731 push @{ $self->{io} }, $slot;
733 unless ($self->{slot}->[$slot]) { # can be loaded (parse before)
734 $self->{slot}->[$slot] = 'empty';
740 my ($self, $slot) = @_;
741 return $self->{slot}->[$slot];
746 my ($self, $slot, $tag) = @_;
747 $self->{slot}->[$slot] = $tag || 'full';
750 $self->{label}->{$tag} = $slot;
756 my ($self, $slot) = @_;
758 unless ($self->{slot}->[$slot]) { # can be loaded (parse before)
759 $self->{slot}->[$slot] = 'empty';
765 my ($self, $drive) = @_;
766 $self->{drive}->[$drive] = 'empty';
771 my ($self, $drive, $slot, $tag) = @_;
772 $self->{drive}->[$drive] = $tag || $slot;
774 $self->{slot}->[$slot] = $tag || 'loaded';
777 $self->{label}->{$tag} = "drive$drive";
783 my ($self, $slot) = @_;
785 # slot don't exists => full
786 if (not defined $self->{slot}->[$slot]) {
790 if ($self->{slot}->[$slot] eq 'empty') {
793 return 1; # vol, full, loaded
796 sub slot_get_first_free
799 for (my $slot=1; $slot < $self->{info}->{slot}; $slot++) {
800 return $slot unless ($self->slot_is_full($slot));
804 sub io_get_first_free
808 foreach my $slot (@{ $self->{io} }) {
809 return $slot unless ($self->slot_is_full($slot));
816 my ($self, $media) = @_;
818 return $self->{label}->{$media} ;
823 my ($self, $media) = @_;
825 return defined $self->{label}->{$media} ;
830 my ($self, $slot) = @_;
832 unless ($self->slot_is_full($slot)) {
833 print "Autochanger $self->{name} slot $slot is empty\n";
838 if ($self->is_slot_loaded($slot)) {
841 print "Autochanger $self->{name} $slot is currently in use\n";
845 # autochanger must have I/O
846 unless ($self->have_io()) {
847 print "Autochanger $self->{name} don't have I/O, you can take media yourself\n";
851 my $dst = $self->io_get_first_free();
854 print "Autochanger $self->{name} you must empty I/O first\n";
857 $self->transfer($slot, $dst);
862 my ($self, $src, $dst) = @_ ;
863 if ($self->{debug}) {
864 print "<pre>$self->{precmd} $self->{mtxcmd} -f $self->{device} transfer $src $dst</pre>\n";
866 my $out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} transfer $src $dst 2>&1`;
869 my $content = $self->get_slot($src);
870 $self->{slot}->[$src] = 'empty';
871 $self->set_slot($dst, $content);
874 $self->{error} = $out;
881 my ($self, $index) = @_;
882 return $self->{drive_name}->[$index];
885 # TODO : do a tapeinfo request to get informations
895 for my $slot (@{$self->{io}})
897 if ($self->is_slot_loaded($slot)) {
898 print "$slot is currently loaded\n";
902 if ($self->slot_is_full($slot))
904 my $free = $self->slot_get_first_free() ;
905 print "move $slot to $free :\n";
908 if ($self->transfer($slot, $free)) {
909 print "<img src='/bweb/T.png' alt='ok'><br/>\n";
911 print "<img src='/bweb/E.png' alt='ok' title='$self->{error}'><br/>\n";
915 $self->{error} = "<img src='/bweb/E.png' alt='ok' title='E : Can t find free slot'><br/>\n";
921 # TODO : this is with mtx status output,
922 # we can do an other function from bacula view (with StorageId)
926 my $bweb = $self->{bweb};
928 # $self->{label} => ('vol1', 'vol2', 'vol3', ..);
929 my $media_list = $bweb->dbh_join( keys %{ $self->{label} });
932 SELECT Media.VolumeName AS volumename,
933 Media.VolStatus AS volstatus,
934 Media.LastWritten AS lastwritten,
935 Media.VolBytes AS volbytes,
936 Media.MediaType AS mediatype,
938 Media.InChanger AS inchanger,
940 $bweb->{sql}->{FROM_UNIXTIME}(
941 $bweb->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
942 + $bweb->{sql}->{TO_SEC}(Media.VolRetention)
945 INNER JOIN Pool USING (PoolId)
947 WHERE Media.VolumeName IN ($media_list)
950 my $all = $bweb->dbh_selectall_hashref($query, 'volumename') ;
952 # TODO : verify slot and bacula slot
956 for (my $slot=1; $slot <= $self->{info}->{slot} ; $slot++) {
958 if ($self->slot_is_full($slot)) {
960 my $vol = $self->{slot}->[$slot];
961 if (defined $all->{$vol}) { # TODO : autochanger without barcodes
963 my $bslot = $all->{$vol}->{slot} ;
964 my $inchanger = $all->{$vol}->{inchanger};
966 # if bacula slot or inchanger flag is bad, we display a message
967 if ($bslot != $slot or !$inchanger) {
968 push @to_update, $slot;
971 $all->{$vol}->{realslot} = $slot;
973 push @{ $param }, $all->{$vol};
975 } else { # empty or no label
976 push @{ $param }, {realslot => $slot,
977 volstatus => 'Unknow',
978 volumename => $self->{slot}->[$slot]} ;
981 push @{ $param }, {realslot => $slot, volumename => 'empty'} ;
985 my $i=0; my $drives = [] ;
986 foreach my $d (@{ $self->{drive} }) {
987 $drives->[$i] = { index => $i,
988 load => $self->{drive}->[$i],
989 name => $self->{drive_name}->[$i],
994 $bweb->display({ Name => $self->{name},
995 nb_drive => $self->{info}->{drive},
996 nb_io => $self->{info}->{io},
999 Update => scalar(@to_update) },
1007 ################################################################
1011 use base q/Bweb::Gui/;
1015 Bweb - main Bweb package
1019 this package is use to compute and display informations
1024 use POSIX qw/strftime/;
1026 our $config_file='/etc/bacula/bweb.conf';
1032 %sql_func - hash to make query mysql/postgresql compliant
1038 UNIX_TIMESTAMP => '',
1039 FROM_UNIXTIME => '',
1040 TO_SEC => " interval '1 second' * ",
1041 SEC_TO_INT => "SEC_TO_INT",
1044 STARTTIME_DAY => " date_trunc('day', Job.StartTime) ",
1045 STARTTIME_HOUR => " date_trunc('hour', Job.StartTime) ",
1046 STARTTIME_MONTH => " date_trunc('month', Job.StartTime) ",
1047 STARTTIME_PHOUR=> " date_part('hour', Job.StartTime) ",
1048 STARTTIME_PDAY => " date_part('day', Job.StartTime) ",
1049 STARTTIME_PMONTH => " date_part('month', Job.StartTime) ",
1052 UNIX_TIMESTAMP => 'UNIX_TIMESTAMP',
1053 FROM_UNIXTIME => 'FROM_UNIXTIME',
1056 SEC_TO_TIME => 'SEC_TO_TIME',
1057 MATCH => " REGEXP ",
1058 STARTTIME_DAY => " DATE_FORMAT(StartTime, '%Y-%m-%d') ",
1059 STARTTIME_HOUR => " DATE_FORMAT(StartTime, '%Y-%m-%d %H') ",
1060 STARTTIME_MONTH => " DATE_FORMAT(StartTime, '%Y-%m') ",
1061 STARTTIME_PHOUR=> " DATE_FORMAT(StartTime, '%H') ",
1062 STARTTIME_PDAY => " DATE_FORMAT(StartTime, '%d') ",
1063 STARTTIME_PMONTH => " DATE_FORMAT(StartTime, '%m') ",
1067 sub dbh_selectall_arrayref
1069 my ($self, $query) = @_;
1070 $self->connect_db();
1071 $self->debug($query);
1072 return $self->{dbh}->selectall_arrayref($query);
1077 my ($self, @what) = @_;
1078 return join(',', $self->dbh_quote(@what)) ;
1083 my ($self, @what) = @_;
1085 $self->connect_db();
1087 return map { $self->{dbh}->quote($_) } @what;
1089 return $self->{dbh}->quote($what[0]) ;
1095 my ($self, $query) = @_ ;
1096 $self->connect_db();
1097 $self->debug($query);
1098 return $self->{dbh}->do($query);
1101 sub dbh_selectall_hashref
1103 my ($self, $query, $join) = @_;
1105 $self->connect_db();
1106 $self->debug($query);
1107 return $self->{dbh}->selectall_hashref($query, $join) ;
1110 sub dbh_selectrow_hashref
1112 my ($self, $query) = @_;
1114 $self->connect_db();
1115 $self->debug($query);
1116 return $self->{dbh}->selectrow_hashref($query) ;
1122 my @unit = qw(b Kb Mb Gb Tb);
1123 my $val = shift || 0;
1125 my $format = '%i %s';
1126 while ($val / 1024 > 1) {
1130 $format = ($i>0)?'%0.1f %s':'%i %s';
1131 return sprintf($format, $val, $unit[$i]);
1134 # display Day, Hour, Year
1140 $val /= 60; # sec -> min
1142 if ($val / 60 <= 1) {
1146 $val /= 60; # min -> hour
1147 if ($val / 24 <= 1) {
1148 return "$val hours";
1151 $val /= 24; # hour -> day
1152 if ($val / 365 < 2) {
1156 $val /= 365 ; # day -> year
1158 return "$val years";
1161 # get Day, Hour, Year
1167 unless ($val =~ /^\s*(\d+)\s*(\w)\w*\s*$/) {
1171 my %times = ( m => 60,
1177 my $mult = $times{$2} || 0;
1187 unless ($self->{dbh}) {
1188 $self->{dbh} = DBI->connect($self->{info}->{dbi},
1189 $self->{info}->{user},
1190 $self->{info}->{password});
1192 $self->error("Can't connect to your database:\n$DBI::errstr\n")
1193 unless ($self->{dbh});
1195 $self->{dbh}->{FetchHashKeyName} = 'NAME_lc';
1197 if ($self->{info}->{dbi} =~ /^dbi:Pg/i) {
1198 $self->{dbh}->do("SET datestyle TO 'ISO, YMD'");
1205 my ($class, %arg) = @_;
1207 dbh => undef, # connect_db();
1209 dbi => '', # DBI:Pg:database=bacula;host=127.0.0.1
1215 map { $self->{lc($_)} = $arg{$_} } keys %arg ;
1217 if ($self->{info}->{dbi} =~ /DBI:(\w+):/i) {
1218 $self->{sql} = $sql_func{$1};
1221 $self->{debug} = $self->{info}->{debug};
1222 $Bweb::Gui::template_dir = $self->{info}->{template_dir};
1230 $self->display($self->{info}, "begin.tpl");
1236 $self->display($self->{info}, "end.tpl");
1244 my $arg = $self->get_form("client", "qre_client");
1246 if ($arg->{qre_client}) {
1247 $where = "WHERE Name $self->{sql}->{MATCH} $arg->{qre_client} ";
1248 } elsif ($arg->{client}) {
1249 $where = "WHERE Name = '$arg->{client}' ";
1253 SELECT Name AS name,
1255 AutoPrune AS autoprune,
1256 FileRetention AS fileretention,
1257 JobRetention AS jobretention
1262 my $all = $self->dbh_selectall_hashref($query, 'name') ;
1264 my $dsp = { ID => $cur_id++,
1265 clients => [ values %$all] };
1267 $self->display($dsp, "client_list.tpl") ;
1272 my ($self, %arg) = @_;
1279 "AND $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
1281 ( $self->{sql}->{UNIX_TIMESTAMP}(NOW())
1283 $self->{sql}->{TO_SEC}($arg{age})
1286 $label = "last " . human_sec($arg{age});
1289 if ($arg{groupby}) {
1290 $limit .= " GROUP BY $arg{groupby} ";
1294 $limit .= " ORDER BY $arg{order} ";
1298 $limit .= " LIMIT $arg{limit} ";
1299 $label .= " limited to $arg{limit}";
1303 $limit .= " OFFSET $arg{offset} ";
1304 $label .= " with $arg{offset} offset ";
1308 $label = 'no filter';
1311 return ($limit, $label);
1316 $bweb->get_form(...) - Get useful stuff
1320 This function get and check parameters against regexp.
1322 If word begin with 'q', the return will be quoted or join quoted
1323 if it's end with 's'.
1328 $bweb->get_form('jobid', 'qclient', 'qpools') ;
1331 qclient => 'plume-fd',
1332 qpools => "'plume-fd', 'test-fd', '...'",
1339 my ($self, @what) = @_;
1340 my %what = map { $_ => 1 } @what;
1360 my %opt_ss =( # string with space
1364 my %opt_s = ( # default to ''
1381 my %opt_p = ( # option with path
1389 my %opt_d = ( # option with date
1394 foreach my $i (@what) {
1395 if (exists $opt_i{$i}) {# integer param
1396 my $value = CGI::param($i) || $opt_i{$i} ;
1397 if ($value =~ /^(\d+)$/) {
1400 } elsif ($opt_s{$i}) { # simple string param
1401 my $value = CGI::param($i) || '';
1402 if ($value =~ /^([\w\d\.-]+)$/) {
1405 } elsif ($opt_ss{$i}) { # simple string param (with space)
1406 my $value = CGI::param($i) || '';
1407 if ($value =~ /^([\w\d\.\-\s]+)$/) {
1410 } elsif ($i =~ /^j(\w+)s$/) { # quote join args
1411 my @value = grep { ! /^\s*$/ } CGI::param($1) ;
1413 $ret{$i} = $self->dbh_join(@value) ;
1416 } elsif ($i =~ /^q(\w+[^s])$/) { # 'arg1'
1417 my $value = CGI::param($1) ;
1419 $ret{$i} = $self->dbh_quote($value);
1422 } elsif ($i =~ /^q(\w+)s$/) { #[ 'arg1', 'arg2']
1423 $ret{$i} = [ map { { name => $self->dbh_quote($_) } }
1424 grep { ! /^\s*$/ } CGI::param($1) ];
1425 } elsif (exists $opt_p{$i}) {
1426 my $value = CGI::param($i) || '';
1427 if ($value =~ /^([\w\d\.\/\s:\@\-]+)$/) {
1430 } elsif (exists $opt_d{$i}) {
1431 my $value = CGI::param($i) || '';
1432 if ($value =~ /^\s*(\d+\s+\w+)$/) {
1439 foreach my $s (CGI::param('slot')) {
1440 if ($s =~ /^(\d+)$/) {
1441 push @{$ret{slots}}, $s;
1447 my $when = CGI::param('when') || '';
1448 if ($when =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})$/) {
1453 if ($what{db_clients}) {
1455 SELECT Client.Name as clientname
1459 my $clients = $self->dbh_selectall_hashref($query, 'clientname');
1460 $ret{db_clients} = [sort {$a->{clientname} cmp $b->{clientname} }
1464 if ($what{db_mediatypes}) {
1466 SELECT MediaType as mediatype
1470 my $medias = $self->dbh_selectall_hashref($query, 'mediatype');
1471 $ret{db_mediatypes} = [sort {$a->{mediatype} cmp $b->{mediatype} }
1475 if ($what{db_locations}) {
1477 SELECT Location as location, Cost as cost FROM Location
1479 my $loc = $self->dbh_selectall_hashref($query, 'location');
1480 $ret{db_locations} = [ sort { $a->{location}
1486 if ($what{db_pools}) {
1487 my $query = "SELECT Name as name FROM Pool";
1489 my $all = $self->dbh_selectall_hashref($query, 'name') ;
1490 $ret{db_pools} = [ sort { $a->{name} cmp $b->{name} } values %$all ];
1493 if ($what{db_filesets}) {
1495 SELECT FileSet.FileSet AS fileset
1499 my $filesets = $self->dbh_selectall_hashref($query, 'fileset');
1501 $ret{db_filesets} = [sort {lc($a->{fileset}) cmp lc($b->{fileset}) }
1502 values %$filesets] ;
1505 if ($what{db_jobnames}) {
1507 SELECT DISTINCT Job.Name AS jobname
1511 my $jobnames = $self->dbh_selectall_hashref($query, 'jobname');
1513 $ret{db_jobnames} = [sort {lc($a->{jobname}) cmp lc($b->{jobname}) }
1514 values %$jobnames] ;
1517 if ($what{db_devices}) {
1519 SELECT Device.Name AS name
1523 my $devices = $self->dbh_selectall_hashref($query, 'name');
1525 $ret{db_devices} = [sort {lc($a->{name}) cmp lc($b->{name}) }
1536 my $fields = $self->get_form(qw/age level status clients filesets
1538 db_clients limit db_filesets width height
1539 qclients qfilesets qjobnames db_jobnames/);
1542 my $url = CGI::url(-full => 0,
1545 $url =~ s/^.+?\?//; # http://path/to/bweb.pl?arg => arg
1547 # this organisation is to keep user choice between 2 click
1548 # TODO : fileset and client selection doesn't work
1557 sub display_client_job
1559 my ($self, %arg) = @_ ;
1561 $arg{order} = ' Job.JobId DESC ';
1562 my ($limit, $label) = $self->get_limit(%arg);
1564 my $clientname = $self->dbh_quote($arg{clientname});
1567 SELECT DISTINCT Job.JobId AS jobid,
1568 Job.Name AS jobname,
1569 FileSet.FileSet AS fileset,
1571 StartTime AS starttime,
1572 JobFiles AS jobfiles,
1573 JobBytes AS jobbytes,
1574 JobStatus AS jobstatus,
1575 JobErrors AS joberrors
1577 FROM Client,Job,FileSet
1578 WHERE Client.Name=$clientname
1579 AND Client.ClientId=Job.ClientId
1580 AND Job.FileSetId=FileSet.FileSetId
1584 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
1586 $self->display({ clientname => $arg{clientname},
1589 Jobs => [ values %$all ],
1591 "display_client_job.tpl") ;
1594 sub get_selected_media_location
1598 my $medias = $self->get_form('jmedias');
1600 unless ($medias->{jmedias}) {
1605 SELECT Media.VolumeName AS volumename, Location.Location AS location
1606 FROM Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
1607 WHERE Media.VolumeName IN ($medias->{jmedias})
1610 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
1612 # { 'vol1' => { [volumename => 'vol1', location => 'ici'],
1623 my $medias = $self->get_selected_media_location();
1629 my $elt = $self->get_form('db_locations');
1631 $self->display({ ID => $cur_id++,
1632 %$elt, # db_locations
1634 sort { $a->{volumename} cmp $b->{volumename} } values %$medias
1644 my $elt = $self->get_form(qw/db_pools db_mediatypes db_locations/) ;
1646 $self->display($elt, "help_extern.tpl");
1649 sub help_extern_compute
1653 my $number = CGI::param('limit') || '' ;
1654 unless ($number =~ /^(\d+)$/) {
1655 return $self->error("Bad arg number : $number ");
1658 my ($sql, undef) = $self->get_param('pools',
1659 'locations', 'mediatypes');
1662 SELECT Media.VolumeName AS volumename,
1663 Media.VolStatus AS volstatus,
1664 Media.LastWritten AS lastwritten,
1665 Media.MediaType AS mediatype,
1666 Media.VolMounts AS volmounts,
1668 Media.Recycle AS recycle,
1669 $self->{sql}->{FROM_UNIXTIME}(
1670 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
1671 + $self->{sql}->{TO_SEC}(Media.VolRetention)
1674 INNER JOIN Pool ON (Pool.PoolId = Media.PoolId)
1675 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
1677 WHERE Media.InChanger = 1
1678 AND Media.VolStatus IN ('Disabled', 'Error', 'Full')
1680 ORDER BY expire DESC, recycle, Media.VolMounts DESC
1684 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
1686 $self->display({ Medias => [ values %$all ] },
1687 "help_extern_compute.tpl");
1694 my $param = $self->get_form(qw/db_locations db_pools db_mediatypes/) ;
1695 $self->display($param, "help_intern.tpl");
1698 sub help_intern_compute
1702 my $number = CGI::param('limit') || '' ;
1703 unless ($number =~ /^(\d+)$/) {
1704 return $self->error("Bad arg number : $number ");
1707 my ($sql, undef) = $self->get_param('pools', 'locations', 'mediatypes');
1709 if (CGI::param('expired')) {
1711 AND ( $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
1712 + $self->{sql}->{TO_SEC}(Media.VolRetention)
1718 SELECT Media.VolumeName AS volumename,
1719 Media.VolStatus AS volstatus,
1720 Media.LastWritten AS lastwritten,
1721 Media.MediaType AS mediatype,
1722 Media.VolMounts AS volmounts,
1724 $self->{sql}->{FROM_UNIXTIME}(
1725 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
1726 + $self->{sql}->{TO_SEC}(Media.VolRetention)
1729 INNER JOIN Pool ON (Pool.PoolId = Media.PoolId)
1730 LEFT JOIN Location ON (Location.LocationId = Media.LocationId)
1732 WHERE Media.InChanger <> 1
1733 AND Media.VolStatus IN ('Purged', 'Full', 'Append')
1734 AND Media.Recycle = 1
1736 ORDER BY Media.VolUseDuration DESC, Media.VolMounts ASC, expire ASC
1740 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
1742 $self->display({ Medias => [ values %$all ] },
1743 "help_intern_compute.tpl");
1749 my ($self, %arg) = @_ ;
1751 my ($limit, $label) = $self->get_limit(%arg);
1755 (SELECT count(Pool.PoolId) FROM Pool) AS nb_pool,
1756 (SELECT count(Media.MediaId) FROM Media) AS nb_media,
1757 (SELECT count(Job.JobId) FROM Job) AS nb_job,
1758 (SELECT sum(VolBytes) FROM Media) AS nb_bytes,
1759 (SELECT count(Job.JobId)
1761 WHERE Job.JobStatus IN ('E','e','f','A')
1764 (SELECT count(Client.ClientId) FROM Client) AS nb_client
1767 my $row = $self->dbh_selectrow_hashref($query) ;
1769 $row->{nb_bytes} = human_size($row->{nb_bytes});
1771 $row->{db_size} = '???';
1772 $row->{label} = $label;
1774 $self->display($row, "general.tpl");
1779 my ($self, @what) = @_ ;
1780 my %elt = map { $_ => 1 } @what;
1785 if ($elt{clients}) {
1786 my @clients = grep { ! /^\s*$/ } CGI::param('client');
1788 $ret{clients} = \@clients;
1789 my $str = $self->dbh_join(@clients);
1790 $limit .= "AND Client.Name IN ($str) ";
1794 if ($elt{filesets}) {
1795 my @filesets = grep { ! /^\s*$/ } CGI::param('fileset');
1797 $ret{filesets} = \@filesets;
1798 my $str = $self->dbh_join(@filesets);
1799 $limit .= "AND FileSet.FileSet IN ($str) ";
1803 if ($elt{mediatypes}) {
1804 my @medias = grep { ! /^\s*$/ } CGI::param('mediatype');
1806 $ret{mediatypes} = \@medias;
1807 my $str = $self->dbh_join(@medias);
1808 $limit .= "AND Media.MediaType IN ($str) ";
1813 my $client = CGI::param('client');
1814 $ret{client} = $client;
1815 $client = $self->dbh_join($client);
1816 $limit .= "AND Client.Name = $client ";
1820 my $level = CGI::param('level') || '';
1821 if ($level =~ /^(\w)$/) {
1823 $limit .= "AND Job.Level = '$1' ";
1828 my $jobid = CGI::param('jobid') || '';
1830 if ($jobid =~ /^(\d+)$/) {
1832 $limit .= "AND Job.JobId = '$1' ";
1837 my $status = CGI::param('status') || '';
1838 if ($status =~ /^(\w)$/) {
1841 $limit .= "AND Job.JobStatus IN ('f','E') ";
1843 $limit .= "AND Job.JobStatus = '$1' ";
1848 if ($elt{volstatus}) {
1849 my $status = CGI::param('volstatus') || '';
1850 if ($status =~ /^(\w+)$/) {
1852 $limit .= "AND Media.VolStatus = '$1' ";
1856 if ($elt{locations}) {
1857 my @location = grep { ! /^\s*$/ } CGI::param('location') ;
1859 $ret{locations} = \@location;
1860 my $str = $self->dbh_join(@location);
1861 $limit .= "AND Location.Location IN ($str) ";
1866 my @pool = grep { ! /^\s*$/ } CGI::param('pool') ;
1868 $ret{pools} = \@pool;
1869 my $str = $self->dbh_join(@pool);
1870 $limit .= "AND Pool.Name IN ($str) ";
1874 if ($elt{location}) {
1875 my $location = CGI::param('location') || '';
1877 $ret{location} = $location;
1878 $location = $self->dbh_quote($location);
1879 $limit .= "AND Location.Location = $location ";
1884 my $pool = CGI::param('pool') || '';
1887 $pool = $self->dbh_quote($pool);
1888 $limit .= "AND Pool.Name = $pool ";
1892 if ($elt{jobtype}) {
1893 my $jobtype = CGI::param('jobtype') || '';
1894 if ($jobtype =~ /^(\w)$/) {
1896 $limit .= "AND Job.Type = '$1' ";
1900 return ($limit, %ret);
1911 my ($self, %arg) = @_ ;
1913 $arg{order} = ' Job.JobId DESC ';
1915 my ($limit, $label) = $self->get_limit(%arg);
1916 my ($where, undef) = $self->get_param('clients',
1925 SELECT Job.JobId AS jobid,
1926 Client.Name AS client,
1927 FileSet.FileSet AS fileset,
1928 Job.Name AS jobname,
1930 StartTime AS starttime,
1931 Pool.Name AS poolname,
1932 JobFiles AS jobfiles,
1933 JobBytes AS jobbytes,
1934 JobStatus AS jobstatus,
1935 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
1936 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime))
1939 JobErrors AS joberrors
1942 Job LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId)
1943 LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId)
1944 WHERE Client.ClientId=Job.ClientId
1945 AND Job.JobStatus != 'R'
1950 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
1952 $self->display({ Filter => $label,
1956 sort { $a->{jobid} <=> $b->{jobid} }
1963 # display job informations
1964 sub display_job_zoom
1966 my ($self, $jobid) = @_ ;
1968 $jobid = $self->dbh_quote($jobid);
1971 SELECT DISTINCT Job.JobId AS jobid,
1972 Client.Name AS client,
1973 Job.Name AS jobname,
1974 FileSet.FileSet AS fileset,
1976 Pool.Name AS poolname,
1977 StartTime AS starttime,
1978 JobFiles AS jobfiles,
1979 JobBytes AS jobbytes,
1980 JobStatus AS jobstatus,
1981 JobErrors AS joberrors,
1982 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
1983 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime)) AS duration
1986 Job LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId)
1987 LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId)
1988 WHERE Client.ClientId=Job.ClientId
1989 AND Job.JobId = $jobid
1992 my $row = $self->dbh_selectrow_hashref($query) ;
1994 # display all volumes associate with this job
1996 SELECT Media.VolumeName as volumename
1997 FROM Job,Media,JobMedia
1998 WHERE Job.JobId = $jobid
1999 AND JobMedia.JobId=Job.JobId
2000 AND JobMedia.MediaId=Media.MediaId
2003 my $all = $self->dbh_selectall_hashref($query, 'volumename');
2005 $row->{volumes} = [ values %$all ] ;
2007 $self->display($row, "display_job_zoom.tpl");
2014 my ($where, %elt) = $self->get_param('pools',
2019 my $arg = $self->get_form('jmedias', 'qre_media');
2021 if ($arg->{jmedias}) {
2022 $where = "AND Media.VolumeName IN ($arg->{jmedias}) $where";
2024 if ($arg->{qre_media}) {
2025 $where = "AND Media.VolumeName $self->{sql}->{MATCH} $arg->{qre_media} $where";
2029 SELECT Media.VolumeName AS volumename,
2030 Media.VolBytes AS volbytes,
2031 Media.VolStatus AS volstatus,
2032 Media.MediaType AS mediatype,
2033 Media.InChanger AS online,
2034 Media.LastWritten AS lastwritten,
2035 Location.Location AS location,
2036 (volbytes*100/COALESCE(media_avg_size.size,-1)) AS volusage,
2037 Pool.Name AS poolname,
2038 $self->{sql}->{FROM_UNIXTIME}(
2039 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2040 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2043 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2044 LEFT JOIN (SELECT avg(Media.VolBytes) AS size,
2045 Media.MediaType AS MediaType
2047 WHERE Media.VolStatus = 'Full'
2048 GROUP BY Media.MediaType
2049 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
2051 WHERE Media.PoolId=Pool.PoolId
2055 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2057 $self->display({ ID => $cur_id++,
2059 Location => $elt{location},
2060 Medias => [ values %$all ]
2062 "display_media.tpl");
2069 my $pool = $self->get_form('db_pools');
2071 foreach my $name (@{ $pool->{db_pools} }) {
2072 CGI::param('pool', $name->{name});
2073 $self->display_media();
2077 sub display_media_zoom
2081 my $medias = $self->get_form('jmedias');
2083 unless ($medias->{jmedias}) {
2084 return $self->error("Can't get media selection");
2088 SELECT InChanger AS online,
2089 VolBytes AS nb_bytes,
2090 VolumeName AS volumename,
2091 VolStatus AS volstatus,
2092 VolMounts AS nb_mounts,
2093 Media.VolUseDuration AS voluseduration,
2094 Media.MaxVolJobs AS maxvoljobs,
2095 Media.MaxVolFiles AS maxvolfiles,
2096 Media.MaxVolBytes AS maxvolbytes,
2097 VolErrors AS nb_errors,
2098 Pool.Name AS poolname,
2099 Location.Location AS location,
2100 Media.Recycle AS recycle,
2101 Media.VolRetention AS volretention,
2102 Media.LastWritten AS lastwritten,
2103 Media.VolReadTime/1000000 AS volreadtime,
2104 Media.VolWriteTime/1000000 AS volwritetime,
2105 Media.RecycleCount AS recyclecount,
2106 Media.Comment AS comment,
2107 $self->{sql}->{FROM_UNIXTIME}(
2108 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2109 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2112 Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2113 WHERE Pool.PoolId = Media.PoolId
2114 AND VolumeName IN ($medias->{jmedias})
2117 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2119 foreach my $media (values %$all) {
2120 my $mq = $self->dbh_quote($media->{volumename});
2123 SELECT DISTINCT Job.JobId AS jobid,
2125 Job.StartTime AS starttime,
2128 Job.JobFiles AS files,
2129 Job.JobBytes AS bytes,
2130 Job.jobstatus AS status
2131 FROM Media,JobMedia,Job
2132 WHERE Media.VolumeName=$mq
2133 AND Media.MediaId=JobMedia.MediaId
2134 AND JobMedia.JobId=Job.JobId
2137 my $jobs = $self->dbh_selectall_hashref($query, 'jobid') ;
2140 SELECT LocationLog.Date AS date,
2141 Location.Location AS location,
2142 LocationLog.Comment AS comment
2143 FROM Media,LocationLog INNER JOIN Location ON (LocationLog.LocationId = Location.LocationId)
2144 WHERE Media.MediaId = LocationLog.MediaId
2145 AND Media.VolumeName = $mq
2149 my $log = $self->dbh_selectall_arrayref($query) ;
2151 $logtxt = join("\n", map { ($_->[0] . ' ' . $_->[1] . ' ' . $_->[2])} @$log ) ;
2154 $self->display({ jobs => [ values %$jobs ],
2155 LocationLog => $logtxt,
2157 "display_media_zoom.tpl");
2165 my $loc = $self->get_form('qlocation');
2166 unless ($loc->{qlocation}) {
2167 return $self->error("Can't get location");
2171 SELECT Location.Location AS location,
2172 Location.Cost AS cost,
2173 Location.Enabled AS enabled
2175 WHERE Location.Location = $loc->{qlocation}
2178 my $row = $self->dbh_selectrow_hashref($query);
2180 $self->display({ ID => $cur_id++,
2181 %$row }, "location_edit.tpl") ;
2189 my $arg = $self->get_form(qw/qlocation qnewlocation cost/) ;
2190 unless ($arg->{qlocation}) {
2191 return $self->error("Can't get location");
2193 unless ($arg->{qnewlocation}) {
2194 return $self->error("Can't get new location name");
2196 unless ($arg->{cost}) {
2197 return $self->error("Can't get new cost");
2200 my $enabled = CGI::param('enabled') || '';
2201 $enabled = $enabled?1:0;
2204 UPDATE Location SET Cost = $arg->{cost},
2205 Location = $arg->{qnewlocation},
2207 WHERE Location.Location = $arg->{qlocation}
2210 $self->dbh_do($query);
2212 $self->display_location();
2218 my $arg = $self->get_form(qw/qlocation/) ;
2220 unless ($arg->{qlocation}) {
2221 return $self->error("Can't get location");
2225 SELECT count(Media.MediaId) AS nb
2226 FROM Media INNER JOIN Location USING (LocationID)
2227 WHERE Location = $arg->{qlocation}
2230 my $res = $self->dbh_selectrow_hashref($query);
2233 return $self->error("Sorry, the location must be empty");
2237 DELETE FROM Location WHERE Location = $arg->{qlocation} LIMIT 1
2240 $self->dbh_do($query);
2242 $self->display_location();
2249 my $arg = $self->get_form(qw/qlocation cost/) ;
2251 unless ($arg->{qlocation}) {
2252 $self->display({}, "location_add.tpl");
2255 unless ($arg->{cost}) {
2256 return $self->error("Can't get new cost");
2259 my $enabled = CGI::param('enabled') || '';
2260 $enabled = $enabled?1:0;
2263 INSERT INTO Location (Location, Cost, Enabled)
2264 VALUES ($arg->{qlocation}, $arg->{cost}, $enabled)
2267 $self->dbh_do($query);
2269 $self->display_location();
2272 sub display_location
2277 SELECT Location.Location AS location,
2278 Location.Cost AS cost,
2279 Location.Enabled AS enabled,
2280 (SELECT count(Media.MediaId)
2282 WHERE Media.LocationId = Location.LocationId
2287 my $location = $self->dbh_selectall_hashref($query, 'location');
2289 $self->display({ ID => $cur_id++,
2290 Locations => [ values %$location ] },
2291 "display_location.tpl");
2298 my $medias = $self->get_selected_media_location();
2303 my $arg = $self->get_form('db_locations', 'qnewlocation');
2305 $self->display({ email => $self->{info}->{email_media},
2307 medias => [ values %$medias ],
2309 "update_location.tpl");
2312 sub get_media_max_size
2314 my ($self, $type) = @_;
2316 "SELECT avg(VolBytes) AS size
2318 WHERE Media.VolStatus = 'Full'
2319 AND Media.MediaType = '$type'
2322 my $res = $self->selectrow_hashref($query);
2325 return $res->{size};
2335 my $media = $self->get_form('qmedia');
2337 unless ($media->{qmedia}) {
2338 return $self->error("Can't get media");
2342 SELECT Media.Slot AS slot,
2343 PoolMedia.Name AS poolname,
2344 Media.VolStatus AS volstatus,
2345 Media.InChanger AS inchanger,
2346 Location.Location AS location,
2347 Media.VolumeName AS volumename,
2348 Media.MaxVolBytes AS maxvolbytes,
2349 Media.MaxVolJobs AS maxvoljobs,
2350 Media.MaxVolFiles AS maxvolfiles,
2351 Media.VolUseDuration AS voluseduration,
2352 Media.VolRetention AS volretention,
2353 Media.Comment AS comment,
2354 PoolRecycle.Name AS poolrecycle
2356 FROM Media INNER JOIN Pool AS PoolMedia ON (Media.PoolId = PoolMedia.PoolId)
2357 LEFT JOIN Pool AS PoolRecycle ON (Media.RecyclePoolId = PoolRecycle.PoolId)
2358 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2360 WHERE Media.VolumeName = $media->{qmedia}
2363 my $row = $self->dbh_selectrow_hashref($query);
2364 $row->{volretention} = human_sec($row->{volretention});
2365 $row->{voluseduration} = human_sec($row->{voluseduration});
2367 my $elt = $self->get_form(qw/db_pools db_locations/);
2372 }, "update_media.tpl");
2379 my $arg = $self->get_form('jmedias', 'qnewlocation') ;
2381 unless ($arg->{jmedias}) {
2382 return $self->error("Can't get selected media");
2385 unless ($arg->{qnewlocation}) {
2386 return $self->error("Can't get new location");
2391 SET LocationId = (SELECT LocationId
2393 WHERE Location = $arg->{qnewlocation})
2394 WHERE Media.VolumeName IN ($arg->{jmedias})
2397 my $nb = $self->dbh_do($query);
2399 print "$nb media updated, you may have to update your autochanger.";
2401 $self->display_media();
2408 my $medias = $self->get_selected_media_location();
2410 return $self->error("Can't get media selection");
2412 my $newloc = CGI::param('newlocation');
2414 my $user = CGI::param('user') || 'unknow';
2415 my $comm = CGI::param('comment') || '';
2416 $comm = $self->dbh_quote("$user: $comm");
2420 foreach my $media (keys %$medias) {
2422 INSERT LocationLog (Date, Comment, MediaId, LocationId, NewVolStatus)
2424 NOW(), $comm, (SELECT MediaId FROM Media WHERE VolumeName = '$media'),
2425 (SELECT LocationId FROM Location WHERE Location = '$medias->{$media}->{location}'),
2426 (SELECT VolStatus FROM Media WHERE VolumeName = '$media')
2429 $self->dbh_do($query);
2430 $self->debug($query);
2434 $q->param('action', 'update_location');
2435 my $url = $q->url(-full => 1, -query=>1);
2437 $self->display({ email => $self->{info}->{email_media},
2439 newlocation => $newloc,
2440 # [ { volumename => 'vol1' }, { volumename => 'vol2'
\81 },..]
2441 medias => [ values %$medias ],
2443 "change_location.tpl");
2447 sub display_client_stats
2449 my ($self, %arg) = @_ ;
2451 my $client = $self->dbh_quote($arg{clientname});
2452 my ($limit, $label) = $self->get_limit(%arg);
2456 count(Job.JobId) AS nb_jobs,
2457 sum(Job.JobBytes) AS nb_bytes,
2458 sum(Job.JobErrors) AS nb_err,
2459 sum(Job.JobFiles) AS nb_files,
2460 Client.Name AS clientname
2461 FROM Job INNER JOIN Client USING (ClientId)
2463 Client.Name = $client
2465 GROUP BY Client.Name
2468 my $row = $self->dbh_selectrow_hashref($query);
2470 $row->{ID} = $cur_id++;
2471 $row->{label} = $label;
2473 $self->display($row, "display_client_stats.tpl");
2476 # poolname can be undef
2479 my ($self, $poolname) = @_ ;
2483 my $arg = $self->get_form('jmediatypes', 'qmediatypes');
2484 if ($arg->{jmediatypes}) {
2485 $whereW = "WHERE MediaType IN ($arg->{jmediatypes}) ";
2486 $whereA = "AND MediaType IN ($arg->{jmediatypes}) ";
2489 # TODO : afficher les tailles et les dates
2492 SELECT subq.volmax AS volmax,
2493 subq.volnum AS volnum,
2494 subq.voltotal AS voltotal,
2496 Pool.Recycle AS recycle,
2497 Pool.VolRetention AS volretention,
2498 Pool.VolUseDuration AS voluseduration,
2499 Pool.MaxVolJobs AS maxvoljobs,
2500 Pool.MaxVolFiles AS maxvolfiles,
2501 Pool.MaxVolBytes AS maxvolbytes,
2502 subq.PoolId AS PoolId
2505 SELECT COALESCE(media_avg_size.volavg,0) * count(Media.MediaId) AS volmax,
2506 count(Media.MediaId) AS volnum,
2507 sum(Media.VolBytes) AS voltotal,
2508 Media.PoolId AS PoolId,
2509 Media.MediaType AS MediaType
2511 LEFT JOIN (SELECT avg(Media.VolBytes) AS volavg,
2512 Media.MediaType AS MediaType
2514 WHERE Media.VolStatus = 'Full'
2515 GROUP BY Media.MediaType
2516 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
2517 GROUP BY Media.MediaType, Media.PoolId, media_avg_size.volavg
2519 LEFT JOIN Pool ON (Pool.PoolId = subq.PoolId)
2523 my $all = $self->dbh_selectall_hashref($query, 'name') ;
2526 SELECT Pool.Name AS name,
2527 sum(VolBytes) AS size
2528 FROM Media JOIN Pool ON (Media.PoolId = Pool.PoolId)
2529 WHERE Media.VolStatus IN ('Recycled', 'Purged')
2533 my $empty = $self->dbh_selectall_hashref($query, 'name');
2535 foreach my $p (values %$all) {
2536 if ($p->{volmax} > 0) { # mysql returns 0.0000
2537 # we remove Recycled/Purged media from pool usage
2538 if (defined $empty->{$p->{name}}) {
2539 $p->{voltotal} -= $empty->{$p->{name}}->{size};
2541 $p->{poolusage} = sprintf('%.2f', $p->{voltotal} * 100/ $p->{volmax}) ;
2543 $p->{poolusage} = 0;
2547 SELECT VolStatus AS volstatus, count(MediaId) AS nb
2549 WHERE PoolId=$p->{poolid}
2553 my $content = $self->dbh_selectall_hashref($query, 'volstatus');
2554 foreach my $t (values %$content) {
2555 $p->{"nb_" . $t->{volstatus}} = $t->{nb} ;
2560 $self->display({ ID => $cur_id++,
2561 MediaType => $arg->{qmediatypes}, # [ { name => type1 } , { name => type2 } ]
2562 Pools => [ values %$all ]},
2563 "display_pool.tpl");
2566 sub display_running_job
2570 my $arg = $self->get_form('client', 'jobid');
2572 if (!$arg->{client} and $arg->{jobid}) {
2575 SELECT Client.Name AS name
2576 FROM Job INNER JOIN Client USING (ClientId)
2577 WHERE Job.JobId = $arg->{jobid}
2580 my $row = $self->dbh_selectrow_hashref($query);
2583 $arg->{client} = $row->{name};
2584 CGI::param('client', $arg->{client});
2588 if ($arg->{client}) {
2589 my $cli = new Bweb::Client(name => $arg->{client});
2590 $cli->display_running_job($self->{info}, $arg->{jobid});
2591 if ($arg->{jobid}) {
2592 $self->get_job_log();
2595 $self->error("Can't get client or jobid");
2599 sub display_running_jobs
2601 my ($self, $display_action) = @_;
2604 SELECT Job.JobId AS jobid,
2605 Job.Name AS jobname,
2607 Job.StartTime AS starttime,
2608 Job.JobFiles AS jobfiles,
2609 Job.JobBytes AS jobbytes,
2610 Job.JobStatus AS jobstatus,
2611 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(NOW())
2612 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime))
2614 Client.Name AS clientname
2615 FROM Job INNER JOIN Client USING (ClientId)
2616 WHERE JobStatus IN ('C','R','B','e','D','F','S','m','M','s','j','c','d','t','p')
2618 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
2620 $self->display({ ID => $cur_id++,
2621 display_action => $display_action,
2622 Jobs => [ values %$all ]},
2623 "running_job.tpl") ;
2626 # return the autochanger list to update
2631 my $arg = $self->get_form('jmedias');
2633 unless ($arg->{jmedias}) {
2634 return $self->error("Can't get media selection");
2638 SELECT Media.VolumeName AS volumename,
2639 Storage.Name AS storage,
2640 Location.Location AS location,
2642 FROM Media INNER JOIN Storage ON (Media.StorageId = Storage.StorageId)
2643 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2644 WHERE Media.VolumeName IN ($arg->{jmedias})
2645 AND Media.InChanger = 1
2648 my $all = $self->dbh_selectall_hashref($query, 'volumename');
2650 foreach my $vol (values %$all) {
2651 my $a = $self->ach_get($vol->{location});
2653 $ret{$vol->{location}} = 1;
2655 unless ($a->{have_status}) {
2657 $a->{have_status} = 1;
2660 print "eject $vol->{volumename} from $vol->{storage} : ";
2661 if ($a->send_to_io($vol->{slot})) {
2662 print "<img src='/bweb/T.png' alt='ok'><br/>";
2664 print "<img src='/bweb/E.png' alt='err'><br/>";
2674 my ($to, $subject, $content) = (CGI::param('email'),
2675 CGI::param('subject'),
2676 CGI::param('content'));
2677 $to =~ s/[^\w\d\.\@<>,]//;
2678 $subject =~ s/[^\w\d\.\[\]]/ /;
2680 open(MAIL, "|mail -s '$subject' '$to'") ;
2681 print MAIL $content;
2691 my $arg = $self->get_form('jobid', 'client');
2693 print CGI::header('text/brestore');
2694 print "jobid=$arg->{jobid}\n" if ($arg->{jobid});
2695 print "client=$arg->{client}\n" if ($arg->{client});
2696 print "\n\nYou have to assign this mime type with /usr/bin/brestore.pl\n";
2700 # TODO : move this to Bweb::Autochanger ?
2701 # TODO : make this internal to not eject tape ?
2707 my ($self, $name) = @_;
2710 return $self->error("Can't get your autochanger name ach");
2713 unless ($self->{info}->{ach_list}) {
2714 return $self->error("Could not find any autochanger");
2717 my $a = $self->{info}->{ach_list}->{$name};
2720 $self->error("Can't get your autochanger $name from your ach_list");
2725 $a->{debug} = $self->{debug};
2732 my ($self, $ach) = @_;
2734 $self->{info}->{ach_list}->{$ach->{name}} = $ach;
2736 $self->{info}->save();
2744 my $arg = $self->get_form('ach');
2746 or !$self->{info}->{ach_list}
2747 or !$self->{info}->{ach_list}->{$arg->{ach}})
2749 return $self->error("Can't get autochanger name");
2752 my $ach = $self->{info}->{ach_list}->{$arg->{ach}};
2756 [ map { { name => $_, index => $i++ } } @{$ach->{drive_name}} ] ;
2758 my $b = $self->get_bconsole();
2760 my @storages = $b->list_storage() ;
2762 $ach->{devices} = [ map { { name => $_ } } @storages ];
2764 $self->display($ach, "ach_add.tpl");
2765 delete $ach->{drives};
2766 delete $ach->{devices};
2773 my $arg = $self->get_form('ach');
2776 or !$self->{info}->{ach_list}
2777 or !$self->{info}->{ach_list}->{$arg->{ach}})
2779 return $self->error("Can't get autochanger name");
2782 delete $self->{info}->{ach_list}->{$arg->{ach}} ;
2784 $self->{info}->save();
2785 $self->{info}->view();
2791 my $arg = $self->get_form('ach', 'mtxcmd', 'device', 'precmd');
2793 my $b = $self->get_bconsole();
2794 my @storages = $b->list_storage() ;
2796 unless ($arg->{ach}) {
2797 $arg->{devices} = [ map { { name => $_ } } @storages ];
2798 return $self->display($arg, "ach_add.tpl");
2802 foreach my $drive (CGI::param('drives'))
2804 unless (grep(/^$drive$/,@storages)) {
2805 return $self->error("Can't find $drive in storage list");
2808 my $index = CGI::param("index_$drive");
2809 unless (defined $index and $index =~ /^(\d+)$/) {
2810 return $self->error("Can't get $drive index");
2813 $drives[$index] = $drive;
2817 return $self->error("Can't get drives from Autochanger");
2820 my $a = new Bweb::Autochanger(name => $arg->{ach},
2821 precmd => $arg->{precmd},
2822 drive_name => \@drives,
2823 device => $arg->{device},
2824 mtxcmd => $arg->{mtxcmd});
2826 $self->ach_register($a) ;
2828 $self->{info}->view();
2834 my $arg = $self->get_form('jobid');
2836 if ($arg->{jobid}) {
2837 my $b = $self->get_bconsole();
2838 my $ret = $b->send_cmd("delete jobid=\"$arg->{jobid}\"");
2842 title => "Delete a job ",
2843 name => "delete jobid=$arg->{jobid}",
2852 my $arg = $self->get_form(qw/media volstatus inchanger pool
2853 slot volretention voluseduration
2854 maxvoljobs maxvolfiles maxvolbytes
2855 qcomment poolrecycle
2858 unless ($arg->{media}) {
2859 return $self->error("Can't find media selection");
2862 my $update = "update volume=$arg->{media} ";
2864 if ($arg->{volstatus}) {
2865 $update .= " volstatus=$arg->{volstatus} ";
2868 if ($arg->{inchanger}) {
2869 $update .= " inchanger=yes " ;
2871 $update .= " slot=$arg->{slot} ";
2874 $update .= " slot=0 inchanger=no ";
2878 $update .= " pool=$arg->{pool} " ;
2881 if (defined $arg->{volretention}) {
2882 $update .= " volretention=\"$arg->{volretention}\" " ;
2885 if (defined $arg->{voluseduration}) {
2886 $update .= " voluse=\"$arg->{voluseduration}\" " ;
2889 if (defined $arg->{maxvoljobs}) {
2890 $update .= " maxvoljobs=$arg->{maxvoljobs} " ;
2893 if (defined $arg->{maxvolfiles}) {
2894 $update .= " maxvolfiles=$arg->{maxvolfiles} " ;
2897 if (defined $arg->{maxvolbytes}) {
2898 $update .= " maxvolbytes=$arg->{maxvolbytes} " ;
2901 my $b = $self->get_bconsole();
2904 content => $b->send_cmd($update),
2905 title => "Update a volume ",
2911 my $media = $self->dbh_quote($arg->{media});
2913 my $loc = CGI::param('location') || '';
2915 $loc = $self->dbh_quote($loc); # is checked by db
2916 push @q, "LocationId=(SELECT LocationId FROM Location WHERE Location=$loc)";
2918 if ($arg->{poolrecycle}) {
2919 push @q, "RecyclePoolId=(SELECT PoolId FROM Pool WHERE Name='$arg->{poolrecycle}')";
2921 if (!$arg->{qcomment}) {
2922 $arg->{qcomment} = "''";
2924 push @q, "Comment=$arg->{qcomment}";
2929 SET " . join (',', @q) . "
2930 WHERE Media.VolumeName = $media
2932 $self->dbh_do($query);
2934 $self->update_media();
2941 my $ach = CGI::param('ach') ;
2942 $ach = $self->ach_get($ach);
2944 return $self->error("Bad autochanger name");
2948 my $b = new Bconsole(pref => $self->{info},timeout => 60,log_stdout => 1);
2949 $b->update_slots($ach->{name});
2957 my $arg = $self->get_form('jobid');
2958 unless ($arg->{jobid}) {
2959 return $self->error("Can't get jobid");
2962 my $t = CGI::param('time') || '';
2965 SELECT Job.Name as name, Client.Name as clientname
2966 FROM Job INNER JOIN Client ON (Job.ClientId = Client.ClientId)
2967 WHERE JobId = $arg->{jobid}
2970 my $row = $self->dbh_selectrow_hashref($query);
2973 return $self->error("Can't find $arg->{jobid} in catalog");
2977 SELECT Time AS time, LogText AS log
2979 WHERE Log.JobId = $arg->{jobid}
2980 OR (Log.JobId = 0 AND Time >= (SELECT StartTime FROM Job WHERE JobId=$arg->{jobid})
2981 AND Time <= (SELECT COALESCE(EndTime,NOW()) FROM Job WHERE JobId=$arg->{jobid})
2986 my $log = $self->dbh_selectall_arrayref($query);
2988 return $self->error("Can't get log for jobid $arg->{jobid}");
2994 $logtxt = join("", map { ($_->[0] . ' ' . $_->[1]) } @$log ) ;
2996 $logtxt = join("", map { $_->[1] } @$log ) ;
2999 $self->display({ lines=> $logtxt,
3000 jobid => $arg->{jobid},
3001 name => $row->{name},
3002 client => $row->{clientname},
3003 }, 'display_log.tpl');
3011 my $arg = $self->get_form('ach', 'slots', 'drive');
3013 unless ($arg->{ach}) {
3014 return $self->error("Can't find autochanger name");
3017 my $a = $self->ach_get($arg->{ach});
3019 return $self->error("Can't find autochanger name in configuration");
3022 my $storage = $a->get_drive_name($arg->{drive});
3024 return $self->error("Can't get your drive name");
3029 if ($arg->{slots}) {
3030 $slots = join(",", @{ $arg->{slots} });
3031 $t += 60*scalar( @{ $arg->{slots} }) ;
3034 my $b = new Bconsole(pref => $self->{info}, timeout => $t,log_stdout => 1);
3035 print "<h1>This command can take long time, be patient...</h1>";
3037 $b->label_barcodes(storage => $storage,
3038 drive => $arg->{drive},
3046 SET LocationId = (SELECT LocationId
3048 WHERE Location = '$arg->{ach}'),
3050 RecyclePoolId = PoolId
3052 WHERE Media.PoolId = (SELECT PoolId
3054 WHERE Name = 'Scratch')
3055 AND (LocationId = 0 OR LocationId IS NULL)
3064 my @volume = CGI::param('media');
3067 return $self->error("Can't get media selection");
3070 my $b = new Bconsole(pref => $self->{info}, timeout => 60);
3073 content => $b->purge_volume(@volume),
3074 title => "Purge media",
3075 name => "purge volume=" . join(' volume=', @volume),
3084 my @volume = CGI::param('media');
3086 return $self->error("Can't get media selection");
3089 my $b = new Bconsole(pref => $self->{info}, timeout => 60);
3092 content => $b->prune_volume(@volume),
3093 title => "Prune media",
3094 name => "prune volume=" . join(' volume=', @volume),
3104 my $arg = $self->get_form('jobid');
3105 unless ($arg->{jobid}) {
3106 return $self->error("Can't get jobid");
3109 my $b = $self->get_bconsole();
3111 content => $b->cancel($arg->{jobid}),
3112 title => "Cancel job",
3113 name => "cancel jobid=$arg->{jobid}",
3119 # Warning, we display current fileset
3122 my $arg = $self->get_form('fileset');
3124 if ($arg->{fileset}) {
3125 my $b = $self->get_bconsole();
3126 my $ret = $b->get_fileset($arg->{fileset});
3127 $self->display({ fileset => $arg->{fileset},
3129 }, "fileset_view.tpl");
3131 $self->error("Can't get fileset name");
3135 sub director_show_sched
3139 my $arg = $self->get_form('days');
3141 my $b = $self->get_bconsole();
3142 my $ret = $b->director_get_sched( $arg->{days} );
3147 }, "scheduled_job.tpl");
3150 sub enable_disable_job
3152 my ($self, $what) = @_ ;
3154 my $name = CGI::param('job') || '';
3155 unless ($name =~ /^[\w\d\.\-\s]+$/) {
3156 return $self->error("Can't find job name");
3159 my $b = $self->get_bconsole();
3169 content => $b->send_cmd("$cmd job=\"$name\""),
3170 title => "$cmd $name",
3171 name => "$cmd job=\"$name\"",
3178 return new Bconsole(pref => $self->{info});
3184 my $b = $self->get_bconsole();
3186 my $joblist = [ map { { name => $_ } } $b->list_job() ];
3188 $self->display({ Jobs => $joblist }, "run_job.tpl");
3193 my ($self, $ouput) = @_;
3196 foreach my $l (split(/\r\n/, $ouput)) {
3197 if ($l =~ /(\w+): name=([\w\d\.\s-]+?)(\s+\w+=.+)?$/) {
3203 if (my @l = $l =~ /(\w+)=([\w\d*]+)/g) {
3209 foreach my $k (keys %arg) {
3210 $lowcase{lc($k)} = $arg{$k} ;
3219 my $b = $self->get_bconsole();
3221 my $job = CGI::param('job') || '';
3223 my $info = $b->send_cmd("show job=\"$job\"");
3224 my $attr = $self->run_parse_job($info);
3226 my $jobs = [ map {{ name => $_ }} $b->list_job() ];
3228 my $pools = [ map { { name => $_ } } $b->list_pool() ];
3229 my $clients = [ map { { name => $_ } }$b->list_client()];
3230 my $filesets= [ map { { name => $_ } }$b->list_fileset() ];
3231 my $storages= [ map { { name => $_ } }$b->list_storage()];
3236 clients => $clients,
3237 filesets => $filesets,
3238 storages => $storages,
3240 }, "run_job_mod.tpl");
3246 my $b = $self->get_bconsole();
3248 my $jobs = [ map {{ name => $_ }} $b->list_job() ];
3258 my $b = $self->get_bconsole();
3260 # TODO: check input (don't use pool, level)
3262 my $arg = $self->get_form('pool', 'level', 'client', 'priority', 'when');
3263 my $job = CGI::param('job') || '';
3264 my $storage = CGI::param('storage') || '';
3266 my $jobid = $b->run(job => $job,
3267 client => $arg->{client},
3268 priority => $arg->{priority},
3269 level => $arg->{level},
3270 storage => $storage,
3271 pool => $arg->{pool},
3272 when => $arg->{when},
3275 print $jobid, $b->{error};
3277 print "<br>You can follow job execution <a href='?action=dsp_cur_job;client=$arg->{client};jobid=$jobid'> here </a>";