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) ",
1052 DB_SIZE => " SELECT pg_database_size(current_database()) ",
1055 UNIX_TIMESTAMP => 'UNIX_TIMESTAMP',
1056 FROM_UNIXTIME => 'FROM_UNIXTIME',
1059 SEC_TO_TIME => 'SEC_TO_TIME',
1060 MATCH => " REGEXP ",
1061 STARTTIME_DAY => " DATE_FORMAT(StartTime, '%Y-%m-%d') ",
1062 STARTTIME_HOUR => " DATE_FORMAT(StartTime, '%Y-%m-%d %H') ",
1063 STARTTIME_MONTH => " DATE_FORMAT(StartTime, '%Y-%m') ",
1064 STARTTIME_PHOUR=> " DATE_FORMAT(StartTime, '%H') ",
1065 STARTTIME_PDAY => " DATE_FORMAT(StartTime, '%d') ",
1066 STARTTIME_PMONTH => " DATE_FORMAT(StartTime, '%m') ",
1067 # with mysql < 5, you have to play with the ugly SHOW command
1068 DB_SIZE => " SELECT 0 ",
1069 # works only with mysql 5
1070 # DB_SIZE => " SELECT sum(DATA_LENGTH) FROM INFORMATION_SCHEMA.TABLES ",
1078 $self->{dbh}->disconnect();
1083 sub dbh_selectall_arrayref
1085 my ($self, $query) = @_;
1086 $self->connect_db();
1087 $self->debug($query);
1088 return $self->{dbh}->selectall_arrayref($query);
1093 my ($self, @what) = @_;
1094 return join(',', $self->dbh_quote(@what)) ;
1099 my ($self, @what) = @_;
1101 $self->connect_db();
1103 return map { $self->{dbh}->quote($_) } @what;
1105 return $self->{dbh}->quote($what[0]) ;
1111 my ($self, $query) = @_ ;
1112 $self->connect_db();
1113 $self->debug($query);
1114 return $self->{dbh}->do($query);
1117 sub dbh_selectall_hashref
1119 my ($self, $query, $join) = @_;
1121 $self->connect_db();
1122 $self->debug($query);
1123 return $self->{dbh}->selectall_hashref($query, $join) ;
1126 sub dbh_selectrow_hashref
1128 my ($self, $query) = @_;
1130 $self->connect_db();
1131 $self->debug($query);
1132 return $self->{dbh}->selectrow_hashref($query) ;
1138 my @unit = qw(b Kb Mb Gb Tb);
1139 my $val = shift || 0;
1141 my $format = '%i %s';
1142 while ($val / 1024 > 1) {
1146 $format = ($i>0)?'%0.1f %s':'%i %s';
1147 return sprintf($format, $val, $unit[$i]);
1150 # display Day, Hour, Year
1156 $val /= 60; # sec -> min
1158 if ($val / 60 <= 1) {
1162 $val /= 60; # min -> hour
1163 if ($val / 24 <= 1) {
1164 return "$val hours";
1167 $val /= 24; # hour -> day
1168 if ($val / 365 < 2) {
1172 $val /= 365 ; # day -> year
1174 return "$val years";
1177 # get Day, Hour, Year
1183 unless ($val =~ /^\s*(\d+)\s*(\w)\w*\s*$/) {
1187 my %times = ( m => 60,
1193 my $mult = $times{$2} || 0;
1203 unless ($self->{dbh}) {
1204 $self->{dbh} = DBI->connect($self->{info}->{dbi},
1205 $self->{info}->{user},
1206 $self->{info}->{password});
1208 $self->error("Can't connect to your database:\n$DBI::errstr\n")
1209 unless ($self->{dbh});
1211 $self->{dbh}->{FetchHashKeyName} = 'NAME_lc';
1213 if ($self->{info}->{dbi} =~ /^dbi:Pg/i) {
1214 $self->{dbh}->do("SET datestyle TO 'ISO, YMD'");
1221 my ($class, %arg) = @_;
1223 dbh => undef, # connect_db();
1225 dbi => '', # DBI:Pg:database=bacula;host=127.0.0.1
1231 map { $self->{lc($_)} = $arg{$_} } keys %arg ;
1233 if ($self->{info}->{dbi} =~ /DBI:(\w+):/i) {
1234 $self->{sql} = $sql_func{$1};
1237 $self->{debug} = $self->{info}->{debug};
1238 $Bweb::Gui::template_dir = $self->{info}->{template_dir};
1246 $self->display($self->{info}, "begin.tpl");
1252 $self->display($self->{info}, "end.tpl");
1260 my $arg = $self->get_form("client", "qre_client");
1262 if ($arg->{qre_client}) {
1263 $where = "WHERE Name $self->{sql}->{MATCH} $arg->{qre_client} ";
1264 } elsif ($arg->{client}) {
1265 $where = "WHERE Name = '$arg->{client}' ";
1269 SELECT Name AS name,
1271 AutoPrune AS autoprune,
1272 FileRetention AS fileretention,
1273 JobRetention AS jobretention
1278 my $all = $self->dbh_selectall_hashref($query, 'name') ;
1280 my $dsp = { ID => $cur_id++,
1281 clients => [ values %$all] };
1283 $self->display($dsp, "client_list.tpl") ;
1288 my ($self, %arg) = @_;
1295 "AND $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
1297 ( $self->{sql}->{UNIX_TIMESTAMP}(NOW())
1299 $self->{sql}->{TO_SEC}($arg{age})
1302 $label = "last " . human_sec($arg{age});
1305 if ($arg{groupby}) {
1306 $limit .= " GROUP BY $arg{groupby} ";
1310 $limit .= " ORDER BY $arg{order} ";
1314 $limit .= " LIMIT $arg{limit} ";
1315 $label .= " limited to $arg{limit}";
1319 $limit .= " OFFSET $arg{offset} ";
1320 $label .= " with $arg{offset} offset ";
1324 $label = 'no filter';
1327 return ($limit, $label);
1332 $bweb->get_form(...) - Get useful stuff
1336 This function get and check parameters against regexp.
1338 If word begin with 'q', the return will be quoted or join quoted
1339 if it's end with 's'.
1344 $bweb->get_form('jobid', 'qclient', 'qpools') ;
1347 qclient => 'plume-fd',
1348 qpools => "'plume-fd', 'test-fd', '...'",
1355 my ($self, @what) = @_;
1356 my %what = map { $_ => 1 } @what;
1376 my %opt_ss =( # string with space
1380 my %opt_s = ( # default to ''
1397 my %opt_p = ( # option with path
1404 my %opt_r = (regexwhere => 1);
1406 my %opt_d = ( # option with date
1411 foreach my $i (@what) {
1412 if (exists $opt_i{$i}) {# integer param
1413 my $value = CGI::param($i) || $opt_i{$i} ;
1414 if ($value =~ /^(\d+)$/) {
1417 } elsif ($opt_s{$i}) { # simple string param
1418 my $value = CGI::param($i) || '';
1419 if ($value =~ /^([\w\d\.-]+)$/) {
1422 } elsif ($opt_ss{$i}) { # simple string param (with space)
1423 my $value = CGI::param($i) || '';
1424 if ($value =~ /^([\w\d\.\-\s]+)$/) {
1427 } elsif ($i =~ /^j(\w+)s$/) { # quote join args
1428 my @value = grep { ! /^\s*$/ } CGI::param($1) ;
1430 $ret{$i} = $self->dbh_join(@value) ;
1433 } elsif ($i =~ /^q(\w+[^s])$/) { # 'arg1'
1434 my $value = CGI::param($1) ;
1436 $ret{$i} = $self->dbh_quote($value);
1439 } elsif ($i =~ /^q(\w+)s$/) { #[ 'arg1', 'arg2']
1440 $ret{$i} = [ map { { name => $self->dbh_quote($_) } }
1441 grep { ! /^\s*$/ } CGI::param($1) ];
1442 } elsif (exists $opt_p{$i}) {
1443 my $value = CGI::param($i) || '';
1444 if ($value =~ /^([\w\d\.\/\s:\@\-]+)$/) {
1447 } elsif (exists $opt_r{$i}) {
1448 my $value = CGI::param($i) || '';
1449 if ($value =~ /^([^'"']+)$/) {
1452 } elsif (exists $opt_d{$i}) {
1453 my $value = CGI::param($i) || '';
1454 if ($value =~ /^\s*(\d+\s+\w+)$/) {
1461 foreach my $s (CGI::param('slot')) {
1462 if ($s =~ /^(\d+)$/) {
1463 push @{$ret{slots}}, $s;
1469 my $when = CGI::param('when') || '';
1470 if ($when =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})$/) {
1475 if ($what{db_clients}) {
1477 SELECT Client.Name as clientname
1481 my $clients = $self->dbh_selectall_hashref($query, 'clientname');
1482 $ret{db_clients} = [sort {$a->{clientname} cmp $b->{clientname} }
1486 if ($what{db_mediatypes}) {
1488 SELECT MediaType as mediatype
1492 my $medias = $self->dbh_selectall_hashref($query, 'mediatype');
1493 $ret{db_mediatypes} = [sort {$a->{mediatype} cmp $b->{mediatype} }
1497 if ($what{db_locations}) {
1499 SELECT Location as location, Cost as cost FROM Location
1501 my $loc = $self->dbh_selectall_hashref($query, 'location');
1502 $ret{db_locations} = [ sort { $a->{location}
1508 if ($what{db_pools}) {
1509 my $query = "SELECT Name as name FROM Pool";
1511 my $all = $self->dbh_selectall_hashref($query, 'name') ;
1512 $ret{db_pools} = [ sort { $a->{name} cmp $b->{name} } values %$all ];
1515 if ($what{db_filesets}) {
1517 SELECT FileSet.FileSet AS fileset
1521 my $filesets = $self->dbh_selectall_hashref($query, 'fileset');
1523 $ret{db_filesets} = [sort {lc($a->{fileset}) cmp lc($b->{fileset}) }
1524 values %$filesets] ;
1527 if ($what{db_jobnames}) {
1529 SELECT DISTINCT Job.Name AS jobname
1533 my $jobnames = $self->dbh_selectall_hashref($query, 'jobname');
1535 $ret{db_jobnames} = [sort {lc($a->{jobname}) cmp lc($b->{jobname}) }
1536 values %$jobnames] ;
1539 if ($what{db_devices}) {
1541 SELECT Device.Name AS name
1545 my $devices = $self->dbh_selectall_hashref($query, 'name');
1547 $ret{db_devices} = [sort {lc($a->{name}) cmp lc($b->{name}) }
1558 my $fields = $self->get_form(qw/age level status clients filesets
1560 db_clients limit db_filesets width height
1561 qclients qfilesets qjobnames db_jobnames/);
1564 my $url = CGI::url(-full => 0,
1567 $url =~ s/^.+?\?//; # http://path/to/bweb.pl?arg => arg
1569 # this organisation is to keep user choice between 2 click
1570 # TODO : fileset and client selection doesn't work
1579 sub display_client_job
1581 my ($self, %arg) = @_ ;
1583 $arg{order} = ' Job.JobId DESC ';
1584 my ($limit, $label) = $self->get_limit(%arg);
1586 my $clientname = $self->dbh_quote($arg{clientname});
1589 SELECT DISTINCT Job.JobId AS jobid,
1590 Job.Name AS jobname,
1591 FileSet.FileSet AS fileset,
1593 StartTime AS starttime,
1594 JobFiles AS jobfiles,
1595 JobBytes AS jobbytes,
1596 JobStatus AS jobstatus,
1597 JobErrors AS joberrors
1599 FROM Client,Job,FileSet
1600 WHERE Client.Name=$clientname
1601 AND Client.ClientId=Job.ClientId
1602 AND Job.FileSetId=FileSet.FileSetId
1606 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
1608 $self->display({ clientname => $arg{clientname},
1611 Jobs => [ values %$all ],
1613 "display_client_job.tpl") ;
1616 sub get_selected_media_location
1620 my $medias = $self->get_form('jmedias');
1622 unless ($medias->{jmedias}) {
1627 SELECT Media.VolumeName AS volumename, Location.Location AS location
1628 FROM Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
1629 WHERE Media.VolumeName IN ($medias->{jmedias})
1632 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
1634 # { 'vol1' => { [volumename => 'vol1', location => 'ici'],
1645 my $medias = $self->get_selected_media_location();
1651 my $elt = $self->get_form('db_locations');
1653 $self->display({ ID => $cur_id++,
1654 %$elt, # db_locations
1656 sort { $a->{volumename} cmp $b->{volumename} } values %$medias
1666 my $elt = $self->get_form(qw/db_pools db_mediatypes db_locations/) ;
1668 $self->display($elt, "help_extern.tpl");
1671 sub help_extern_compute
1675 my $number = CGI::param('limit') || '' ;
1676 unless ($number =~ /^(\d+)$/) {
1677 return $self->error("Bad arg number : $number ");
1680 my ($sql, undef) = $self->get_param('pools',
1681 'locations', 'mediatypes');
1684 SELECT Media.VolumeName AS volumename,
1685 Media.VolStatus AS volstatus,
1686 Media.LastWritten AS lastwritten,
1687 Media.MediaType AS mediatype,
1688 Media.VolMounts AS volmounts,
1690 Media.Recycle AS recycle,
1691 $self->{sql}->{FROM_UNIXTIME}(
1692 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
1693 + $self->{sql}->{TO_SEC}(Media.VolRetention)
1696 INNER JOIN Pool ON (Pool.PoolId = Media.PoolId)
1697 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
1699 WHERE Media.InChanger = 1
1700 AND Media.VolStatus IN ('Disabled', 'Error', 'Full')
1702 ORDER BY expire DESC, recycle, Media.VolMounts DESC
1706 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
1708 $self->display({ Medias => [ values %$all ] },
1709 "help_extern_compute.tpl");
1716 my $param = $self->get_form(qw/db_locations db_pools db_mediatypes/) ;
1717 $self->display($param, "help_intern.tpl");
1720 sub help_intern_compute
1724 my $number = CGI::param('limit') || '' ;
1725 unless ($number =~ /^(\d+)$/) {
1726 return $self->error("Bad arg number : $number ");
1729 my ($sql, undef) = $self->get_param('pools', 'locations', 'mediatypes');
1731 if (CGI::param('expired')) {
1733 AND ( $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
1734 + $self->{sql}->{TO_SEC}(Media.VolRetention)
1740 SELECT Media.VolumeName AS volumename,
1741 Media.VolStatus AS volstatus,
1742 Media.LastWritten AS lastwritten,
1743 Media.MediaType AS mediatype,
1744 Media.VolMounts AS volmounts,
1746 $self->{sql}->{FROM_UNIXTIME}(
1747 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
1748 + $self->{sql}->{TO_SEC}(Media.VolRetention)
1751 INNER JOIN Pool ON (Pool.PoolId = Media.PoolId)
1752 LEFT JOIN Location ON (Location.LocationId = Media.LocationId)
1754 WHERE Media.InChanger <> 1
1755 AND Media.VolStatus IN ('Purged', 'Full', 'Append')
1756 AND Media.Recycle = 1
1758 ORDER BY Media.VolUseDuration DESC, Media.VolMounts ASC, expire ASC
1762 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
1764 $self->display({ Medias => [ values %$all ] },
1765 "help_intern_compute.tpl");
1771 my ($self, %arg) = @_ ;
1773 my ($limit, $label) = $self->get_limit(%arg);
1777 (SELECT count(Pool.PoolId) FROM Pool) AS nb_pool,
1778 (SELECT count(Media.MediaId) FROM Media) AS nb_media,
1779 (SELECT count(Job.JobId) FROM Job) AS nb_job,
1780 (SELECT sum(VolBytes) FROM Media) AS nb_bytes,
1781 ($self->{sql}->{DB_SIZE}) AS db_size,
1782 (SELECT count(Job.JobId)
1784 WHERE Job.JobStatus IN ('E','e','f','A')
1787 (SELECT count(Client.ClientId) FROM Client) AS nb_client
1790 my $row = $self->dbh_selectrow_hashref($query) ;
1792 $row->{nb_bytes} = human_size($row->{nb_bytes});
1794 $row->{db_size} = human_size($row->{db_size});
1795 $row->{label} = $label;
1797 $self->display($row, "general.tpl");
1802 my ($self, @what) = @_ ;
1803 my %elt = map { $_ => 1 } @what;
1808 if ($elt{clients}) {
1809 my @clients = grep { ! /^\s*$/ } CGI::param('client');
1811 $ret{clients} = \@clients;
1812 my $str = $self->dbh_join(@clients);
1813 $limit .= "AND Client.Name IN ($str) ";
1817 if ($elt{filesets}) {
1818 my @filesets = grep { ! /^\s*$/ } CGI::param('fileset');
1820 $ret{filesets} = \@filesets;
1821 my $str = $self->dbh_join(@filesets);
1822 $limit .= "AND FileSet.FileSet IN ($str) ";
1826 if ($elt{mediatypes}) {
1827 my @medias = grep { ! /^\s*$/ } CGI::param('mediatype');
1829 $ret{mediatypes} = \@medias;
1830 my $str = $self->dbh_join(@medias);
1831 $limit .= "AND Media.MediaType IN ($str) ";
1836 my $client = CGI::param('client');
1837 $ret{client} = $client;
1838 $client = $self->dbh_join($client);
1839 $limit .= "AND Client.Name = $client ";
1843 my $level = CGI::param('level') || '';
1844 if ($level =~ /^(\w)$/) {
1846 $limit .= "AND Job.Level = '$1' ";
1851 my $jobid = CGI::param('jobid') || '';
1853 if ($jobid =~ /^(\d+)$/) {
1855 $limit .= "AND Job.JobId = '$1' ";
1860 my $status = CGI::param('status') || '';
1861 if ($status =~ /^(\w)$/) {
1864 $limit .= "AND Job.JobStatus IN ('f','E') ";
1865 } elsif ($1 eq 'W') {
1866 $limit .= "AND Job.JobStatus = 'T' AND Job.JobErrors > 0 ";
1868 $limit .= "AND Job.JobStatus = '$1' ";
1873 if ($elt{volstatus}) {
1874 my $status = CGI::param('volstatus') || '';
1875 if ($status =~ /^(\w+)$/) {
1877 $limit .= "AND Media.VolStatus = '$1' ";
1881 if ($elt{locations}) {
1882 my @location = grep { ! /^\s*$/ } CGI::param('location') ;
1884 $ret{locations} = \@location;
1885 my $str = $self->dbh_join(@location);
1886 $limit .= "AND Location.Location IN ($str) ";
1891 my @pool = grep { ! /^\s*$/ } CGI::param('pool') ;
1893 $ret{pools} = \@pool;
1894 my $str = $self->dbh_join(@pool);
1895 $limit .= "AND Pool.Name IN ($str) ";
1899 if ($elt{location}) {
1900 my $location = CGI::param('location') || '';
1902 $ret{location} = $location;
1903 $location = $self->dbh_quote($location);
1904 $limit .= "AND Location.Location = $location ";
1909 my $pool = CGI::param('pool') || '';
1912 $pool = $self->dbh_quote($pool);
1913 $limit .= "AND Pool.Name = $pool ";
1917 if ($elt{jobtype}) {
1918 my $jobtype = CGI::param('jobtype') || '';
1919 if ($jobtype =~ /^(\w)$/) {
1921 $limit .= "AND Job.Type = '$1' ";
1925 return ($limit, %ret);
1936 my ($self, %arg) = @_ ;
1938 $arg{order} = ' Job.JobId DESC ';
1940 my ($limit, $label) = $self->get_limit(%arg);
1941 my ($where, undef) = $self->get_param('clients',
1950 SELECT Job.JobId AS jobid,
1951 Client.Name AS client,
1952 FileSet.FileSet AS fileset,
1953 Job.Name AS jobname,
1955 StartTime AS starttime,
1957 Pool.Name AS poolname,
1958 JobFiles AS jobfiles,
1959 JobBytes AS jobbytes,
1960 JobStatus AS jobstatus,
1961 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
1962 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime))
1965 JobErrors AS joberrors
1968 Job LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId)
1969 LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId)
1970 WHERE Client.ClientId=Job.ClientId
1971 AND Job.JobStatus != 'R'
1976 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
1978 $self->display({ Filter => $label,
1982 sort { $a->{jobid} <=> $b->{jobid} }
1989 # display job informations
1990 sub display_job_zoom
1992 my ($self, $jobid) = @_ ;
1994 $jobid = $self->dbh_quote($jobid);
1997 SELECT DISTINCT Job.JobId AS jobid,
1998 Client.Name AS client,
1999 Job.Name AS jobname,
2000 FileSet.FileSet AS fileset,
2002 Pool.Name AS poolname,
2003 StartTime AS starttime,
2004 JobFiles AS jobfiles,
2005 JobBytes AS jobbytes,
2006 JobStatus AS jobstatus,
2007 JobErrors AS joberrors,
2008 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
2009 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime)) AS duration
2012 Job LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId)
2013 LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId)
2014 WHERE Client.ClientId=Job.ClientId
2015 AND Job.JobId = $jobid
2018 my $row = $self->dbh_selectrow_hashref($query) ;
2020 # display all volumes associate with this job
2022 SELECT Media.VolumeName as volumename
2023 FROM Job,Media,JobMedia
2024 WHERE Job.JobId = $jobid
2025 AND JobMedia.JobId=Job.JobId
2026 AND JobMedia.MediaId=Media.MediaId
2029 my $all = $self->dbh_selectall_hashref($query, 'volumename');
2031 $row->{volumes} = [ values %$all ] ;
2033 $self->display($row, "display_job_zoom.tpl");
2038 my ($self, %arg) = @_ ;
2040 my ($limit, $label) = $self->get_limit(%arg);
2041 my ($where, %elt) = $self->get_param('pools',
2046 my $arg = $self->get_form('jmedias', 'qre_media');
2048 if ($arg->{jmedias}) {
2049 $where = "AND Media.VolumeName IN ($arg->{jmedias}) $where";
2051 if ($arg->{qre_media}) {
2052 $where = "AND Media.VolumeName $self->{sql}->{MATCH} $arg->{qre_media} $where";
2056 SELECT Media.VolumeName AS volumename,
2057 Media.VolBytes AS volbytes,
2058 Media.VolStatus AS volstatus,
2059 Media.MediaType AS mediatype,
2060 Media.InChanger AS online,
2061 Media.LastWritten AS lastwritten,
2062 Location.Location AS location,
2063 (volbytes*100/COALESCE(media_avg_size.size,-1)) AS volusage,
2064 Pool.Name AS poolname,
2065 $self->{sql}->{FROM_UNIXTIME}(
2066 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2067 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2070 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2071 LEFT JOIN (SELECT avg(Media.VolBytes) AS size,
2072 Media.MediaType AS MediaType
2074 WHERE Media.VolStatus = 'Full'
2075 GROUP BY Media.MediaType
2076 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
2078 WHERE Media.PoolId=Pool.PoolId
2083 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2085 $self->display({ ID => $cur_id++,
2087 Location => $elt{location},
2088 Medias => [ values %$all ]
2090 "display_media.tpl");
2097 my $pool = $self->get_form('db_pools');
2099 foreach my $name (@{ $pool->{db_pools} }) {
2100 CGI::param('pool', $name->{name});
2101 $self->display_media();
2105 sub display_media_zoom
2109 my $medias = $self->get_form('jmedias');
2111 unless ($medias->{jmedias}) {
2112 return $self->error("Can't get media selection");
2116 SELECT InChanger AS online,
2117 VolBytes AS nb_bytes,
2118 VolumeName AS volumename,
2119 VolStatus AS volstatus,
2120 VolMounts AS nb_mounts,
2121 Media.VolUseDuration AS voluseduration,
2122 Media.MaxVolJobs AS maxvoljobs,
2123 Media.MaxVolFiles AS maxvolfiles,
2124 Media.MaxVolBytes AS maxvolbytes,
2125 VolErrors AS nb_errors,
2126 Pool.Name AS poolname,
2127 Location.Location AS location,
2128 Media.Recycle AS recycle,
2129 Media.VolRetention AS volretention,
2130 Media.LastWritten AS lastwritten,
2131 Media.VolReadTime/1000000 AS volreadtime,
2132 Media.VolWriteTime/1000000 AS volwritetime,
2133 Media.RecycleCount AS recyclecount,
2134 Media.Comment AS comment,
2135 $self->{sql}->{FROM_UNIXTIME}(
2136 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2137 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2140 Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2141 WHERE Pool.PoolId = Media.PoolId
2142 AND VolumeName IN ($medias->{jmedias})
2145 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2147 foreach my $media (values %$all) {
2148 my $mq = $self->dbh_quote($media->{volumename});
2151 SELECT DISTINCT Job.JobId AS jobid,
2153 Job.StartTime AS starttime,
2156 Job.JobFiles AS files,
2157 Job.JobBytes AS bytes,
2158 Job.jobstatus AS status
2159 FROM Media,JobMedia,Job
2160 WHERE Media.VolumeName=$mq
2161 AND Media.MediaId=JobMedia.MediaId
2162 AND JobMedia.JobId=Job.JobId
2165 my $jobs = $self->dbh_selectall_hashref($query, 'jobid') ;
2168 SELECT LocationLog.Date AS date,
2169 Location.Location AS location,
2170 LocationLog.Comment AS comment
2171 FROM Media,LocationLog INNER JOIN Location ON (LocationLog.LocationId = Location.LocationId)
2172 WHERE Media.MediaId = LocationLog.MediaId
2173 AND Media.VolumeName = $mq
2177 my $log = $self->dbh_selectall_arrayref($query) ;
2179 $logtxt = join("\n", map { ($_->[0] . ' ' . $_->[1] . ' ' . $_->[2])} @$log ) ;
2182 $self->display({ jobs => [ values %$jobs ],
2183 LocationLog => $logtxt,
2185 "display_media_zoom.tpl");
2193 my $loc = $self->get_form('qlocation');
2194 unless ($loc->{qlocation}) {
2195 return $self->error("Can't get location");
2199 SELECT Location.Location AS location,
2200 Location.Cost AS cost,
2201 Location.Enabled AS enabled
2203 WHERE Location.Location = $loc->{qlocation}
2206 my $row = $self->dbh_selectrow_hashref($query);
2208 $self->display({ ID => $cur_id++,
2209 %$row }, "location_edit.tpl") ;
2217 my $arg = $self->get_form(qw/qlocation qnewlocation cost/) ;
2218 unless ($arg->{qlocation}) {
2219 return $self->error("Can't get location");
2221 unless ($arg->{qnewlocation}) {
2222 return $self->error("Can't get new location name");
2224 unless ($arg->{cost}) {
2225 return $self->error("Can't get new cost");
2228 my $enabled = CGI::param('enabled') || '';
2229 $enabled = $enabled?1:0;
2232 UPDATE Location SET Cost = $arg->{cost},
2233 Location = $arg->{qnewlocation},
2235 WHERE Location.Location = $arg->{qlocation}
2238 $self->dbh_do($query);
2240 $self->display_location();
2246 my $arg = $self->get_form(qw/qlocation/) ;
2248 unless ($arg->{qlocation}) {
2249 return $self->error("Can't get location");
2253 SELECT count(Media.MediaId) AS nb
2254 FROM Media INNER JOIN Location USING (LocationID)
2255 WHERE Location = $arg->{qlocation}
2258 my $res = $self->dbh_selectrow_hashref($query);
2261 return $self->error("Sorry, the location must be empty");
2265 DELETE FROM Location WHERE Location = $arg->{qlocation} LIMIT 1
2268 $self->dbh_do($query);
2270 $self->display_location();
2277 my $arg = $self->get_form(qw/qlocation cost/) ;
2279 unless ($arg->{qlocation}) {
2280 $self->display({}, "location_add.tpl");
2283 unless ($arg->{cost}) {
2284 return $self->error("Can't get new cost");
2287 my $enabled = CGI::param('enabled') || '';
2288 $enabled = $enabled?1:0;
2291 INSERT INTO Location (Location, Cost, Enabled)
2292 VALUES ($arg->{qlocation}, $arg->{cost}, $enabled)
2295 $self->dbh_do($query);
2297 $self->display_location();
2300 sub display_location
2305 SELECT Location.Location AS location,
2306 Location.Cost AS cost,
2307 Location.Enabled AS enabled,
2308 (SELECT count(Media.MediaId)
2310 WHERE Media.LocationId = Location.LocationId
2315 my $location = $self->dbh_selectall_hashref($query, 'location');
2317 $self->display({ ID => $cur_id++,
2318 Locations => [ values %$location ] },
2319 "display_location.tpl");
2326 my $medias = $self->get_selected_media_location();
2331 my $arg = $self->get_form('db_locations', 'qnewlocation');
2333 $self->display({ email => $self->{info}->{email_media},
2335 medias => [ values %$medias ],
2337 "update_location.tpl");
2340 sub get_media_max_size
2342 my ($self, $type) = @_;
2344 "SELECT avg(VolBytes) AS size
2346 WHERE Media.VolStatus = 'Full'
2347 AND Media.MediaType = '$type'
2350 my $res = $self->selectrow_hashref($query);
2353 return $res->{size};
2363 my $media = $self->get_form('qmedia');
2365 unless ($media->{qmedia}) {
2366 return $self->error("Can't get media");
2370 SELECT Media.Slot AS slot,
2371 PoolMedia.Name AS poolname,
2372 Media.VolStatus AS volstatus,
2373 Media.InChanger AS inchanger,
2374 Location.Location AS location,
2375 Media.VolumeName AS volumename,
2376 Media.MaxVolBytes AS maxvolbytes,
2377 Media.MaxVolJobs AS maxvoljobs,
2378 Media.MaxVolFiles AS maxvolfiles,
2379 Media.VolUseDuration AS voluseduration,
2380 Media.VolRetention AS volretention,
2381 Media.Comment AS comment,
2382 PoolRecycle.Name AS poolrecycle
2384 FROM Media INNER JOIN Pool AS PoolMedia ON (Media.PoolId = PoolMedia.PoolId)
2385 LEFT JOIN Pool AS PoolRecycle ON (Media.RecyclePoolId = PoolRecycle.PoolId)
2386 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2388 WHERE Media.VolumeName = $media->{qmedia}
2391 my $row = $self->dbh_selectrow_hashref($query);
2392 $row->{volretention} = human_sec($row->{volretention});
2393 $row->{voluseduration} = human_sec($row->{voluseduration});
2395 my $elt = $self->get_form(qw/db_pools db_locations/);
2400 }, "update_media.tpl");
2407 my $arg = $self->get_form('jmedias', 'qnewlocation') ;
2409 unless ($arg->{jmedias}) {
2410 return $self->error("Can't get selected media");
2413 unless ($arg->{qnewlocation}) {
2414 return $self->error("Can't get new location");
2419 SET LocationId = (SELECT LocationId
2421 WHERE Location = $arg->{qnewlocation})
2422 WHERE Media.VolumeName IN ($arg->{jmedias})
2425 my $nb = $self->dbh_do($query);
2427 print "$nb media updated, you may have to update your autochanger.";
2429 $self->display_media();
2436 my $medias = $self->get_selected_media_location();
2438 return $self->error("Can't get media selection");
2440 my $newloc = CGI::param('newlocation');
2442 my $user = CGI::param('user') || 'unknown';
2443 my $comm = CGI::param('comment') || '';
2444 $comm = $self->dbh_quote("$user: $comm");
2448 foreach my $media (keys %$medias) {
2450 INSERT LocationLog (Date, Comment, MediaId, LocationId, NewVolStatus)
2452 NOW(), $comm, (SELECT MediaId FROM Media WHERE VolumeName = '$media'),
2453 (SELECT LocationId FROM Location WHERE Location = '$medias->{$media}->{location}'),
2454 (SELECT VolStatus FROM Media WHERE VolumeName = '$media')
2457 $self->dbh_do($query);
2458 $self->debug($query);
2462 $q->param('action', 'update_location');
2463 my $url = $q->url(-full => 1, -query=>1);
2465 $self->display({ email => $self->{info}->{email_media},
2467 newlocation => $newloc,
2468 # [ { volumename => 'vol1' }, { volumename => 'vol2'
\81\81 },..]
2469 medias => [ values %$medias ],
2471 "change_location.tpl");
2475 sub display_client_stats
2477 my ($self, %arg) = @_ ;
2479 my $client = $self->dbh_quote($arg{clientname});
2480 my ($limit, $label) = $self->get_limit(%arg);
2484 count(Job.JobId) AS nb_jobs,
2485 sum(Job.JobBytes) AS nb_bytes,
2486 sum(Job.JobErrors) AS nb_err,
2487 sum(Job.JobFiles) AS nb_files,
2488 Client.Name AS clientname
2489 FROM Job INNER JOIN Client USING (ClientId)
2491 Client.Name = $client
2493 GROUP BY Client.Name
2496 my $row = $self->dbh_selectrow_hashref($query);
2498 $row->{ID} = $cur_id++;
2499 $row->{label} = $label;
2501 $self->display($row, "display_client_stats.tpl");
2504 # poolname can be undef
2507 my ($self, $poolname) = @_ ;
2511 my $arg = $self->get_form('jmediatypes', 'qmediatypes');
2512 if ($arg->{jmediatypes}) {
2513 $whereW = "WHERE MediaType IN ($arg->{jmediatypes}) ";
2514 $whereA = "AND MediaType IN ($arg->{jmediatypes}) ";
2517 # TODO : afficher les tailles et les dates
2520 SELECT subq.volmax AS volmax,
2521 subq.volnum AS volnum,
2522 subq.voltotal AS voltotal,
2524 Pool.Recycle AS recycle,
2525 Pool.VolRetention AS volretention,
2526 Pool.VolUseDuration AS voluseduration,
2527 Pool.MaxVolJobs AS maxvoljobs,
2528 Pool.MaxVolFiles AS maxvolfiles,
2529 Pool.MaxVolBytes AS maxvolbytes,
2530 subq.PoolId AS PoolId
2533 SELECT COALESCE(media_avg_size.volavg,0) * count(Media.MediaId) AS volmax,
2534 count(Media.MediaId) AS volnum,
2535 sum(Media.VolBytes) AS voltotal,
2536 Media.PoolId AS PoolId,
2537 Media.MediaType AS MediaType
2539 LEFT JOIN (SELECT avg(Media.VolBytes) AS volavg,
2540 Media.MediaType AS MediaType
2542 WHERE Media.VolStatus = 'Full'
2543 GROUP BY Media.MediaType
2544 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
2545 GROUP BY Media.MediaType, Media.PoolId, media_avg_size.volavg
2547 LEFT JOIN Pool ON (Pool.PoolId = subq.PoolId)
2551 my $all = $self->dbh_selectall_hashref($query, 'name') ;
2554 SELECT Pool.Name AS name,
2555 sum(VolBytes) AS size
2556 FROM Media JOIN Pool ON (Media.PoolId = Pool.PoolId)
2557 WHERE Media.VolStatus IN ('Recycled', 'Purged')
2561 my $empty = $self->dbh_selectall_hashref($query, 'name');
2563 foreach my $p (values %$all) {
2564 if ($p->{volmax} > 0) { # mysql returns 0.0000
2565 # we remove Recycled/Purged media from pool usage
2566 if (defined $empty->{$p->{name}}) {
2567 $p->{voltotal} -= $empty->{$p->{name}}->{size};
2569 $p->{poolusage} = sprintf('%.2f', $p->{voltotal} * 100/ $p->{volmax}) ;
2571 $p->{poolusage} = 0;
2575 SELECT VolStatus AS volstatus, count(MediaId) AS nb
2577 WHERE PoolId=$p->{poolid}
2581 my $content = $self->dbh_selectall_hashref($query, 'volstatus');
2582 foreach my $t (values %$content) {
2583 $p->{"nb_" . $t->{volstatus}} = $t->{nb} ;
2588 $self->display({ ID => $cur_id++,
2589 MediaType => $arg->{qmediatypes}, # [ { name => type1 } , { name => type2 } ]
2590 Pools => [ values %$all ]},
2591 "display_pool.tpl");
2594 sub display_running_job
2598 my $arg = $self->get_form('client', 'jobid');
2600 if (!$arg->{client} and $arg->{jobid}) {
2603 SELECT Client.Name AS name
2604 FROM Job INNER JOIN Client USING (ClientId)
2605 WHERE Job.JobId = $arg->{jobid}
2608 my $row = $self->dbh_selectrow_hashref($query);
2611 $arg->{client} = $row->{name};
2612 CGI::param('client', $arg->{client});
2616 if ($arg->{client}) {
2617 my $cli = new Bweb::Client(name => $arg->{client});
2618 $cli->display_running_job($self->{info}, $arg->{jobid});
2619 if ($arg->{jobid}) {
2620 $self->get_job_log();
2623 $self->error("Can't get client or jobid");
2627 sub display_running_jobs
2629 my ($self, $display_action) = @_;
2632 SELECT Job.JobId AS jobid,
2633 Job.Name AS jobname,
2635 Job.StartTime AS starttime,
2636 Job.JobFiles AS jobfiles,
2637 Job.JobBytes AS jobbytes,
2638 Job.JobStatus AS jobstatus,
2639 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(NOW())
2640 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime))
2642 Client.Name AS clientname
2643 FROM Job INNER JOIN Client USING (ClientId)
2644 WHERE JobStatus IN ('C','R','B','e','D','F','S','m','M','s','j','c','d','t','p')
2646 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
2648 $self->display({ ID => $cur_id++,
2649 display_action => $display_action,
2650 Jobs => [ values %$all ]},
2651 "running_job.tpl") ;
2654 # return the autochanger list to update
2659 my $arg = $self->get_form('jmedias');
2661 unless ($arg->{jmedias}) {
2662 return $self->error("Can't get media selection");
2666 SELECT Media.VolumeName AS volumename,
2667 Storage.Name AS storage,
2668 Location.Location AS location,
2670 FROM Media INNER JOIN Storage ON (Media.StorageId = Storage.StorageId)
2671 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2672 WHERE Media.VolumeName IN ($arg->{jmedias})
2673 AND Media.InChanger = 1
2676 my $all = $self->dbh_selectall_hashref($query, 'volumename');
2678 foreach my $vol (values %$all) {
2679 my $a = $self->ach_get($vol->{location});
2681 $ret{$vol->{location}} = 1;
2683 unless ($a->{have_status}) {
2685 $a->{have_status} = 1;
2688 print "eject $vol->{volumename} from $vol->{storage} : ";
2689 if ($a->send_to_io($vol->{slot})) {
2690 print "<img src='/bweb/T.png' alt='ok'><br/>";
2692 print "<img src='/bweb/E.png' alt='err'><br/>";
2702 my ($to, $subject, $content) = (CGI::param('email'),
2703 CGI::param('subject'),
2704 CGI::param('content'));
2705 $to =~ s/[^\w\d\.\@<>,]//;
2706 $subject =~ s/[^\w\d\.\[\]]/ /;
2708 open(MAIL, "|mail -s '$subject' '$to'") ;
2709 print MAIL $content;
2719 my $arg = $self->get_form('jobid', 'client');
2721 print CGI::header('text/brestore');
2722 print "jobid=$arg->{jobid}\n" if ($arg->{jobid});
2723 print "client=$arg->{client}\n" if ($arg->{client});
2724 print "\n\nYou have to assign this mime type with /usr/bin/brestore.pl\n";
2728 # TODO : move this to Bweb::Autochanger ?
2729 # TODO : make this internal to not eject tape ?
2735 my ($self, $name) = @_;
2738 return $self->error("Can't get your autochanger name ach");
2741 unless ($self->{info}->{ach_list}) {
2742 return $self->error("Could not find any autochanger");
2745 my $a = $self->{info}->{ach_list}->{$name};
2748 $self->error("Can't get your autochanger $name from your ach_list");
2753 $a->{debug} = $self->{debug};
2760 my ($self, $ach) = @_;
2762 $self->{info}->{ach_list}->{$ach->{name}} = $ach;
2764 $self->{info}->save();
2772 my $arg = $self->get_form('ach');
2774 or !$self->{info}->{ach_list}
2775 or !$self->{info}->{ach_list}->{$arg->{ach}})
2777 return $self->error("Can't get autochanger name");
2780 my $ach = $self->{info}->{ach_list}->{$arg->{ach}};
2784 [ map { { name => $_, index => $i++ } } @{$ach->{drive_name}} ] ;
2786 my $b = $self->get_bconsole();
2788 my @storages = $b->list_storage() ;
2790 $ach->{devices} = [ map { { name => $_ } } @storages ];
2792 $self->display($ach, "ach_add.tpl");
2793 delete $ach->{drives};
2794 delete $ach->{devices};
2801 my $arg = $self->get_form('ach');
2804 or !$self->{info}->{ach_list}
2805 or !$self->{info}->{ach_list}->{$arg->{ach}})
2807 return $self->error("Can't get autochanger name");
2810 delete $self->{info}->{ach_list}->{$arg->{ach}} ;
2812 $self->{info}->save();
2813 $self->{info}->view();
2819 my $arg = $self->get_form('ach', 'mtxcmd', 'device', 'precmd');
2821 my $b = $self->get_bconsole();
2822 my @storages = $b->list_storage() ;
2824 unless ($arg->{ach}) {
2825 $arg->{devices} = [ map { { name => $_ } } @storages ];
2826 return $self->display($arg, "ach_add.tpl");
2830 foreach my $drive (CGI::param('drives'))
2832 unless (grep(/^$drive$/,@storages)) {
2833 return $self->error("Can't find $drive in storage list");
2836 my $index = CGI::param("index_$drive");
2837 unless (defined $index and $index =~ /^(\d+)$/) {
2838 return $self->error("Can't get $drive index");
2841 $drives[$index] = $drive;
2845 return $self->error("Can't get drives from Autochanger");
2848 my $a = new Bweb::Autochanger(name => $arg->{ach},
2849 precmd => $arg->{precmd},
2850 drive_name => \@drives,
2851 device => $arg->{device},
2852 mtxcmd => $arg->{mtxcmd});
2854 $self->ach_register($a) ;
2856 $self->{info}->view();
2862 my $arg = $self->get_form('jobid');
2864 if ($arg->{jobid}) {
2865 my $b = $self->get_bconsole();
2866 my $ret = $b->send_cmd("delete jobid=\"$arg->{jobid}\"");
2870 title => "Delete a job ",
2871 name => "delete jobid=$arg->{jobid}",
2880 my $arg = $self->get_form(qw/media volstatus inchanger pool
2881 slot volretention voluseduration
2882 maxvoljobs maxvolfiles maxvolbytes
2883 qcomment poolrecycle
2886 unless ($arg->{media}) {
2887 return $self->error("Can't find media selection");
2890 my $update = "update volume=$arg->{media} ";
2892 if ($arg->{volstatus}) {
2893 $update .= " volstatus=$arg->{volstatus} ";
2896 if ($arg->{inchanger}) {
2897 $update .= " inchanger=yes " ;
2899 $update .= " slot=$arg->{slot} ";
2902 $update .= " slot=0 inchanger=no ";
2906 $update .= " pool=$arg->{pool} " ;
2909 if (defined $arg->{volretention}) {
2910 $update .= " volretention=\"$arg->{volretention}\" " ;
2913 if (defined $arg->{voluseduration}) {
2914 $update .= " voluse=\"$arg->{voluseduration}\" " ;
2917 if (defined $arg->{maxvoljobs}) {
2918 $update .= " maxvoljobs=$arg->{maxvoljobs} " ;
2921 if (defined $arg->{maxvolfiles}) {
2922 $update .= " maxvolfiles=$arg->{maxvolfiles} " ;
2925 if (defined $arg->{maxvolbytes}) {
2926 $update .= " maxvolbytes=$arg->{maxvolbytes} " ;
2929 my $b = $self->get_bconsole();
2932 content => $b->send_cmd($update),
2933 title => "Update a volume ",
2939 my $media = $self->dbh_quote($arg->{media});
2941 my $loc = CGI::param('location') || '';
2943 $loc = $self->dbh_quote($loc); # is checked by db
2944 push @q, "LocationId=(SELECT LocationId FROM Location WHERE Location=$loc)";
2946 if ($arg->{poolrecycle}) {
2947 push @q, "RecyclePoolId=(SELECT PoolId FROM Pool WHERE Name='$arg->{poolrecycle}')";
2949 if (!$arg->{qcomment}) {
2950 $arg->{qcomment} = "''";
2952 push @q, "Comment=$arg->{qcomment}";
2957 SET " . join (',', @q) . "
2958 WHERE Media.VolumeName = $media
2960 $self->dbh_do($query);
2962 $self->update_media();
2969 my $ach = CGI::param('ach') ;
2970 $ach = $self->ach_get($ach);
2972 return $self->error("Bad autochanger name");
2976 my $b = new Bconsole(pref => $self->{info},timeout => 60,log_stdout => 1);
2977 $b->update_slots($ach->{name});
2985 my $arg = $self->get_form('jobid', 'limit', 'offset');
2986 unless ($arg->{jobid}) {
2987 return $self->error("Can't get jobid");
2990 if ($arg->{limit} == 100) {
2991 $arg->{limit} = 1000;
2994 my $t = CGI::param('time') || $self->{info}->{display_log_time} || '';
2997 SELECT Job.Name as name, Client.Name as clientname
2998 FROM Job INNER JOIN Client ON (Job.ClientId = Client.ClientId)
2999 WHERE JobId = $arg->{jobid}
3002 my $row = $self->dbh_selectrow_hashref($query);
3005 return $self->error("Can't find $arg->{jobid} in catalog");
3009 SELECT Time AS time, LogText AS log
3011 WHERE Log.JobId = $arg->{jobid}
3012 OR (Log.JobId = 0 AND Time >= (SELECT StartTime FROM Job WHERE JobId=$arg->{jobid})
3013 AND Time <= (SELECT COALESCE(EndTime,NOW()) FROM Job WHERE JobId=$arg->{jobid})
3017 OFFSET $arg->{offset}
3020 my $log = $self->dbh_selectall_arrayref($query);
3022 return $self->error("Can't get log for jobid $arg->{jobid}");
3028 $logtxt = join("", map { ($_->[0] . ' ' . $_->[1]) } @$log ) ;
3030 $logtxt = join("", map { $_->[1] } @$log ) ;
3033 $self->display({ lines=> $logtxt,
3034 jobid => $arg->{jobid},
3035 name => $row->{name},
3036 client => $row->{clientname},
3037 offset => $arg->{offset},
3038 limit => $arg->{limit},
3039 }, 'display_log.tpl');
3047 my $arg = $self->get_form('ach', 'slots', 'drive');
3049 unless ($arg->{ach}) {
3050 return $self->error("Can't find autochanger name");
3053 my $a = $self->ach_get($arg->{ach});
3055 return $self->error("Can't find autochanger name in configuration");
3058 my $storage = $a->get_drive_name($arg->{drive});
3060 return $self->error("Can't get your drive name");
3065 if ($arg->{slots}) {
3066 $slots = join(",", @{ $arg->{slots} });
3067 $t += 60*scalar( @{ $arg->{slots} }) ;
3070 my $b = new Bconsole(pref => $self->{info}, timeout => $t,log_stdout => 1);
3071 print "<h1>This command can take long time, be patient...</h1>";
3073 $b->label_barcodes(storage => $storage,
3074 drive => $arg->{drive},
3082 SET LocationId = (SELECT LocationId
3084 WHERE Location = '$arg->{ach}'),
3086 RecyclePoolId = PoolId
3088 WHERE Media.PoolId = (SELECT PoolId
3090 WHERE Name = 'Scratch')
3091 AND (LocationId = 0 OR LocationId IS NULL)
3100 my @volume = CGI::param('media');
3103 return $self->error("Can't get media selection");
3106 my $b = new Bconsole(pref => $self->{info}, timeout => 60);
3109 content => $b->purge_volume(@volume),
3110 title => "Purge media",
3111 name => "purge volume=" . join(' volume=', @volume),
3120 my @volume = CGI::param('media');
3122 return $self->error("Can't get media selection");
3125 my $b = new Bconsole(pref => $self->{info}, timeout => 60);
3128 content => $b->prune_volume(@volume),
3129 title => "Prune media",
3130 name => "prune volume=" . join(' volume=', @volume),
3140 my $arg = $self->get_form('jobid');
3141 unless ($arg->{jobid}) {
3142 return $self->error("Can't get jobid");
3145 my $b = $self->get_bconsole();
3147 content => $b->cancel($arg->{jobid}),
3148 title => "Cancel job",
3149 name => "cancel jobid=$arg->{jobid}",
3155 # Warning, we display current fileset
3158 my $arg = $self->get_form('fileset');
3160 if ($arg->{fileset}) {
3161 my $b = $self->get_bconsole();
3162 my $ret = $b->get_fileset($arg->{fileset});
3163 $self->display({ fileset => $arg->{fileset},
3165 }, "fileset_view.tpl");
3167 $self->error("Can't get fileset name");
3171 sub director_show_sched
3175 my $arg = $self->get_form('days');
3177 my $b = $self->get_bconsole();
3178 my $ret = $b->director_get_sched( $arg->{days} );
3183 }, "scheduled_job.tpl");
3186 sub enable_disable_job
3188 my ($self, $what) = @_ ;
3190 my $name = CGI::param('job') || '';
3191 unless ($name =~ /^[\w\d\.\-\s]+$/) {
3192 return $self->error("Can't find job name");
3195 my $b = $self->get_bconsole();
3205 content => $b->send_cmd("$cmd job=\"$name\""),
3206 title => "$cmd $name",
3207 name => "$cmd job=\"$name\"",
3214 return new Bconsole(pref => $self->{info});
3220 my $b = $self->get_bconsole();
3222 my $joblist = [ map { { name => $_ } } $b->list_job() ];
3224 $self->display({ Jobs => $joblist }, "run_job.tpl");
3229 my ($self, $ouput) = @_;
3232 foreach my $l (split(/\r\n/, $ouput)) {
3233 if ($l =~ /(\w+): name=([\w\d\.\s-]+?)(\s+\w+=.+)?$/) {
3239 if (my @l = $l =~ /(\w+)=([\w\d*]+)/g) {
3245 foreach my $k (keys %arg) {
3246 $lowcase{lc($k)} = $arg{$k} ;
3255 my $b = $self->get_bconsole();
3257 my $job = CGI::param('job') || '';
3259 # we take informations from director, and we overwrite with user wish
3260 my $info = $b->send_cmd("show job=\"$job\"");
3261 my $attr = $self->run_parse_job($info);
3263 my $arg = $self->get_form('pool', 'level', 'client', 'fileset', 'storage');
3264 my %job_opt = (%$attr, %$arg);
3266 my $jobs = [ map {{ name => $_ }} $b->list_job() ];
3268 my $pools = [ map { { name => $_ } } $b->list_pool() ];
3269 my $clients = [ map { { name => $_ } }$b->list_client()];
3270 my $filesets= [ map { { name => $_ } }$b->list_fileset() ];
3271 my $storages= [ map { { name => $_ } }$b->list_storage()];
3276 clients => $clients,
3277 filesets => $filesets,
3278 storages => $storages,
3280 }, "run_job_mod.tpl");
3286 my $b = $self->get_bconsole();
3288 my $jobs = [ map {{ name => $_ }} $b->list_job() ];
3298 my $b = $self->get_bconsole();
3300 # TODO: check input (don't use pool, level)
3302 my $arg = $self->get_form('pool', 'level', 'client', 'priority', 'when', 'fileset');
3303 my $job = CGI::param('job') || '';
3304 my $storage = CGI::param('storage') || '';
3306 my $jobid = $b->run(job => $job,
3307 client => $arg->{client},
3308 priority => $arg->{priority},
3309 level => $arg->{level},
3310 storage => $storage,
3311 pool => $arg->{pool},
3312 fileset => $arg->{fileset},
3313 when => $arg->{when},
3316 print $jobid, $b->{error};
3318 print "<br>You can follow job execution <a href='?action=dsp_cur_job;client=$arg->{client};jobid=$jobid'> here </a>";