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,
1932 Pool.Name AS poolname,
1933 JobFiles AS jobfiles,
1934 JobBytes AS jobbytes,
1935 JobStatus AS jobstatus,
1936 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
1937 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime))
1940 JobErrors AS joberrors
1943 Job LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId)
1944 LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId)
1945 WHERE Client.ClientId=Job.ClientId
1946 AND Job.JobStatus != 'R'
1951 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
1953 $self->display({ Filter => $label,
1957 sort { $a->{jobid} <=> $b->{jobid} }
1964 # display job informations
1965 sub display_job_zoom
1967 my ($self, $jobid) = @_ ;
1969 $jobid = $self->dbh_quote($jobid);
1972 SELECT DISTINCT Job.JobId AS jobid,
1973 Client.Name AS client,
1974 Job.Name AS jobname,
1975 FileSet.FileSet AS fileset,
1977 Pool.Name AS poolname,
1978 StartTime AS starttime,
1979 JobFiles AS jobfiles,
1980 JobBytes AS jobbytes,
1981 JobStatus AS jobstatus,
1982 JobErrors AS joberrors,
1983 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
1984 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime)) AS duration
1987 Job LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId)
1988 LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId)
1989 WHERE Client.ClientId=Job.ClientId
1990 AND Job.JobId = $jobid
1993 my $row = $self->dbh_selectrow_hashref($query) ;
1995 # display all volumes associate with this job
1997 SELECT Media.VolumeName as volumename
1998 FROM Job,Media,JobMedia
1999 WHERE Job.JobId = $jobid
2000 AND JobMedia.JobId=Job.JobId
2001 AND JobMedia.MediaId=Media.MediaId
2004 my $all = $self->dbh_selectall_hashref($query, 'volumename');
2006 $row->{volumes} = [ values %$all ] ;
2008 $self->display($row, "display_job_zoom.tpl");
2015 my ($where, %elt) = $self->get_param('pools',
2020 my $arg = $self->get_form('jmedias', 'qre_media');
2022 if ($arg->{jmedias}) {
2023 $where = "AND Media.VolumeName IN ($arg->{jmedias}) $where";
2025 if ($arg->{qre_media}) {
2026 $where = "AND Media.VolumeName $self->{sql}->{MATCH} $arg->{qre_media} $where";
2030 SELECT Media.VolumeName AS volumename,
2031 Media.VolBytes AS volbytes,
2032 Media.VolStatus AS volstatus,
2033 Media.MediaType AS mediatype,
2034 Media.InChanger AS online,
2035 Media.LastWritten AS lastwritten,
2036 Location.Location AS location,
2037 (volbytes*100/COALESCE(media_avg_size.size,-1)) AS volusage,
2038 Pool.Name AS poolname,
2039 $self->{sql}->{FROM_UNIXTIME}(
2040 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2041 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2044 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2045 LEFT JOIN (SELECT avg(Media.VolBytes) AS size,
2046 Media.MediaType AS MediaType
2048 WHERE Media.VolStatus = 'Full'
2049 GROUP BY Media.MediaType
2050 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
2052 WHERE Media.PoolId=Pool.PoolId
2056 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2058 $self->display({ ID => $cur_id++,
2060 Location => $elt{location},
2061 Medias => [ values %$all ]
2063 "display_media.tpl");
2070 my $pool = $self->get_form('db_pools');
2072 foreach my $name (@{ $pool->{db_pools} }) {
2073 CGI::param('pool', $name->{name});
2074 $self->display_media();
2078 sub display_media_zoom
2082 my $medias = $self->get_form('jmedias');
2084 unless ($medias->{jmedias}) {
2085 return $self->error("Can't get media selection");
2089 SELECT InChanger AS online,
2090 VolBytes AS nb_bytes,
2091 VolumeName AS volumename,
2092 VolStatus AS volstatus,
2093 VolMounts AS nb_mounts,
2094 Media.VolUseDuration AS voluseduration,
2095 Media.MaxVolJobs AS maxvoljobs,
2096 Media.MaxVolFiles AS maxvolfiles,
2097 Media.MaxVolBytes AS maxvolbytes,
2098 VolErrors AS nb_errors,
2099 Pool.Name AS poolname,
2100 Location.Location AS location,
2101 Media.Recycle AS recycle,
2102 Media.VolRetention AS volretention,
2103 Media.LastWritten AS lastwritten,
2104 Media.VolReadTime/1000000 AS volreadtime,
2105 Media.VolWriteTime/1000000 AS volwritetime,
2106 Media.RecycleCount AS recyclecount,
2107 Media.Comment AS comment,
2108 $self->{sql}->{FROM_UNIXTIME}(
2109 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2110 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2113 Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2114 WHERE Pool.PoolId = Media.PoolId
2115 AND VolumeName IN ($medias->{jmedias})
2118 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2120 foreach my $media (values %$all) {
2121 my $mq = $self->dbh_quote($media->{volumename});
2124 SELECT DISTINCT Job.JobId AS jobid,
2126 Job.StartTime AS starttime,
2129 Job.JobFiles AS files,
2130 Job.JobBytes AS bytes,
2131 Job.jobstatus AS status
2132 FROM Media,JobMedia,Job
2133 WHERE Media.VolumeName=$mq
2134 AND Media.MediaId=JobMedia.MediaId
2135 AND JobMedia.JobId=Job.JobId
2138 my $jobs = $self->dbh_selectall_hashref($query, 'jobid') ;
2141 SELECT LocationLog.Date AS date,
2142 Location.Location AS location,
2143 LocationLog.Comment AS comment
2144 FROM Media,LocationLog INNER JOIN Location ON (LocationLog.LocationId = Location.LocationId)
2145 WHERE Media.MediaId = LocationLog.MediaId
2146 AND Media.VolumeName = $mq
2150 my $log = $self->dbh_selectall_arrayref($query) ;
2152 $logtxt = join("\n", map { ($_->[0] . ' ' . $_->[1] . ' ' . $_->[2])} @$log ) ;
2155 $self->display({ jobs => [ values %$jobs ],
2156 LocationLog => $logtxt,
2158 "display_media_zoom.tpl");
2166 my $loc = $self->get_form('qlocation');
2167 unless ($loc->{qlocation}) {
2168 return $self->error("Can't get location");
2172 SELECT Location.Location AS location,
2173 Location.Cost AS cost,
2174 Location.Enabled AS enabled
2176 WHERE Location.Location = $loc->{qlocation}
2179 my $row = $self->dbh_selectrow_hashref($query);
2181 $self->display({ ID => $cur_id++,
2182 %$row }, "location_edit.tpl") ;
2190 my $arg = $self->get_form(qw/qlocation qnewlocation cost/) ;
2191 unless ($arg->{qlocation}) {
2192 return $self->error("Can't get location");
2194 unless ($arg->{qnewlocation}) {
2195 return $self->error("Can't get new location name");
2197 unless ($arg->{cost}) {
2198 return $self->error("Can't get new cost");
2201 my $enabled = CGI::param('enabled') || '';
2202 $enabled = $enabled?1:0;
2205 UPDATE Location SET Cost = $arg->{cost},
2206 Location = $arg->{qnewlocation},
2208 WHERE Location.Location = $arg->{qlocation}
2211 $self->dbh_do($query);
2213 $self->display_location();
2219 my $arg = $self->get_form(qw/qlocation/) ;
2221 unless ($arg->{qlocation}) {
2222 return $self->error("Can't get location");
2226 SELECT count(Media.MediaId) AS nb
2227 FROM Media INNER JOIN Location USING (LocationID)
2228 WHERE Location = $arg->{qlocation}
2231 my $res = $self->dbh_selectrow_hashref($query);
2234 return $self->error("Sorry, the location must be empty");
2238 DELETE FROM Location WHERE Location = $arg->{qlocation} LIMIT 1
2241 $self->dbh_do($query);
2243 $self->display_location();
2250 my $arg = $self->get_form(qw/qlocation cost/) ;
2252 unless ($arg->{qlocation}) {
2253 $self->display({}, "location_add.tpl");
2256 unless ($arg->{cost}) {
2257 return $self->error("Can't get new cost");
2260 my $enabled = CGI::param('enabled') || '';
2261 $enabled = $enabled?1:0;
2264 INSERT INTO Location (Location, Cost, Enabled)
2265 VALUES ($arg->{qlocation}, $arg->{cost}, $enabled)
2268 $self->dbh_do($query);
2270 $self->display_location();
2273 sub display_location
2278 SELECT Location.Location AS location,
2279 Location.Cost AS cost,
2280 Location.Enabled AS enabled,
2281 (SELECT count(Media.MediaId)
2283 WHERE Media.LocationId = Location.LocationId
2288 my $location = $self->dbh_selectall_hashref($query, 'location');
2290 $self->display({ ID => $cur_id++,
2291 Locations => [ values %$location ] },
2292 "display_location.tpl");
2299 my $medias = $self->get_selected_media_location();
2304 my $arg = $self->get_form('db_locations', 'qnewlocation');
2306 $self->display({ email => $self->{info}->{email_media},
2308 medias => [ values %$medias ],
2310 "update_location.tpl");
2313 sub get_media_max_size
2315 my ($self, $type) = @_;
2317 "SELECT avg(VolBytes) AS size
2319 WHERE Media.VolStatus = 'Full'
2320 AND Media.MediaType = '$type'
2323 my $res = $self->selectrow_hashref($query);
2326 return $res->{size};
2336 my $media = $self->get_form('qmedia');
2338 unless ($media->{qmedia}) {
2339 return $self->error("Can't get media");
2343 SELECT Media.Slot AS slot,
2344 PoolMedia.Name AS poolname,
2345 Media.VolStatus AS volstatus,
2346 Media.InChanger AS inchanger,
2347 Location.Location AS location,
2348 Media.VolumeName AS volumename,
2349 Media.MaxVolBytes AS maxvolbytes,
2350 Media.MaxVolJobs AS maxvoljobs,
2351 Media.MaxVolFiles AS maxvolfiles,
2352 Media.VolUseDuration AS voluseduration,
2353 Media.VolRetention AS volretention,
2354 Media.Comment AS comment,
2355 PoolRecycle.Name AS poolrecycle
2357 FROM Media INNER JOIN Pool AS PoolMedia ON (Media.PoolId = PoolMedia.PoolId)
2358 LEFT JOIN Pool AS PoolRecycle ON (Media.RecyclePoolId = PoolRecycle.PoolId)
2359 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2361 WHERE Media.VolumeName = $media->{qmedia}
2364 my $row = $self->dbh_selectrow_hashref($query);
2365 $row->{volretention} = human_sec($row->{volretention});
2366 $row->{voluseduration} = human_sec($row->{voluseduration});
2368 my $elt = $self->get_form(qw/db_pools db_locations/);
2373 }, "update_media.tpl");
2380 my $arg = $self->get_form('jmedias', 'qnewlocation') ;
2382 unless ($arg->{jmedias}) {
2383 return $self->error("Can't get selected media");
2386 unless ($arg->{qnewlocation}) {
2387 return $self->error("Can't get new location");
2392 SET LocationId = (SELECT LocationId
2394 WHERE Location = $arg->{qnewlocation})
2395 WHERE Media.VolumeName IN ($arg->{jmedias})
2398 my $nb = $self->dbh_do($query);
2400 print "$nb media updated, you may have to update your autochanger.";
2402 $self->display_media();
2409 my $medias = $self->get_selected_media_location();
2411 return $self->error("Can't get media selection");
2413 my $newloc = CGI::param('newlocation');
2415 my $user = CGI::param('user') || 'unknow';
2416 my $comm = CGI::param('comment') || '';
2417 $comm = $self->dbh_quote("$user: $comm");
2421 foreach my $media (keys %$medias) {
2423 INSERT LocationLog (Date, Comment, MediaId, LocationId, NewVolStatus)
2425 NOW(), $comm, (SELECT MediaId FROM Media WHERE VolumeName = '$media'),
2426 (SELECT LocationId FROM Location WHERE Location = '$medias->{$media}->{location}'),
2427 (SELECT VolStatus FROM Media WHERE VolumeName = '$media')
2430 $self->dbh_do($query);
2431 $self->debug($query);
2435 $q->param('action', 'update_location');
2436 my $url = $q->url(-full => 1, -query=>1);
2438 $self->display({ email => $self->{info}->{email_media},
2440 newlocation => $newloc,
2441 # [ { volumename => 'vol1' }, { volumename => 'vol2'
\81 },..]
2442 medias => [ values %$medias ],
2444 "change_location.tpl");
2448 sub display_client_stats
2450 my ($self, %arg) = @_ ;
2452 my $client = $self->dbh_quote($arg{clientname});
2453 my ($limit, $label) = $self->get_limit(%arg);
2457 count(Job.JobId) AS nb_jobs,
2458 sum(Job.JobBytes) AS nb_bytes,
2459 sum(Job.JobErrors) AS nb_err,
2460 sum(Job.JobFiles) AS nb_files,
2461 Client.Name AS clientname
2462 FROM Job INNER JOIN Client USING (ClientId)
2464 Client.Name = $client
2466 GROUP BY Client.Name
2469 my $row = $self->dbh_selectrow_hashref($query);
2471 $row->{ID} = $cur_id++;
2472 $row->{label} = $label;
2474 $self->display($row, "display_client_stats.tpl");
2477 # poolname can be undef
2480 my ($self, $poolname) = @_ ;
2484 my $arg = $self->get_form('jmediatypes', 'qmediatypes');
2485 if ($arg->{jmediatypes}) {
2486 $whereW = "WHERE MediaType IN ($arg->{jmediatypes}) ";
2487 $whereA = "AND MediaType IN ($arg->{jmediatypes}) ";
2490 # TODO : afficher les tailles et les dates
2493 SELECT subq.volmax AS volmax,
2494 subq.volnum AS volnum,
2495 subq.voltotal AS voltotal,
2497 Pool.Recycle AS recycle,
2498 Pool.VolRetention AS volretention,
2499 Pool.VolUseDuration AS voluseduration,
2500 Pool.MaxVolJobs AS maxvoljobs,
2501 Pool.MaxVolFiles AS maxvolfiles,
2502 Pool.MaxVolBytes AS maxvolbytes,
2503 subq.PoolId AS PoolId
2506 SELECT COALESCE(media_avg_size.volavg,0) * count(Media.MediaId) AS volmax,
2507 count(Media.MediaId) AS volnum,
2508 sum(Media.VolBytes) AS voltotal,
2509 Media.PoolId AS PoolId,
2510 Media.MediaType AS MediaType
2512 LEFT JOIN (SELECT avg(Media.VolBytes) AS volavg,
2513 Media.MediaType AS MediaType
2515 WHERE Media.VolStatus = 'Full'
2516 GROUP BY Media.MediaType
2517 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
2518 GROUP BY Media.MediaType, Media.PoolId, media_avg_size.volavg
2520 LEFT JOIN Pool ON (Pool.PoolId = subq.PoolId)
2524 my $all = $self->dbh_selectall_hashref($query, 'name') ;
2527 SELECT Pool.Name AS name,
2528 sum(VolBytes) AS size
2529 FROM Media JOIN Pool ON (Media.PoolId = Pool.PoolId)
2530 WHERE Media.VolStatus IN ('Recycled', 'Purged')
2534 my $empty = $self->dbh_selectall_hashref($query, 'name');
2536 foreach my $p (values %$all) {
2537 if ($p->{volmax} > 0) { # mysql returns 0.0000
2538 # we remove Recycled/Purged media from pool usage
2539 if (defined $empty->{$p->{name}}) {
2540 $p->{voltotal} -= $empty->{$p->{name}}->{size};
2542 $p->{poolusage} = sprintf('%.2f', $p->{voltotal} * 100/ $p->{volmax}) ;
2544 $p->{poolusage} = 0;
2548 SELECT VolStatus AS volstatus, count(MediaId) AS nb
2550 WHERE PoolId=$p->{poolid}
2554 my $content = $self->dbh_selectall_hashref($query, 'volstatus');
2555 foreach my $t (values %$content) {
2556 $p->{"nb_" . $t->{volstatus}} = $t->{nb} ;
2561 $self->display({ ID => $cur_id++,
2562 MediaType => $arg->{qmediatypes}, # [ { name => type1 } , { name => type2 } ]
2563 Pools => [ values %$all ]},
2564 "display_pool.tpl");
2567 sub display_running_job
2571 my $arg = $self->get_form('client', 'jobid');
2573 if (!$arg->{client} and $arg->{jobid}) {
2576 SELECT Client.Name AS name
2577 FROM Job INNER JOIN Client USING (ClientId)
2578 WHERE Job.JobId = $arg->{jobid}
2581 my $row = $self->dbh_selectrow_hashref($query);
2584 $arg->{client} = $row->{name};
2585 CGI::param('client', $arg->{client});
2589 if ($arg->{client}) {
2590 my $cli = new Bweb::Client(name => $arg->{client});
2591 $cli->display_running_job($self->{info}, $arg->{jobid});
2592 if ($arg->{jobid}) {
2593 $self->get_job_log();
2596 $self->error("Can't get client or jobid");
2600 sub display_running_jobs
2602 my ($self, $display_action) = @_;
2605 SELECT Job.JobId AS jobid,
2606 Job.Name AS jobname,
2608 Job.StartTime AS starttime,
2609 Job.JobFiles AS jobfiles,
2610 Job.JobBytes AS jobbytes,
2611 Job.JobStatus AS jobstatus,
2612 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(NOW())
2613 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime))
2615 Client.Name AS clientname
2616 FROM Job INNER JOIN Client USING (ClientId)
2617 WHERE JobStatus IN ('C','R','B','e','D','F','S','m','M','s','j','c','d','t','p')
2619 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
2621 $self->display({ ID => $cur_id++,
2622 display_action => $display_action,
2623 Jobs => [ values %$all ]},
2624 "running_job.tpl") ;
2627 # return the autochanger list to update
2632 my $arg = $self->get_form('jmedias');
2634 unless ($arg->{jmedias}) {
2635 return $self->error("Can't get media selection");
2639 SELECT Media.VolumeName AS volumename,
2640 Storage.Name AS storage,
2641 Location.Location AS location,
2643 FROM Media INNER JOIN Storage ON (Media.StorageId = Storage.StorageId)
2644 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2645 WHERE Media.VolumeName IN ($arg->{jmedias})
2646 AND Media.InChanger = 1
2649 my $all = $self->dbh_selectall_hashref($query, 'volumename');
2651 foreach my $vol (values %$all) {
2652 my $a = $self->ach_get($vol->{location});
2654 $ret{$vol->{location}} = 1;
2656 unless ($a->{have_status}) {
2658 $a->{have_status} = 1;
2661 print "eject $vol->{volumename} from $vol->{storage} : ";
2662 if ($a->send_to_io($vol->{slot})) {
2663 print "<img src='/bweb/T.png' alt='ok'><br/>";
2665 print "<img src='/bweb/E.png' alt='err'><br/>";
2675 my ($to, $subject, $content) = (CGI::param('email'),
2676 CGI::param('subject'),
2677 CGI::param('content'));
2678 $to =~ s/[^\w\d\.\@<>,]//;
2679 $subject =~ s/[^\w\d\.\[\]]/ /;
2681 open(MAIL, "|mail -s '$subject' '$to'") ;
2682 print MAIL $content;
2692 my $arg = $self->get_form('jobid', 'client');
2694 print CGI::header('text/brestore');
2695 print "jobid=$arg->{jobid}\n" if ($arg->{jobid});
2696 print "client=$arg->{client}\n" if ($arg->{client});
2697 print "\n\nYou have to assign this mime type with /usr/bin/brestore.pl\n";
2701 # TODO : move this to Bweb::Autochanger ?
2702 # TODO : make this internal to not eject tape ?
2708 my ($self, $name) = @_;
2711 return $self->error("Can't get your autochanger name ach");
2714 unless ($self->{info}->{ach_list}) {
2715 return $self->error("Could not find any autochanger");
2718 my $a = $self->{info}->{ach_list}->{$name};
2721 $self->error("Can't get your autochanger $name from your ach_list");
2726 $a->{debug} = $self->{debug};
2733 my ($self, $ach) = @_;
2735 $self->{info}->{ach_list}->{$ach->{name}} = $ach;
2737 $self->{info}->save();
2745 my $arg = $self->get_form('ach');
2747 or !$self->{info}->{ach_list}
2748 or !$self->{info}->{ach_list}->{$arg->{ach}})
2750 return $self->error("Can't get autochanger name");
2753 my $ach = $self->{info}->{ach_list}->{$arg->{ach}};
2757 [ map { { name => $_, index => $i++ } } @{$ach->{drive_name}} ] ;
2759 my $b = $self->get_bconsole();
2761 my @storages = $b->list_storage() ;
2763 $ach->{devices} = [ map { { name => $_ } } @storages ];
2765 $self->display($ach, "ach_add.tpl");
2766 delete $ach->{drives};
2767 delete $ach->{devices};
2774 my $arg = $self->get_form('ach');
2777 or !$self->{info}->{ach_list}
2778 or !$self->{info}->{ach_list}->{$arg->{ach}})
2780 return $self->error("Can't get autochanger name");
2783 delete $self->{info}->{ach_list}->{$arg->{ach}} ;
2785 $self->{info}->save();
2786 $self->{info}->view();
2792 my $arg = $self->get_form('ach', 'mtxcmd', 'device', 'precmd');
2794 my $b = $self->get_bconsole();
2795 my @storages = $b->list_storage() ;
2797 unless ($arg->{ach}) {
2798 $arg->{devices} = [ map { { name => $_ } } @storages ];
2799 return $self->display($arg, "ach_add.tpl");
2803 foreach my $drive (CGI::param('drives'))
2805 unless (grep(/^$drive$/,@storages)) {
2806 return $self->error("Can't find $drive in storage list");
2809 my $index = CGI::param("index_$drive");
2810 unless (defined $index and $index =~ /^(\d+)$/) {
2811 return $self->error("Can't get $drive index");
2814 $drives[$index] = $drive;
2818 return $self->error("Can't get drives from Autochanger");
2821 my $a = new Bweb::Autochanger(name => $arg->{ach},
2822 precmd => $arg->{precmd},
2823 drive_name => \@drives,
2824 device => $arg->{device},
2825 mtxcmd => $arg->{mtxcmd});
2827 $self->ach_register($a) ;
2829 $self->{info}->view();
2835 my $arg = $self->get_form('jobid');
2837 if ($arg->{jobid}) {
2838 my $b = $self->get_bconsole();
2839 my $ret = $b->send_cmd("delete jobid=\"$arg->{jobid}\"");
2843 title => "Delete a job ",
2844 name => "delete jobid=$arg->{jobid}",
2853 my $arg = $self->get_form(qw/media volstatus inchanger pool
2854 slot volretention voluseduration
2855 maxvoljobs maxvolfiles maxvolbytes
2856 qcomment poolrecycle
2859 unless ($arg->{media}) {
2860 return $self->error("Can't find media selection");
2863 my $update = "update volume=$arg->{media} ";
2865 if ($arg->{volstatus}) {
2866 $update .= " volstatus=$arg->{volstatus} ";
2869 if ($arg->{inchanger}) {
2870 $update .= " inchanger=yes " ;
2872 $update .= " slot=$arg->{slot} ";
2875 $update .= " slot=0 inchanger=no ";
2879 $update .= " pool=$arg->{pool} " ;
2882 if (defined $arg->{volretention}) {
2883 $update .= " volretention=\"$arg->{volretention}\" " ;
2886 if (defined $arg->{voluseduration}) {
2887 $update .= " voluse=\"$arg->{voluseduration}\" " ;
2890 if (defined $arg->{maxvoljobs}) {
2891 $update .= " maxvoljobs=$arg->{maxvoljobs} " ;
2894 if (defined $arg->{maxvolfiles}) {
2895 $update .= " maxvolfiles=$arg->{maxvolfiles} " ;
2898 if (defined $arg->{maxvolbytes}) {
2899 $update .= " maxvolbytes=$arg->{maxvolbytes} " ;
2902 my $b = $self->get_bconsole();
2905 content => $b->send_cmd($update),
2906 title => "Update a volume ",
2912 my $media = $self->dbh_quote($arg->{media});
2914 my $loc = CGI::param('location') || '';
2916 $loc = $self->dbh_quote($loc); # is checked by db
2917 push @q, "LocationId=(SELECT LocationId FROM Location WHERE Location=$loc)";
2919 if ($arg->{poolrecycle}) {
2920 push @q, "RecyclePoolId=(SELECT PoolId FROM Pool WHERE Name='$arg->{poolrecycle}')";
2922 if (!$arg->{qcomment}) {
2923 $arg->{qcomment} = "''";
2925 push @q, "Comment=$arg->{qcomment}";
2930 SET " . join (',', @q) . "
2931 WHERE Media.VolumeName = $media
2933 $self->dbh_do($query);
2935 $self->update_media();
2942 my $ach = CGI::param('ach') ;
2943 $ach = $self->ach_get($ach);
2945 return $self->error("Bad autochanger name");
2949 my $b = new Bconsole(pref => $self->{info},timeout => 60,log_stdout => 1);
2950 $b->update_slots($ach->{name});
2958 my $arg = $self->get_form('jobid');
2959 unless ($arg->{jobid}) {
2960 return $self->error("Can't get jobid");
2963 my $t = CGI::param('time') || '';
2966 SELECT Job.Name as name, Client.Name as clientname
2967 FROM Job INNER JOIN Client ON (Job.ClientId = Client.ClientId)
2968 WHERE JobId = $arg->{jobid}
2971 my $row = $self->dbh_selectrow_hashref($query);
2974 return $self->error("Can't find $arg->{jobid} in catalog");
2978 SELECT Time AS time, LogText AS log
2980 WHERE Log.JobId = $arg->{jobid}
2981 OR (Log.JobId = 0 AND Time >= (SELECT StartTime FROM Job WHERE JobId=$arg->{jobid})
2982 AND Time <= (SELECT COALESCE(EndTime,NOW()) FROM Job WHERE JobId=$arg->{jobid})
2987 my $log = $self->dbh_selectall_arrayref($query);
2989 return $self->error("Can't get log for jobid $arg->{jobid}");
2995 $logtxt = join("", map { ($_->[0] . ' ' . $_->[1]) } @$log ) ;
2997 $logtxt = join("", map { $_->[1] } @$log ) ;
3000 $self->display({ lines=> $logtxt,
3001 jobid => $arg->{jobid},
3002 name => $row->{name},
3003 client => $row->{clientname},
3004 }, 'display_log.tpl');
3012 my $arg = $self->get_form('ach', 'slots', 'drive');
3014 unless ($arg->{ach}) {
3015 return $self->error("Can't find autochanger name");
3018 my $a = $self->ach_get($arg->{ach});
3020 return $self->error("Can't find autochanger name in configuration");
3023 my $storage = $a->get_drive_name($arg->{drive});
3025 return $self->error("Can't get your drive name");
3030 if ($arg->{slots}) {
3031 $slots = join(",", @{ $arg->{slots} });
3032 $t += 60*scalar( @{ $arg->{slots} }) ;
3035 my $b = new Bconsole(pref => $self->{info}, timeout => $t,log_stdout => 1);
3036 print "<h1>This command can take long time, be patient...</h1>";
3038 $b->label_barcodes(storage => $storage,
3039 drive => $arg->{drive},
3047 SET LocationId = (SELECT LocationId
3049 WHERE Location = '$arg->{ach}'),
3051 RecyclePoolId = PoolId
3053 WHERE Media.PoolId = (SELECT PoolId
3055 WHERE Name = 'Scratch')
3056 AND (LocationId = 0 OR LocationId IS NULL)
3065 my @volume = CGI::param('media');
3068 return $self->error("Can't get media selection");
3071 my $b = new Bconsole(pref => $self->{info}, timeout => 60);
3074 content => $b->purge_volume(@volume),
3075 title => "Purge media",
3076 name => "purge volume=" . join(' volume=', @volume),
3085 my @volume = CGI::param('media');
3087 return $self->error("Can't get media selection");
3090 my $b = new Bconsole(pref => $self->{info}, timeout => 60);
3093 content => $b->prune_volume(@volume),
3094 title => "Prune media",
3095 name => "prune volume=" . join(' volume=', @volume),
3105 my $arg = $self->get_form('jobid');
3106 unless ($arg->{jobid}) {
3107 return $self->error("Can't get jobid");
3110 my $b = $self->get_bconsole();
3112 content => $b->cancel($arg->{jobid}),
3113 title => "Cancel job",
3114 name => "cancel jobid=$arg->{jobid}",
3120 # Warning, we display current fileset
3123 my $arg = $self->get_form('fileset');
3125 if ($arg->{fileset}) {
3126 my $b = $self->get_bconsole();
3127 my $ret = $b->get_fileset($arg->{fileset});
3128 $self->display({ fileset => $arg->{fileset},
3130 }, "fileset_view.tpl");
3132 $self->error("Can't get fileset name");
3136 sub director_show_sched
3140 my $arg = $self->get_form('days');
3142 my $b = $self->get_bconsole();
3143 my $ret = $b->director_get_sched( $arg->{days} );
3148 }, "scheduled_job.tpl");
3151 sub enable_disable_job
3153 my ($self, $what) = @_ ;
3155 my $name = CGI::param('job') || '';
3156 unless ($name =~ /^[\w\d\.\-\s]+$/) {
3157 return $self->error("Can't find job name");
3160 my $b = $self->get_bconsole();
3170 content => $b->send_cmd("$cmd job=\"$name\""),
3171 title => "$cmd $name",
3172 name => "$cmd job=\"$name\"",
3179 return new Bconsole(pref => $self->{info});
3185 my $b = $self->get_bconsole();
3187 my $joblist = [ map { { name => $_ } } $b->list_job() ];
3189 $self->display({ Jobs => $joblist }, "run_job.tpl");
3194 my ($self, $ouput) = @_;
3197 foreach my $l (split(/\r\n/, $ouput)) {
3198 if ($l =~ /(\w+): name=([\w\d\.\s-]+?)(\s+\w+=.+)?$/) {
3204 if (my @l = $l =~ /(\w+)=([\w\d*]+)/g) {
3210 foreach my $k (keys %arg) {
3211 $lowcase{lc($k)} = $arg{$k} ;
3220 my $b = $self->get_bconsole();
3222 my $job = CGI::param('job') || '';
3224 my $info = $b->send_cmd("show job=\"$job\"");
3225 my $attr = $self->run_parse_job($info);
3227 my $jobs = [ map {{ name => $_ }} $b->list_job() ];
3229 my $pools = [ map { { name => $_ } } $b->list_pool() ];
3230 my $clients = [ map { { name => $_ } }$b->list_client()];
3231 my $filesets= [ map { { name => $_ } }$b->list_fileset() ];
3232 my $storages= [ map { { name => $_ } }$b->list_storage()];
3237 clients => $clients,
3238 filesets => $filesets,
3239 storages => $storages,
3241 }, "run_job_mod.tpl");
3247 my $b = $self->get_bconsole();
3249 my $jobs = [ map {{ name => $_ }} $b->list_job() ];
3259 my $b = $self->get_bconsole();
3261 # TODO: check input (don't use pool, level)
3263 my $arg = $self->get_form('pool', 'level', 'client', 'priority', 'when');
3264 my $job = CGI::param('job') || '';
3265 my $storage = CGI::param('storage') || '';
3267 my $jobid = $b->run(job => $job,
3268 client => $arg->{client},
3269 priority => $arg->{priority},
3270 level => $arg->{level},
3271 storage => $storage,
3272 pool => $arg->{pool},
3273 when => $arg->{when},
3276 print $jobid, $b->{error};
3278 print "<br>You can follow job execution <a href='?action=dsp_cur_job;client=$arg->{client};jobid=$jobid'> here </a>";