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!^(.+)?$!,
218 stat_job_table => qr!^(\w*)$!,
219 display_log_time => qr!^(on)?$!,
224 load - load config_file
228 this function load the specified config_file.
236 unless (open(FP, $self->{config_file}))
238 return $self->error("can't load config_file $self->{config_file} : $!");
240 my $f=''; my $tmpbuffer;
241 while(read FP,$tmpbuffer,4096)
249 no strict; # I have no idea of the contents of the file
256 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...") ;
259 foreach my $k (keys %$VAR1) {
260 $self->{$k} = $VAR1->{$k};
268 load_old - load old configuration format
276 unless (open(FP, $self->{config_file}))
278 return $self->error("$self->{config_file} : $!");
281 while (my $line = <FP>)
284 my ($k, $v) = split(/\s*=\s*/, $line, 2);
296 save - save the current configuration to config_file
304 if ($self->{ach_list}) {
305 # shortcut for display_begin
306 $self->{achs} = [ map {{ name => $_ }}
307 keys %{$self->{ach_list}}
311 unless (open(FP, ">$self->{config_file}"))
313 return $self->error("$self->{config_file} : $!\n" .
314 "You must add this to your config file\n"
315 . Data::Dumper::Dumper($self));
318 print FP Data::Dumper::Dumper($self);
326 edit, view, modify - html form ouput
334 $self->display($self, "config_edit.tpl");
340 $self->display($self, "config_view.tpl");
350 foreach my $k (CGI::param())
352 next unless (exists $k_re{$k}) ;
353 my $val = CGI::param($k);
354 if ($val =~ $k_re{$k}) {
357 $self->{error} .= "bad parameter : $k = [$val]";
363 if ($self->{error}) { # an error as occured
364 $self->display($self, 'error.tpl');
372 ################################################################
374 package Bweb::Client;
376 use base q/Bweb::Gui/;
380 Bweb::Client - Bacula FD
384 this package is use to do all Client operations like, parse status etc...
388 $client = new Bweb::Client(name => 'zog-fd');
389 $client->status(); # do a 'status client=zog-fd'
395 display_running_job - Html display of a running job
399 this function is used to display information about a current job
403 sub display_running_job
405 my ($self, $conf, $jobid) = @_ ;
407 my $status = $self->status($conf);
410 if ($status->{$jobid}) {
411 $self->display($status->{$jobid}, "client_job_status.tpl");
414 for my $id (keys %$status) {
415 $self->display($status->{$id}, "client_job_status.tpl");
422 $client = new Bweb::Client(name => 'plume-fd');
424 $client->status($bweb);
428 dirty hack to parse "status client=xxx-fd"
432 JobId 105 Job Full_plume.2006-06-06_17.22.23 is running.
433 Backup Job started: 06-jun-06 17:22
434 Files=8,971 Bytes=194,484,132 Bytes/sec=7,480,158
435 Files Examined=10,697
436 Processing file: /home/eric/.openoffice.org2/user/config/standard.sod
442 JobName => Full_plume.2006-06-06_17.22.23,
445 Bytes => 194,484,132,
455 my ($self, $conf) = @_ ;
457 if (defined $self->{cur_jobs}) {
458 return $self->{cur_jobs} ;
462 my $b = new Bconsole(pref => $conf);
463 my $ret = $b->send_cmd("st client=$self->{name}");
467 for my $r (split(/\n/, $ret)) {
469 $r =~ s/(^\s+|\s+$)//g;
470 if ($r =~ /JobId (\d+) Job (\S+)/) {
472 $arg->{$jobid} = { @param, JobId => $jobid } ;
476 @param = ( JobName => $2 );
478 } elsif ($r =~ /=.+=/) {
479 push @param, split(/\s+|\s*=\s*/, $r) ;
481 } elsif ($r =~ /=/) { # one per line
482 push @param, split(/\s*=\s*/, $r) ;
484 } elsif ($r =~ /:/) { # one per line
485 push @param, split(/\s*:\s*/, $r, 2) ;
489 if ($jobid and @param) {
490 $arg->{$jobid} = { @param,
492 Client => $self->{name},
496 $self->{cur_jobs} = $arg ;
502 ################################################################
504 package Bweb::Autochanger;
506 use base q/Bweb::Gui/;
510 Bweb::Autochanger - Object to manage Autochanger
514 this package will parse the mtx output and manage drives.
518 $auto = new Bweb::Autochanger(precmd => 'sudo');
520 $auto = new Bweb::Autochanger(precmd => 'ssh root@robot');
524 $auto->slot_is_full(10);
525 $auto->transfer(10, 11);
531 my ($class, %arg) = @_;
534 name => '', # autochanger name
535 label => {}, # where are volume { label1 => 40, label2 => drive0 }
536 drive => [], # drive use [ 'media1', 'empty', ..]
537 slot => [], # slot use [ undef, 'empty', 'empty', ..] no slot 0
538 io => [], # io slot number list [ 41, 42, 43...]
539 info => {slot => 0, # informations (slot, drive, io)
543 mtxcmd => '/usr/sbin/mtx',
545 device => '/dev/changer',
546 precmd => '', # ssh command
547 bweb => undef, # link to bacula web object (use for display)
550 map { $self->{lc($_)} = $arg{$_} } keys %arg ;
557 status - parse the output of mtx status
561 this function will launch mtx status and parse the output. it will
562 give a perlish view of the autochanger content.
564 it uses ssh if the autochanger is on a other host.
571 my @out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} status` ;
573 # TODO : reset all infos
574 $self->{info}->{drive} = 0;
575 $self->{info}->{slot} = 0;
576 $self->{info}->{io} = 0;
578 #my @out = `cat /home/eric/travail/brestore/plume/mtx` ;
581 # Storage Changer /dev/changer:2 Drives, 45 Slots ( 5 Import/Export )
582 #Data Transfer Element 0:Full (Storage Element 1 Loaded):VolumeTag = 000000
583 #Data Transfer Element 1:Empty
584 # Storage Element 1:Empty
585 # Storage Element 2:Full :VolumeTag=000002
586 # Storage Element 3:Empty
587 # Storage Element 4:Full :VolumeTag=000004
588 # Storage Element 5:Full :VolumeTag=000001
589 # Storage Element 6:Full :VolumeTag=000003
590 # Storage Element 7:Empty
591 # Storage Element 41 IMPORT/EXPORT:Empty
592 # Storage Element 41 IMPORT/EXPORT:Full :VolumeTag=000002
597 # Storage Element 7:Empty
598 # Storage Element 2:Full :VolumeTag=000002
599 if ($l =~ /Storage Element (\d+):(Empty|Full)(\s+:VolumeTag=([\w\d]+))?/){
602 $self->set_empty_slot($1);
604 $self->set_slot($1, $4);
607 } elsif ($l =~ /Data Transfer.+(\d+):(Full|Empty)(\s+.Storage Element (\d+) Loaded.(:VolumeTag = ([\w\d]+))?)?/) {
610 $self->set_empty_drive($1);
612 $self->set_drive($1, $4, $6);
615 } elsif ($l =~ /Storage Element (\d+).+IMPORT\/EXPORT:(Empty|Full)( :VolumeTag=([\d\w]+))?/)
618 $self->set_empty_io($1);
620 $self->set_io($1, $4);
623 # Storage Changer /dev/changer:2 Drives, 30 Slots ( 1 Import/Export )
625 } elsif ($l =~ /Storage Changer .+:(\d+) Drives, (\d+) Slots/) {
626 $self->{info}->{drive} = $1;
627 $self->{info}->{slot} = $2;
628 if ($l =~ /(\d+)\s+Import/) {
629 $self->{info}->{io} = $1 ;
631 $self->{info}->{io} = 0;
636 $self->debug($self) ;
641 my ($self, $slot) = @_;
644 if ($self->{slot}->[$slot] eq 'loaded') {
648 my $label = $self->{slot}->[$slot] ;
650 return $self->is_media_loaded($label);
655 my ($self, $drive, $slot) = @_;
657 return 0 if (not defined $drive or $self->{drive}->[$drive] eq 'empty') ;
658 return 0 if ($self->slot_is_full($slot)) ;
660 my $out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} unload $slot $drive 2>&1`;
663 my $content = $self->get_slot($slot);
664 print "content = $content<br/> $drive => $slot<br/>";
665 $self->set_empty_drive($drive);
666 $self->set_slot($slot, $content);
669 $self->{error} = $out;
674 # TODO: load/unload have to use mtx script from bacula
677 my ($self, $drive, $slot) = @_;
679 return 0 if (not defined $drive or $self->{drive}->[$drive] ne 'empty') ;
680 return 0 unless ($self->slot_is_full($slot)) ;
682 print "Loading drive $drive with slot $slot<br/>\n";
683 my $out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} load $slot $drive 2>&1`;
686 my $content = $self->get_slot($slot);
687 print "content = $content<br/> $slot => $drive<br/>";
688 $self->set_drive($drive, $slot, $content);
691 $self->{error} = $out;
699 my ($self, $media) = @_;
701 unless ($self->{label}->{$media}) {
705 if ($self->{label}->{$media} =~ /drive\d+/) {
715 return (defined $self->{info}->{io} and $self->{info}->{io} > 0);
720 my ($self, $slot, $tag) = @_;
721 $self->{slot}->[$slot] = $tag || 'full';
722 push @{ $self->{io} }, $slot;
725 $self->{label}->{$tag} = $slot;
731 my ($self, $slot) = @_;
733 push @{ $self->{io} }, $slot;
735 unless ($self->{slot}->[$slot]) { # can be loaded (parse before)
736 $self->{slot}->[$slot] = 'empty';
742 my ($self, $slot) = @_;
743 return $self->{slot}->[$slot];
748 my ($self, $slot, $tag) = @_;
749 $self->{slot}->[$slot] = $tag || 'full';
752 $self->{label}->{$tag} = $slot;
758 my ($self, $slot) = @_;
760 unless ($self->{slot}->[$slot]) { # can be loaded (parse before)
761 $self->{slot}->[$slot] = 'empty';
767 my ($self, $drive) = @_;
768 $self->{drive}->[$drive] = 'empty';
773 my ($self, $drive, $slot, $tag) = @_;
774 $self->{drive}->[$drive] = $tag || $slot;
776 $self->{slot}->[$slot] = $tag || 'loaded';
779 $self->{label}->{$tag} = "drive$drive";
785 my ($self, $slot) = @_;
787 # slot don't exists => full
788 if (not defined $self->{slot}->[$slot]) {
792 if ($self->{slot}->[$slot] eq 'empty') {
795 return 1; # vol, full, loaded
798 sub slot_get_first_free
801 for (my $slot=1; $slot < $self->{info}->{slot}; $slot++) {
802 return $slot unless ($self->slot_is_full($slot));
806 sub io_get_first_free
810 foreach my $slot (@{ $self->{io} }) {
811 return $slot unless ($self->slot_is_full($slot));
818 my ($self, $media) = @_;
820 return $self->{label}->{$media} ;
825 my ($self, $media) = @_;
827 return defined $self->{label}->{$media} ;
832 my ($self, $slot) = @_;
834 unless ($self->slot_is_full($slot)) {
835 print "Autochanger $self->{name} slot $slot is empty\n";
840 if ($self->is_slot_loaded($slot)) {
843 print "Autochanger $self->{name} $slot is currently in use\n";
847 # autochanger must have I/O
848 unless ($self->have_io()) {
849 print "Autochanger $self->{name} don't have I/O, you can take media yourself\n";
853 my $dst = $self->io_get_first_free();
856 print "Autochanger $self->{name} you must empty I/O first\n";
859 $self->transfer($slot, $dst);
864 my ($self, $src, $dst) = @_ ;
865 if ($self->{debug}) {
866 print "<pre>$self->{precmd} $self->{mtxcmd} -f $self->{device} transfer $src $dst</pre>\n";
868 my $out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} transfer $src $dst 2>&1`;
871 my $content = $self->get_slot($src);
872 $self->{slot}->[$src] = 'empty';
873 $self->set_slot($dst, $content);
876 $self->{error} = $out;
883 my ($self, $index) = @_;
884 return $self->{drive_name}->[$index];
887 # TODO : do a tapeinfo request to get informations
897 for my $slot (@{$self->{io}})
899 if ($self->is_slot_loaded($slot)) {
900 print "$slot is currently loaded\n";
904 if ($self->slot_is_full($slot))
906 my $free = $self->slot_get_first_free() ;
907 print "move $slot to $free :\n";
910 if ($self->transfer($slot, $free)) {
911 print "<img src='/bweb/T.png' alt='ok'><br/>\n";
913 print "<img src='/bweb/E.png' alt='ok' title='$self->{error}'><br/>\n";
917 $self->{error} = "<img src='/bweb/E.png' alt='ok' title='E : Can t find free slot'><br/>\n";
923 # TODO : this is with mtx status output,
924 # we can do an other function from bacula view (with StorageId)
928 my $bweb = $self->{bweb};
930 # $self->{label} => ('vol1', 'vol2', 'vol3', ..);
931 my $media_list = $bweb->dbh_join( keys %{ $self->{label} });
934 SELECT Media.VolumeName AS volumename,
935 Media.VolStatus AS volstatus,
936 Media.LastWritten AS lastwritten,
937 Media.VolBytes AS volbytes,
938 Media.MediaType AS mediatype,
940 Media.InChanger AS inchanger,
942 $bweb->{sql}->{FROM_UNIXTIME}(
943 $bweb->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
944 + $bweb->{sql}->{TO_SEC}(Media.VolRetention)
947 INNER JOIN Pool USING (PoolId)
949 WHERE Media.VolumeName IN ($media_list)
952 my $all = $bweb->dbh_selectall_hashref($query, 'volumename') ;
954 # TODO : verify slot and bacula slot
958 for (my $slot=1; $slot <= $self->{info}->{slot} ; $slot++) {
960 if ($self->slot_is_full($slot)) {
962 my $vol = $self->{slot}->[$slot];
963 if (defined $all->{$vol}) { # TODO : autochanger without barcodes
965 my $bslot = $all->{$vol}->{slot} ;
966 my $inchanger = $all->{$vol}->{inchanger};
968 # if bacula slot or inchanger flag is bad, we display a message
969 if ($bslot != $slot or !$inchanger) {
970 push @to_update, $slot;
973 $all->{$vol}->{realslot} = $slot;
975 push @{ $param }, $all->{$vol};
977 } else { # empty or no label
978 push @{ $param }, {realslot => $slot,
979 volstatus => 'Unknown',
980 volumename => $self->{slot}->[$slot]} ;
983 push @{ $param }, {realslot => $slot, volumename => 'empty'} ;
987 my $i=0; my $drives = [] ;
988 foreach my $d (@{ $self->{drive} }) {
989 $drives->[$i] = { index => $i,
990 load => $self->{drive}->[$i],
991 name => $self->{drive_name}->[$i],
996 $bweb->display({ Name => $self->{name},
997 nb_drive => $self->{info}->{drive},
998 nb_io => $self->{info}->{io},
1001 Update => scalar(@to_update) },
1009 ################################################################
1013 use base q/Bweb::Gui/;
1017 Bweb - main Bweb package
1021 this package is use to compute and display informations
1026 use POSIX qw/strftime/;
1028 our $config_file='/etc/bacula/bweb.conf';
1034 %sql_func - hash to make query mysql/postgresql compliant
1040 UNIX_TIMESTAMP => '',
1041 FROM_UNIXTIME => '',
1042 TO_SEC => " interval '1 second' * ",
1043 SEC_TO_INT => "SEC_TO_INT",
1046 STARTTIME_DAY => " date_trunc('day', Job.StartTime) ",
1047 STARTTIME_HOUR => " date_trunc('hour', Job.StartTime) ",
1048 STARTTIME_MONTH => " date_trunc('month', Job.StartTime) ",
1049 STARTTIME_PHOUR=> " date_part('hour', Job.StartTime) ",
1050 STARTTIME_PDAY => " date_part('day', Job.StartTime) ",
1051 STARTTIME_PMONTH => " date_part('month', Job.StartTime) ",
1054 UNIX_TIMESTAMP => 'UNIX_TIMESTAMP',
1055 FROM_UNIXTIME => 'FROM_UNIXTIME',
1058 SEC_TO_TIME => 'SEC_TO_TIME',
1059 MATCH => " REGEXP ",
1060 STARTTIME_DAY => " DATE_FORMAT(StartTime, '%Y-%m-%d') ",
1061 STARTTIME_HOUR => " DATE_FORMAT(StartTime, '%Y-%m-%d %H') ",
1062 STARTTIME_MONTH => " DATE_FORMAT(StartTime, '%Y-%m') ",
1063 STARTTIME_PHOUR=> " DATE_FORMAT(StartTime, '%H') ",
1064 STARTTIME_PDAY => " DATE_FORMAT(StartTime, '%d') ",
1065 STARTTIME_PMONTH => " DATE_FORMAT(StartTime, '%m') ",
1069 sub dbh_selectall_arrayref
1071 my ($self, $query) = @_;
1072 $self->connect_db();
1073 $self->debug($query);
1074 return $self->{dbh}->selectall_arrayref($query);
1079 my ($self, @what) = @_;
1080 return join(',', $self->dbh_quote(@what)) ;
1085 my ($self, @what) = @_;
1087 $self->connect_db();
1089 return map { $self->{dbh}->quote($_) } @what;
1091 return $self->{dbh}->quote($what[0]) ;
1097 my ($self, $query) = @_ ;
1098 $self->connect_db();
1099 $self->debug($query);
1100 return $self->{dbh}->do($query);
1103 sub dbh_selectall_hashref
1105 my ($self, $query, $join) = @_;
1107 $self->connect_db();
1108 $self->debug($query);
1109 return $self->{dbh}->selectall_hashref($query, $join) ;
1112 sub dbh_selectrow_hashref
1114 my ($self, $query) = @_;
1116 $self->connect_db();
1117 $self->debug($query);
1118 return $self->{dbh}->selectrow_hashref($query) ;
1124 my @unit = qw(b Kb Mb Gb Tb);
1125 my $val = shift || 0;
1127 my $format = '%i %s';
1128 while ($val / 1024 > 1) {
1132 $format = ($i>0)?'%0.1f %s':'%i %s';
1133 return sprintf($format, $val, $unit[$i]);
1136 # display Day, Hour, Year
1142 $val /= 60; # sec -> min
1144 if ($val / 60 <= 1) {
1148 $val /= 60; # min -> hour
1149 if ($val / 24 <= 1) {
1150 return "$val hours";
1153 $val /= 24; # hour -> day
1154 if ($val / 365 < 2) {
1158 $val /= 365 ; # day -> year
1160 return "$val years";
1163 # get Day, Hour, Year
1169 unless ($val =~ /^\s*(\d+)\s*(\w)\w*\s*$/) {
1173 my %times = ( m => 60,
1179 my $mult = $times{$2} || 0;
1189 unless ($self->{dbh}) {
1190 $self->{dbh} = DBI->connect($self->{info}->{dbi},
1191 $self->{info}->{user},
1192 $self->{info}->{password});
1194 $self->error("Can't connect to your database:\n$DBI::errstr\n")
1195 unless ($self->{dbh});
1197 $self->{dbh}->{FetchHashKeyName} = 'NAME_lc';
1199 if ($self->{info}->{dbi} =~ /^dbi:Pg/i) {
1200 $self->{dbh}->do("SET datestyle TO 'ISO, YMD'");
1207 my ($class, %arg) = @_;
1209 dbh => undef, # connect_db();
1211 dbi => '', # DBI:Pg:database=bacula;host=127.0.0.1
1217 map { $self->{lc($_)} = $arg{$_} } keys %arg ;
1219 if ($self->{info}->{dbi} =~ /DBI:(\w+):/i) {
1220 $self->{sql} = $sql_func{$1};
1223 $self->{debug} = $self->{info}->{debug};
1224 $Bweb::Gui::template_dir = $self->{info}->{template_dir};
1232 $self->display($self->{info}, "begin.tpl");
1238 $self->display($self->{info}, "end.tpl");
1246 my $arg = $self->get_form("client", "qre_client");
1248 if ($arg->{qre_client}) {
1249 $where = "WHERE Name $self->{sql}->{MATCH} $arg->{qre_client} ";
1250 } elsif ($arg->{client}) {
1251 $where = "WHERE Name = '$arg->{client}' ";
1255 SELECT Name AS name,
1257 AutoPrune AS autoprune,
1258 FileRetention AS fileretention,
1259 JobRetention AS jobretention
1264 my $all = $self->dbh_selectall_hashref($query, 'name') ;
1266 my $dsp = { ID => $cur_id++,
1267 clients => [ values %$all] };
1269 $self->display($dsp, "client_list.tpl") ;
1274 my ($self, %arg) = @_;
1281 "AND $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
1283 ( $self->{sql}->{UNIX_TIMESTAMP}(NOW())
1285 $self->{sql}->{TO_SEC}($arg{age})
1288 $label = "last " . human_sec($arg{age});
1291 if ($arg{groupby}) {
1292 $limit .= " GROUP BY $arg{groupby} ";
1296 $limit .= " ORDER BY $arg{order} ";
1300 $limit .= " LIMIT $arg{limit} ";
1301 $label .= " limited to $arg{limit}";
1305 $limit .= " OFFSET $arg{offset} ";
1306 $label .= " with $arg{offset} offset ";
1310 $label = 'no filter';
1313 return ($limit, $label);
1318 $bweb->get_form(...) - Get useful stuff
1322 This function get and check parameters against regexp.
1324 If word begin with 'q', the return will be quoted or join quoted
1325 if it's end with 's'.
1330 $bweb->get_form('jobid', 'qclient', 'qpools') ;
1333 qclient => 'plume-fd',
1334 qpools => "'plume-fd', 'test-fd', '...'",
1341 my ($self, @what) = @_;
1342 my %what = map { $_ => 1 } @what;
1362 my %opt_ss =( # string with space
1366 my %opt_s = ( # default to ''
1383 my %opt_p = ( # option with path
1391 my %opt_d = ( # option with date
1396 foreach my $i (@what) {
1397 if (exists $opt_i{$i}) {# integer param
1398 my $value = CGI::param($i) || $opt_i{$i} ;
1399 if ($value =~ /^(\d+)$/) {
1402 } elsif ($opt_s{$i}) { # simple string param
1403 my $value = CGI::param($i) || '';
1404 if ($value =~ /^([\w\d\.-]+)$/) {
1407 } elsif ($opt_ss{$i}) { # simple string param (with space)
1408 my $value = CGI::param($i) || '';
1409 if ($value =~ /^([\w\d\.\-\s]+)$/) {
1412 } elsif ($i =~ /^j(\w+)s$/) { # quote join args
1413 my @value = grep { ! /^\s*$/ } CGI::param($1) ;
1415 $ret{$i} = $self->dbh_join(@value) ;
1418 } elsif ($i =~ /^q(\w+[^s])$/) { # 'arg1'
1419 my $value = CGI::param($1) ;
1421 $ret{$i} = $self->dbh_quote($value);
1424 } elsif ($i =~ /^q(\w+)s$/) { #[ 'arg1', 'arg2']
1425 $ret{$i} = [ map { { name => $self->dbh_quote($_) } }
1426 grep { ! /^\s*$/ } CGI::param($1) ];
1427 } elsif (exists $opt_p{$i}) {
1428 my $value = CGI::param($i) || '';
1429 if ($value =~ /^([\w\d\.\/\s:\@\-]+)$/) {
1432 } elsif (exists $opt_d{$i}) {
1433 my $value = CGI::param($i) || '';
1434 if ($value =~ /^\s*(\d+\s+\w+)$/) {
1441 foreach my $s (CGI::param('slot')) {
1442 if ($s =~ /^(\d+)$/) {
1443 push @{$ret{slots}}, $s;
1449 my $when = CGI::param('when') || '';
1450 if ($when =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})$/) {
1455 if ($what{db_clients}) {
1457 SELECT Client.Name as clientname
1461 my $clients = $self->dbh_selectall_hashref($query, 'clientname');
1462 $ret{db_clients} = [sort {$a->{clientname} cmp $b->{clientname} }
1466 if ($what{db_mediatypes}) {
1468 SELECT MediaType as mediatype
1472 my $medias = $self->dbh_selectall_hashref($query, 'mediatype');
1473 $ret{db_mediatypes} = [sort {$a->{mediatype} cmp $b->{mediatype} }
1477 if ($what{db_locations}) {
1479 SELECT Location as location, Cost as cost FROM Location
1481 my $loc = $self->dbh_selectall_hashref($query, 'location');
1482 $ret{db_locations} = [ sort { $a->{location}
1488 if ($what{db_pools}) {
1489 my $query = "SELECT Name as name FROM Pool";
1491 my $all = $self->dbh_selectall_hashref($query, 'name') ;
1492 $ret{db_pools} = [ sort { $a->{name} cmp $b->{name} } values %$all ];
1495 if ($what{db_filesets}) {
1497 SELECT FileSet.FileSet AS fileset
1501 my $filesets = $self->dbh_selectall_hashref($query, 'fileset');
1503 $ret{db_filesets} = [sort {lc($a->{fileset}) cmp lc($b->{fileset}) }
1504 values %$filesets] ;
1507 if ($what{db_jobnames}) {
1509 SELECT DISTINCT Job.Name AS jobname
1513 my $jobnames = $self->dbh_selectall_hashref($query, 'jobname');
1515 $ret{db_jobnames} = [sort {lc($a->{jobname}) cmp lc($b->{jobname}) }
1516 values %$jobnames] ;
1519 if ($what{db_devices}) {
1521 SELECT Device.Name AS name
1525 my $devices = $self->dbh_selectall_hashref($query, 'name');
1527 $ret{db_devices} = [sort {lc($a->{name}) cmp lc($b->{name}) }
1538 my $fields = $self->get_form(qw/age level status clients filesets
1540 db_clients limit db_filesets width height
1541 qclients qfilesets qjobnames db_jobnames/);
1544 my $url = CGI::url(-full => 0,
1547 $url =~ s/^.+?\?//; # http://path/to/bweb.pl?arg => arg
1549 # this organisation is to keep user choice between 2 click
1550 # TODO : fileset and client selection doesn't work
1559 sub display_client_job
1561 my ($self, %arg) = @_ ;
1563 $arg{order} = ' Job.JobId DESC ';
1564 my ($limit, $label) = $self->get_limit(%arg);
1566 my $clientname = $self->dbh_quote($arg{clientname});
1569 SELECT DISTINCT Job.JobId AS jobid,
1570 Job.Name AS jobname,
1571 FileSet.FileSet AS fileset,
1573 StartTime AS starttime,
1574 JobFiles AS jobfiles,
1575 JobBytes AS jobbytes,
1576 JobStatus AS jobstatus,
1577 JobErrors AS joberrors
1579 FROM Client,Job,FileSet
1580 WHERE Client.Name=$clientname
1581 AND Client.ClientId=Job.ClientId
1582 AND Job.FileSetId=FileSet.FileSetId
1586 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
1588 $self->display({ clientname => $arg{clientname},
1591 Jobs => [ values %$all ],
1593 "display_client_job.tpl") ;
1596 sub get_selected_media_location
1600 my $medias = $self->get_form('jmedias');
1602 unless ($medias->{jmedias}) {
1607 SELECT Media.VolumeName AS volumename, Location.Location AS location
1608 FROM Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
1609 WHERE Media.VolumeName IN ($medias->{jmedias})
1612 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
1614 # { 'vol1' => { [volumename => 'vol1', location => 'ici'],
1625 my $medias = $self->get_selected_media_location();
1631 my $elt = $self->get_form('db_locations');
1633 $self->display({ ID => $cur_id++,
1634 %$elt, # db_locations
1636 sort { $a->{volumename} cmp $b->{volumename} } values %$medias
1646 my $elt = $self->get_form(qw/db_pools db_mediatypes db_locations/) ;
1648 $self->display($elt, "help_extern.tpl");
1651 sub help_extern_compute
1655 my $number = CGI::param('limit') || '' ;
1656 unless ($number =~ /^(\d+)$/) {
1657 return $self->error("Bad arg number : $number ");
1660 my ($sql, undef) = $self->get_param('pools',
1661 'locations', 'mediatypes');
1664 SELECT Media.VolumeName AS volumename,
1665 Media.VolStatus AS volstatus,
1666 Media.LastWritten AS lastwritten,
1667 Media.MediaType AS mediatype,
1668 Media.VolMounts AS volmounts,
1670 Media.Recycle AS recycle,
1671 $self->{sql}->{FROM_UNIXTIME}(
1672 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
1673 + $self->{sql}->{TO_SEC}(Media.VolRetention)
1676 INNER JOIN Pool ON (Pool.PoolId = Media.PoolId)
1677 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
1679 WHERE Media.InChanger = 1
1680 AND Media.VolStatus IN ('Disabled', 'Error', 'Full')
1682 ORDER BY expire DESC, recycle, Media.VolMounts DESC
1686 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
1688 $self->display({ Medias => [ values %$all ] },
1689 "help_extern_compute.tpl");
1696 my $param = $self->get_form(qw/db_locations db_pools db_mediatypes/) ;
1697 $self->display($param, "help_intern.tpl");
1700 sub help_intern_compute
1704 my $number = CGI::param('limit') || '' ;
1705 unless ($number =~ /^(\d+)$/) {
1706 return $self->error("Bad arg number : $number ");
1709 my ($sql, undef) = $self->get_param('pools', 'locations', 'mediatypes');
1711 if (CGI::param('expired')) {
1713 AND ( $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
1714 + $self->{sql}->{TO_SEC}(Media.VolRetention)
1720 SELECT Media.VolumeName AS volumename,
1721 Media.VolStatus AS volstatus,
1722 Media.LastWritten AS lastwritten,
1723 Media.MediaType AS mediatype,
1724 Media.VolMounts AS volmounts,
1726 $self->{sql}->{FROM_UNIXTIME}(
1727 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
1728 + $self->{sql}->{TO_SEC}(Media.VolRetention)
1731 INNER JOIN Pool ON (Pool.PoolId = Media.PoolId)
1732 LEFT JOIN Location ON (Location.LocationId = Media.LocationId)
1734 WHERE Media.InChanger <> 1
1735 AND Media.VolStatus IN ('Purged', 'Full', 'Append')
1736 AND Media.Recycle = 1
1738 ORDER BY Media.VolUseDuration DESC, Media.VolMounts ASC, expire ASC
1742 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
1744 $self->display({ Medias => [ values %$all ] },
1745 "help_intern_compute.tpl");
1751 my ($self, %arg) = @_ ;
1753 my ($limit, $label) = $self->get_limit(%arg);
1757 (SELECT count(Pool.PoolId) FROM Pool) AS nb_pool,
1758 (SELECT count(Media.MediaId) FROM Media) AS nb_media,
1759 (SELECT count(Job.JobId) FROM Job) AS nb_job,
1760 (SELECT sum(VolBytes) FROM Media) AS nb_bytes,
1761 (SELECT count(Job.JobId)
1763 WHERE Job.JobStatus IN ('E','e','f','A')
1766 (SELECT count(Client.ClientId) FROM Client) AS nb_client
1769 my $row = $self->dbh_selectrow_hashref($query) ;
1771 $row->{nb_bytes} = human_size($row->{nb_bytes});
1773 $row->{db_size} = '???';
1774 $row->{label} = $label;
1776 $self->display($row, "general.tpl");
1781 my ($self, @what) = @_ ;
1782 my %elt = map { $_ => 1 } @what;
1787 if ($elt{clients}) {
1788 my @clients = grep { ! /^\s*$/ } CGI::param('client');
1790 $ret{clients} = \@clients;
1791 my $str = $self->dbh_join(@clients);
1792 $limit .= "AND Client.Name IN ($str) ";
1796 if ($elt{filesets}) {
1797 my @filesets = grep { ! /^\s*$/ } CGI::param('fileset');
1799 $ret{filesets} = \@filesets;
1800 my $str = $self->dbh_join(@filesets);
1801 $limit .= "AND FileSet.FileSet IN ($str) ";
1805 if ($elt{mediatypes}) {
1806 my @medias = grep { ! /^\s*$/ } CGI::param('mediatype');
1808 $ret{mediatypes} = \@medias;
1809 my $str = $self->dbh_join(@medias);
1810 $limit .= "AND Media.MediaType IN ($str) ";
1815 my $client = CGI::param('client');
1816 $ret{client} = $client;
1817 $client = $self->dbh_join($client);
1818 $limit .= "AND Client.Name = $client ";
1822 my $level = CGI::param('level') || '';
1823 if ($level =~ /^(\w)$/) {
1825 $limit .= "AND Job.Level = '$1' ";
1830 my $jobid = CGI::param('jobid') || '';
1832 if ($jobid =~ /^(\d+)$/) {
1834 $limit .= "AND Job.JobId = '$1' ";
1839 my $status = CGI::param('status') || '';
1840 if ($status =~ /^(\w)$/) {
1843 $limit .= "AND Job.JobStatus IN ('f','E') ";
1844 } elsif ($1 eq 'W') {
1845 $limit .= "AND Job.JobStatus = 'T' AND Job.JobErrors > 0 ";
1847 $limit .= "AND Job.JobStatus = '$1' ";
1852 if ($elt{volstatus}) {
1853 my $status = CGI::param('volstatus') || '';
1854 if ($status =~ /^(\w+)$/) {
1856 $limit .= "AND Media.VolStatus = '$1' ";
1860 if ($elt{locations}) {
1861 my @location = grep { ! /^\s*$/ } CGI::param('location') ;
1863 $ret{locations} = \@location;
1864 my $str = $self->dbh_join(@location);
1865 $limit .= "AND Location.Location IN ($str) ";
1870 my @pool = grep { ! /^\s*$/ } CGI::param('pool') ;
1872 $ret{pools} = \@pool;
1873 my $str = $self->dbh_join(@pool);
1874 $limit .= "AND Pool.Name IN ($str) ";
1878 if ($elt{location}) {
1879 my $location = CGI::param('location') || '';
1881 $ret{location} = $location;
1882 $location = $self->dbh_quote($location);
1883 $limit .= "AND Location.Location = $location ";
1888 my $pool = CGI::param('pool') || '';
1891 $pool = $self->dbh_quote($pool);
1892 $limit .= "AND Pool.Name = $pool ";
1896 if ($elt{jobtype}) {
1897 my $jobtype = CGI::param('jobtype') || '';
1898 if ($jobtype =~ /^(\w)$/) {
1900 $limit .= "AND Job.Type = '$1' ";
1904 return ($limit, %ret);
1915 my ($self, %arg) = @_ ;
1917 $arg{order} = ' Job.JobId DESC ';
1919 my ($limit, $label) = $self->get_limit(%arg);
1920 my ($where, undef) = $self->get_param('clients',
1929 SELECT Job.JobId AS jobid,
1930 Client.Name AS client,
1931 FileSet.FileSet AS fileset,
1932 Job.Name AS jobname,
1934 StartTime AS starttime,
1936 Pool.Name AS poolname,
1937 JobFiles AS jobfiles,
1938 JobBytes AS jobbytes,
1939 JobStatus AS jobstatus,
1940 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
1941 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime))
1944 JobErrors AS joberrors
1947 Job LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId)
1948 LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId)
1949 WHERE Client.ClientId=Job.ClientId
1950 AND Job.JobStatus != 'R'
1955 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
1957 $self->display({ Filter => $label,
1961 sort { $a->{jobid} <=> $b->{jobid} }
1968 # display job informations
1969 sub display_job_zoom
1971 my ($self, $jobid) = @_ ;
1973 $jobid = $self->dbh_quote($jobid);
1976 SELECT DISTINCT Job.JobId AS jobid,
1977 Client.Name AS client,
1978 Job.Name AS jobname,
1979 FileSet.FileSet AS fileset,
1981 Pool.Name AS poolname,
1982 StartTime AS starttime,
1983 JobFiles AS jobfiles,
1984 JobBytes AS jobbytes,
1985 JobStatus AS jobstatus,
1986 JobErrors AS joberrors,
1987 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
1988 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime)) AS duration
1991 Job LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId)
1992 LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId)
1993 WHERE Client.ClientId=Job.ClientId
1994 AND Job.JobId = $jobid
1997 my $row = $self->dbh_selectrow_hashref($query) ;
1999 # display all volumes associate with this job
2001 SELECT Media.VolumeName as volumename
2002 FROM Job,Media,JobMedia
2003 WHERE Job.JobId = $jobid
2004 AND JobMedia.JobId=Job.JobId
2005 AND JobMedia.MediaId=Media.MediaId
2008 my $all = $self->dbh_selectall_hashref($query, 'volumename');
2010 $row->{volumes} = [ values %$all ] ;
2012 $self->display($row, "display_job_zoom.tpl");
2019 my ($where, %elt) = $self->get_param('pools',
2024 my $arg = $self->get_form('jmedias', 'qre_media');
2026 if ($arg->{jmedias}) {
2027 $where = "AND Media.VolumeName IN ($arg->{jmedias}) $where";
2029 if ($arg->{qre_media}) {
2030 $where = "AND Media.VolumeName $self->{sql}->{MATCH} $arg->{qre_media} $where";
2034 SELECT Media.VolumeName AS volumename,
2035 Media.VolBytes AS volbytes,
2036 Media.VolStatus AS volstatus,
2037 Media.MediaType AS mediatype,
2038 Media.InChanger AS online,
2039 Media.LastWritten AS lastwritten,
2040 Location.Location AS location,
2041 (volbytes*100/COALESCE(media_avg_size.size,-1)) AS volusage,
2042 Pool.Name AS poolname,
2043 $self->{sql}->{FROM_UNIXTIME}(
2044 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2045 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2048 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2049 LEFT JOIN (SELECT avg(Media.VolBytes) AS size,
2050 Media.MediaType AS MediaType
2052 WHERE Media.VolStatus = 'Full'
2053 GROUP BY Media.MediaType
2054 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
2056 WHERE Media.PoolId=Pool.PoolId
2060 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2062 $self->display({ ID => $cur_id++,
2064 Location => $elt{location},
2065 Medias => [ values %$all ]
2067 "display_media.tpl");
2074 my $pool = $self->get_form('db_pools');
2076 foreach my $name (@{ $pool->{db_pools} }) {
2077 CGI::param('pool', $name->{name});
2078 $self->display_media();
2082 sub display_media_zoom
2086 my $medias = $self->get_form('jmedias');
2088 unless ($medias->{jmedias}) {
2089 return $self->error("Can't get media selection");
2093 SELECT InChanger AS online,
2094 VolBytes AS nb_bytes,
2095 VolumeName AS volumename,
2096 VolStatus AS volstatus,
2097 VolMounts AS nb_mounts,
2098 Media.VolUseDuration AS voluseduration,
2099 Media.MaxVolJobs AS maxvoljobs,
2100 Media.MaxVolFiles AS maxvolfiles,
2101 Media.MaxVolBytes AS maxvolbytes,
2102 VolErrors AS nb_errors,
2103 Pool.Name AS poolname,
2104 Location.Location AS location,
2105 Media.Recycle AS recycle,
2106 Media.VolRetention AS volretention,
2107 Media.LastWritten AS lastwritten,
2108 Media.VolReadTime/1000000 AS volreadtime,
2109 Media.VolWriteTime/1000000 AS volwritetime,
2110 Media.RecycleCount AS recyclecount,
2111 Media.Comment AS comment,
2112 $self->{sql}->{FROM_UNIXTIME}(
2113 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2114 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2117 Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2118 WHERE Pool.PoolId = Media.PoolId
2119 AND VolumeName IN ($medias->{jmedias})
2122 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2124 foreach my $media (values %$all) {
2125 my $mq = $self->dbh_quote($media->{volumename});
2128 SELECT DISTINCT Job.JobId AS jobid,
2130 Job.StartTime AS starttime,
2133 Job.JobFiles AS files,
2134 Job.JobBytes AS bytes,
2135 Job.jobstatus AS status
2136 FROM Media,JobMedia,Job
2137 WHERE Media.VolumeName=$mq
2138 AND Media.MediaId=JobMedia.MediaId
2139 AND JobMedia.JobId=Job.JobId
2142 my $jobs = $self->dbh_selectall_hashref($query, 'jobid') ;
2145 SELECT LocationLog.Date AS date,
2146 Location.Location AS location,
2147 LocationLog.Comment AS comment
2148 FROM Media,LocationLog INNER JOIN Location ON (LocationLog.LocationId = Location.LocationId)
2149 WHERE Media.MediaId = LocationLog.MediaId
2150 AND Media.VolumeName = $mq
2154 my $log = $self->dbh_selectall_arrayref($query) ;
2156 $logtxt = join("\n", map { ($_->[0] . ' ' . $_->[1] . ' ' . $_->[2])} @$log ) ;
2159 $self->display({ jobs => [ values %$jobs ],
2160 LocationLog => $logtxt,
2162 "display_media_zoom.tpl");
2170 my $loc = $self->get_form('qlocation');
2171 unless ($loc->{qlocation}) {
2172 return $self->error("Can't get location");
2176 SELECT Location.Location AS location,
2177 Location.Cost AS cost,
2178 Location.Enabled AS enabled
2180 WHERE Location.Location = $loc->{qlocation}
2183 my $row = $self->dbh_selectrow_hashref($query);
2185 $self->display({ ID => $cur_id++,
2186 %$row }, "location_edit.tpl") ;
2194 my $arg = $self->get_form(qw/qlocation qnewlocation cost/) ;
2195 unless ($arg->{qlocation}) {
2196 return $self->error("Can't get location");
2198 unless ($arg->{qnewlocation}) {
2199 return $self->error("Can't get new location name");
2201 unless ($arg->{cost}) {
2202 return $self->error("Can't get new cost");
2205 my $enabled = CGI::param('enabled') || '';
2206 $enabled = $enabled?1:0;
2209 UPDATE Location SET Cost = $arg->{cost},
2210 Location = $arg->{qnewlocation},
2212 WHERE Location.Location = $arg->{qlocation}
2215 $self->dbh_do($query);
2217 $self->display_location();
2223 my $arg = $self->get_form(qw/qlocation/) ;
2225 unless ($arg->{qlocation}) {
2226 return $self->error("Can't get location");
2230 SELECT count(Media.MediaId) AS nb
2231 FROM Media INNER JOIN Location USING (LocationID)
2232 WHERE Location = $arg->{qlocation}
2235 my $res = $self->dbh_selectrow_hashref($query);
2238 return $self->error("Sorry, the location must be empty");
2242 DELETE FROM Location WHERE Location = $arg->{qlocation} LIMIT 1
2245 $self->dbh_do($query);
2247 $self->display_location();
2254 my $arg = $self->get_form(qw/qlocation cost/) ;
2256 unless ($arg->{qlocation}) {
2257 $self->display({}, "location_add.tpl");
2260 unless ($arg->{cost}) {
2261 return $self->error("Can't get new cost");
2264 my $enabled = CGI::param('enabled') || '';
2265 $enabled = $enabled?1:0;
2268 INSERT INTO Location (Location, Cost, Enabled)
2269 VALUES ($arg->{qlocation}, $arg->{cost}, $enabled)
2272 $self->dbh_do($query);
2274 $self->display_location();
2277 sub display_location
2282 SELECT Location.Location AS location,
2283 Location.Cost AS cost,
2284 Location.Enabled AS enabled,
2285 (SELECT count(Media.MediaId)
2287 WHERE Media.LocationId = Location.LocationId
2292 my $location = $self->dbh_selectall_hashref($query, 'location');
2294 $self->display({ ID => $cur_id++,
2295 Locations => [ values %$location ] },
2296 "display_location.tpl");
2303 my $medias = $self->get_selected_media_location();
2308 my $arg = $self->get_form('db_locations', 'qnewlocation');
2310 $self->display({ email => $self->{info}->{email_media},
2312 medias => [ values %$medias ],
2314 "update_location.tpl");
2317 sub get_media_max_size
2319 my ($self, $type) = @_;
2321 "SELECT avg(VolBytes) AS size
2323 WHERE Media.VolStatus = 'Full'
2324 AND Media.MediaType = '$type'
2327 my $res = $self->selectrow_hashref($query);
2330 return $res->{size};
2340 my $media = $self->get_form('qmedia');
2342 unless ($media->{qmedia}) {
2343 return $self->error("Can't get media");
2347 SELECT Media.Slot AS slot,
2348 PoolMedia.Name AS poolname,
2349 Media.VolStatus AS volstatus,
2350 Media.InChanger AS inchanger,
2351 Location.Location AS location,
2352 Media.VolumeName AS volumename,
2353 Media.MaxVolBytes AS maxvolbytes,
2354 Media.MaxVolJobs AS maxvoljobs,
2355 Media.MaxVolFiles AS maxvolfiles,
2356 Media.VolUseDuration AS voluseduration,
2357 Media.VolRetention AS volretention,
2358 Media.Comment AS comment,
2359 PoolRecycle.Name AS poolrecycle
2361 FROM Media INNER JOIN Pool AS PoolMedia ON (Media.PoolId = PoolMedia.PoolId)
2362 LEFT JOIN Pool AS PoolRecycle ON (Media.RecyclePoolId = PoolRecycle.PoolId)
2363 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2365 WHERE Media.VolumeName = $media->{qmedia}
2368 my $row = $self->dbh_selectrow_hashref($query);
2369 $row->{volretention} = human_sec($row->{volretention});
2370 $row->{voluseduration} = human_sec($row->{voluseduration});
2372 my $elt = $self->get_form(qw/db_pools db_locations/);
2377 }, "update_media.tpl");
2384 my $arg = $self->get_form('jmedias', 'qnewlocation') ;
2386 unless ($arg->{jmedias}) {
2387 return $self->error("Can't get selected media");
2390 unless ($arg->{qnewlocation}) {
2391 return $self->error("Can't get new location");
2396 SET LocationId = (SELECT LocationId
2398 WHERE Location = $arg->{qnewlocation})
2399 WHERE Media.VolumeName IN ($arg->{jmedias})
2402 my $nb = $self->dbh_do($query);
2404 print "$nb media updated, you may have to update your autochanger.";
2406 $self->display_media();
2413 my $medias = $self->get_selected_media_location();
2415 return $self->error("Can't get media selection");
2417 my $newloc = CGI::param('newlocation');
2419 my $user = CGI::param('user') || 'unknown';
2420 my $comm = CGI::param('comment') || '';
2421 $comm = $self->dbh_quote("$user: $comm");
2425 foreach my $media (keys %$medias) {
2427 INSERT LocationLog (Date, Comment, MediaId, LocationId, NewVolStatus)
2429 NOW(), $comm, (SELECT MediaId FROM Media WHERE VolumeName = '$media'),
2430 (SELECT LocationId FROM Location WHERE Location = '$medias->{$media}->{location}'),
2431 (SELECT VolStatus FROM Media WHERE VolumeName = '$media')
2434 $self->dbh_do($query);
2435 $self->debug($query);
2439 $q->param('action', 'update_location');
2440 my $url = $q->url(-full => 1, -query=>1);
2442 $self->display({ email => $self->{info}->{email_media},
2444 newlocation => $newloc,
2445 # [ { volumename => 'vol1' }, { volumename => 'vol2'
\81 },..]
2446 medias => [ values %$medias ],
2448 "change_location.tpl");
2452 sub display_client_stats
2454 my ($self, %arg) = @_ ;
2456 my $client = $self->dbh_quote($arg{clientname});
2457 my ($limit, $label) = $self->get_limit(%arg);
2461 count(Job.JobId) AS nb_jobs,
2462 sum(Job.JobBytes) AS nb_bytes,
2463 sum(Job.JobErrors) AS nb_err,
2464 sum(Job.JobFiles) AS nb_files,
2465 Client.Name AS clientname
2466 FROM Job INNER JOIN Client USING (ClientId)
2468 Client.Name = $client
2470 GROUP BY Client.Name
2473 my $row = $self->dbh_selectrow_hashref($query);
2475 $row->{ID} = $cur_id++;
2476 $row->{label} = $label;
2478 $self->display($row, "display_client_stats.tpl");
2481 # poolname can be undef
2484 my ($self, $poolname) = @_ ;
2488 my $arg = $self->get_form('jmediatypes', 'qmediatypes');
2489 if ($arg->{jmediatypes}) {
2490 $whereW = "WHERE MediaType IN ($arg->{jmediatypes}) ";
2491 $whereA = "AND MediaType IN ($arg->{jmediatypes}) ";
2494 # TODO : afficher les tailles et les dates
2497 SELECT subq.volmax AS volmax,
2498 subq.volnum AS volnum,
2499 subq.voltotal AS voltotal,
2501 Pool.Recycle AS recycle,
2502 Pool.VolRetention AS volretention,
2503 Pool.VolUseDuration AS voluseduration,
2504 Pool.MaxVolJobs AS maxvoljobs,
2505 Pool.MaxVolFiles AS maxvolfiles,
2506 Pool.MaxVolBytes AS maxvolbytes,
2507 subq.PoolId AS PoolId
2510 SELECT COALESCE(media_avg_size.volavg,0) * count(Media.MediaId) AS volmax,
2511 count(Media.MediaId) AS volnum,
2512 sum(Media.VolBytes) AS voltotal,
2513 Media.PoolId AS PoolId,
2514 Media.MediaType AS MediaType
2516 LEFT JOIN (SELECT avg(Media.VolBytes) AS volavg,
2517 Media.MediaType AS MediaType
2519 WHERE Media.VolStatus = 'Full'
2520 GROUP BY Media.MediaType
2521 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
2522 GROUP BY Media.MediaType, Media.PoolId, media_avg_size.volavg
2524 LEFT JOIN Pool ON (Pool.PoolId = subq.PoolId)
2528 my $all = $self->dbh_selectall_hashref($query, 'name') ;
2531 SELECT Pool.Name AS name,
2532 sum(VolBytes) AS size
2533 FROM Media JOIN Pool ON (Media.PoolId = Pool.PoolId)
2534 WHERE Media.VolStatus IN ('Recycled', 'Purged')
2538 my $empty = $self->dbh_selectall_hashref($query, 'name');
2540 foreach my $p (values %$all) {
2541 if ($p->{volmax} > 0) { # mysql returns 0.0000
2542 # we remove Recycled/Purged media from pool usage
2543 if (defined $empty->{$p->{name}}) {
2544 $p->{voltotal} -= $empty->{$p->{name}}->{size};
2546 $p->{poolusage} = sprintf('%.2f', $p->{voltotal} * 100/ $p->{volmax}) ;
2548 $p->{poolusage} = 0;
2552 SELECT VolStatus AS volstatus, count(MediaId) AS nb
2554 WHERE PoolId=$p->{poolid}
2558 my $content = $self->dbh_selectall_hashref($query, 'volstatus');
2559 foreach my $t (values %$content) {
2560 $p->{"nb_" . $t->{volstatus}} = $t->{nb} ;
2565 $self->display({ ID => $cur_id++,
2566 MediaType => $arg->{qmediatypes}, # [ { name => type1 } , { name => type2 } ]
2567 Pools => [ values %$all ]},
2568 "display_pool.tpl");
2571 sub display_running_job
2575 my $arg = $self->get_form('client', 'jobid');
2577 if (!$arg->{client} and $arg->{jobid}) {
2580 SELECT Client.Name AS name
2581 FROM Job INNER JOIN Client USING (ClientId)
2582 WHERE Job.JobId = $arg->{jobid}
2585 my $row = $self->dbh_selectrow_hashref($query);
2588 $arg->{client} = $row->{name};
2589 CGI::param('client', $arg->{client});
2593 if ($arg->{client}) {
2594 my $cli = new Bweb::Client(name => $arg->{client});
2595 $cli->display_running_job($self->{info}, $arg->{jobid});
2596 if ($arg->{jobid}) {
2597 $self->get_job_log();
2600 $self->error("Can't get client or jobid");
2604 sub display_running_jobs
2606 my ($self, $display_action) = @_;
2609 SELECT Job.JobId AS jobid,
2610 Job.Name AS jobname,
2612 Job.StartTime AS starttime,
2613 Job.JobFiles AS jobfiles,
2614 Job.JobBytes AS jobbytes,
2615 Job.JobStatus AS jobstatus,
2616 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(NOW())
2617 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime))
2619 Client.Name AS clientname
2620 FROM Job INNER JOIN Client USING (ClientId)
2621 WHERE JobStatus IN ('C','R','B','e','D','F','S','m','M','s','j','c','d','t','p')
2623 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
2625 $self->display({ ID => $cur_id++,
2626 display_action => $display_action,
2627 Jobs => [ values %$all ]},
2628 "running_job.tpl") ;
2631 # return the autochanger list to update
2636 my $arg = $self->get_form('jmedias');
2638 unless ($arg->{jmedias}) {
2639 return $self->error("Can't get media selection");
2643 SELECT Media.VolumeName AS volumename,
2644 Storage.Name AS storage,
2645 Location.Location AS location,
2647 FROM Media INNER JOIN Storage ON (Media.StorageId = Storage.StorageId)
2648 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2649 WHERE Media.VolumeName IN ($arg->{jmedias})
2650 AND Media.InChanger = 1
2653 my $all = $self->dbh_selectall_hashref($query, 'volumename');
2655 foreach my $vol (values %$all) {
2656 my $a = $self->ach_get($vol->{location});
2658 $ret{$vol->{location}} = 1;
2660 unless ($a->{have_status}) {
2662 $a->{have_status} = 1;
2665 print "eject $vol->{volumename} from $vol->{storage} : ";
2666 if ($a->send_to_io($vol->{slot})) {
2667 print "<img src='/bweb/T.png' alt='ok'><br/>";
2669 print "<img src='/bweb/E.png' alt='err'><br/>";
2679 my ($to, $subject, $content) = (CGI::param('email'),
2680 CGI::param('subject'),
2681 CGI::param('content'));
2682 $to =~ s/[^\w\d\.\@<>,]//;
2683 $subject =~ s/[^\w\d\.\[\]]/ /;
2685 open(MAIL, "|mail -s '$subject' '$to'") ;
2686 print MAIL $content;
2696 my $arg = $self->get_form('jobid', 'client');
2698 print CGI::header('text/brestore');
2699 print "jobid=$arg->{jobid}\n" if ($arg->{jobid});
2700 print "client=$arg->{client}\n" if ($arg->{client});
2701 print "\n\nYou have to assign this mime type with /usr/bin/brestore.pl\n";
2705 # TODO : move this to Bweb::Autochanger ?
2706 # TODO : make this internal to not eject tape ?
2712 my ($self, $name) = @_;
2715 return $self->error("Can't get your autochanger name ach");
2718 unless ($self->{info}->{ach_list}) {
2719 return $self->error("Could not find any autochanger");
2722 my $a = $self->{info}->{ach_list}->{$name};
2725 $self->error("Can't get your autochanger $name from your ach_list");
2730 $a->{debug} = $self->{debug};
2737 my ($self, $ach) = @_;
2739 $self->{info}->{ach_list}->{$ach->{name}} = $ach;
2741 $self->{info}->save();
2749 my $arg = $self->get_form('ach');
2751 or !$self->{info}->{ach_list}
2752 or !$self->{info}->{ach_list}->{$arg->{ach}})
2754 return $self->error("Can't get autochanger name");
2757 my $ach = $self->{info}->{ach_list}->{$arg->{ach}};
2761 [ map { { name => $_, index => $i++ } } @{$ach->{drive_name}} ] ;
2763 my $b = $self->get_bconsole();
2765 my @storages = $b->list_storage() ;
2767 $ach->{devices} = [ map { { name => $_ } } @storages ];
2769 $self->display($ach, "ach_add.tpl");
2770 delete $ach->{drives};
2771 delete $ach->{devices};
2778 my $arg = $self->get_form('ach');
2781 or !$self->{info}->{ach_list}
2782 or !$self->{info}->{ach_list}->{$arg->{ach}})
2784 return $self->error("Can't get autochanger name");
2787 delete $self->{info}->{ach_list}->{$arg->{ach}} ;
2789 $self->{info}->save();
2790 $self->{info}->view();
2796 my $arg = $self->get_form('ach', 'mtxcmd', 'device', 'precmd');
2798 my $b = $self->get_bconsole();
2799 my @storages = $b->list_storage() ;
2801 unless ($arg->{ach}) {
2802 $arg->{devices} = [ map { { name => $_ } } @storages ];
2803 return $self->display($arg, "ach_add.tpl");
2807 foreach my $drive (CGI::param('drives'))
2809 unless (grep(/^$drive$/,@storages)) {
2810 return $self->error("Can't find $drive in storage list");
2813 my $index = CGI::param("index_$drive");
2814 unless (defined $index and $index =~ /^(\d+)$/) {
2815 return $self->error("Can't get $drive index");
2818 $drives[$index] = $drive;
2822 return $self->error("Can't get drives from Autochanger");
2825 my $a = new Bweb::Autochanger(name => $arg->{ach},
2826 precmd => $arg->{precmd},
2827 drive_name => \@drives,
2828 device => $arg->{device},
2829 mtxcmd => $arg->{mtxcmd});
2831 $self->ach_register($a) ;
2833 $self->{info}->view();
2839 my $arg = $self->get_form('jobid');
2841 if ($arg->{jobid}) {
2842 my $b = $self->get_bconsole();
2843 my $ret = $b->send_cmd("delete jobid=\"$arg->{jobid}\"");
2847 title => "Delete a job ",
2848 name => "delete jobid=$arg->{jobid}",
2857 my $arg = $self->get_form(qw/media volstatus inchanger pool
2858 slot volretention voluseduration
2859 maxvoljobs maxvolfiles maxvolbytes
2860 qcomment poolrecycle
2863 unless ($arg->{media}) {
2864 return $self->error("Can't find media selection");
2867 my $update = "update volume=$arg->{media} ";
2869 if ($arg->{volstatus}) {
2870 $update .= " volstatus=$arg->{volstatus} ";
2873 if ($arg->{inchanger}) {
2874 $update .= " inchanger=yes " ;
2876 $update .= " slot=$arg->{slot} ";
2879 $update .= " slot=0 inchanger=no ";
2883 $update .= " pool=$arg->{pool} " ;
2886 if (defined $arg->{volretention}) {
2887 $update .= " volretention=\"$arg->{volretention}\" " ;
2890 if (defined $arg->{voluseduration}) {
2891 $update .= " voluse=\"$arg->{voluseduration}\" " ;
2894 if (defined $arg->{maxvoljobs}) {
2895 $update .= " maxvoljobs=$arg->{maxvoljobs} " ;
2898 if (defined $arg->{maxvolfiles}) {
2899 $update .= " maxvolfiles=$arg->{maxvolfiles} " ;
2902 if (defined $arg->{maxvolbytes}) {
2903 $update .= " maxvolbytes=$arg->{maxvolbytes} " ;
2906 my $b = $self->get_bconsole();
2909 content => $b->send_cmd($update),
2910 title => "Update a volume ",
2916 my $media = $self->dbh_quote($arg->{media});
2918 my $loc = CGI::param('location') || '';
2920 $loc = $self->dbh_quote($loc); # is checked by db
2921 push @q, "LocationId=(SELECT LocationId FROM Location WHERE Location=$loc)";
2923 if ($arg->{poolrecycle}) {
2924 push @q, "RecyclePoolId=(SELECT PoolId FROM Pool WHERE Name='$arg->{poolrecycle}')";
2926 if (!$arg->{qcomment}) {
2927 $arg->{qcomment} = "''";
2929 push @q, "Comment=$arg->{qcomment}";
2934 SET " . join (',', @q) . "
2935 WHERE Media.VolumeName = $media
2937 $self->dbh_do($query);
2939 $self->update_media();
2946 my $ach = CGI::param('ach') ;
2947 $ach = $self->ach_get($ach);
2949 return $self->error("Bad autochanger name");
2953 my $b = new Bconsole(pref => $self->{info},timeout => 60,log_stdout => 1);
2954 $b->update_slots($ach->{name});
2962 my $arg = $self->get_form('jobid', 'limit', 'offset');
2963 unless ($arg->{jobid}) {
2964 return $self->error("Can't get jobid");
2967 if ($arg->{limit} == 100) {
2968 $arg->{limit} = 1000;
2971 my $t = CGI::param('time') || $self->{info}->{display_log_time} || '';
2974 SELECT Job.Name as name, Client.Name as clientname
2975 FROM Job INNER JOIN Client ON (Job.ClientId = Client.ClientId)
2976 WHERE JobId = $arg->{jobid}
2979 my $row = $self->dbh_selectrow_hashref($query);
2982 return $self->error("Can't find $arg->{jobid} in catalog");
2986 SELECT Time AS time, LogText AS log
2988 WHERE Log.JobId = $arg->{jobid}
2989 OR (Log.JobId = 0 AND Time >= (SELECT StartTime FROM Job WHERE JobId=$arg->{jobid})
2990 AND Time <= (SELECT COALESCE(EndTime,NOW()) FROM Job WHERE JobId=$arg->{jobid})
2994 OFFSET $arg->{offset}
2997 my $log = $self->dbh_selectall_arrayref($query);
2999 return $self->error("Can't get log for jobid $arg->{jobid}");
3005 $logtxt = join("", map { ($_->[0] . ' ' . $_->[1]) } @$log ) ;
3007 $logtxt = join("", map { $_->[1] } @$log ) ;
3010 $self->display({ lines=> $logtxt,
3011 jobid => $arg->{jobid},
3012 name => $row->{name},
3013 client => $row->{clientname},
3014 offset => $arg->{offset},
3015 limit => $arg->{limit},
3016 }, 'display_log.tpl');
3024 my $arg = $self->get_form('ach', 'slots', 'drive');
3026 unless ($arg->{ach}) {
3027 return $self->error("Can't find autochanger name");
3030 my $a = $self->ach_get($arg->{ach});
3032 return $self->error("Can't find autochanger name in configuration");
3035 my $storage = $a->get_drive_name($arg->{drive});
3037 return $self->error("Can't get your drive name");
3042 if ($arg->{slots}) {
3043 $slots = join(",", @{ $arg->{slots} });
3044 $t += 60*scalar( @{ $arg->{slots} }) ;
3047 my $b = new Bconsole(pref => $self->{info}, timeout => $t,log_stdout => 1);
3048 print "<h1>This command can take long time, be patient...</h1>";
3050 $b->label_barcodes(storage => $storage,
3051 drive => $arg->{drive},
3059 SET LocationId = (SELECT LocationId
3061 WHERE Location = '$arg->{ach}'),
3063 RecyclePoolId = PoolId
3065 WHERE Media.PoolId = (SELECT PoolId
3067 WHERE Name = 'Scratch')
3068 AND (LocationId = 0 OR LocationId IS NULL)
3077 my @volume = CGI::param('media');
3080 return $self->error("Can't get media selection");
3083 my $b = new Bconsole(pref => $self->{info}, timeout => 60);
3086 content => $b->purge_volume(@volume),
3087 title => "Purge media",
3088 name => "purge volume=" . join(' volume=', @volume),
3097 my @volume = CGI::param('media');
3099 return $self->error("Can't get media selection");
3102 my $b = new Bconsole(pref => $self->{info}, timeout => 60);
3105 content => $b->prune_volume(@volume),
3106 title => "Prune media",
3107 name => "prune volume=" . join(' volume=', @volume),
3117 my $arg = $self->get_form('jobid');
3118 unless ($arg->{jobid}) {
3119 return $self->error("Can't get jobid");
3122 my $b = $self->get_bconsole();
3124 content => $b->cancel($arg->{jobid}),
3125 title => "Cancel job",
3126 name => "cancel jobid=$arg->{jobid}",
3132 # Warning, we display current fileset
3135 my $arg = $self->get_form('fileset');
3137 if ($arg->{fileset}) {
3138 my $b = $self->get_bconsole();
3139 my $ret = $b->get_fileset($arg->{fileset});
3140 $self->display({ fileset => $arg->{fileset},
3142 }, "fileset_view.tpl");
3144 $self->error("Can't get fileset name");
3148 sub director_show_sched
3152 my $arg = $self->get_form('days');
3154 my $b = $self->get_bconsole();
3155 my $ret = $b->director_get_sched( $arg->{days} );
3160 }, "scheduled_job.tpl");
3163 sub enable_disable_job
3165 my ($self, $what) = @_ ;
3167 my $name = CGI::param('job') || '';
3168 unless ($name =~ /^[\w\d\.\-\s]+$/) {
3169 return $self->error("Can't find job name");
3172 my $b = $self->get_bconsole();
3182 content => $b->send_cmd("$cmd job=\"$name\""),
3183 title => "$cmd $name",
3184 name => "$cmd job=\"$name\"",
3191 return new Bconsole(pref => $self->{info});
3197 my $b = $self->get_bconsole();
3199 my $joblist = [ map { { name => $_ } } $b->list_job() ];
3201 $self->display({ Jobs => $joblist }, "run_job.tpl");
3206 my ($self, $ouput) = @_;
3209 foreach my $l (split(/\r\n/, $ouput)) {
3210 if ($l =~ /(\w+): name=([\w\d\.\s-]+?)(\s+\w+=.+)?$/) {
3216 if (my @l = $l =~ /(\w+)=([\w\d*]+)/g) {
3222 foreach my $k (keys %arg) {
3223 $lowcase{lc($k)} = $arg{$k} ;
3232 my $b = $self->get_bconsole();
3234 my $job = CGI::param('job') || '';
3236 my $info = $b->send_cmd("show job=\"$job\"");
3237 my $attr = $self->run_parse_job($info);
3239 my $jobs = [ map {{ name => $_ }} $b->list_job() ];
3241 my $pools = [ map { { name => $_ } } $b->list_pool() ];
3242 my $clients = [ map { { name => $_ } }$b->list_client()];
3243 my $filesets= [ map { { name => $_ } }$b->list_fileset() ];
3244 my $storages= [ map { { name => $_ } }$b->list_storage()];
3249 clients => $clients,
3250 filesets => $filesets,
3251 storages => $storages,
3253 }, "run_job_mod.tpl");
3259 my $b = $self->get_bconsole();
3261 my $jobs = [ map {{ name => $_ }} $b->list_job() ];
3271 my $b = $self->get_bconsole();
3273 # TODO: check input (don't use pool, level)
3275 my $arg = $self->get_form('pool', 'level', 'client', 'priority', 'when');
3276 my $job = CGI::param('job') || '';
3277 my $storage = CGI::param('storage') || '';
3279 my $jobid = $b->run(job => $job,
3280 client => $arg->{client},
3281 priority => $arg->{priority},
3282 level => $arg->{level},
3283 storage => $storage,
3284 pool => $arg->{pool},
3285 when => $arg->{when},
3288 print $jobid, $b->{error};
3290 print "<br>You can follow job execution <a href='?action=dsp_cur_job;client=$arg->{client};jobid=$jobid'> here </a>";