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");
2040 my ($where, %elt) = $self->get_param('pools',
2045 my $arg = $self->get_form('jmedias', 'qre_media');
2047 if ($arg->{jmedias}) {
2048 $where = "AND Media.VolumeName IN ($arg->{jmedias}) $where";
2050 if ($arg->{qre_media}) {
2051 $where = "AND Media.VolumeName $self->{sql}->{MATCH} $arg->{qre_media} $where";
2055 SELECT Media.VolumeName AS volumename,
2056 Media.VolBytes AS volbytes,
2057 Media.VolStatus AS volstatus,
2058 Media.MediaType AS mediatype,
2059 Media.InChanger AS online,
2060 Media.LastWritten AS lastwritten,
2061 Location.Location AS location,
2062 (volbytes*100/COALESCE(media_avg_size.size,-1)) AS volusage,
2063 Pool.Name AS poolname,
2064 $self->{sql}->{FROM_UNIXTIME}(
2065 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2066 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2069 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2070 LEFT JOIN (SELECT avg(Media.VolBytes) AS size,
2071 Media.MediaType AS MediaType
2073 WHERE Media.VolStatus = 'Full'
2074 GROUP BY Media.MediaType
2075 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
2077 WHERE Media.PoolId=Pool.PoolId
2081 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2083 $self->display({ ID => $cur_id++,
2085 Location => $elt{location},
2086 Medias => [ values %$all ]
2088 "display_media.tpl");
2095 my $pool = $self->get_form('db_pools');
2097 foreach my $name (@{ $pool->{db_pools} }) {
2098 CGI::param('pool', $name->{name});
2099 $self->display_media();
2103 sub display_media_zoom
2107 my $medias = $self->get_form('jmedias');
2109 unless ($medias->{jmedias}) {
2110 return $self->error("Can't get media selection");
2114 SELECT InChanger AS online,
2115 VolBytes AS nb_bytes,
2116 VolumeName AS volumename,
2117 VolStatus AS volstatus,
2118 VolMounts AS nb_mounts,
2119 Media.VolUseDuration AS voluseduration,
2120 Media.MaxVolJobs AS maxvoljobs,
2121 Media.MaxVolFiles AS maxvolfiles,
2122 Media.MaxVolBytes AS maxvolbytes,
2123 VolErrors AS nb_errors,
2124 Pool.Name AS poolname,
2125 Location.Location AS location,
2126 Media.Recycle AS recycle,
2127 Media.VolRetention AS volretention,
2128 Media.LastWritten AS lastwritten,
2129 Media.VolReadTime/1000000 AS volreadtime,
2130 Media.VolWriteTime/1000000 AS volwritetime,
2131 Media.RecycleCount AS recyclecount,
2132 Media.Comment AS comment,
2133 $self->{sql}->{FROM_UNIXTIME}(
2134 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2135 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2138 Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2139 WHERE Pool.PoolId = Media.PoolId
2140 AND VolumeName IN ($medias->{jmedias})
2143 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2145 foreach my $media (values %$all) {
2146 my $mq = $self->dbh_quote($media->{volumename});
2149 SELECT DISTINCT Job.JobId AS jobid,
2151 Job.StartTime AS starttime,
2154 Job.JobFiles AS files,
2155 Job.JobBytes AS bytes,
2156 Job.jobstatus AS status
2157 FROM Media,JobMedia,Job
2158 WHERE Media.VolumeName=$mq
2159 AND Media.MediaId=JobMedia.MediaId
2160 AND JobMedia.JobId=Job.JobId
2163 my $jobs = $self->dbh_selectall_hashref($query, 'jobid') ;
2166 SELECT LocationLog.Date AS date,
2167 Location.Location AS location,
2168 LocationLog.Comment AS comment
2169 FROM Media,LocationLog INNER JOIN Location ON (LocationLog.LocationId = Location.LocationId)
2170 WHERE Media.MediaId = LocationLog.MediaId
2171 AND Media.VolumeName = $mq
2175 my $log = $self->dbh_selectall_arrayref($query) ;
2177 $logtxt = join("\n", map { ($_->[0] . ' ' . $_->[1] . ' ' . $_->[2])} @$log ) ;
2180 $self->display({ jobs => [ values %$jobs ],
2181 LocationLog => $logtxt,
2183 "display_media_zoom.tpl");
2191 my $loc = $self->get_form('qlocation');
2192 unless ($loc->{qlocation}) {
2193 return $self->error("Can't get location");
2197 SELECT Location.Location AS location,
2198 Location.Cost AS cost,
2199 Location.Enabled AS enabled
2201 WHERE Location.Location = $loc->{qlocation}
2204 my $row = $self->dbh_selectrow_hashref($query);
2206 $self->display({ ID => $cur_id++,
2207 %$row }, "location_edit.tpl") ;
2215 my $arg = $self->get_form(qw/qlocation qnewlocation cost/) ;
2216 unless ($arg->{qlocation}) {
2217 return $self->error("Can't get location");
2219 unless ($arg->{qnewlocation}) {
2220 return $self->error("Can't get new location name");
2222 unless ($arg->{cost}) {
2223 return $self->error("Can't get new cost");
2226 my $enabled = CGI::param('enabled') || '';
2227 $enabled = $enabled?1:0;
2230 UPDATE Location SET Cost = $arg->{cost},
2231 Location = $arg->{qnewlocation},
2233 WHERE Location.Location = $arg->{qlocation}
2236 $self->dbh_do($query);
2238 $self->display_location();
2244 my $arg = $self->get_form(qw/qlocation/) ;
2246 unless ($arg->{qlocation}) {
2247 return $self->error("Can't get location");
2251 SELECT count(Media.MediaId) AS nb
2252 FROM Media INNER JOIN Location USING (LocationID)
2253 WHERE Location = $arg->{qlocation}
2256 my $res = $self->dbh_selectrow_hashref($query);
2259 return $self->error("Sorry, the location must be empty");
2263 DELETE FROM Location WHERE Location = $arg->{qlocation} LIMIT 1
2266 $self->dbh_do($query);
2268 $self->display_location();
2275 my $arg = $self->get_form(qw/qlocation cost/) ;
2277 unless ($arg->{qlocation}) {
2278 $self->display({}, "location_add.tpl");
2281 unless ($arg->{cost}) {
2282 return $self->error("Can't get new cost");
2285 my $enabled = CGI::param('enabled') || '';
2286 $enabled = $enabled?1:0;
2289 INSERT INTO Location (Location, Cost, Enabled)
2290 VALUES ($arg->{qlocation}, $arg->{cost}, $enabled)
2293 $self->dbh_do($query);
2295 $self->display_location();
2298 sub display_location
2303 SELECT Location.Location AS location,
2304 Location.Cost AS cost,
2305 Location.Enabled AS enabled,
2306 (SELECT count(Media.MediaId)
2308 WHERE Media.LocationId = Location.LocationId
2313 my $location = $self->dbh_selectall_hashref($query, 'location');
2315 $self->display({ ID => $cur_id++,
2316 Locations => [ values %$location ] },
2317 "display_location.tpl");
2324 my $medias = $self->get_selected_media_location();
2329 my $arg = $self->get_form('db_locations', 'qnewlocation');
2331 $self->display({ email => $self->{info}->{email_media},
2333 medias => [ values %$medias ],
2335 "update_location.tpl");
2338 sub get_media_max_size
2340 my ($self, $type) = @_;
2342 "SELECT avg(VolBytes) AS size
2344 WHERE Media.VolStatus = 'Full'
2345 AND Media.MediaType = '$type'
2348 my $res = $self->selectrow_hashref($query);
2351 return $res->{size};
2361 my $media = $self->get_form('qmedia');
2363 unless ($media->{qmedia}) {
2364 return $self->error("Can't get media");
2368 SELECT Media.Slot AS slot,
2369 PoolMedia.Name AS poolname,
2370 Media.VolStatus AS volstatus,
2371 Media.InChanger AS inchanger,
2372 Location.Location AS location,
2373 Media.VolumeName AS volumename,
2374 Media.MaxVolBytes AS maxvolbytes,
2375 Media.MaxVolJobs AS maxvoljobs,
2376 Media.MaxVolFiles AS maxvolfiles,
2377 Media.VolUseDuration AS voluseduration,
2378 Media.VolRetention AS volretention,
2379 Media.Comment AS comment,
2380 PoolRecycle.Name AS poolrecycle
2382 FROM Media INNER JOIN Pool AS PoolMedia ON (Media.PoolId = PoolMedia.PoolId)
2383 LEFT JOIN Pool AS PoolRecycle ON (Media.RecyclePoolId = PoolRecycle.PoolId)
2384 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2386 WHERE Media.VolumeName = $media->{qmedia}
2389 my $row = $self->dbh_selectrow_hashref($query);
2390 $row->{volretention} = human_sec($row->{volretention});
2391 $row->{voluseduration} = human_sec($row->{voluseduration});
2393 my $elt = $self->get_form(qw/db_pools db_locations/);
2398 }, "update_media.tpl");
2405 my $arg = $self->get_form('jmedias', 'qnewlocation') ;
2407 unless ($arg->{jmedias}) {
2408 return $self->error("Can't get selected media");
2411 unless ($arg->{qnewlocation}) {
2412 return $self->error("Can't get new location");
2417 SET LocationId = (SELECT LocationId
2419 WHERE Location = $arg->{qnewlocation})
2420 WHERE Media.VolumeName IN ($arg->{jmedias})
2423 my $nb = $self->dbh_do($query);
2425 print "$nb media updated, you may have to update your autochanger.";
2427 $self->display_media();
2434 my $medias = $self->get_selected_media_location();
2436 return $self->error("Can't get media selection");
2438 my $newloc = CGI::param('newlocation');
2440 my $user = CGI::param('user') || 'unknown';
2441 my $comm = CGI::param('comment') || '';
2442 $comm = $self->dbh_quote("$user: $comm");
2446 foreach my $media (keys %$medias) {
2448 INSERT LocationLog (Date, Comment, MediaId, LocationId, NewVolStatus)
2450 NOW(), $comm, (SELECT MediaId FROM Media WHERE VolumeName = '$media'),
2451 (SELECT LocationId FROM Location WHERE Location = '$medias->{$media}->{location}'),
2452 (SELECT VolStatus FROM Media WHERE VolumeName = '$media')
2455 $self->dbh_do($query);
2456 $self->debug($query);
2460 $q->param('action', 'update_location');
2461 my $url = $q->url(-full => 1, -query=>1);
2463 $self->display({ email => $self->{info}->{email_media},
2465 newlocation => $newloc,
2466 # [ { volumename => 'vol1' }, { volumename => 'vol2'
\81\81 },..]
2467 medias => [ values %$medias ],
2469 "change_location.tpl");
2473 sub display_client_stats
2475 my ($self, %arg) = @_ ;
2477 my $client = $self->dbh_quote($arg{clientname});
2478 my ($limit, $label) = $self->get_limit(%arg);
2482 count(Job.JobId) AS nb_jobs,
2483 sum(Job.JobBytes) AS nb_bytes,
2484 sum(Job.JobErrors) AS nb_err,
2485 sum(Job.JobFiles) AS nb_files,
2486 Client.Name AS clientname
2487 FROM Job INNER JOIN Client USING (ClientId)
2489 Client.Name = $client
2491 GROUP BY Client.Name
2494 my $row = $self->dbh_selectrow_hashref($query);
2496 $row->{ID} = $cur_id++;
2497 $row->{label} = $label;
2499 $self->display($row, "display_client_stats.tpl");
2502 # poolname can be undef
2505 my ($self, $poolname) = @_ ;
2509 my $arg = $self->get_form('jmediatypes', 'qmediatypes');
2510 if ($arg->{jmediatypes}) {
2511 $whereW = "WHERE MediaType IN ($arg->{jmediatypes}) ";
2512 $whereA = "AND MediaType IN ($arg->{jmediatypes}) ";
2515 # TODO : afficher les tailles et les dates
2518 SELECT subq.volmax AS volmax,
2519 subq.volnum AS volnum,
2520 subq.voltotal AS voltotal,
2522 Pool.Recycle AS recycle,
2523 Pool.VolRetention AS volretention,
2524 Pool.VolUseDuration AS voluseduration,
2525 Pool.MaxVolJobs AS maxvoljobs,
2526 Pool.MaxVolFiles AS maxvolfiles,
2527 Pool.MaxVolBytes AS maxvolbytes,
2528 subq.PoolId AS PoolId
2531 SELECT COALESCE(media_avg_size.volavg,0) * count(Media.MediaId) AS volmax,
2532 count(Media.MediaId) AS volnum,
2533 sum(Media.VolBytes) AS voltotal,
2534 Media.PoolId AS PoolId,
2535 Media.MediaType AS MediaType
2537 LEFT JOIN (SELECT avg(Media.VolBytes) AS volavg,
2538 Media.MediaType AS MediaType
2540 WHERE Media.VolStatus = 'Full'
2541 GROUP BY Media.MediaType
2542 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
2543 GROUP BY Media.MediaType, Media.PoolId, media_avg_size.volavg
2545 LEFT JOIN Pool ON (Pool.PoolId = subq.PoolId)
2549 my $all = $self->dbh_selectall_hashref($query, 'name') ;
2552 SELECT Pool.Name AS name,
2553 sum(VolBytes) AS size
2554 FROM Media JOIN Pool ON (Media.PoolId = Pool.PoolId)
2555 WHERE Media.VolStatus IN ('Recycled', 'Purged')
2559 my $empty = $self->dbh_selectall_hashref($query, 'name');
2561 foreach my $p (values %$all) {
2562 if ($p->{volmax} > 0) { # mysql returns 0.0000
2563 # we remove Recycled/Purged media from pool usage
2564 if (defined $empty->{$p->{name}}) {
2565 $p->{voltotal} -= $empty->{$p->{name}}->{size};
2567 $p->{poolusage} = sprintf('%.2f', $p->{voltotal} * 100/ $p->{volmax}) ;
2569 $p->{poolusage} = 0;
2573 SELECT VolStatus AS volstatus, count(MediaId) AS nb
2575 WHERE PoolId=$p->{poolid}
2579 my $content = $self->dbh_selectall_hashref($query, 'volstatus');
2580 foreach my $t (values %$content) {
2581 $p->{"nb_" . $t->{volstatus}} = $t->{nb} ;
2586 $self->display({ ID => $cur_id++,
2587 MediaType => $arg->{qmediatypes}, # [ { name => type1 } , { name => type2 } ]
2588 Pools => [ values %$all ]},
2589 "display_pool.tpl");
2592 sub display_running_job
2596 my $arg = $self->get_form('client', 'jobid');
2598 if (!$arg->{client} and $arg->{jobid}) {
2601 SELECT Client.Name AS name
2602 FROM Job INNER JOIN Client USING (ClientId)
2603 WHERE Job.JobId = $arg->{jobid}
2606 my $row = $self->dbh_selectrow_hashref($query);
2609 $arg->{client} = $row->{name};
2610 CGI::param('client', $arg->{client});
2614 if ($arg->{client}) {
2615 my $cli = new Bweb::Client(name => $arg->{client});
2616 $cli->display_running_job($self->{info}, $arg->{jobid});
2617 if ($arg->{jobid}) {
2618 $self->get_job_log();
2621 $self->error("Can't get client or jobid");
2625 sub display_running_jobs
2627 my ($self, $display_action) = @_;
2630 SELECT Job.JobId AS jobid,
2631 Job.Name AS jobname,
2633 Job.StartTime AS starttime,
2634 Job.JobFiles AS jobfiles,
2635 Job.JobBytes AS jobbytes,
2636 Job.JobStatus AS jobstatus,
2637 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(NOW())
2638 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime))
2640 Client.Name AS clientname
2641 FROM Job INNER JOIN Client USING (ClientId)
2642 WHERE JobStatus IN ('C','R','B','e','D','F','S','m','M','s','j','c','d','t','p')
2644 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
2646 $self->display({ ID => $cur_id++,
2647 display_action => $display_action,
2648 Jobs => [ values %$all ]},
2649 "running_job.tpl") ;
2652 # return the autochanger list to update
2657 my $arg = $self->get_form('jmedias');
2659 unless ($arg->{jmedias}) {
2660 return $self->error("Can't get media selection");
2664 SELECT Media.VolumeName AS volumename,
2665 Storage.Name AS storage,
2666 Location.Location AS location,
2668 FROM Media INNER JOIN Storage ON (Media.StorageId = Storage.StorageId)
2669 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2670 WHERE Media.VolumeName IN ($arg->{jmedias})
2671 AND Media.InChanger = 1
2674 my $all = $self->dbh_selectall_hashref($query, 'volumename');
2676 foreach my $vol (values %$all) {
2677 my $a = $self->ach_get($vol->{location});
2679 $ret{$vol->{location}} = 1;
2681 unless ($a->{have_status}) {
2683 $a->{have_status} = 1;
2686 print "eject $vol->{volumename} from $vol->{storage} : ";
2687 if ($a->send_to_io($vol->{slot})) {
2688 print "<img src='/bweb/T.png' alt='ok'><br/>";
2690 print "<img src='/bweb/E.png' alt='err'><br/>";
2700 my ($to, $subject, $content) = (CGI::param('email'),
2701 CGI::param('subject'),
2702 CGI::param('content'));
2703 $to =~ s/[^\w\d\.\@<>,]//;
2704 $subject =~ s/[^\w\d\.\[\]]/ /;
2706 open(MAIL, "|mail -s '$subject' '$to'") ;
2707 print MAIL $content;
2717 my $arg = $self->get_form('jobid', 'client');
2719 print CGI::header('text/brestore');
2720 print "jobid=$arg->{jobid}\n" if ($arg->{jobid});
2721 print "client=$arg->{client}\n" if ($arg->{client});
2722 print "\n\nYou have to assign this mime type with /usr/bin/brestore.pl\n";
2726 # TODO : move this to Bweb::Autochanger ?
2727 # TODO : make this internal to not eject tape ?
2733 my ($self, $name) = @_;
2736 return $self->error("Can't get your autochanger name ach");
2739 unless ($self->{info}->{ach_list}) {
2740 return $self->error("Could not find any autochanger");
2743 my $a = $self->{info}->{ach_list}->{$name};
2746 $self->error("Can't get your autochanger $name from your ach_list");
2751 $a->{debug} = $self->{debug};
2758 my ($self, $ach) = @_;
2760 $self->{info}->{ach_list}->{$ach->{name}} = $ach;
2762 $self->{info}->save();
2770 my $arg = $self->get_form('ach');
2772 or !$self->{info}->{ach_list}
2773 or !$self->{info}->{ach_list}->{$arg->{ach}})
2775 return $self->error("Can't get autochanger name");
2778 my $ach = $self->{info}->{ach_list}->{$arg->{ach}};
2782 [ map { { name => $_, index => $i++ } } @{$ach->{drive_name}} ] ;
2784 my $b = $self->get_bconsole();
2786 my @storages = $b->list_storage() ;
2788 $ach->{devices} = [ map { { name => $_ } } @storages ];
2790 $self->display($ach, "ach_add.tpl");
2791 delete $ach->{drives};
2792 delete $ach->{devices};
2799 my $arg = $self->get_form('ach');
2802 or !$self->{info}->{ach_list}
2803 or !$self->{info}->{ach_list}->{$arg->{ach}})
2805 return $self->error("Can't get autochanger name");
2808 delete $self->{info}->{ach_list}->{$arg->{ach}} ;
2810 $self->{info}->save();
2811 $self->{info}->view();
2817 my $arg = $self->get_form('ach', 'mtxcmd', 'device', 'precmd');
2819 my $b = $self->get_bconsole();
2820 my @storages = $b->list_storage() ;
2822 unless ($arg->{ach}) {
2823 $arg->{devices} = [ map { { name => $_ } } @storages ];
2824 return $self->display($arg, "ach_add.tpl");
2828 foreach my $drive (CGI::param('drives'))
2830 unless (grep(/^$drive$/,@storages)) {
2831 return $self->error("Can't find $drive in storage list");
2834 my $index = CGI::param("index_$drive");
2835 unless (defined $index and $index =~ /^(\d+)$/) {
2836 return $self->error("Can't get $drive index");
2839 $drives[$index] = $drive;
2843 return $self->error("Can't get drives from Autochanger");
2846 my $a = new Bweb::Autochanger(name => $arg->{ach},
2847 precmd => $arg->{precmd},
2848 drive_name => \@drives,
2849 device => $arg->{device},
2850 mtxcmd => $arg->{mtxcmd});
2852 $self->ach_register($a) ;
2854 $self->{info}->view();
2860 my $arg = $self->get_form('jobid');
2862 if ($arg->{jobid}) {
2863 my $b = $self->get_bconsole();
2864 my $ret = $b->send_cmd("delete jobid=\"$arg->{jobid}\"");
2868 title => "Delete a job ",
2869 name => "delete jobid=$arg->{jobid}",
2878 my $arg = $self->get_form(qw/media volstatus inchanger pool
2879 slot volretention voluseduration
2880 maxvoljobs maxvolfiles maxvolbytes
2881 qcomment poolrecycle
2884 unless ($arg->{media}) {
2885 return $self->error("Can't find media selection");
2888 my $update = "update volume=$arg->{media} ";
2890 if ($arg->{volstatus}) {
2891 $update .= " volstatus=$arg->{volstatus} ";
2894 if ($arg->{inchanger}) {
2895 $update .= " inchanger=yes " ;
2897 $update .= " slot=$arg->{slot} ";
2900 $update .= " slot=0 inchanger=no ";
2904 $update .= " pool=$arg->{pool} " ;
2907 if (defined $arg->{volretention}) {
2908 $update .= " volretention=\"$arg->{volretention}\" " ;
2911 if (defined $arg->{voluseduration}) {
2912 $update .= " voluse=\"$arg->{voluseduration}\" " ;
2915 if (defined $arg->{maxvoljobs}) {
2916 $update .= " maxvoljobs=$arg->{maxvoljobs} " ;
2919 if (defined $arg->{maxvolfiles}) {
2920 $update .= " maxvolfiles=$arg->{maxvolfiles} " ;
2923 if (defined $arg->{maxvolbytes}) {
2924 $update .= " maxvolbytes=$arg->{maxvolbytes} " ;
2927 my $b = $self->get_bconsole();
2930 content => $b->send_cmd($update),
2931 title => "Update a volume ",
2937 my $media = $self->dbh_quote($arg->{media});
2939 my $loc = CGI::param('location') || '';
2941 $loc = $self->dbh_quote($loc); # is checked by db
2942 push @q, "LocationId=(SELECT LocationId FROM Location WHERE Location=$loc)";
2944 if ($arg->{poolrecycle}) {
2945 push @q, "RecyclePoolId=(SELECT PoolId FROM Pool WHERE Name='$arg->{poolrecycle}')";
2947 if (!$arg->{qcomment}) {
2948 $arg->{qcomment} = "''";
2950 push @q, "Comment=$arg->{qcomment}";
2955 SET " . join (',', @q) . "
2956 WHERE Media.VolumeName = $media
2958 $self->dbh_do($query);
2960 $self->update_media();
2967 my $ach = CGI::param('ach') ;
2968 $ach = $self->ach_get($ach);
2970 return $self->error("Bad autochanger name");
2974 my $b = new Bconsole(pref => $self->{info},timeout => 60,log_stdout => 1);
2975 $b->update_slots($ach->{name});
2983 my $arg = $self->get_form('jobid', 'limit', 'offset');
2984 unless ($arg->{jobid}) {
2985 return $self->error("Can't get jobid");
2988 if ($arg->{limit} == 100) {
2989 $arg->{limit} = 1000;
2992 my $t = CGI::param('time') || $self->{info}->{display_log_time} || '';
2995 SELECT Job.Name as name, Client.Name as clientname
2996 FROM Job INNER JOIN Client ON (Job.ClientId = Client.ClientId)
2997 WHERE JobId = $arg->{jobid}
3000 my $row = $self->dbh_selectrow_hashref($query);
3003 return $self->error("Can't find $arg->{jobid} in catalog");
3007 SELECT Time AS time, LogText AS log
3009 WHERE Log.JobId = $arg->{jobid}
3010 OR (Log.JobId = 0 AND Time >= (SELECT StartTime FROM Job WHERE JobId=$arg->{jobid})
3011 AND Time <= (SELECT COALESCE(EndTime,NOW()) FROM Job WHERE JobId=$arg->{jobid})
3015 OFFSET $arg->{offset}
3018 my $log = $self->dbh_selectall_arrayref($query);
3020 return $self->error("Can't get log for jobid $arg->{jobid}");
3026 $logtxt = join("", map { ($_->[0] . ' ' . $_->[1]) } @$log ) ;
3028 $logtxt = join("", map { $_->[1] } @$log ) ;
3031 $self->display({ lines=> $logtxt,
3032 jobid => $arg->{jobid},
3033 name => $row->{name},
3034 client => $row->{clientname},
3035 offset => $arg->{offset},
3036 limit => $arg->{limit},
3037 }, 'display_log.tpl');
3045 my $arg = $self->get_form('ach', 'slots', 'drive');
3047 unless ($arg->{ach}) {
3048 return $self->error("Can't find autochanger name");
3051 my $a = $self->ach_get($arg->{ach});
3053 return $self->error("Can't find autochanger name in configuration");
3056 my $storage = $a->get_drive_name($arg->{drive});
3058 return $self->error("Can't get your drive name");
3063 if ($arg->{slots}) {
3064 $slots = join(",", @{ $arg->{slots} });
3065 $t += 60*scalar( @{ $arg->{slots} }) ;
3068 my $b = new Bconsole(pref => $self->{info}, timeout => $t,log_stdout => 1);
3069 print "<h1>This command can take long time, be patient...</h1>";
3071 $b->label_barcodes(storage => $storage,
3072 drive => $arg->{drive},
3080 SET LocationId = (SELECT LocationId
3082 WHERE Location = '$arg->{ach}'),
3084 RecyclePoolId = PoolId
3086 WHERE Media.PoolId = (SELECT PoolId
3088 WHERE Name = 'Scratch')
3089 AND (LocationId = 0 OR LocationId IS NULL)
3098 my @volume = CGI::param('media');
3101 return $self->error("Can't get media selection");
3104 my $b = new Bconsole(pref => $self->{info}, timeout => 60);
3107 content => $b->purge_volume(@volume),
3108 title => "Purge media",
3109 name => "purge volume=" . join(' volume=', @volume),
3118 my @volume = CGI::param('media');
3120 return $self->error("Can't get media selection");
3123 my $b = new Bconsole(pref => $self->{info}, timeout => 60);
3126 content => $b->prune_volume(@volume),
3127 title => "Prune media",
3128 name => "prune volume=" . join(' volume=', @volume),
3138 my $arg = $self->get_form('jobid');
3139 unless ($arg->{jobid}) {
3140 return $self->error("Can't get jobid");
3143 my $b = $self->get_bconsole();
3145 content => $b->cancel($arg->{jobid}),
3146 title => "Cancel job",
3147 name => "cancel jobid=$arg->{jobid}",
3153 # Warning, we display current fileset
3156 my $arg = $self->get_form('fileset');
3158 if ($arg->{fileset}) {
3159 my $b = $self->get_bconsole();
3160 my $ret = $b->get_fileset($arg->{fileset});
3161 $self->display({ fileset => $arg->{fileset},
3163 }, "fileset_view.tpl");
3165 $self->error("Can't get fileset name");
3169 sub director_show_sched
3173 my $arg = $self->get_form('days');
3175 my $b = $self->get_bconsole();
3176 my $ret = $b->director_get_sched( $arg->{days} );
3181 }, "scheduled_job.tpl");
3184 sub enable_disable_job
3186 my ($self, $what) = @_ ;
3188 my $name = CGI::param('job') || '';
3189 unless ($name =~ /^[\w\d\.\-\s]+$/) {
3190 return $self->error("Can't find job name");
3193 my $b = $self->get_bconsole();
3203 content => $b->send_cmd("$cmd job=\"$name\""),
3204 title => "$cmd $name",
3205 name => "$cmd job=\"$name\"",
3212 return new Bconsole(pref => $self->{info});
3218 my $b = $self->get_bconsole();
3220 my $joblist = [ map { { name => $_ } } $b->list_job() ];
3222 $self->display({ Jobs => $joblist }, "run_job.tpl");
3227 my ($self, $ouput) = @_;
3230 foreach my $l (split(/\r\n/, $ouput)) {
3231 if ($l =~ /(\w+): name=([\w\d\.\s-]+?)(\s+\w+=.+)?$/) {
3237 if (my @l = $l =~ /(\w+)=([\w\d*]+)/g) {
3243 foreach my $k (keys %arg) {
3244 $lowcase{lc($k)} = $arg{$k} ;
3253 my $b = $self->get_bconsole();
3255 my $job = CGI::param('job') || '';
3257 # we take informations from director, and we overwrite with user wish
3258 my $info = $b->send_cmd("show job=\"$job\"");
3259 my $attr = $self->run_parse_job($info);
3261 my $arg = $self->get_form('pool', 'level', 'client', 'fileset', 'storage');
3262 my %job_opt = (%$attr, %$arg);
3264 my $jobs = [ map {{ name => $_ }} $b->list_job() ];
3266 my $pools = [ map { { name => $_ } } $b->list_pool() ];
3267 my $clients = [ map { { name => $_ } }$b->list_client()];
3268 my $filesets= [ map { { name => $_ } }$b->list_fileset() ];
3269 my $storages= [ map { { name => $_ } }$b->list_storage()];
3274 clients => $clients,
3275 filesets => $filesets,
3276 storages => $storages,
3278 }, "run_job_mod.tpl");
3284 my $b = $self->get_bconsole();
3286 my $jobs = [ map {{ name => $_ }} $b->list_job() ];
3296 my $b = $self->get_bconsole();
3298 # TODO: check input (don't use pool, level)
3300 my $arg = $self->get_form('pool', 'level', 'client', 'priority', 'when', 'fileset');
3301 my $job = CGI::param('job') || '';
3302 my $storage = CGI::param('storage') || '';
3304 my $jobid = $b->run(job => $job,
3305 client => $arg->{client},
3306 priority => $arg->{priority},
3307 level => $arg->{level},
3308 storage => $storage,
3309 pool => $arg->{pool},
3310 fileset => $arg->{fileset},
3311 when => $arg->{when},
3314 print $jobid, $b->{error};
3316 print "<br>You can follow job execution <a href='?action=dsp_cur_job;client=$arg->{client};jobid=$jobid'> here </a>";