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 ",
1074 sub dbh_selectall_arrayref
1076 my ($self, $query) = @_;
1077 $self->connect_db();
1078 $self->debug($query);
1079 return $self->{dbh}->selectall_arrayref($query);
1084 my ($self, @what) = @_;
1085 return join(',', $self->dbh_quote(@what)) ;
1090 my ($self, @what) = @_;
1092 $self->connect_db();
1094 return map { $self->{dbh}->quote($_) } @what;
1096 return $self->{dbh}->quote($what[0]) ;
1102 my ($self, $query) = @_ ;
1103 $self->connect_db();
1104 $self->debug($query);
1105 return $self->{dbh}->do($query);
1108 sub dbh_selectall_hashref
1110 my ($self, $query, $join) = @_;
1112 $self->connect_db();
1113 $self->debug($query);
1114 return $self->{dbh}->selectall_hashref($query, $join) ;
1117 sub dbh_selectrow_hashref
1119 my ($self, $query) = @_;
1121 $self->connect_db();
1122 $self->debug($query);
1123 return $self->{dbh}->selectrow_hashref($query) ;
1129 my @unit = qw(b Kb Mb Gb Tb);
1130 my $val = shift || 0;
1132 my $format = '%i %s';
1133 while ($val / 1024 > 1) {
1137 $format = ($i>0)?'%0.1f %s':'%i %s';
1138 return sprintf($format, $val, $unit[$i]);
1141 # display Day, Hour, Year
1147 $val /= 60; # sec -> min
1149 if ($val / 60 <= 1) {
1153 $val /= 60; # min -> hour
1154 if ($val / 24 <= 1) {
1155 return "$val hours";
1158 $val /= 24; # hour -> day
1159 if ($val / 365 < 2) {
1163 $val /= 365 ; # day -> year
1165 return "$val years";
1168 # get Day, Hour, Year
1174 unless ($val =~ /^\s*(\d+)\s*(\w)\w*\s*$/) {
1178 my %times = ( m => 60,
1184 my $mult = $times{$2} || 0;
1194 unless ($self->{dbh}) {
1195 $self->{dbh} = DBI->connect($self->{info}->{dbi},
1196 $self->{info}->{user},
1197 $self->{info}->{password});
1199 $self->error("Can't connect to your database:\n$DBI::errstr\n")
1200 unless ($self->{dbh});
1202 $self->{dbh}->{FetchHashKeyName} = 'NAME_lc';
1204 if ($self->{info}->{dbi} =~ /^dbi:Pg/i) {
1205 $self->{dbh}->do("SET datestyle TO 'ISO, YMD'");
1212 my ($class, %arg) = @_;
1214 dbh => undef, # connect_db();
1216 dbi => '', # DBI:Pg:database=bacula;host=127.0.0.1
1222 map { $self->{lc($_)} = $arg{$_} } keys %arg ;
1224 if ($self->{info}->{dbi} =~ /DBI:(\w+):/i) {
1225 $self->{sql} = $sql_func{$1};
1228 $self->{debug} = $self->{info}->{debug};
1229 $Bweb::Gui::template_dir = $self->{info}->{template_dir};
1237 $self->display($self->{info}, "begin.tpl");
1243 $self->display($self->{info}, "end.tpl");
1251 my $arg = $self->get_form("client", "qre_client");
1253 if ($arg->{qre_client}) {
1254 $where = "WHERE Name $self->{sql}->{MATCH} $arg->{qre_client} ";
1255 } elsif ($arg->{client}) {
1256 $where = "WHERE Name = '$arg->{client}' ";
1260 SELECT Name AS name,
1262 AutoPrune AS autoprune,
1263 FileRetention AS fileretention,
1264 JobRetention AS jobretention
1269 my $all = $self->dbh_selectall_hashref($query, 'name') ;
1271 my $dsp = { ID => $cur_id++,
1272 clients => [ values %$all] };
1274 $self->display($dsp, "client_list.tpl") ;
1279 my ($self, %arg) = @_;
1286 "AND $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
1288 ( $self->{sql}->{UNIX_TIMESTAMP}(NOW())
1290 $self->{sql}->{TO_SEC}($arg{age})
1293 $label = "last " . human_sec($arg{age});
1296 if ($arg{groupby}) {
1297 $limit .= " GROUP BY $arg{groupby} ";
1301 $limit .= " ORDER BY $arg{order} ";
1305 $limit .= " LIMIT $arg{limit} ";
1306 $label .= " limited to $arg{limit}";
1310 $limit .= " OFFSET $arg{offset} ";
1311 $label .= " with $arg{offset} offset ";
1315 $label = 'no filter';
1318 return ($limit, $label);
1323 $bweb->get_form(...) - Get useful stuff
1327 This function get and check parameters against regexp.
1329 If word begin with 'q', the return will be quoted or join quoted
1330 if it's end with 's'.
1335 $bweb->get_form('jobid', 'qclient', 'qpools') ;
1338 qclient => 'plume-fd',
1339 qpools => "'plume-fd', 'test-fd', '...'",
1346 my ($self, @what) = @_;
1347 my %what = map { $_ => 1 } @what;
1367 my %opt_ss =( # string with space
1371 my %opt_s = ( # default to ''
1388 my %opt_p = ( # option with path
1396 my %opt_d = ( # option with date
1401 foreach my $i (@what) {
1402 if (exists $opt_i{$i}) {# integer param
1403 my $value = CGI::param($i) || $opt_i{$i} ;
1404 if ($value =~ /^(\d+)$/) {
1407 } elsif ($opt_s{$i}) { # simple string param
1408 my $value = CGI::param($i) || '';
1409 if ($value =~ /^([\w\d\.-]+)$/) {
1412 } elsif ($opt_ss{$i}) { # simple string param (with space)
1413 my $value = CGI::param($i) || '';
1414 if ($value =~ /^([\w\d\.\-\s]+)$/) {
1417 } elsif ($i =~ /^j(\w+)s$/) { # quote join args
1418 my @value = grep { ! /^\s*$/ } CGI::param($1) ;
1420 $ret{$i} = $self->dbh_join(@value) ;
1423 } elsif ($i =~ /^q(\w+[^s])$/) { # 'arg1'
1424 my $value = CGI::param($1) ;
1426 $ret{$i} = $self->dbh_quote($value);
1429 } elsif ($i =~ /^q(\w+)s$/) { #[ 'arg1', 'arg2']
1430 $ret{$i} = [ map { { name => $self->dbh_quote($_) } }
1431 grep { ! /^\s*$/ } CGI::param($1) ];
1432 } elsif (exists $opt_p{$i}) {
1433 my $value = CGI::param($i) || '';
1434 if ($value =~ /^([\w\d\.\/\s:\@\-]+)$/) {
1437 } elsif (exists $opt_d{$i}) {
1438 my $value = CGI::param($i) || '';
1439 if ($value =~ /^\s*(\d+\s+\w+)$/) {
1446 foreach my $s (CGI::param('slot')) {
1447 if ($s =~ /^(\d+)$/) {
1448 push @{$ret{slots}}, $s;
1454 my $when = CGI::param('when') || '';
1455 if ($when =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})$/) {
1460 if ($what{db_clients}) {
1462 SELECT Client.Name as clientname
1466 my $clients = $self->dbh_selectall_hashref($query, 'clientname');
1467 $ret{db_clients} = [sort {$a->{clientname} cmp $b->{clientname} }
1471 if ($what{db_mediatypes}) {
1473 SELECT MediaType as mediatype
1477 my $medias = $self->dbh_selectall_hashref($query, 'mediatype');
1478 $ret{db_mediatypes} = [sort {$a->{mediatype} cmp $b->{mediatype} }
1482 if ($what{db_locations}) {
1484 SELECT Location as location, Cost as cost FROM Location
1486 my $loc = $self->dbh_selectall_hashref($query, 'location');
1487 $ret{db_locations} = [ sort { $a->{location}
1493 if ($what{db_pools}) {
1494 my $query = "SELECT Name as name FROM Pool";
1496 my $all = $self->dbh_selectall_hashref($query, 'name') ;
1497 $ret{db_pools} = [ sort { $a->{name} cmp $b->{name} } values %$all ];
1500 if ($what{db_filesets}) {
1502 SELECT FileSet.FileSet AS fileset
1506 my $filesets = $self->dbh_selectall_hashref($query, 'fileset');
1508 $ret{db_filesets} = [sort {lc($a->{fileset}) cmp lc($b->{fileset}) }
1509 values %$filesets] ;
1512 if ($what{db_jobnames}) {
1514 SELECT DISTINCT Job.Name AS jobname
1518 my $jobnames = $self->dbh_selectall_hashref($query, 'jobname');
1520 $ret{db_jobnames} = [sort {lc($a->{jobname}) cmp lc($b->{jobname}) }
1521 values %$jobnames] ;
1524 if ($what{db_devices}) {
1526 SELECT Device.Name AS name
1530 my $devices = $self->dbh_selectall_hashref($query, 'name');
1532 $ret{db_devices} = [sort {lc($a->{name}) cmp lc($b->{name}) }
1543 my $fields = $self->get_form(qw/age level status clients filesets
1545 db_clients limit db_filesets width height
1546 qclients qfilesets qjobnames db_jobnames/);
1549 my $url = CGI::url(-full => 0,
1552 $url =~ s/^.+?\?//; # http://path/to/bweb.pl?arg => arg
1554 # this organisation is to keep user choice between 2 click
1555 # TODO : fileset and client selection doesn't work
1564 sub display_client_job
1566 my ($self, %arg) = @_ ;
1568 $arg{order} = ' Job.JobId DESC ';
1569 my ($limit, $label) = $self->get_limit(%arg);
1571 my $clientname = $self->dbh_quote($arg{clientname});
1574 SELECT DISTINCT Job.JobId AS jobid,
1575 Job.Name AS jobname,
1576 FileSet.FileSet AS fileset,
1578 StartTime AS starttime,
1579 JobFiles AS jobfiles,
1580 JobBytes AS jobbytes,
1581 JobStatus AS jobstatus,
1582 JobErrors AS joberrors
1584 FROM Client,Job,FileSet
1585 WHERE Client.Name=$clientname
1586 AND Client.ClientId=Job.ClientId
1587 AND Job.FileSetId=FileSet.FileSetId
1591 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
1593 $self->display({ clientname => $arg{clientname},
1596 Jobs => [ values %$all ],
1598 "display_client_job.tpl") ;
1601 sub get_selected_media_location
1605 my $medias = $self->get_form('jmedias');
1607 unless ($medias->{jmedias}) {
1612 SELECT Media.VolumeName AS volumename, Location.Location AS location
1613 FROM Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
1614 WHERE Media.VolumeName IN ($medias->{jmedias})
1617 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
1619 # { 'vol1' => { [volumename => 'vol1', location => 'ici'],
1630 my $medias = $self->get_selected_media_location();
1636 my $elt = $self->get_form('db_locations');
1638 $self->display({ ID => $cur_id++,
1639 %$elt, # db_locations
1641 sort { $a->{volumename} cmp $b->{volumename} } values %$medias
1651 my $elt = $self->get_form(qw/db_pools db_mediatypes db_locations/) ;
1653 $self->display($elt, "help_extern.tpl");
1656 sub help_extern_compute
1660 my $number = CGI::param('limit') || '' ;
1661 unless ($number =~ /^(\d+)$/) {
1662 return $self->error("Bad arg number : $number ");
1665 my ($sql, undef) = $self->get_param('pools',
1666 'locations', 'mediatypes');
1669 SELECT Media.VolumeName AS volumename,
1670 Media.VolStatus AS volstatus,
1671 Media.LastWritten AS lastwritten,
1672 Media.MediaType AS mediatype,
1673 Media.VolMounts AS volmounts,
1675 Media.Recycle AS recycle,
1676 $self->{sql}->{FROM_UNIXTIME}(
1677 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
1678 + $self->{sql}->{TO_SEC}(Media.VolRetention)
1681 INNER JOIN Pool ON (Pool.PoolId = Media.PoolId)
1682 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
1684 WHERE Media.InChanger = 1
1685 AND Media.VolStatus IN ('Disabled', 'Error', 'Full')
1687 ORDER BY expire DESC, recycle, Media.VolMounts DESC
1691 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
1693 $self->display({ Medias => [ values %$all ] },
1694 "help_extern_compute.tpl");
1701 my $param = $self->get_form(qw/db_locations db_pools db_mediatypes/) ;
1702 $self->display($param, "help_intern.tpl");
1705 sub help_intern_compute
1709 my $number = CGI::param('limit') || '' ;
1710 unless ($number =~ /^(\d+)$/) {
1711 return $self->error("Bad arg number : $number ");
1714 my ($sql, undef) = $self->get_param('pools', 'locations', 'mediatypes');
1716 if (CGI::param('expired')) {
1718 AND ( $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
1719 + $self->{sql}->{TO_SEC}(Media.VolRetention)
1725 SELECT Media.VolumeName AS volumename,
1726 Media.VolStatus AS volstatus,
1727 Media.LastWritten AS lastwritten,
1728 Media.MediaType AS mediatype,
1729 Media.VolMounts AS volmounts,
1731 $self->{sql}->{FROM_UNIXTIME}(
1732 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
1733 + $self->{sql}->{TO_SEC}(Media.VolRetention)
1736 INNER JOIN Pool ON (Pool.PoolId = Media.PoolId)
1737 LEFT JOIN Location ON (Location.LocationId = Media.LocationId)
1739 WHERE Media.InChanger <> 1
1740 AND Media.VolStatus IN ('Purged', 'Full', 'Append')
1741 AND Media.Recycle = 1
1743 ORDER BY Media.VolUseDuration DESC, Media.VolMounts ASC, expire ASC
1747 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
1749 $self->display({ Medias => [ values %$all ] },
1750 "help_intern_compute.tpl");
1756 my ($self, %arg) = @_ ;
1758 my ($limit, $label) = $self->get_limit(%arg);
1762 (SELECT count(Pool.PoolId) FROM Pool) AS nb_pool,
1763 (SELECT count(Media.MediaId) FROM Media) AS nb_media,
1764 (SELECT count(Job.JobId) FROM Job) AS nb_job,
1765 (SELECT sum(VolBytes) FROM Media) AS nb_bytes,
1766 ($self->{sql}->{DB_SIZE}) AS db_size,
1767 (SELECT count(Job.JobId)
1769 WHERE Job.JobStatus IN ('E','e','f','A')
1772 (SELECT count(Client.ClientId) FROM Client) AS nb_client
1775 my $row = $self->dbh_selectrow_hashref($query) ;
1777 $row->{nb_bytes} = human_size($row->{nb_bytes});
1779 $row->{db_size} = human_size($row->{db_size});
1780 $row->{label} = $label;
1782 $self->display($row, "general.tpl");
1787 my ($self, @what) = @_ ;
1788 my %elt = map { $_ => 1 } @what;
1793 if ($elt{clients}) {
1794 my @clients = grep { ! /^\s*$/ } CGI::param('client');
1796 $ret{clients} = \@clients;
1797 my $str = $self->dbh_join(@clients);
1798 $limit .= "AND Client.Name IN ($str) ";
1802 if ($elt{filesets}) {
1803 my @filesets = grep { ! /^\s*$/ } CGI::param('fileset');
1805 $ret{filesets} = \@filesets;
1806 my $str = $self->dbh_join(@filesets);
1807 $limit .= "AND FileSet.FileSet IN ($str) ";
1811 if ($elt{mediatypes}) {
1812 my @medias = grep { ! /^\s*$/ } CGI::param('mediatype');
1814 $ret{mediatypes} = \@medias;
1815 my $str = $self->dbh_join(@medias);
1816 $limit .= "AND Media.MediaType IN ($str) ";
1821 my $client = CGI::param('client');
1822 $ret{client} = $client;
1823 $client = $self->dbh_join($client);
1824 $limit .= "AND Client.Name = $client ";
1828 my $level = CGI::param('level') || '';
1829 if ($level =~ /^(\w)$/) {
1831 $limit .= "AND Job.Level = '$1' ";
1836 my $jobid = CGI::param('jobid') || '';
1838 if ($jobid =~ /^(\d+)$/) {
1840 $limit .= "AND Job.JobId = '$1' ";
1845 my $status = CGI::param('status') || '';
1846 if ($status =~ /^(\w)$/) {
1849 $limit .= "AND Job.JobStatus IN ('f','E') ";
1850 } elsif ($1 eq 'W') {
1851 $limit .= "AND Job.JobStatus = 'T' AND Job.JobErrors > 0 ";
1853 $limit .= "AND Job.JobStatus = '$1' ";
1858 if ($elt{volstatus}) {
1859 my $status = CGI::param('volstatus') || '';
1860 if ($status =~ /^(\w+)$/) {
1862 $limit .= "AND Media.VolStatus = '$1' ";
1866 if ($elt{locations}) {
1867 my @location = grep { ! /^\s*$/ } CGI::param('location') ;
1869 $ret{locations} = \@location;
1870 my $str = $self->dbh_join(@location);
1871 $limit .= "AND Location.Location IN ($str) ";
1876 my @pool = grep { ! /^\s*$/ } CGI::param('pool') ;
1878 $ret{pools} = \@pool;
1879 my $str = $self->dbh_join(@pool);
1880 $limit .= "AND Pool.Name IN ($str) ";
1884 if ($elt{location}) {
1885 my $location = CGI::param('location') || '';
1887 $ret{location} = $location;
1888 $location = $self->dbh_quote($location);
1889 $limit .= "AND Location.Location = $location ";
1894 my $pool = CGI::param('pool') || '';
1897 $pool = $self->dbh_quote($pool);
1898 $limit .= "AND Pool.Name = $pool ";
1902 if ($elt{jobtype}) {
1903 my $jobtype = CGI::param('jobtype') || '';
1904 if ($jobtype =~ /^(\w)$/) {
1906 $limit .= "AND Job.Type = '$1' ";
1910 return ($limit, %ret);
1921 my ($self, %arg) = @_ ;
1923 $arg{order} = ' Job.JobId DESC ';
1925 my ($limit, $label) = $self->get_limit(%arg);
1926 my ($where, undef) = $self->get_param('clients',
1935 SELECT Job.JobId AS jobid,
1936 Client.Name AS client,
1937 FileSet.FileSet AS fileset,
1938 Job.Name AS jobname,
1940 StartTime AS starttime,
1942 Pool.Name AS poolname,
1943 JobFiles AS jobfiles,
1944 JobBytes AS jobbytes,
1945 JobStatus AS jobstatus,
1946 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
1947 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime))
1950 JobErrors AS joberrors
1953 Job LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId)
1954 LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId)
1955 WHERE Client.ClientId=Job.ClientId
1956 AND Job.JobStatus != 'R'
1961 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
1963 $self->display({ Filter => $label,
1967 sort { $a->{jobid} <=> $b->{jobid} }
1974 # display job informations
1975 sub display_job_zoom
1977 my ($self, $jobid) = @_ ;
1979 $jobid = $self->dbh_quote($jobid);
1982 SELECT DISTINCT Job.JobId AS jobid,
1983 Client.Name AS client,
1984 Job.Name AS jobname,
1985 FileSet.FileSet AS fileset,
1987 Pool.Name AS poolname,
1988 StartTime AS starttime,
1989 JobFiles AS jobfiles,
1990 JobBytes AS jobbytes,
1991 JobStatus AS jobstatus,
1992 JobErrors AS joberrors,
1993 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(EndTime)
1994 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime)) AS duration
1997 Job LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId)
1998 LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId)
1999 WHERE Client.ClientId=Job.ClientId
2000 AND Job.JobId = $jobid
2003 my $row = $self->dbh_selectrow_hashref($query) ;
2005 # display all volumes associate with this job
2007 SELECT Media.VolumeName as volumename
2008 FROM Job,Media,JobMedia
2009 WHERE Job.JobId = $jobid
2010 AND JobMedia.JobId=Job.JobId
2011 AND JobMedia.MediaId=Media.MediaId
2014 my $all = $self->dbh_selectall_hashref($query, 'volumename');
2016 $row->{volumes} = [ values %$all ] ;
2018 $self->display($row, "display_job_zoom.tpl");
2025 my ($where, %elt) = $self->get_param('pools',
2030 my $arg = $self->get_form('jmedias', 'qre_media');
2032 if ($arg->{jmedias}) {
2033 $where = "AND Media.VolumeName IN ($arg->{jmedias}) $where";
2035 if ($arg->{qre_media}) {
2036 $where = "AND Media.VolumeName $self->{sql}->{MATCH} $arg->{qre_media} $where";
2040 SELECT Media.VolumeName AS volumename,
2041 Media.VolBytes AS volbytes,
2042 Media.VolStatus AS volstatus,
2043 Media.MediaType AS mediatype,
2044 Media.InChanger AS online,
2045 Media.LastWritten AS lastwritten,
2046 Location.Location AS location,
2047 (volbytes*100/COALESCE(media_avg_size.size,-1)) AS volusage,
2048 Pool.Name AS poolname,
2049 $self->{sql}->{FROM_UNIXTIME}(
2050 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2051 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2054 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2055 LEFT JOIN (SELECT avg(Media.VolBytes) AS size,
2056 Media.MediaType AS MediaType
2058 WHERE Media.VolStatus = 'Full'
2059 GROUP BY Media.MediaType
2060 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
2062 WHERE Media.PoolId=Pool.PoolId
2066 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2068 $self->display({ ID => $cur_id++,
2070 Location => $elt{location},
2071 Medias => [ values %$all ]
2073 "display_media.tpl");
2080 my $pool = $self->get_form('db_pools');
2082 foreach my $name (@{ $pool->{db_pools} }) {
2083 CGI::param('pool', $name->{name});
2084 $self->display_media();
2088 sub display_media_zoom
2092 my $medias = $self->get_form('jmedias');
2094 unless ($medias->{jmedias}) {
2095 return $self->error("Can't get media selection");
2099 SELECT InChanger AS online,
2100 VolBytes AS nb_bytes,
2101 VolumeName AS volumename,
2102 VolStatus AS volstatus,
2103 VolMounts AS nb_mounts,
2104 Media.VolUseDuration AS voluseduration,
2105 Media.MaxVolJobs AS maxvoljobs,
2106 Media.MaxVolFiles AS maxvolfiles,
2107 Media.MaxVolBytes AS maxvolbytes,
2108 VolErrors AS nb_errors,
2109 Pool.Name AS poolname,
2110 Location.Location AS location,
2111 Media.Recycle AS recycle,
2112 Media.VolRetention AS volretention,
2113 Media.LastWritten AS lastwritten,
2114 Media.VolReadTime/1000000 AS volreadtime,
2115 Media.VolWriteTime/1000000 AS volwritetime,
2116 Media.RecycleCount AS recyclecount,
2117 Media.Comment AS comment,
2118 $self->{sql}->{FROM_UNIXTIME}(
2119 $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten)
2120 + $self->{sql}->{TO_SEC}(Media.VolRetention)
2123 Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2124 WHERE Pool.PoolId = Media.PoolId
2125 AND VolumeName IN ($medias->{jmedias})
2128 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2130 foreach my $media (values %$all) {
2131 my $mq = $self->dbh_quote($media->{volumename});
2134 SELECT DISTINCT Job.JobId AS jobid,
2136 Job.StartTime AS starttime,
2139 Job.JobFiles AS files,
2140 Job.JobBytes AS bytes,
2141 Job.jobstatus AS status
2142 FROM Media,JobMedia,Job
2143 WHERE Media.VolumeName=$mq
2144 AND Media.MediaId=JobMedia.MediaId
2145 AND JobMedia.JobId=Job.JobId
2148 my $jobs = $self->dbh_selectall_hashref($query, 'jobid') ;
2151 SELECT LocationLog.Date AS date,
2152 Location.Location AS location,
2153 LocationLog.Comment AS comment
2154 FROM Media,LocationLog INNER JOIN Location ON (LocationLog.LocationId = Location.LocationId)
2155 WHERE Media.MediaId = LocationLog.MediaId
2156 AND Media.VolumeName = $mq
2160 my $log = $self->dbh_selectall_arrayref($query) ;
2162 $logtxt = join("\n", map { ($_->[0] . ' ' . $_->[1] . ' ' . $_->[2])} @$log ) ;
2165 $self->display({ jobs => [ values %$jobs ],
2166 LocationLog => $logtxt,
2168 "display_media_zoom.tpl");
2176 my $loc = $self->get_form('qlocation');
2177 unless ($loc->{qlocation}) {
2178 return $self->error("Can't get location");
2182 SELECT Location.Location AS location,
2183 Location.Cost AS cost,
2184 Location.Enabled AS enabled
2186 WHERE Location.Location = $loc->{qlocation}
2189 my $row = $self->dbh_selectrow_hashref($query);
2191 $self->display({ ID => $cur_id++,
2192 %$row }, "location_edit.tpl") ;
2200 my $arg = $self->get_form(qw/qlocation qnewlocation cost/) ;
2201 unless ($arg->{qlocation}) {
2202 return $self->error("Can't get location");
2204 unless ($arg->{qnewlocation}) {
2205 return $self->error("Can't get new location name");
2207 unless ($arg->{cost}) {
2208 return $self->error("Can't get new cost");
2211 my $enabled = CGI::param('enabled') || '';
2212 $enabled = $enabled?1:0;
2215 UPDATE Location SET Cost = $arg->{cost},
2216 Location = $arg->{qnewlocation},
2218 WHERE Location.Location = $arg->{qlocation}
2221 $self->dbh_do($query);
2223 $self->display_location();
2229 my $arg = $self->get_form(qw/qlocation/) ;
2231 unless ($arg->{qlocation}) {
2232 return $self->error("Can't get location");
2236 SELECT count(Media.MediaId) AS nb
2237 FROM Media INNER JOIN Location USING (LocationID)
2238 WHERE Location = $arg->{qlocation}
2241 my $res = $self->dbh_selectrow_hashref($query);
2244 return $self->error("Sorry, the location must be empty");
2248 DELETE FROM Location WHERE Location = $arg->{qlocation} LIMIT 1
2251 $self->dbh_do($query);
2253 $self->display_location();
2260 my $arg = $self->get_form(qw/qlocation cost/) ;
2262 unless ($arg->{qlocation}) {
2263 $self->display({}, "location_add.tpl");
2266 unless ($arg->{cost}) {
2267 return $self->error("Can't get new cost");
2270 my $enabled = CGI::param('enabled') || '';
2271 $enabled = $enabled?1:0;
2274 INSERT INTO Location (Location, Cost, Enabled)
2275 VALUES ($arg->{qlocation}, $arg->{cost}, $enabled)
2278 $self->dbh_do($query);
2280 $self->display_location();
2283 sub display_location
2288 SELECT Location.Location AS location,
2289 Location.Cost AS cost,
2290 Location.Enabled AS enabled,
2291 (SELECT count(Media.MediaId)
2293 WHERE Media.LocationId = Location.LocationId
2298 my $location = $self->dbh_selectall_hashref($query, 'location');
2300 $self->display({ ID => $cur_id++,
2301 Locations => [ values %$location ] },
2302 "display_location.tpl");
2309 my $medias = $self->get_selected_media_location();
2314 my $arg = $self->get_form('db_locations', 'qnewlocation');
2316 $self->display({ email => $self->{info}->{email_media},
2318 medias => [ values %$medias ],
2320 "update_location.tpl");
2323 sub get_media_max_size
2325 my ($self, $type) = @_;
2327 "SELECT avg(VolBytes) AS size
2329 WHERE Media.VolStatus = 'Full'
2330 AND Media.MediaType = '$type'
2333 my $res = $self->selectrow_hashref($query);
2336 return $res->{size};
2346 my $media = $self->get_form('qmedia');
2348 unless ($media->{qmedia}) {
2349 return $self->error("Can't get media");
2353 SELECT Media.Slot AS slot,
2354 PoolMedia.Name AS poolname,
2355 Media.VolStatus AS volstatus,
2356 Media.InChanger AS inchanger,
2357 Location.Location AS location,
2358 Media.VolumeName AS volumename,
2359 Media.MaxVolBytes AS maxvolbytes,
2360 Media.MaxVolJobs AS maxvoljobs,
2361 Media.MaxVolFiles AS maxvolfiles,
2362 Media.VolUseDuration AS voluseduration,
2363 Media.VolRetention AS volretention,
2364 Media.Comment AS comment,
2365 PoolRecycle.Name AS poolrecycle
2367 FROM Media INNER JOIN Pool AS PoolMedia ON (Media.PoolId = PoolMedia.PoolId)
2368 LEFT JOIN Pool AS PoolRecycle ON (Media.RecyclePoolId = PoolRecycle.PoolId)
2369 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2371 WHERE Media.VolumeName = $media->{qmedia}
2374 my $row = $self->dbh_selectrow_hashref($query);
2375 $row->{volretention} = human_sec($row->{volretention});
2376 $row->{voluseduration} = human_sec($row->{voluseduration});
2378 my $elt = $self->get_form(qw/db_pools db_locations/);
2383 }, "update_media.tpl");
2390 my $arg = $self->get_form('jmedias', 'qnewlocation') ;
2392 unless ($arg->{jmedias}) {
2393 return $self->error("Can't get selected media");
2396 unless ($arg->{qnewlocation}) {
2397 return $self->error("Can't get new location");
2402 SET LocationId = (SELECT LocationId
2404 WHERE Location = $arg->{qnewlocation})
2405 WHERE Media.VolumeName IN ($arg->{jmedias})
2408 my $nb = $self->dbh_do($query);
2410 print "$nb media updated, you may have to update your autochanger.";
2412 $self->display_media();
2419 my $medias = $self->get_selected_media_location();
2421 return $self->error("Can't get media selection");
2423 my $newloc = CGI::param('newlocation');
2425 my $user = CGI::param('user') || 'unknown';
2426 my $comm = CGI::param('comment') || '';
2427 $comm = $self->dbh_quote("$user: $comm");
2431 foreach my $media (keys %$medias) {
2433 INSERT LocationLog (Date, Comment, MediaId, LocationId, NewVolStatus)
2435 NOW(), $comm, (SELECT MediaId FROM Media WHERE VolumeName = '$media'),
2436 (SELECT LocationId FROM Location WHERE Location = '$medias->{$media}->{location}'),
2437 (SELECT VolStatus FROM Media WHERE VolumeName = '$media')
2440 $self->dbh_do($query);
2441 $self->debug($query);
2445 $q->param('action', 'update_location');
2446 my $url = $q->url(-full => 1, -query=>1);
2448 $self->display({ email => $self->{info}->{email_media},
2450 newlocation => $newloc,
2451 # [ { volumename => 'vol1' }, { volumename => 'vol2'
\81 },..]
2452 medias => [ values %$medias ],
2454 "change_location.tpl");
2458 sub display_client_stats
2460 my ($self, %arg) = @_ ;
2462 my $client = $self->dbh_quote($arg{clientname});
2463 my ($limit, $label) = $self->get_limit(%arg);
2467 count(Job.JobId) AS nb_jobs,
2468 sum(Job.JobBytes) AS nb_bytes,
2469 sum(Job.JobErrors) AS nb_err,
2470 sum(Job.JobFiles) AS nb_files,
2471 Client.Name AS clientname
2472 FROM Job INNER JOIN Client USING (ClientId)
2474 Client.Name = $client
2476 GROUP BY Client.Name
2479 my $row = $self->dbh_selectrow_hashref($query);
2481 $row->{ID} = $cur_id++;
2482 $row->{label} = $label;
2484 $self->display($row, "display_client_stats.tpl");
2487 # poolname can be undef
2490 my ($self, $poolname) = @_ ;
2494 my $arg = $self->get_form('jmediatypes', 'qmediatypes');
2495 if ($arg->{jmediatypes}) {
2496 $whereW = "WHERE MediaType IN ($arg->{jmediatypes}) ";
2497 $whereA = "AND MediaType IN ($arg->{jmediatypes}) ";
2500 # TODO : afficher les tailles et les dates
2503 SELECT subq.volmax AS volmax,
2504 subq.volnum AS volnum,
2505 subq.voltotal AS voltotal,
2507 Pool.Recycle AS recycle,
2508 Pool.VolRetention AS volretention,
2509 Pool.VolUseDuration AS voluseduration,
2510 Pool.MaxVolJobs AS maxvoljobs,
2511 Pool.MaxVolFiles AS maxvolfiles,
2512 Pool.MaxVolBytes AS maxvolbytes,
2513 subq.PoolId AS PoolId
2516 SELECT COALESCE(media_avg_size.volavg,0) * count(Media.MediaId) AS volmax,
2517 count(Media.MediaId) AS volnum,
2518 sum(Media.VolBytes) AS voltotal,
2519 Media.PoolId AS PoolId,
2520 Media.MediaType AS MediaType
2522 LEFT JOIN (SELECT avg(Media.VolBytes) AS volavg,
2523 Media.MediaType AS MediaType
2525 WHERE Media.VolStatus = 'Full'
2526 GROUP BY Media.MediaType
2527 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
2528 GROUP BY Media.MediaType, Media.PoolId, media_avg_size.volavg
2530 LEFT JOIN Pool ON (Pool.PoolId = subq.PoolId)
2534 my $all = $self->dbh_selectall_hashref($query, 'name') ;
2537 SELECT Pool.Name AS name,
2538 sum(VolBytes) AS size
2539 FROM Media JOIN Pool ON (Media.PoolId = Pool.PoolId)
2540 WHERE Media.VolStatus IN ('Recycled', 'Purged')
2544 my $empty = $self->dbh_selectall_hashref($query, 'name');
2546 foreach my $p (values %$all) {
2547 if ($p->{volmax} > 0) { # mysql returns 0.0000
2548 # we remove Recycled/Purged media from pool usage
2549 if (defined $empty->{$p->{name}}) {
2550 $p->{voltotal} -= $empty->{$p->{name}}->{size};
2552 $p->{poolusage} = sprintf('%.2f', $p->{voltotal} * 100/ $p->{volmax}) ;
2554 $p->{poolusage} = 0;
2558 SELECT VolStatus AS volstatus, count(MediaId) AS nb
2560 WHERE PoolId=$p->{poolid}
2564 my $content = $self->dbh_selectall_hashref($query, 'volstatus');
2565 foreach my $t (values %$content) {
2566 $p->{"nb_" . $t->{volstatus}} = $t->{nb} ;
2571 $self->display({ ID => $cur_id++,
2572 MediaType => $arg->{qmediatypes}, # [ { name => type1 } , { name => type2 } ]
2573 Pools => [ values %$all ]},
2574 "display_pool.tpl");
2577 sub display_running_job
2581 my $arg = $self->get_form('client', 'jobid');
2583 if (!$arg->{client} and $arg->{jobid}) {
2586 SELECT Client.Name AS name
2587 FROM Job INNER JOIN Client USING (ClientId)
2588 WHERE Job.JobId = $arg->{jobid}
2591 my $row = $self->dbh_selectrow_hashref($query);
2594 $arg->{client} = $row->{name};
2595 CGI::param('client', $arg->{client});
2599 if ($arg->{client}) {
2600 my $cli = new Bweb::Client(name => $arg->{client});
2601 $cli->display_running_job($self->{info}, $arg->{jobid});
2602 if ($arg->{jobid}) {
2603 $self->get_job_log();
2606 $self->error("Can't get client or jobid");
2610 sub display_running_jobs
2612 my ($self, $display_action) = @_;
2615 SELECT Job.JobId AS jobid,
2616 Job.Name AS jobname,
2618 Job.StartTime AS starttime,
2619 Job.JobFiles AS jobfiles,
2620 Job.JobBytes AS jobbytes,
2621 Job.JobStatus AS jobstatus,
2622 $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(NOW())
2623 - $self->{sql}->{UNIX_TIMESTAMP}(StartTime))
2625 Client.Name AS clientname
2626 FROM Job INNER JOIN Client USING (ClientId)
2627 WHERE JobStatus IN ('C','R','B','e','D','F','S','m','M','s','j','c','d','t','p')
2629 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
2631 $self->display({ ID => $cur_id++,
2632 display_action => $display_action,
2633 Jobs => [ values %$all ]},
2634 "running_job.tpl") ;
2637 # return the autochanger list to update
2642 my $arg = $self->get_form('jmedias');
2644 unless ($arg->{jmedias}) {
2645 return $self->error("Can't get media selection");
2649 SELECT Media.VolumeName AS volumename,
2650 Storage.Name AS storage,
2651 Location.Location AS location,
2653 FROM Media INNER JOIN Storage ON (Media.StorageId = Storage.StorageId)
2654 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2655 WHERE Media.VolumeName IN ($arg->{jmedias})
2656 AND Media.InChanger = 1
2659 my $all = $self->dbh_selectall_hashref($query, 'volumename');
2661 foreach my $vol (values %$all) {
2662 my $a = $self->ach_get($vol->{location});
2664 $ret{$vol->{location}} = 1;
2666 unless ($a->{have_status}) {
2668 $a->{have_status} = 1;
2671 print "eject $vol->{volumename} from $vol->{storage} : ";
2672 if ($a->send_to_io($vol->{slot})) {
2673 print "<img src='/bweb/T.png' alt='ok'><br/>";
2675 print "<img src='/bweb/E.png' alt='err'><br/>";
2685 my ($to, $subject, $content) = (CGI::param('email'),
2686 CGI::param('subject'),
2687 CGI::param('content'));
2688 $to =~ s/[^\w\d\.\@<>,]//;
2689 $subject =~ s/[^\w\d\.\[\]]/ /;
2691 open(MAIL, "|mail -s '$subject' '$to'") ;
2692 print MAIL $content;
2702 my $arg = $self->get_form('jobid', 'client');
2704 print CGI::header('text/brestore');
2705 print "jobid=$arg->{jobid}\n" if ($arg->{jobid});
2706 print "client=$arg->{client}\n" if ($arg->{client});
2707 print "\n\nYou have to assign this mime type with /usr/bin/brestore.pl\n";
2711 # TODO : move this to Bweb::Autochanger ?
2712 # TODO : make this internal to not eject tape ?
2718 my ($self, $name) = @_;
2721 return $self->error("Can't get your autochanger name ach");
2724 unless ($self->{info}->{ach_list}) {
2725 return $self->error("Could not find any autochanger");
2728 my $a = $self->{info}->{ach_list}->{$name};
2731 $self->error("Can't get your autochanger $name from your ach_list");
2736 $a->{debug} = $self->{debug};
2743 my ($self, $ach) = @_;
2745 $self->{info}->{ach_list}->{$ach->{name}} = $ach;
2747 $self->{info}->save();
2755 my $arg = $self->get_form('ach');
2757 or !$self->{info}->{ach_list}
2758 or !$self->{info}->{ach_list}->{$arg->{ach}})
2760 return $self->error("Can't get autochanger name");
2763 my $ach = $self->{info}->{ach_list}->{$arg->{ach}};
2767 [ map { { name => $_, index => $i++ } } @{$ach->{drive_name}} ] ;
2769 my $b = $self->get_bconsole();
2771 my @storages = $b->list_storage() ;
2773 $ach->{devices} = [ map { { name => $_ } } @storages ];
2775 $self->display($ach, "ach_add.tpl");
2776 delete $ach->{drives};
2777 delete $ach->{devices};
2784 my $arg = $self->get_form('ach');
2787 or !$self->{info}->{ach_list}
2788 or !$self->{info}->{ach_list}->{$arg->{ach}})
2790 return $self->error("Can't get autochanger name");
2793 delete $self->{info}->{ach_list}->{$arg->{ach}} ;
2795 $self->{info}->save();
2796 $self->{info}->view();
2802 my $arg = $self->get_form('ach', 'mtxcmd', 'device', 'precmd');
2804 my $b = $self->get_bconsole();
2805 my @storages = $b->list_storage() ;
2807 unless ($arg->{ach}) {
2808 $arg->{devices} = [ map { { name => $_ } } @storages ];
2809 return $self->display($arg, "ach_add.tpl");
2813 foreach my $drive (CGI::param('drives'))
2815 unless (grep(/^$drive$/,@storages)) {
2816 return $self->error("Can't find $drive in storage list");
2819 my $index = CGI::param("index_$drive");
2820 unless (defined $index and $index =~ /^(\d+)$/) {
2821 return $self->error("Can't get $drive index");
2824 $drives[$index] = $drive;
2828 return $self->error("Can't get drives from Autochanger");
2831 my $a = new Bweb::Autochanger(name => $arg->{ach},
2832 precmd => $arg->{precmd},
2833 drive_name => \@drives,
2834 device => $arg->{device},
2835 mtxcmd => $arg->{mtxcmd});
2837 $self->ach_register($a) ;
2839 $self->{info}->view();
2845 my $arg = $self->get_form('jobid');
2847 if ($arg->{jobid}) {
2848 my $b = $self->get_bconsole();
2849 my $ret = $b->send_cmd("delete jobid=\"$arg->{jobid}\"");
2853 title => "Delete a job ",
2854 name => "delete jobid=$arg->{jobid}",
2863 my $arg = $self->get_form(qw/media volstatus inchanger pool
2864 slot volretention voluseduration
2865 maxvoljobs maxvolfiles maxvolbytes
2866 qcomment poolrecycle
2869 unless ($arg->{media}) {
2870 return $self->error("Can't find media selection");
2873 my $update = "update volume=$arg->{media} ";
2875 if ($arg->{volstatus}) {
2876 $update .= " volstatus=$arg->{volstatus} ";
2879 if ($arg->{inchanger}) {
2880 $update .= " inchanger=yes " ;
2882 $update .= " slot=$arg->{slot} ";
2885 $update .= " slot=0 inchanger=no ";
2889 $update .= " pool=$arg->{pool} " ;
2892 if (defined $arg->{volretention}) {
2893 $update .= " volretention=\"$arg->{volretention}\" " ;
2896 if (defined $arg->{voluseduration}) {
2897 $update .= " voluse=\"$arg->{voluseduration}\" " ;
2900 if (defined $arg->{maxvoljobs}) {
2901 $update .= " maxvoljobs=$arg->{maxvoljobs} " ;
2904 if (defined $arg->{maxvolfiles}) {
2905 $update .= " maxvolfiles=$arg->{maxvolfiles} " ;
2908 if (defined $arg->{maxvolbytes}) {
2909 $update .= " maxvolbytes=$arg->{maxvolbytes} " ;
2912 my $b = $self->get_bconsole();
2915 content => $b->send_cmd($update),
2916 title => "Update a volume ",
2922 my $media = $self->dbh_quote($arg->{media});
2924 my $loc = CGI::param('location') || '';
2926 $loc = $self->dbh_quote($loc); # is checked by db
2927 push @q, "LocationId=(SELECT LocationId FROM Location WHERE Location=$loc)";
2929 if ($arg->{poolrecycle}) {
2930 push @q, "RecyclePoolId=(SELECT PoolId FROM Pool WHERE Name='$arg->{poolrecycle}')";
2932 if (!$arg->{qcomment}) {
2933 $arg->{qcomment} = "''";
2935 push @q, "Comment=$arg->{qcomment}";
2940 SET " . join (',', @q) . "
2941 WHERE Media.VolumeName = $media
2943 $self->dbh_do($query);
2945 $self->update_media();
2952 my $ach = CGI::param('ach') ;
2953 $ach = $self->ach_get($ach);
2955 return $self->error("Bad autochanger name");
2959 my $b = new Bconsole(pref => $self->{info},timeout => 60,log_stdout => 1);
2960 $b->update_slots($ach->{name});
2968 my $arg = $self->get_form('jobid', 'limit', 'offset');
2969 unless ($arg->{jobid}) {
2970 return $self->error("Can't get jobid");
2973 if ($arg->{limit} == 100) {
2974 $arg->{limit} = 1000;
2977 my $t = CGI::param('time') || $self->{info}->{display_log_time} || '';
2980 SELECT Job.Name as name, Client.Name as clientname
2981 FROM Job INNER JOIN Client ON (Job.ClientId = Client.ClientId)
2982 WHERE JobId = $arg->{jobid}
2985 my $row = $self->dbh_selectrow_hashref($query);
2988 return $self->error("Can't find $arg->{jobid} in catalog");
2992 SELECT Time AS time, LogText AS log
2994 WHERE Log.JobId = $arg->{jobid}
2995 OR (Log.JobId = 0 AND Time >= (SELECT StartTime FROM Job WHERE JobId=$arg->{jobid})
2996 AND Time <= (SELECT COALESCE(EndTime,NOW()) FROM Job WHERE JobId=$arg->{jobid})
3000 OFFSET $arg->{offset}
3003 my $log = $self->dbh_selectall_arrayref($query);
3005 return $self->error("Can't get log for jobid $arg->{jobid}");
3011 $logtxt = join("", map { ($_->[0] . ' ' . $_->[1]) } @$log ) ;
3013 $logtxt = join("", map { $_->[1] } @$log ) ;
3016 $self->display({ lines=> $logtxt,
3017 jobid => $arg->{jobid},
3018 name => $row->{name},
3019 client => $row->{clientname},
3020 offset => $arg->{offset},
3021 limit => $arg->{limit},
3022 }, 'display_log.tpl');
3030 my $arg = $self->get_form('ach', 'slots', 'drive');
3032 unless ($arg->{ach}) {
3033 return $self->error("Can't find autochanger name");
3036 my $a = $self->ach_get($arg->{ach});
3038 return $self->error("Can't find autochanger name in configuration");
3041 my $storage = $a->get_drive_name($arg->{drive});
3043 return $self->error("Can't get your drive name");
3048 if ($arg->{slots}) {
3049 $slots = join(",", @{ $arg->{slots} });
3050 $t += 60*scalar( @{ $arg->{slots} }) ;
3053 my $b = new Bconsole(pref => $self->{info}, timeout => $t,log_stdout => 1);
3054 print "<h1>This command can take long time, be patient...</h1>";
3056 $b->label_barcodes(storage => $storage,
3057 drive => $arg->{drive},
3065 SET LocationId = (SELECT LocationId
3067 WHERE Location = '$arg->{ach}'),
3069 RecyclePoolId = PoolId
3071 WHERE Media.PoolId = (SELECT PoolId
3073 WHERE Name = 'Scratch')
3074 AND (LocationId = 0 OR LocationId IS NULL)
3083 my @volume = CGI::param('media');
3086 return $self->error("Can't get media selection");
3089 my $b = new Bconsole(pref => $self->{info}, timeout => 60);
3092 content => $b->purge_volume(@volume),
3093 title => "Purge media",
3094 name => "purge volume=" . join(' volume=', @volume),
3103 my @volume = CGI::param('media');
3105 return $self->error("Can't get media selection");
3108 my $b = new Bconsole(pref => $self->{info}, timeout => 60);
3111 content => $b->prune_volume(@volume),
3112 title => "Prune media",
3113 name => "prune volume=" . join(' volume=', @volume),
3123 my $arg = $self->get_form('jobid');
3124 unless ($arg->{jobid}) {
3125 return $self->error("Can't get jobid");
3128 my $b = $self->get_bconsole();
3130 content => $b->cancel($arg->{jobid}),
3131 title => "Cancel job",
3132 name => "cancel jobid=$arg->{jobid}",
3138 # Warning, we display current fileset
3141 my $arg = $self->get_form('fileset');
3143 if ($arg->{fileset}) {
3144 my $b = $self->get_bconsole();
3145 my $ret = $b->get_fileset($arg->{fileset});
3146 $self->display({ fileset => $arg->{fileset},
3148 }, "fileset_view.tpl");
3150 $self->error("Can't get fileset name");
3154 sub director_show_sched
3158 my $arg = $self->get_form('days');
3160 my $b = $self->get_bconsole();
3161 my $ret = $b->director_get_sched( $arg->{days} );
3166 }, "scheduled_job.tpl");
3169 sub enable_disable_job
3171 my ($self, $what) = @_ ;
3173 my $name = CGI::param('job') || '';
3174 unless ($name =~ /^[\w\d\.\-\s]+$/) {
3175 return $self->error("Can't find job name");
3178 my $b = $self->get_bconsole();
3188 content => $b->send_cmd("$cmd job=\"$name\""),
3189 title => "$cmd $name",
3190 name => "$cmd job=\"$name\"",
3197 return new Bconsole(pref => $self->{info});
3203 my $b = $self->get_bconsole();
3205 my $joblist = [ map { { name => $_ } } $b->list_job() ];
3207 $self->display({ Jobs => $joblist }, "run_job.tpl");
3212 my ($self, $ouput) = @_;
3215 foreach my $l (split(/\r\n/, $ouput)) {
3216 if ($l =~ /(\w+): name=([\w\d\.\s-]+?)(\s+\w+=.+)?$/) {
3222 if (my @l = $l =~ /(\w+)=([\w\d*]+)/g) {
3228 foreach my $k (keys %arg) {
3229 $lowcase{lc($k)} = $arg{$k} ;
3238 my $b = $self->get_bconsole();
3240 my $job = CGI::param('job') || '';
3242 # we take informations from director, and we overwrite with user wish
3243 my $info = $b->send_cmd("show job=\"$job\"");
3244 my $attr = $self->run_parse_job($info);
3246 my $arg = $self->get_form('pool', 'level', 'client', 'fileset', 'storage');
3247 my %job_opt = (%$attr, %$arg);
3249 my $jobs = [ map {{ name => $_ }} $b->list_job() ];
3251 my $pools = [ map { { name => $_ } } $b->list_pool() ];
3252 my $clients = [ map { { name => $_ } }$b->list_client()];
3253 my $filesets= [ map { { name => $_ } }$b->list_fileset() ];
3254 my $storages= [ map { { name => $_ } }$b->list_storage()];
3259 clients => $clients,
3260 filesets => $filesets,
3261 storages => $storages,
3263 }, "run_job_mod.tpl");
3269 my $b = $self->get_bconsole();
3271 my $jobs = [ map {{ name => $_ }} $b->list_job() ];
3281 my $b = $self->get_bconsole();
3283 # TODO: check input (don't use pool, level)
3285 my $arg = $self->get_form('pool', 'level', 'client', 'priority', 'when', 'fileset');
3286 my $job = CGI::param('job') || '';
3287 my $storage = CGI::param('storage') || '';
3289 my $jobid = $b->run(job => $job,
3290 client => $arg->{client},
3291 priority => $arg->{priority},
3292 level => $arg->{level},
3293 storage => $storage,
3294 pool => $arg->{pool},
3295 fileset => $arg->{fileset},
3296 when => $arg->{when},
3299 print $jobid, $b->{error};
3301 print "<br>You can follow job execution <a href='?action=dsp_cur_job;client=$arg->{client};jobid=$jobid'> here </a>";