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
1405 my %opt_d = ( # option with date
1410 foreach my $i (@what) {
1411 if (exists $opt_i{$i}) {# integer param
1412 my $value = CGI::param($i) || $opt_i{$i} ;
1413 if ($value =~ /^(\d+)$/) {
1416 } elsif ($opt_s{$i}) { # simple string param
1417 my $value = CGI::param($i) || '';
1418 if ($value =~ /^([\w\d\.-]+)$/) {
1421 } elsif ($opt_ss{$i}) { # simple string param (with space)
1422 my $value = CGI::param($i) || '';
1423 if ($value =~ /^([\w\d\.\-\s]+)$/) {
1426 } elsif ($i =~ /^j(\w+)s$/) { # quote join args
1427 my @value = grep { ! /^\s*$/ } CGI::param($1) ;
1429 $ret{$i} = $self->dbh_join(@value) ;
1432 } elsif ($i =~ /^q(\w+[^s])$/) { # 'arg1'
1433 my $value = CGI::param($1) ;
1435 $ret{$i} = $self->dbh_quote($value);
1438 } elsif ($i =~ /^q(\w+)s$/) { #[ 'arg1', 'arg2']
1439 $ret{$i} = [ map { { name => $self->dbh_quote($_) } }
1440 grep { ! /^\s*$/ } CGI::param($1) ];
1441 } elsif (exists $opt_p{$i}) {
1442 my $value = CGI::param($i) || '';
1443 if ($value =~ /^([\w\d\.\/\s:\@\-]+)$/) {
1446 } elsif (exists $opt_d{$i}) {
1447 my $value = CGI::param($i) || '';
1448 if ($value =~ /^\s*(\d+\s+\w+)$/) {
1455 foreach my $s (CGI::param('slot')) {
1456 if ($s =~ /^(\d+)$/) {
1457 push @{$ret{slots}}, $s;
1463 my $when = CGI::param('when') || '';
1464 if ($when =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})$/) {
1469 if ($what{db_clients}) {
1471 SELECT Client.Name as clientname
1475 my $clients = $self->dbh_selectall_hashref($query, 'clientname');
1476 $ret{db_clients} = [sort {$a->{clientname} cmp $b->{clientname} }
1480 if ($what{db_mediatypes}) {
1482 SELECT MediaType as mediatype
1486 my $medias = $self->dbh_selectall_hashref($query, 'mediatype');
1487 $ret{db_mediatypes} = [sort {$a->{mediatype} cmp $b->{mediatype} }
1491 if ($what{db_locations}) {
1493 SELECT Location as location, Cost as cost FROM Location
1495 my $loc = $self->dbh_selectall_hashref($query, 'location');
1496 $ret{db_locations} = [ sort { $a->{location}
1502 if ($what{db_pools}) {
1503 my $query = "SELECT Name as name FROM Pool";
1505 my $all = $self->dbh_selectall_hashref($query, 'name') ;
1506 $ret{db_pools} = [ sort { $a->{name} cmp $b->{name} } values %$all ];
1509 if ($what{db_filesets}) {
1511 SELECT FileSet.FileSet AS fileset
1515 my $filesets = $self->dbh_selectall_hashref($query, 'fileset');
1517 $ret{db_filesets} = [sort {lc($a->{fileset}) cmp lc($b->{fileset}) }
1518 values %$filesets] ;
1521 if ($what{db_jobnames}) {
1523 SELECT DISTINCT Job.Name AS jobname
1527 my $jobnames = $self->dbh_selectall_hashref($query, 'jobname');
1529 $ret{db_jobnames} = [sort {lc($a->{jobname}) cmp lc($b->{jobname}) }
1530 values %$jobnames] ;
1533 if ($what{db_devices}) {
1535 SELECT Device.Name AS name
1539 my $devices = $self->dbh_selectall_hashref($query, 'name');
1541 $ret{db_devices} = [sort {lc($a->{name}) cmp lc($b->{name}) }
1552 my $fields = $self->get_form(qw/age level status clients filesets
1554 db_clients limit db_filesets width height
1555 qclients qfilesets qjobnames db_jobnames/);
1558 my $url = CGI::url(-full => 0,
1561 $url =~ s/^.+?\?//; # http://path/to/bweb.pl?arg => arg
1563 # this organisation is to keep user choice between 2 click
1564 # TODO : fileset and client selection doesn't work
1573 sub display_client_job
1575 my ($self, %arg) = @_ ;
1577 $arg{order} = ' Job.JobId DESC ';
1578 my ($limit, $label) = $self->get_limit(%arg);
1580 my $clientname = $self->dbh_quote($arg{clientname});
1583 SELECT DISTINCT Job.JobId AS jobid,
1584 Job.Name AS jobname,
1585 FileSet.FileSet AS fileset,
1587 StartTime AS starttime,
1588 JobFiles AS jobfiles,
1589 JobBytes AS jobbytes,
1590 JobStatus AS jobstatus,
1591 JobErrors AS joberrors
1593 FROM Client,Job,FileSet
1594 WHERE Client.Name=$clientname
1595 AND Client.ClientId=Job.ClientId
1596 AND Job.FileSetId=FileSet.FileSetId
1600 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
1602 $self->display({ clientname => $arg{clientname},
1605 Jobs => [ values %$all ],
1607 "display_client_job.tpl") ;
1610 sub get_selected_media_location
1614 my $medias = $self->get_form('jmedias');
1616 unless ($medias->{jmedias}) {
1621 SELECT Media.VolumeName AS volumename, Location.Location AS location
1622 FROM Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
1623 WHERE Media.VolumeName IN ($medias->{jmedias})
1626 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
1628 # { 'vol1' => { [volumename => 'vol1', location => 'ici'],
1639 my $medias = $self->get_selected_media_location();
1645 my $elt = $self->get_form('db_locations');
1647 $self->display({ ID => $cur_id++,
1648 %$elt, # db_locations
1650 sort { $a->{volumename} cmp $b->{volumename} } values %$medias
1660 my $elt = $self->get_form(qw/db_pools db_mediatypes db_locations/) ;
1662 $self->display($elt, "help_extern.tpl");
1665 sub help_extern_compute
1669 my $number = CGI::param('limit') || '' ;
1670 unless ($number =~ /^(\d+)$/) {
1671 return $self->error("Bad arg number : $number ");
1674 my ($sql, undef) = $self->get_param('pools',
1675 'locations', 'mediatypes');
1678 SELECT Media.VolumeName AS volumename,
1679 Media.VolStatus AS volstatus,
1680 Media.LastWritten AS lastwritten,
1681 Media.MediaType AS mediatype,
1682 Media.VolMounts AS volmounts,
1684 Media.Recycle AS recycle,
1685 $self->{sql}->{FROM_UNIXTIME}(
1686 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
1687 + $self->{sql}->{TO_SEC}(Media.VolRetention)
1690 INNER JOIN Pool ON (Pool.PoolId = Media.PoolId)
1691 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
1693 WHERE Media.InChanger = 1
1694 AND Media.VolStatus IN ('Disabled', 'Error', 'Full')
1696 ORDER BY expire DESC, recycle, Media.VolMounts DESC
1700 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
1702 $self->display({ Medias => [ values %$all ] },
1703 "help_extern_compute.tpl");
1710 my $param = $self->get_form(qw/db_locations db_pools db_mediatypes/) ;
1711 $self->display($param, "help_intern.tpl");
1714 sub help_intern_compute
1718 my $number = CGI::param('limit') || '' ;
1719 unless ($number =~ /^(\d+)$/) {
1720 return $self->error("Bad arg number : $number ");
1723 my ($sql, undef) = $self->get_param('pools', 'locations', 'mediatypes');
1725 if (CGI::param('expired')) {
1727 AND ( $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
1728 + $self->{sql}->{TO_SEC}(Media.VolRetention)
1734 SELECT Media.VolumeName AS volumename,
1735 Media.VolStatus AS volstatus,
1736 Media.LastWritten AS lastwritten,
1737 Media.MediaType AS mediatype,
1738 Media.VolMounts AS volmounts,
1740 $self->{sql}->{FROM_UNIXTIME}(
1741 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
1742 + $self->{sql}->{TO_SEC}(Media.VolRetention)
1745 INNER JOIN Pool ON (Pool.PoolId = Media.PoolId)
1746 LEFT JOIN Location ON (Location.LocationId = Media.LocationId)
1748 WHERE Media.InChanger <> 1
1749 AND Media.VolStatus IN ('Purged', 'Full', 'Append')
1750 AND Media.Recycle = 1
1752 ORDER BY Media.VolUseDuration DESC, Media.VolMounts ASC, expire ASC
1756 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
1758 $self->display({ Medias => [ values %$all ] },
1759 "help_intern_compute.tpl");
1765 my ($self, %arg) = @_ ;
1767 my ($limit, $label) = $self->get_limit(%arg);
1771 (SELECT count(Pool.PoolId) FROM Pool) AS nb_pool,
1772 (SELECT count(Media.MediaId) FROM Media) AS nb_media,
1773 (SELECT count(Job.JobId) FROM Job) AS nb_job,
1774 (SELECT sum(VolBytes) FROM Media) AS nb_bytes,
1775 ($self->{sql}->{DB_SIZE}) AS db_size,
1776 (SELECT count(Job.JobId)
1778 WHERE Job.JobStatus IN ('E','e','f','A')
1781 (SELECT count(Client.ClientId) FROM Client) AS nb_client
1784 my $row = $self->dbh_selectrow_hashref($query) ;
1786 $row->{nb_bytes} = human_size($row->{nb_bytes});
1788 $row->{db_size} = human_size($row->{db_size});
1789 $row->{label} = $label;
1791 $self->display($row, "general.tpl");
1796 my ($self, @what) = @_ ;
1797 my %elt = map { $_ => 1 } @what;
1802 if ($elt{clients}) {
1803 my @clients = grep { ! /^\s*$/ } CGI::param('client');
1805 $ret{clients} = \@clients;
1806 my $str = $self->dbh_join(@clients);
1807 $limit .= "AND Client.Name IN ($str) ";
1811 if ($elt{filesets}) {
1812 my @filesets = grep { ! /^\s*$/ } CGI::param('fileset');
1814 $ret{filesets} = \@filesets;
1815 my $str = $self->dbh_join(@filesets);
1816 $limit .= "AND FileSet.FileSet IN ($str) ";
1820 if ($elt{mediatypes}) {
1821 my @medias = grep { ! /^\s*$/ } CGI::param('mediatype');
1823 $ret{mediatypes} = \@medias;
1824 my $str = $self->dbh_join(@medias);
1825 $limit .= "AND Media.MediaType IN ($str) ";
1830 my $client = CGI::param('client');
1831 $ret{client} = $client;
1832 $client = $self->dbh_join($client);
1833 $limit .= "AND Client.Name = $client ";
1837 my $level = CGI::param('level') || '';
1838 if ($level =~ /^(\w)$/) {
1840 $limit .= "AND Job.Level = '$1' ";
1845 my $jobid = CGI::param('jobid') || '';
1847 if ($jobid =~ /^(\d+)$/) {
1849 $limit .= "AND Job.JobId = '$1' ";
1854 my $status = CGI::param('status') || '';
1855 if ($status =~ /^(\w)$/) {
1858 $limit .= "AND Job.JobStatus IN ('f','E') ";
1859 } elsif ($1 eq 'W') {
1860 $limit .= "AND Job.JobStatus = 'T' AND Job.JobErrors > 0 ";
1862 $limit .= "AND Job.JobStatus = '$1' ";
1867 if ($elt{volstatus}) {
1868 my $status = CGI::param('volstatus') || '';
1869 if ($status =~ /^(\w+)$/) {
1871 $limit .= "AND Media.VolStatus = '$1' ";
1875 if ($elt{locations}) {
1876 my @location = grep { ! /^\s*$/ } CGI::param('location') ;
1878 $ret{locations} = \@location;
1879 my $str = $self->dbh_join(@location);
1880 $limit .= "AND Location.Location IN ($str) ";
1885 my @pool = grep { ! /^\s*$/ } CGI::param('pool') ;
1887 $ret{pools} = \@pool;
1888 my $str = $self->dbh_join(@pool);
1889 $limit .= "AND Pool.Name IN ($str) ";
1893 if ($elt{location}) {
1894 my $location = CGI::param('location') || '';
1896 $ret{location} = $location;
1897 $location = $self->dbh_quote($location);
1898 $limit .= "AND Location.Location = $location ";
1903 my $pool = CGI::param('pool') || '';
1906 $pool = $self->dbh_quote($pool);
1907 $limit .= "AND Pool.Name = $pool ";
1911 if ($elt{jobtype}) {
1912 my $jobtype = CGI::param('jobtype') || '';
1913 if ($jobtype =~ /^(\w)$/) {
1915 $limit .= "AND Job.Type = '$1' ";
1919 return ($limit, %ret);
1930 my ($self, %arg) = @_ ;
1932 $arg{order} = ' Job.JobId DESC ';
1934 my ($limit, $label) = $self->get_limit(%arg);
1935 my ($where, undef) = $self->get_param('clients',
1944 SELECT Job.JobId AS jobid,
1945 Client.Name AS client,
1946 FileSet.FileSet AS fileset,
1947 Job.Name AS jobname,
1949 StartTime AS starttime,
1951 Pool.Name AS poolname,
1952 JobFiles AS jobfiles,
1953 JobBytes AS jobbytes,
1954 JobStatus AS jobstatus,
1955 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
1956 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime))
1959 JobErrors AS joberrors
1962 Job LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId)
1963 LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId)
1964 WHERE Client.ClientId=Job.ClientId
1965 AND Job.JobStatus != 'R'
1970 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
1972 $self->display({ Filter => $label,
1976 sort { $a->{jobid} <=> $b->{jobid} }
1983 # display job informations
1984 sub display_job_zoom
1986 my ($self, $jobid) = @_ ;
1988 $jobid = $self->dbh_quote($jobid);
1991 SELECT DISTINCT Job.JobId AS jobid,
1992 Client.Name AS client,
1993 Job.Name AS jobname,
1994 FileSet.FileSet AS fileset,
1996 Pool.Name AS poolname,
1997 StartTime AS starttime,
1998 JobFiles AS jobfiles,
1999 JobBytes AS jobbytes,
2000 JobStatus AS jobstatus,
2001 JobErrors AS joberrors,
2002 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
2003 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime)) AS duration
2006 Job LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId)
2007 LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId)
2008 WHERE Client.ClientId=Job.ClientId
2009 AND Job.JobId = $jobid
2012 my $row = $self->dbh_selectrow_hashref($query) ;
2014 # display all volumes associate with this job
2016 SELECT Media.VolumeName as volumename
2017 FROM Job,Media,JobMedia
2018 WHERE Job.JobId = $jobid
2019 AND JobMedia.JobId=Job.JobId
2020 AND JobMedia.MediaId=Media.MediaId
2023 my $all = $self->dbh_selectall_hashref($query, 'volumename');
2025 $row->{volumes} = [ values %$all ] ;
2027 $self->display($row, "display_job_zoom.tpl");
2034 my ($where, %elt) = $self->get_param('pools',
2039 my $arg = $self->get_form('jmedias', 'qre_media');
2041 if ($arg->{jmedias}) {
2042 $where = "AND Media.VolumeName IN ($arg->{jmedias}) $where";
2044 if ($arg->{qre_media}) {
2045 $where = "AND Media.VolumeName $self->{sql}->{MATCH} $arg->{qre_media} $where";
2049 SELECT Media.VolumeName AS volumename,
2050 Media.VolBytes AS volbytes,
2051 Media.VolStatus AS volstatus,
2052 Media.MediaType AS mediatype,
2053 Media.InChanger AS online,
2054 Media.LastWritten AS lastwritten,
2055 Location.Location AS location,
2056 (volbytes*100/COALESCE(media_avg_size.size,-1)) AS volusage,
2057 Pool.Name AS poolname,
2058 $self->{sql}->{FROM_UNIXTIME}(
2059 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2060 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2063 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2064 LEFT JOIN (SELECT avg(Media.VolBytes) AS size,
2065 Media.MediaType AS MediaType
2067 WHERE Media.VolStatus = 'Full'
2068 GROUP BY Media.MediaType
2069 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
2071 WHERE Media.PoolId=Pool.PoolId
2075 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2077 $self->display({ ID => $cur_id++,
2079 Location => $elt{location},
2080 Medias => [ values %$all ]
2082 "display_media.tpl");
2089 my $pool = $self->get_form('db_pools');
2091 foreach my $name (@{ $pool->{db_pools} }) {
2092 CGI::param('pool', $name->{name});
2093 $self->display_media();
2097 sub display_media_zoom
2101 my $medias = $self->get_form('jmedias');
2103 unless ($medias->{jmedias}) {
2104 return $self->error("Can't get media selection");
2108 SELECT InChanger AS online,
2109 VolBytes AS nb_bytes,
2110 VolumeName AS volumename,
2111 VolStatus AS volstatus,
2112 VolMounts AS nb_mounts,
2113 Media.VolUseDuration AS voluseduration,
2114 Media.MaxVolJobs AS maxvoljobs,
2115 Media.MaxVolFiles AS maxvolfiles,
2116 Media.MaxVolBytes AS maxvolbytes,
2117 VolErrors AS nb_errors,
2118 Pool.Name AS poolname,
2119 Location.Location AS location,
2120 Media.Recycle AS recycle,
2121 Media.VolRetention AS volretention,
2122 Media.LastWritten AS lastwritten,
2123 Media.VolReadTime/1000000 AS volreadtime,
2124 Media.VolWriteTime/1000000 AS volwritetime,
2125 Media.RecycleCount AS recyclecount,
2126 Media.Comment AS comment,
2127 $self->{sql}->{FROM_UNIXTIME}(
2128 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2129 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2132 Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2133 WHERE Pool.PoolId = Media.PoolId
2134 AND VolumeName IN ($medias->{jmedias})
2137 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2139 foreach my $media (values %$all) {
2140 my $mq = $self->dbh_quote($media->{volumename});
2143 SELECT DISTINCT Job.JobId AS jobid,
2145 Job.StartTime AS starttime,
2148 Job.JobFiles AS files,
2149 Job.JobBytes AS bytes,
2150 Job.jobstatus AS status
2151 FROM Media,JobMedia,Job
2152 WHERE Media.VolumeName=$mq
2153 AND Media.MediaId=JobMedia.MediaId
2154 AND JobMedia.JobId=Job.JobId
2157 my $jobs = $self->dbh_selectall_hashref($query, 'jobid') ;
2160 SELECT LocationLog.Date AS date,
2161 Location.Location AS location,
2162 LocationLog.Comment AS comment
2163 FROM Media,LocationLog INNER JOIN Location ON (LocationLog.LocationId = Location.LocationId)
2164 WHERE Media.MediaId = LocationLog.MediaId
2165 AND Media.VolumeName = $mq
2169 my $log = $self->dbh_selectall_arrayref($query) ;
2171 $logtxt = join("\n", map { ($_->[0] . ' ' . $_->[1] . ' ' . $_->[2])} @$log ) ;
2174 $self->display({ jobs => [ values %$jobs ],
2175 LocationLog => $logtxt,
2177 "display_media_zoom.tpl");
2185 my $loc = $self->get_form('qlocation');
2186 unless ($loc->{qlocation}) {
2187 return $self->error("Can't get location");
2191 SELECT Location.Location AS location,
2192 Location.Cost AS cost,
2193 Location.Enabled AS enabled
2195 WHERE Location.Location = $loc->{qlocation}
2198 my $row = $self->dbh_selectrow_hashref($query);
2200 $self->display({ ID => $cur_id++,
2201 %$row }, "location_edit.tpl") ;
2209 my $arg = $self->get_form(qw/qlocation qnewlocation cost/) ;
2210 unless ($arg->{qlocation}) {
2211 return $self->error("Can't get location");
2213 unless ($arg->{qnewlocation}) {
2214 return $self->error("Can't get new location name");
2216 unless ($arg->{cost}) {
2217 return $self->error("Can't get new cost");
2220 my $enabled = CGI::param('enabled') || '';
2221 $enabled = $enabled?1:0;
2224 UPDATE Location SET Cost = $arg->{cost},
2225 Location = $arg->{qnewlocation},
2227 WHERE Location.Location = $arg->{qlocation}
2230 $self->dbh_do($query);
2232 $self->display_location();
2238 my $arg = $self->get_form(qw/qlocation/) ;
2240 unless ($arg->{qlocation}) {
2241 return $self->error("Can't get location");
2245 SELECT count(Media.MediaId) AS nb
2246 FROM Media INNER JOIN Location USING (LocationID)
2247 WHERE Location = $arg->{qlocation}
2250 my $res = $self->dbh_selectrow_hashref($query);
2253 return $self->error("Sorry, the location must be empty");
2257 DELETE FROM Location WHERE Location = $arg->{qlocation} LIMIT 1
2260 $self->dbh_do($query);
2262 $self->display_location();
2269 my $arg = $self->get_form(qw/qlocation cost/) ;
2271 unless ($arg->{qlocation}) {
2272 $self->display({}, "location_add.tpl");
2275 unless ($arg->{cost}) {
2276 return $self->error("Can't get new cost");
2279 my $enabled = CGI::param('enabled') || '';
2280 $enabled = $enabled?1:0;
2283 INSERT INTO Location (Location, Cost, Enabled)
2284 VALUES ($arg->{qlocation}, $arg->{cost}, $enabled)
2287 $self->dbh_do($query);
2289 $self->display_location();
2292 sub display_location
2297 SELECT Location.Location AS location,
2298 Location.Cost AS cost,
2299 Location.Enabled AS enabled,
2300 (SELECT count(Media.MediaId)
2302 WHERE Media.LocationId = Location.LocationId
2307 my $location = $self->dbh_selectall_hashref($query, 'location');
2309 $self->display({ ID => $cur_id++,
2310 Locations => [ values %$location ] },
2311 "display_location.tpl");
2318 my $medias = $self->get_selected_media_location();
2323 my $arg = $self->get_form('db_locations', 'qnewlocation');
2325 $self->display({ email => $self->{info}->{email_media},
2327 medias => [ values %$medias ],
2329 "update_location.tpl");
2332 sub get_media_max_size
2334 my ($self, $type) = @_;
2336 "SELECT avg(VolBytes) AS size
2338 WHERE Media.VolStatus = 'Full'
2339 AND Media.MediaType = '$type'
2342 my $res = $self->selectrow_hashref($query);
2345 return $res->{size};
2355 my $media = $self->get_form('qmedia');
2357 unless ($media->{qmedia}) {
2358 return $self->error("Can't get media");
2362 SELECT Media.Slot AS slot,
2363 PoolMedia.Name AS poolname,
2364 Media.VolStatus AS volstatus,
2365 Media.InChanger AS inchanger,
2366 Location.Location AS location,
2367 Media.VolumeName AS volumename,
2368 Media.MaxVolBytes AS maxvolbytes,
2369 Media.MaxVolJobs AS maxvoljobs,
2370 Media.MaxVolFiles AS maxvolfiles,
2371 Media.VolUseDuration AS voluseduration,
2372 Media.VolRetention AS volretention,
2373 Media.Comment AS comment,
2374 PoolRecycle.Name AS poolrecycle
2376 FROM Media INNER JOIN Pool AS PoolMedia ON (Media.PoolId = PoolMedia.PoolId)
2377 LEFT JOIN Pool AS PoolRecycle ON (Media.RecyclePoolId = PoolRecycle.PoolId)
2378 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2380 WHERE Media.VolumeName = $media->{qmedia}
2383 my $row = $self->dbh_selectrow_hashref($query);
2384 $row->{volretention} = human_sec($row->{volretention});
2385 $row->{voluseduration} = human_sec($row->{voluseduration});
2387 my $elt = $self->get_form(qw/db_pools db_locations/);
2392 }, "update_media.tpl");
2399 my $arg = $self->get_form('jmedias', 'qnewlocation') ;
2401 unless ($arg->{jmedias}) {
2402 return $self->error("Can't get selected media");
2405 unless ($arg->{qnewlocation}) {
2406 return $self->error("Can't get new location");
2411 SET LocationId = (SELECT LocationId
2413 WHERE Location = $arg->{qnewlocation})
2414 WHERE Media.VolumeName IN ($arg->{jmedias})
2417 my $nb = $self->dbh_do($query);
2419 print "$nb media updated, you may have to update your autochanger.";
2421 $self->display_media();
2428 my $medias = $self->get_selected_media_location();
2430 return $self->error("Can't get media selection");
2432 my $newloc = CGI::param('newlocation');
2434 my $user = CGI::param('user') || 'unknown';
2435 my $comm = CGI::param('comment') || '';
2436 $comm = $self->dbh_quote("$user: $comm");
2440 foreach my $media (keys %$medias) {
2442 INSERT LocationLog (Date, Comment, MediaId, LocationId, NewVolStatus)
2444 NOW(), $comm, (SELECT MediaId FROM Media WHERE VolumeName = '$media'),
2445 (SELECT LocationId FROM Location WHERE Location = '$medias->{$media}->{location}'),
2446 (SELECT VolStatus FROM Media WHERE VolumeName = '$media')
2449 $self->dbh_do($query);
2450 $self->debug($query);
2454 $q->param('action', 'update_location');
2455 my $url = $q->url(-full => 1, -query=>1);
2457 $self->display({ email => $self->{info}->{email_media},
2459 newlocation => $newloc,
2460 # [ { volumename => 'vol1' }, { volumename => 'vol2'
\81 },..]
2461 medias => [ values %$medias ],
2463 "change_location.tpl");
2467 sub display_client_stats
2469 my ($self, %arg) = @_ ;
2471 my $client = $self->dbh_quote($arg{clientname});
2472 my ($limit, $label) = $self->get_limit(%arg);
2476 count(Job.JobId) AS nb_jobs,
2477 sum(Job.JobBytes) AS nb_bytes,
2478 sum(Job.JobErrors) AS nb_err,
2479 sum(Job.JobFiles) AS nb_files,
2480 Client.Name AS clientname
2481 FROM Job INNER JOIN Client USING (ClientId)
2483 Client.Name = $client
2485 GROUP BY Client.Name
2488 my $row = $self->dbh_selectrow_hashref($query);
2490 $row->{ID} = $cur_id++;
2491 $row->{label} = $label;
2493 $self->display($row, "display_client_stats.tpl");
2496 # poolname can be undef
2499 my ($self, $poolname) = @_ ;
2503 my $arg = $self->get_form('jmediatypes', 'qmediatypes');
2504 if ($arg->{jmediatypes}) {
2505 $whereW = "WHERE MediaType IN ($arg->{jmediatypes}) ";
2506 $whereA = "AND MediaType IN ($arg->{jmediatypes}) ";
2509 # TODO : afficher les tailles et les dates
2512 SELECT subq.volmax AS volmax,
2513 subq.volnum AS volnum,
2514 subq.voltotal AS voltotal,
2516 Pool.Recycle AS recycle,
2517 Pool.VolRetention AS volretention,
2518 Pool.VolUseDuration AS voluseduration,
2519 Pool.MaxVolJobs AS maxvoljobs,
2520 Pool.MaxVolFiles AS maxvolfiles,
2521 Pool.MaxVolBytes AS maxvolbytes,
2522 subq.PoolId AS PoolId
2525 SELECT COALESCE(media_avg_size.volavg,0) * count(Media.MediaId) AS volmax,
2526 count(Media.MediaId) AS volnum,
2527 sum(Media.VolBytes) AS voltotal,
2528 Media.PoolId AS PoolId,
2529 Media.MediaType AS MediaType
2531 LEFT JOIN (SELECT avg(Media.VolBytes) AS volavg,
2532 Media.MediaType AS MediaType
2534 WHERE Media.VolStatus = 'Full'
2535 GROUP BY Media.MediaType
2536 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
2537 GROUP BY Media.MediaType, Media.PoolId, media_avg_size.volavg
2539 LEFT JOIN Pool ON (Pool.PoolId = subq.PoolId)
2543 my $all = $self->dbh_selectall_hashref($query, 'name') ;
2546 SELECT Pool.Name AS name,
2547 sum(VolBytes) AS size
2548 FROM Media JOIN Pool ON (Media.PoolId = Pool.PoolId)
2549 WHERE Media.VolStatus IN ('Recycled', 'Purged')
2553 my $empty = $self->dbh_selectall_hashref($query, 'name');
2555 foreach my $p (values %$all) {
2556 if ($p->{volmax} > 0) { # mysql returns 0.0000
2557 # we remove Recycled/Purged media from pool usage
2558 if (defined $empty->{$p->{name}}) {
2559 $p->{voltotal} -= $empty->{$p->{name}}->{size};
2561 $p->{poolusage} = sprintf('%.2f', $p->{voltotal} * 100/ $p->{volmax}) ;
2563 $p->{poolusage} = 0;
2567 SELECT VolStatus AS volstatus, count(MediaId) AS nb
2569 WHERE PoolId=$p->{poolid}
2573 my $content = $self->dbh_selectall_hashref($query, 'volstatus');
2574 foreach my $t (values %$content) {
2575 $p->{"nb_" . $t->{volstatus}} = $t->{nb} ;
2580 $self->display({ ID => $cur_id++,
2581 MediaType => $arg->{qmediatypes}, # [ { name => type1 } , { name => type2 } ]
2582 Pools => [ values %$all ]},
2583 "display_pool.tpl");
2586 sub display_running_job
2590 my $arg = $self->get_form('client', 'jobid');
2592 if (!$arg->{client} and $arg->{jobid}) {
2595 SELECT Client.Name AS name
2596 FROM Job INNER JOIN Client USING (ClientId)
2597 WHERE Job.JobId = $arg->{jobid}
2600 my $row = $self->dbh_selectrow_hashref($query);
2603 $arg->{client} = $row->{name};
2604 CGI::param('client', $arg->{client});
2608 if ($arg->{client}) {
2609 my $cli = new Bweb::Client(name => $arg->{client});
2610 $cli->display_running_job($self->{info}, $arg->{jobid});
2611 if ($arg->{jobid}) {
2612 $self->get_job_log();
2615 $self->error("Can't get client or jobid");
2619 sub display_running_jobs
2621 my ($self, $display_action) = @_;
2624 SELECT Job.JobId AS jobid,
2625 Job.Name AS jobname,
2627 Job.StartTime AS starttime,
2628 Job.JobFiles AS jobfiles,
2629 Job.JobBytes AS jobbytes,
2630 Job.JobStatus AS jobstatus,
2631 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(NOW())
2632 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime))
2634 Client.Name AS clientname
2635 FROM Job INNER JOIN Client USING (ClientId)
2636 WHERE JobStatus IN ('C','R','B','e','D','F','S','m','M','s','j','c','d','t','p')
2638 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
2640 $self->display({ ID => $cur_id++,
2641 display_action => $display_action,
2642 Jobs => [ values %$all ]},
2643 "running_job.tpl") ;
2646 # return the autochanger list to update
2651 my $arg = $self->get_form('jmedias');
2653 unless ($arg->{jmedias}) {
2654 return $self->error("Can't get media selection");
2658 SELECT Media.VolumeName AS volumename,
2659 Storage.Name AS storage,
2660 Location.Location AS location,
2662 FROM Media INNER JOIN Storage ON (Media.StorageId = Storage.StorageId)
2663 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2664 WHERE Media.VolumeName IN ($arg->{jmedias})
2665 AND Media.InChanger = 1
2668 my $all = $self->dbh_selectall_hashref($query, 'volumename');
2670 foreach my $vol (values %$all) {
2671 my $a = $self->ach_get($vol->{location});
2673 $ret{$vol->{location}} = 1;
2675 unless ($a->{have_status}) {
2677 $a->{have_status} = 1;
2680 print "eject $vol->{volumename} from $vol->{storage} : ";
2681 if ($a->send_to_io($vol->{slot})) {
2682 print "<img src='/bweb/T.png' alt='ok'><br/>";
2684 print "<img src='/bweb/E.png' alt='err'><br/>";
2694 my ($to, $subject, $content) = (CGI::param('email'),
2695 CGI::param('subject'),
2696 CGI::param('content'));
2697 $to =~ s/[^\w\d\.\@<>,]//;
2698 $subject =~ s/[^\w\d\.\[\]]/ /;
2700 open(MAIL, "|mail -s '$subject' '$to'") ;
2701 print MAIL $content;
2711 my $arg = $self->get_form('jobid', 'client');
2713 print CGI::header('text/brestore');
2714 print "jobid=$arg->{jobid}\n" if ($arg->{jobid});
2715 print "client=$arg->{client}\n" if ($arg->{client});
2716 print "\n\nYou have to assign this mime type with /usr/bin/brestore.pl\n";
2720 # TODO : move this to Bweb::Autochanger ?
2721 # TODO : make this internal to not eject tape ?
2727 my ($self, $name) = @_;
2730 return $self->error("Can't get your autochanger name ach");
2733 unless ($self->{info}->{ach_list}) {
2734 return $self->error("Could not find any autochanger");
2737 my $a = $self->{info}->{ach_list}->{$name};
2740 $self->error("Can't get your autochanger $name from your ach_list");
2745 $a->{debug} = $self->{debug};
2752 my ($self, $ach) = @_;
2754 $self->{info}->{ach_list}->{$ach->{name}} = $ach;
2756 $self->{info}->save();
2764 my $arg = $self->get_form('ach');
2766 or !$self->{info}->{ach_list}
2767 or !$self->{info}->{ach_list}->{$arg->{ach}})
2769 return $self->error("Can't get autochanger name");
2772 my $ach = $self->{info}->{ach_list}->{$arg->{ach}};
2776 [ map { { name => $_, index => $i++ } } @{$ach->{drive_name}} ] ;
2778 my $b = $self->get_bconsole();
2780 my @storages = $b->list_storage() ;
2782 $ach->{devices} = [ map { { name => $_ } } @storages ];
2784 $self->display($ach, "ach_add.tpl");
2785 delete $ach->{drives};
2786 delete $ach->{devices};
2793 my $arg = $self->get_form('ach');
2796 or !$self->{info}->{ach_list}
2797 or !$self->{info}->{ach_list}->{$arg->{ach}})
2799 return $self->error("Can't get autochanger name");
2802 delete $self->{info}->{ach_list}->{$arg->{ach}} ;
2804 $self->{info}->save();
2805 $self->{info}->view();
2811 my $arg = $self->get_form('ach', 'mtxcmd', 'device', 'precmd');
2813 my $b = $self->get_bconsole();
2814 my @storages = $b->list_storage() ;
2816 unless ($arg->{ach}) {
2817 $arg->{devices} = [ map { { name => $_ } } @storages ];
2818 return $self->display($arg, "ach_add.tpl");
2822 foreach my $drive (CGI::param('drives'))
2824 unless (grep(/^$drive$/,@storages)) {
2825 return $self->error("Can't find $drive in storage list");
2828 my $index = CGI::param("index_$drive");
2829 unless (defined $index and $index =~ /^(\d+)$/) {
2830 return $self->error("Can't get $drive index");
2833 $drives[$index] = $drive;
2837 return $self->error("Can't get drives from Autochanger");
2840 my $a = new Bweb::Autochanger(name => $arg->{ach},
2841 precmd => $arg->{precmd},
2842 drive_name => \@drives,
2843 device => $arg->{device},
2844 mtxcmd => $arg->{mtxcmd});
2846 $self->ach_register($a) ;
2848 $self->{info}->view();
2854 my $arg = $self->get_form('jobid');
2856 if ($arg->{jobid}) {
2857 my $b = $self->get_bconsole();
2858 my $ret = $b->send_cmd("delete jobid=\"$arg->{jobid}\"");
2862 title => "Delete a job ",
2863 name => "delete jobid=$arg->{jobid}",
2872 my $arg = $self->get_form(qw/media volstatus inchanger pool
2873 slot volretention voluseduration
2874 maxvoljobs maxvolfiles maxvolbytes
2875 qcomment poolrecycle
2878 unless ($arg->{media}) {
2879 return $self->error("Can't find media selection");
2882 my $update = "update volume=$arg->{media} ";
2884 if ($arg->{volstatus}) {
2885 $update .= " volstatus=$arg->{volstatus} ";
2888 if ($arg->{inchanger}) {
2889 $update .= " inchanger=yes " ;
2891 $update .= " slot=$arg->{slot} ";
2894 $update .= " slot=0 inchanger=no ";
2898 $update .= " pool=$arg->{pool} " ;
2901 if (defined $arg->{volretention}) {
2902 $update .= " volretention=\"$arg->{volretention}\" " ;
2905 if (defined $arg->{voluseduration}) {
2906 $update .= " voluse=\"$arg->{voluseduration}\" " ;
2909 if (defined $arg->{maxvoljobs}) {
2910 $update .= " maxvoljobs=$arg->{maxvoljobs} " ;
2913 if (defined $arg->{maxvolfiles}) {
2914 $update .= " maxvolfiles=$arg->{maxvolfiles} " ;
2917 if (defined $arg->{maxvolbytes}) {
2918 $update .= " maxvolbytes=$arg->{maxvolbytes} " ;
2921 my $b = $self->get_bconsole();
2924 content => $b->send_cmd($update),
2925 title => "Update a volume ",
2931 my $media = $self->dbh_quote($arg->{media});
2933 my $loc = CGI::param('location') || '';
2935 $loc = $self->dbh_quote($loc); # is checked by db
2936 push @q, "LocationId=(SELECT LocationId FROM Location WHERE Location=$loc)";
2938 if ($arg->{poolrecycle}) {
2939 push @q, "RecyclePoolId=(SELECT PoolId FROM Pool WHERE Name='$arg->{poolrecycle}')";
2941 if (!$arg->{qcomment}) {
2942 $arg->{qcomment} = "''";
2944 push @q, "Comment=$arg->{qcomment}";
2949 SET " . join (',', @q) . "
2950 WHERE Media.VolumeName = $media
2952 $self->dbh_do($query);
2954 $self->update_media();
2961 my $ach = CGI::param('ach') ;
2962 $ach = $self->ach_get($ach);
2964 return $self->error("Bad autochanger name");
2968 my $b = new Bconsole(pref => $self->{info},timeout => 60,log_stdout => 1);
2969 $b->update_slots($ach->{name});
2977 my $arg = $self->get_form('jobid', 'limit', 'offset');
2978 unless ($arg->{jobid}) {
2979 return $self->error("Can't get jobid");
2982 if ($arg->{limit} == 100) {
2983 $arg->{limit} = 1000;
2986 my $t = CGI::param('time') || $self->{info}->{display_log_time} || '';
2989 SELECT Job.Name as name, Client.Name as clientname
2990 FROM Job INNER JOIN Client ON (Job.ClientId = Client.ClientId)
2991 WHERE JobId = $arg->{jobid}
2994 my $row = $self->dbh_selectrow_hashref($query);
2997 return $self->error("Can't find $arg->{jobid} in catalog");
3001 SELECT Time AS time, LogText AS log
3003 WHERE Log.JobId = $arg->{jobid}
3004 OR (Log.JobId = 0 AND Time >= (SELECT StartTime FROM Job WHERE JobId=$arg->{jobid})
3005 AND Time <= (SELECT COALESCE(EndTime,NOW()) FROM Job WHERE JobId=$arg->{jobid})
3009 OFFSET $arg->{offset}
3012 my $log = $self->dbh_selectall_arrayref($query);
3014 return $self->error("Can't get log for jobid $arg->{jobid}");
3020 $logtxt = join("", map { ($_->[0] . ' ' . $_->[1]) } @$log ) ;
3022 $logtxt = join("", map { $_->[1] } @$log ) ;
3025 $self->display({ lines=> $logtxt,
3026 jobid => $arg->{jobid},
3027 name => $row->{name},
3028 client => $row->{clientname},
3029 offset => $arg->{offset},
3030 limit => $arg->{limit},
3031 }, 'display_log.tpl');
3039 my $arg = $self->get_form('ach', 'slots', 'drive');
3041 unless ($arg->{ach}) {
3042 return $self->error("Can't find autochanger name");
3045 my $a = $self->ach_get($arg->{ach});
3047 return $self->error("Can't find autochanger name in configuration");
3050 my $storage = $a->get_drive_name($arg->{drive});
3052 return $self->error("Can't get your drive name");
3057 if ($arg->{slots}) {
3058 $slots = join(",", @{ $arg->{slots} });
3059 $t += 60*scalar( @{ $arg->{slots} }) ;
3062 my $b = new Bconsole(pref => $self->{info}, timeout => $t,log_stdout => 1);
3063 print "<h1>This command can take long time, be patient...</h1>";
3065 $b->label_barcodes(storage => $storage,
3066 drive => $arg->{drive},
3074 SET LocationId = (SELECT LocationId
3076 WHERE Location = '$arg->{ach}'),
3078 RecyclePoolId = PoolId
3080 WHERE Media.PoolId = (SELECT PoolId
3082 WHERE Name = 'Scratch')
3083 AND (LocationId = 0 OR LocationId IS NULL)
3092 my @volume = CGI::param('media');
3095 return $self->error("Can't get media selection");
3098 my $b = new Bconsole(pref => $self->{info}, timeout => 60);
3101 content => $b->purge_volume(@volume),
3102 title => "Purge media",
3103 name => "purge volume=" . join(' volume=', @volume),
3112 my @volume = CGI::param('media');
3114 return $self->error("Can't get media selection");
3117 my $b = new Bconsole(pref => $self->{info}, timeout => 60);
3120 content => $b->prune_volume(@volume),
3121 title => "Prune media",
3122 name => "prune volume=" . join(' volume=', @volume),
3132 my $arg = $self->get_form('jobid');
3133 unless ($arg->{jobid}) {
3134 return $self->error("Can't get jobid");
3137 my $b = $self->get_bconsole();
3139 content => $b->cancel($arg->{jobid}),
3140 title => "Cancel job",
3141 name => "cancel jobid=$arg->{jobid}",
3147 # Warning, we display current fileset
3150 my $arg = $self->get_form('fileset');
3152 if ($arg->{fileset}) {
3153 my $b = $self->get_bconsole();
3154 my $ret = $b->get_fileset($arg->{fileset});
3155 $self->display({ fileset => $arg->{fileset},
3157 }, "fileset_view.tpl");
3159 $self->error("Can't get fileset name");
3163 sub director_show_sched
3167 my $arg = $self->get_form('days');
3169 my $b = $self->get_bconsole();
3170 my $ret = $b->director_get_sched( $arg->{days} );
3175 }, "scheduled_job.tpl");
3178 sub enable_disable_job
3180 my ($self, $what) = @_ ;
3182 my $name = CGI::param('job') || '';
3183 unless ($name =~ /^[\w\d\.\-\s]+$/) {
3184 return $self->error("Can't find job name");
3187 my $b = $self->get_bconsole();
3197 content => $b->send_cmd("$cmd job=\"$name\""),
3198 title => "$cmd $name",
3199 name => "$cmd job=\"$name\"",
3206 return new Bconsole(pref => $self->{info});
3212 my $b = $self->get_bconsole();
3214 my $joblist = [ map { { name => $_ } } $b->list_job() ];
3216 $self->display({ Jobs => $joblist }, "run_job.tpl");
3221 my ($self, $ouput) = @_;
3224 foreach my $l (split(/\r\n/, $ouput)) {
3225 if ($l =~ /(\w+): name=([\w\d\.\s-]+?)(\s+\w+=.+)?$/) {
3231 if (my @l = $l =~ /(\w+)=([\w\d*]+)/g) {
3237 foreach my $k (keys %arg) {
3238 $lowcase{lc($k)} = $arg{$k} ;
3247 my $b = $self->get_bconsole();
3249 my $job = CGI::param('job') || '';
3251 # we take informations from director, and we overwrite with user wish
3252 my $info = $b->send_cmd("show job=\"$job\"");
3253 my $attr = $self->run_parse_job($info);
3255 my $arg = $self->get_form('pool', 'level', 'client', 'fileset', 'storage');
3256 my %job_opt = (%$attr, %$arg);
3258 my $jobs = [ map {{ name => $_ }} $b->list_job() ];
3260 my $pools = [ map { { name => $_ } } $b->list_pool() ];
3261 my $clients = [ map { { name => $_ } }$b->list_client()];
3262 my $filesets= [ map { { name => $_ } }$b->list_fileset() ];
3263 my $storages= [ map { { name => $_ } }$b->list_storage()];
3268 clients => $clients,
3269 filesets => $filesets,
3270 storages => $storages,
3272 }, "run_job_mod.tpl");
3278 my $b = $self->get_bconsole();
3280 my $jobs = [ map {{ name => $_ }} $b->list_job() ];
3290 my $b = $self->get_bconsole();
3292 # TODO: check input (don't use pool, level)
3294 my $arg = $self->get_form('pool', 'level', 'client', 'priority', 'when', 'fileset');
3295 my $job = CGI::param('job') || '';
3296 my $storage = CGI::param('storage') || '';
3298 my $jobid = $b->run(job => $job,
3299 client => $arg->{client},
3300 priority => $arg->{priority},
3301 level => $arg->{level},
3302 storage => $storage,
3303 pool => $arg->{pool},
3304 fileset => $arg->{fileset},
3305 when => $arg->{when},
3308 print $jobid, $b->{error};
3310 print "<br>You can follow job execution <a href='?action=dsp_cur_job;client=$arg->{client};jobid=$jobid'> here </a>";