1 ################################################################
6 Bweb - A Bacula web interface
7 Bacula® - The Network Backup Solution
9 Copyright (C) 2006-2011 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.
14 This program is Free Software; you can redistribute it and/or
15 modify it under the terms of version three of the GNU Affero General Public
16 License as published by the Free Software Foundation and included
19 This program is distributed in the hope that it will be useful, but
20 WITHOUT ANY WARRANTY; without even the implied warranty of
21 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
22 Affero General Public License for more details.
24 You should have received a copy of the GNU Affero General Public License
25 along with this program; if not, write to the Free Software
26 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
29 Bacula® is a registered trademark of Kern Sibbald.
30 The licensor of Bacula is the Free Software Foundation Europe
31 (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
32 Switzerland, email:ftf@fsfeurope.org.
40 Bweb::Gui - Base package for all Bweb object
44 This package define base fonction like new, display, etc..
49 our $template_dir='/usr/share/bweb/tpl';
53 new - creation a of new Bweb object
57 This function take an hash of argument and place them
60 IE : $obj = new Obj(name => 'test', age => '10');
62 $obj->{name} eq 'test' and $obj->{age} eq 10
68 my ($class, %arg) = @_;
74 map { $self->{lc($_)} = $arg{$_} } keys %arg ;
81 my ($self, $what) = @_;
85 print "<pre>" . Data::Dumper::Dumper($what) . "</pre>";
87 print "<pre>$what</pre>";
94 open(FP, ">>/tmp/log");
95 print FP Data::Dumper::Dumper(\@_);
101 my ($self, $what) = @_;
103 my $old = $self->{debug};
106 $self->{debug} = $old;
111 error - display an error to the user
115 this function set $self->{error} with arg, display a message with
116 error.tpl and return 0
121 return $self->error("Can't use this file");
128 my ($self, $what) = @_;
129 $self->{error} = $what;
130 $self->display($self, 'error.tpl');
134 # send content type the first time, see man CGI to overwrite
136 my $send_content_type_done=0;
137 sub send_content_type
139 my ($self, %arg) = @_;
140 my $info = $self->{info} || $self;
142 if (!$send_content_type_done) { # display it once
143 $send_content_type_done = 1;
145 %arg = (-type => 'text/html', %arg);
146 print CGI::header(%arg);
152 display - display an html page with HTML::Template
156 this function is use to render all html codes. it takes an
157 ref hash as arg in which all param are usable in template.
159 it will use user template_dir then global template_dir
160 to search the template file.
162 hash keys are not sensitive. See HTML::Template for more
163 explanations about the hash ref. (it's can be quiet hard to understand)
165 It uses the following variables: template_dir lang director
169 $ref = { name => 'me', age => 26 };
170 $self->display($ref, "people.tpl");
176 my ($self, $hash, $tpl) = @_ ;
177 my $info = $self->{info} || $self;
179 my $dir = $info->{template_dir} || $template_dir;
180 my $lang = $self->{current_lang} || $info->{lang} || 'en';
181 my $template = HTML::Template->new(filename => $tpl,
182 path =>["$dir/$lang",
185 die_on_bad_params => 0,
186 case_sensitive => 0);
188 foreach my $var (qw/limit offset/) {
190 unless ($hash->{$var}) {
191 my $value = CGI::param($var) || '';
193 if ($value =~ /^(\d+)$/) {
194 $template->param($var, $1) ;
199 $template->param('thisurl', CGI::url(-relative => 1, -query=>1));
200 $template->param('loginname', CGI::remote_user());
202 $template->param($hash);
203 $self->send_content_type();
204 print $template->output();
208 ################################################################
210 package Bweb::Config;
212 use base q/Bweb::Gui/;
216 Bweb::Config - read, write, display, modify configuration
220 this package is used for manage configuration
224 $conf = new Bweb::Config(config_file => '/path/to/conf');
234 $CGI::POST_MAX=102400; # Limit post to 100kB
235 $CGI::DISABLE_UPLOADS=0; # Allow file uploads
237 =head1 PACKAGE VARIABLE
239 %k_re - hash of all acceptable option.
243 this variable permit to check all option with a regexp.
247 our %k_re = ( dbi => qr/^(dbi:(Pg|mysql|SQLite):(?:\w+=[\w\d\.\/\-]+;?)+)$/i,
248 user => qr/^([\w\d\.-]+)$/i,
249 password => qr/^(.*)$/,
250 fv_write_path => qr!^([/\w\d\.-]*)$!,
251 template_dir => qr!^([/\w\d\.-]+)$!,
252 debug => qr/^(on)?$/,
253 lang => qr/^(\w\w)?$/,
254 email_media => qr/^([\w\d\.-]+@[\d\w\.-]+)$/,
255 graph_font => qr!^([/\w\d\.-]+.ttf)?$!,
256 bconsole => qr!^(.+)?$!,
257 wiki_url => qr!(.*)$!,
258 stat_job_table => qr!^(\w*)$!,
259 display_log_time => qr!^(on)?$!,
260 enable_security => qr/^(on)?$/,
261 enable_security_acl => qr/^(on)?$/,
262 default_age => qr/^((?:\d+(?:[ywdhms]\s*?)?)+)\s*$/,
263 name => qr/^([\w\s\d\.\-]+)$/,
264 dir_ver => qr/^(\d+(\.\d+)?)$/,
269 url => qr!^(https?://[\w\.\d/@?;]+)$!,
274 load - load config_file
278 this function load the specified config_file.
286 unless (open(FP, $self->{config_file}))
288 return $self->error("can't load config_file $self->{config_file} : $!");
290 my $f=''; my $tmpbuffer;
291 while(read FP,$tmpbuffer,4096)
299 no strict; # I have no idea of the contents of the file
304 return $self->error("Something is wrong with your configuration file...") ;
307 # keep a backup of the original config
308 foreach my $k (keys %$VAR1) {
309 if (exists $k_re{$k} and defined $VAR1->{$k}) {
310 $self->{$k} = $VAR1->{$k};
318 save - save the current configuration to config_file
326 if ($self->{ach_list}) {
327 # shortcut for display_begin
328 $self->{achs} = [ map {{ name => $_ }}
329 keys %{$self->{ach_list}}
333 unless (open(FP, ">$self->{config_file}"))
335 return $self->error("$self->{config_file} : $!\n" .
336 "You must add this to your config file\n"
337 . Data::Dumper::Dumper($self));
340 print FP Data::Dumper::Dumper($self);
348 edit, view, modify - html form ouput
356 $self->display($self, "config_edit.tpl");
362 $self->display($self, "config_view.tpl");
370 # we need to reset checkbox first
372 $self->{display_log_time} = 0;
373 $self->{enable_security} = 0;
374 $self->{enable_security_acl} = 0;
376 foreach my $k (CGI::param())
378 next unless (exists $k_re{$k}) ;
379 my $val = CGI::param($k);
380 if ($val =~ $k_re{$k}) {
383 $self->{error} .= "bad parameter : $k = [$val]";
389 if ($self->{error}) { # an error as occured
390 $self->display($self, 'error.tpl');
398 ################################################################
400 package Bweb::Client;
402 use base q/Bweb::Gui/;
406 Bweb::Client - Bacula FD
410 this package is use to do all Client operations like, parse status etc...
414 $client = new Bweb::Client(name => 'zog-fd');
415 $client->status(); # do a 'status client=zog-fd'
421 display_running_job - Html display of a running job
425 this function is used to display information about a current job
429 sub display_running_job
431 my ($self, $bweb, $jobid, $infos) = @_ ;
432 my $status = $self->status($bweb->{info});
435 if ($status->{$jobid}) {
436 $status = $status->{$jobid};
437 $status->{last_jobbytes} = $infos->{jobbytes};
438 $status->{last_jobfiles} = $infos->{jobfiles};
439 $status->{corr_jobbytes} = $infos->{corr_jobbytes};
440 $status->{corr_jobfiles} = $infos->{corr_jobfiles};
441 $status->{jobbytes}=$status->{Bytes};
442 $status->{jobbytes} =~ s![^\d]!!g;
443 $status->{jobfiles}=$status->{'Files Examined'};
444 $status->{jobfiles} =~ s/,//g;
445 $bweb->display($status, "client_job_status.tpl");
448 for my $id (keys %$status) {
449 $bweb->display($status->{$id}, "client_job_status.tpl");
456 $client = new Bweb::Client(name => 'plume-fd');
458 $client->status($bweb);
462 dirty hack to parse "status client=xxx-fd"
466 JobId 105 Job Full_plume.2006-06-06_17.22.23 is running.
467 Backup Job started: 06-jun-06 17:22
468 Files=8,971 Bytes=194,484,132 Bytes/sec=7,480,158
469 Files Examined=10,697
470 Processing file: /home/eric/.openoffice.org2/user/config/standard.sod
476 JobName => Full_plume.2006-06-06_17.22.23,
479 Bytes => 194,484,132,
489 my ($self, $conf) = @_ ;
491 if (defined $self->{cur_jobs}) {
492 return $self->{cur_jobs} ;
496 my $b = new Bconsole(pref => $conf);
497 my $ret = $b->send_cmd("st client=$self->{name}");
501 for my $r (split(/\n/, $ret)) {
503 $r =~ s/(^\s+|\s+$)//g;
504 if ($r =~ /JobId (\d+) Job (\S+)/) {
506 $arg->{$jobid} = { @param, JobId => $jobid } ;
510 @param = ( JobName => $2 );
512 } elsif ($r =~ /=.+=/) {
513 push @param, split(/\s+|\s*=\s*/, $r) ;
515 } elsif ($r =~ /=/) { # one per line
516 push @param, split(/\s*=\s*/, $r) ;
518 } elsif ($r =~ /:/) { # one per line
519 push @param, split(/\s*:\s*/, $r, 2) ;
523 if ($jobid and @param) {
524 $arg->{$jobid} = { @param,
526 Client => $self->{name},
530 $self->{cur_jobs} = $arg ;
536 ################################################################
538 package Bweb::Autochanger;
540 use base q/Bweb::Gui/;
544 Bweb::Autochanger - Object to manage Autochanger
548 this package will parse the mtx output and manage drives.
552 $auto = new Bweb::Autochanger(precmd => 'sudo');
554 $auto = new Bweb::Autochanger(precmd => 'ssh root@robot');
558 $auto->slot_is_full(10);
559 $auto->transfer(10, 11);
565 my ($class, %arg) = @_;
568 name => '', # autochanger name
569 label => {}, # where are volume { label1 => 40, label2 => drive0 }
570 drive => [], # drive use [ 'media1', 'empty', ..]
571 slot => [], # slot use [ undef, 'empty', 'empty', ..] no slot 0
572 io => [], # io slot number list [ 41, 42, 43...]
573 info => {slot => 0, # informations (slot, drive, io)
577 mtxcmd => '/usr/sbin/mtx',
579 device => '/dev/changer',
580 precmd => '', # ssh command
581 bweb => undef, # link to bacula web object (use for display)
584 map { $self->{lc($_)} = $arg{$_} } keys %arg ;
591 status - parse the output of mtx status
595 this function will launch mtx status and parse the output. it will
596 give a perlish view of the autochanger content.
598 it uses ssh if the autochanger is on a other host.
605 my @out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} status` ;
607 # TODO : reset all infos
608 $self->{info}->{drive} = 0;
609 $self->{info}->{slot} = 0;
610 $self->{info}->{io} = 0;
612 #my @out = `cat /home/eric/travail/brestore/plume/mtx` ;
615 # Storage Changer /dev/changer:2 Drives, 45 Slots ( 5 Import/Export )
616 #Data Transfer Element 0:Full (Storage Element 1 Loaded):VolumeTag = 000000
617 #Data Transfer Element 1:Empty
618 # Storage Element 1:Empty
619 # Storage Element 2:Full :VolumeTag=000002
620 # Storage Element 3:Empty
621 # Storage Element 4:Full :VolumeTag=000004
622 # Storage Element 5:Full :VolumeTag=000001
623 # Storage Element 6:Full :VolumeTag=000003
624 # Storage Element 7:Empty
625 # Storage Element 41 IMPORT/EXPORT:Empty
626 # Storage Element 41 IMPORT/EXPORT:Full :VolumeTag=000002
631 # Storage Element 7:Empty
632 # Storage Element 2:Full :VolumeTag=000002
633 if ($l =~ /Storage Element (\d+):(Empty|Full)(\s+:VolumeTag=([\w\d.-]+))?/){
636 $self->set_empty_slot($1);
638 $self->set_slot($1, $4);
641 } elsif ($l =~ /Data Transfer.+(\d+):(Full|Empty)(\s+.Storage Element (\d+) Loaded.(:VolumeTag = ([\w\d.-]+))?)?/) {
644 $self->set_empty_drive($1);
646 $self->set_drive($1, $4, $6);
649 } elsif ($l =~ /Storage Element (\d+).+IMPORT\/EXPORT:(Empty|Full)( :VolumeTag=([\d\w.-]+))?/)
652 $self->set_empty_io($1);
654 $self->set_io($1, $4);
657 # Storage Changer /dev/changer:2 Drives, 30 Slots ( 1 Import/Export )
659 } elsif ($l =~ /Storage Changer .+:(\d+) Drives, (\d+) Slots/) {
660 $self->{info}->{drive} = $1;
661 $self->{info}->{slot} = $2;
662 if ($l =~ /(\d+)\s+Import/) {
663 $self->{info}->{io} = $1 ;
665 $self->{info}->{io} = 0;
670 $self->debug($self) ;
675 my ($self, $slot) = @_;
678 if ($self->{slot}->[$slot] eq 'loaded') {
682 my $label = $self->{slot}->[$slot] ;
684 return $self->is_media_loaded($label);
689 my ($self, $drive, $slot) = @_;
691 return 0 if (not defined $drive or $self->{drive}->[$drive] eq 'empty') ;
692 return 0 if ($self->slot_is_full($slot)) ;
694 my $out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} unload $slot $drive 2>&1`;
697 my $content = $self->get_slot($slot);
698 print "content = $content<br/> $drive => $slot<br/>";
699 $self->set_empty_drive($drive);
700 $self->set_slot($slot, $content);
703 $self->{error} = $out;
708 # TODO: load/unload have to use mtx script from bacula
711 my ($self, $drive, $slot) = @_;
713 return 0 if (not defined $drive or $self->{drive}->[$drive] ne 'empty') ;
714 return 0 unless ($self->slot_is_full($slot)) ;
716 print "Loading drive $drive with slot $slot<br/>\n";
717 my $out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} load $slot $drive 2>&1`;
720 my $content = $self->get_slot($slot);
721 print "content = $content<br/> $slot => $drive<br/>";
722 $self->set_drive($drive, $slot, $content);
725 $self->{error} = $out;
733 my ($self, $media) = @_;
735 unless ($self->{label}->{$media}) {
739 if ($self->{label}->{$media} =~ /drive\d+/) {
749 return (defined $self->{info}->{io} and $self->{info}->{io} > 0);
754 my ($self, $slot, $tag) = @_;
755 $self->{slot}->[$slot] = $tag || 'full';
756 push @{ $self->{io} }, $slot;
759 $self->{label}->{$tag} = $slot;
765 my ($self, $slot) = @_;
767 push @{ $self->{io} }, $slot;
769 unless ($self->{slot}->[$slot]) { # can be loaded (parse before)
770 $self->{slot}->[$slot] = 'empty';
776 my ($self, $slot) = @_;
777 return $self->{slot}->[$slot];
782 my ($self, $slot, $tag) = @_;
783 $self->{slot}->[$slot] = $tag || 'full';
786 $self->{label}->{$tag} = $slot;
792 my ($self, $slot) = @_;
794 unless ($self->{slot}->[$slot]) { # can be loaded (parse before)
795 $self->{slot}->[$slot] = 'empty';
801 my ($self, $drive) = @_;
802 $self->{drive}->[$drive] = 'empty';
807 my ($self, $drive, $slot, $tag) = @_;
808 $self->{drive}->[$drive] = $tag || $slot;
809 $self->{drive_slot}->[$drive] = $slot;
811 $self->{slot}->[$slot] = $tag || 'loaded';
814 $self->{label}->{$tag} = "drive$drive";
820 my ($self, $slot) = @_;
822 # slot don't exists => full
823 if (not defined $self->{slot}->[$slot]) {
827 if ($self->{slot}->[$slot] eq 'empty') {
830 return 1; # vol, full, loaded
833 sub slot_get_first_free
836 for (my $slot=1; $slot < $self->{info}->{slot}; $slot++) {
837 return $slot unless ($self->slot_is_full($slot));
841 sub io_get_first_free
845 foreach my $slot (@{ $self->{io} }) {
846 return $slot unless ($self->slot_is_full($slot));
853 my ($self, $media) = @_;
855 return $self->{label}->{$media} ;
860 my ($self, $media) = @_;
862 return defined $self->{label}->{$media} ;
867 my ($self, $slot) = @_;
869 unless ($self->slot_is_full($slot)) {
870 print "Autochanger $self->{name} slot $slot is empty<br>\n";
875 if ($self->is_slot_loaded($slot)) {
878 print "Autochanger $self->{name} $slot is currently in use<br>\n";
882 # autochanger must have I/O
883 unless ($self->have_io()) {
884 print "Autochanger $self->{name} don't have I/O, you can take media yourself<br>\n";
888 my $dst = $self->io_get_first_free();
891 print "Autochanger $self->{name} mailbox is full, you must empty I/O first<br>\n";
895 $self->transfer($slot, $dst);
900 my ($self, $src, $dst) = @_ ;
901 if ($self->{debug}) {
902 print "<pre>$self->{precmd} $self->{mtxcmd} -f $self->{device} transfer $src $dst</pre>\n";
904 my $out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} transfer $src $dst 2>&1`;
907 my $content = $self->get_slot($src);
908 $self->{slot}->[$src] = 'empty';
909 $self->set_slot($dst, $content);
912 $self->{error} = $out;
919 my ($self, $index) = @_;
920 return $self->{drive_name}->[$index];
923 # TODO : do a tapeinfo request to get informations
933 print "<table><tr>\n";
934 for my $slot (@{$self->{io}})
936 if ($self->is_slot_loaded($slot)) {
937 print "<td></td><td>Slot $slot is currently loaded</td></tr>\n";
941 if ($self->slot_is_full($slot))
943 my $free = $self->slot_get_first_free() ;
944 print "</tr><tr><td>move slot $slot to $free :</td>";
947 if ($self->transfer($slot, $free)) {
948 print "<td><img src='/bweb/T.png' alt='ok'></td>\n";
950 print "<td><img src='/bweb/E.png' alt='ok' title='$self->{error}'></td>\n";
954 $self->{error} = "<td><img src='/bweb/E.png' alt='ok' title='E : Can t find free slot'></td>\n";
958 print "</tr></table>\n";
961 # TODO : this is with mtx status output,
962 # we can do an other function from bacula view (with StorageId)
966 my $bweb = $self->{bweb};
968 # $self->{label} => ('vol1', 'vol2', 'vol3', ..);
969 my $media_list = $bweb->dbh_join( keys %{ $self->{label} });
972 SELECT Media.VolumeName AS volumename,
973 Media.VolStatus AS volstatus,
974 Media.LastWritten AS lastwritten,
975 Media.VolBytes AS volbytes,
976 Media.MediaType AS mediatype,
978 Media.InChanger AS inchanger,
980 $bweb->{sql}->{MEDIA_EXPIRE} AS expire
982 INNER JOIN Pool USING (PoolId)
984 WHERE Media.VolumeName IN ($media_list)
987 my $all = $bweb->dbh_selectall_hashref($query, 'volumename') ;
989 # TODO : verify slot and bacula slot
993 for (my $slot=1; $slot <= $self->{info}->{slot} ; $slot++) {
995 if ($self->slot_is_full($slot)) {
997 my $vol = $self->{slot}->[$slot];
998 if (defined $all->{$vol}) { # TODO : autochanger without barcodes
1000 my $bslot = $all->{$vol}->{slot} ;
1001 my $inchanger = $all->{$vol}->{inchanger};
1003 # if bacula slot or inchanger flag is bad, we display a message
1004 if ($bslot != $slot or !$inchanger) {
1005 push @to_update, $slot;
1008 $all->{$vol}->{realslot} = $slot;
1010 push @{ $param }, $all->{$vol};
1012 } else { # empty or no label
1013 push @{ $param }, {realslot => $slot,
1014 volstatus => 'Unknown',
1015 volumename => $self->{slot}->[$slot]} ;
1018 push @{ $param }, {realslot => $slot, volumename => 'empty'} ;
1022 my $i=0; my $drives = [] ;
1023 foreach my $d (@{ $self->{drive} }) {
1024 $drives->[$i] = { index => $i,
1025 load => $self->{drive}->[$i],
1026 name => $self->{drive_name}->[$i],
1031 $bweb->display({ Name => $self->{name},
1032 nb_drive => $self->{info}->{drive},
1033 nb_io => $self->{info}->{io},
1036 Update => scalar(@to_update) },
1043 ################################################################
1045 package Bweb::Sched;
1046 use base q/Bweb::Gui/;
1050 Bweb::Sched() - Bweb package that parse show schedule ouput
1052 new Bweb::Sched(format => '%Y-%m-%d', year => 2008);
1056 my $b = $bweb->get_bconsole();
1057 my $s = $b->send_cmd("show schedule");
1058 my $sched = new Bweb::Sched(begin => '2007-01-01', end => '2007-01-02 12:00');
1059 $sched->parse_scheds(split(/\r?\n/, $s));
1070 'level' => 'Differential',
1077 my ($class, @arg) = @_;
1078 my $self = $class->SUPER::new(@arg);
1080 # we compare the current schedule date with begin and end
1081 # in a float form ex: 20071212.1243 > 20070101
1082 if ($self->{begin} and $self->{end}) {
1083 ($self->{fbegin}, $self->{fend}) = ($self->{begin}, $self->{end});
1084 $self->{fbegin} =~ s/(-|:)//g; $self->{fbegin} =~ s/ /./;
1085 $self->{fend} =~ s/(-|:)//g; $self->{fend} =~ s/ /./;
1088 bless($self,$class);
1090 if ($self->{bconsole}) {
1091 my $sel = $self->{name}?"=\"$self->{name}\"":'';
1092 my $b = $self->{bconsole};
1093 my $out = $b->send_cmd("show schedule$sel");
1094 $self->{show_output}=$out;
1095 $self->parse_scheds(split(/\r?\n/, $out));
1096 undef $self->{bconsole}; # useless now
1102 # cleanup and add a schedule
1105 my ($self, $name, $info) = @_;
1106 # bacula uses dates that start from 0, we start from 1
1107 foreach (@{$info->{mday}},@{$info->{month}}) { $_++ };
1110 $info->{event} = [ $self->get_events($info, $self->{format}) ];
1112 foreach my $i (qw/hour mday month wday wom woy mins/) {
1116 push @{$self->{schedules}->{$name}}, $info;
1119 # return the name of all schedules
1122 my ($self, $name) = @_;
1124 return keys %{ $self->{schedules} };
1127 # return an array of all schedule
1130 my ($self, $sched) = @_;
1131 return $self->{schedules}->{$sched};
1134 # return an ref array of all events
1135 # [ '2007-12-19 04:05', '2007-12-19 04:05' ]
1138 my ($self, $sched) = @_;
1139 return $sched->{event};
1142 # return the pool name
1145 my ($self, $sched) = @_;
1146 return $sched->{pool} || '';
1149 # return the level name (Incremental, Differential, Full)
1152 my ($self, $sched) = @_;
1153 return $sched->{level};
1156 # parse bacula sched bitmap
1159 my ($self, @output) = @_;
1166 foreach my $ligne (@output) {
1167 if ($ligne =~ /Schedule: name=(.+)/) {
1168 if ($name and $elt) {
1169 $elt->{level} = $run;
1170 $self->add_sched($name, $elt);
1175 elsif ($ligne =~ /Run Level=(.+)/) {
1176 if ($name and $elt) {
1177 $elt->{level} = $run;
1178 $self->add_sched($name, $elt);
1183 elsif ($ligne =~ /(hour|mday|month|mins)=(.+)/) {
1184 # All theses lines have the same format
1186 my ($k,$v) = ($1,$2);
1187 # we get all values (0 1 4 9)
1188 $elt->{$k}=[split (/\s/,$v)];
1190 # we make a bitmap for this kind of data (0 0 1 0 0 0 1) for a quick access
1191 elsif ($ligne =~ /(wday|wom|woy)=(.+)/) {
1192 my ($k,$v) = ($1,$2);
1193 foreach my $e (split (/\s/,$v)) {
1197 elsif ($ligne =~ /Pool: name=(.+?) PoolType/) {
1202 if ($name and $elt) {
1203 $elt->{level} = $run;
1204 $self->add_sched($name, $elt);
1208 use Date::Calc qw(:all);
1210 # read bacula schedule bitmap and get $format date string
1214 my ($self, $s,$format) = @_;
1215 my $year = $self->{year} || ((localtime($Bweb::btime))[5] + 1900);
1216 $format = $format || '%u-%02u-%02u %02u:%02u';
1218 foreach my $m (@{$s->{month}}) # mois de l'annee
1220 foreach my $md (@{$s->{mday}}) # jour du mois
1222 # print " m=$m md=$md\n";
1223 # we check if this day exists (31 fev)
1224 next if (!check_date($year,$m,$md));
1225 # print " check_date ok\n";
1227 my $w = ($md-1)/7; # we use the same thing than bacula
1228 next if (! $s->{wom}->[$w]);
1229 # print " wom ok\n";
1231 # on recupere le jour de la semaine
1232 my $wd = Day_of_Week($year,$m,$md);
1234 my ($w1,undef) = Week_of_Year($year,$m,$wd);
1235 next if (! $s->{woy}->[$w1-1]); # bacula 0-51
1236 # print " woy ok\n";
1238 $wd = 0 if ($wd == 7) ; # sunday is 0 on bacula
1239 next if (! $s->{wday}->[$wd]);
1240 # print " wday ok\n";
1242 foreach my $h (@{$s->{hour}}) # hour of the day
1244 foreach my $min (@{$s->{mins}}) # minute
1246 if ($self->{fbegin}) {
1248 my $d = sprintf('%d%0.2d%0.2d.%0.2d%0.2d',
1249 $year,$m,$md,$h,$min);
1250 next if ($d < $self->{fbegin} or $d > $self->{fend});
1252 push @ret, sprintf($format, $year,$m,$md,$h,$min);
1261 ################################################################
1265 use base q/Bweb::Gui/;
1269 Bweb - main Bweb package
1273 this package is use to compute and display informations
1278 use POSIX qw/strftime/;
1280 our $config_file= '/etc/bacula/bweb.conf';
1282 if ($ENV{BWEBCONF} && -f $ENV{BWEBCONF}) {
1283 $config_file = $ENV{BWEBCONF};
1290 %sql_func - hash to make query mysql/postgresql compliant
1296 UNIX_TIMESTAMP => '',
1297 FROM_UNIXTIME => '',
1298 TO_SEC => " interval '1 second' * ",
1299 SEC_TO_INT => "SEC_TO_INT",
1302 MEDIA_EXPIRE => "date_part('epoch', Media.LastWritten) + Media.VolRetention",
1303 ENDTIME_SEC => " date_part('epoch', EndTime) ",
1304 JOB_DURATION => " date_part('epoch', EndTime) - date_part('epoch', StartTime) ",
1305 STARTTIME_SEC => " date_part('epoch', Job.StartTime) ",
1306 STARTTIME_DAY => " date_trunc('day', Job.StartTime) ",
1307 STARTTIME_HOUR => " date_trunc('hour', Job.StartTime) ",
1308 STARTTIME_MONTH => " date_trunc('month', Job.StartTime) ",
1309 STARTTIME_WEEK => " date_trunc('week', Job.StartTime) ",
1310 STARTTIME_PHOUR=> " date_part('hour', Job.StartTime) ",
1311 STARTTIME_PDAY => " date_part('day', Job.StartTime) ",
1312 STARTTIME_PMONTH => " date_part('month', Job.StartTime) ",
1313 STARTTIME_PWEEK => " date_part('week', Job.StartTime) ",
1314 DB_SIZE => " SELECT pg_database_size(current_database()) ",
1315 CAT_POOL_TYPE => " MediaType || '_' || Pool.Name ",
1318 #NOW => "TIMESTAMP '2010-07-15 00:00:00' "
1321 UNIX_TIMESTAMP => 'UNIX_TIMESTAMP',
1322 FROM_UNIXTIME => 'FROM_UNIXTIME',
1325 SEC_TO_TIME => 'SEC_TO_TIME',
1326 MATCH => " REGEXP ",
1327 MEDIA_EXPIRE => 'UNIX_TIMESTAMP(Media.LastWritten)+Media.VolRetention',
1328 ENDTIME_SEC => " UNIX_TIMESTAMP(EndTime) ",
1329 JOB_DURATION => " UNIX_TIMESTAMP(EndTime) - UNIX_TIMESTAMP(StartTime) ",
1330 STARTTIME_SEC => " UNIX_TIMESTAMP(Job.StartTime) ",
1331 STARTTIME_DAY => " DATE_FORMAT(Job.StartTime, '%Y-%m-%d') ",
1332 STARTTIME_HOUR => " DATE_FORMAT(Job.StartTime, '%Y-%m-%d %H') ",
1333 STARTTIME_MONTH => " DATE_FORMAT(Job.StartTime, '%Y-%m') ",
1334 STARTTIME_WEEK => " DATE_FORMAT(Job.StartTime, '%Y-%v') ",
1335 STARTTIME_PHOUR=> " DATE_FORMAT(Job.StartTime, '%H') ",
1336 STARTTIME_PDAY => " DATE_FORMAT(Job.StartTime, '%d') ",
1337 STARTTIME_PMONTH => " DATE_FORMAT(Job.StartTime, '%m') ",
1338 STARTTIME_PWEEK => " DATE_FORMAT(Job.StartTime, '%v') ",
1339 # with mysql < 5, you have to play with the ugly SHOW command
1340 #DB_SIZE => " SELECT 0 ",
1341 # works only with mysql 5
1342 DB_SIZE => " SELECT sum(DATA_LENGTH) FROM INFORMATION_SCHEMA.TABLES ",
1343 CAT_POOL_TYPE => " CONCAT(MediaType,'_',Pool.Name) ",
1344 CONCAT_SEP => " SEPARATOR '' ",
1348 UNIX_TIMESTAMP => '',
1349 FROM_UNIXTIME => '',
1353 MATCH => " REGEXP ",
1354 MEDIA_EXPIRE => "strftime('%s', Media.LastWritten) + Media.VolRetention",
1355 ENDTIME_SEC => " strftime('%s', EndTime) ",
1356 STARTTIME_SEC => " strftime('%s', Job.StartTime) ",
1357 JOB_DURATION => " strftime('%s', EndTime) - strftime('%s', StartTime)",
1359 STARTTIME_DAY => " strftime('%Y-%m-%d', Job.StartTime) ",
1360 STARTTIME_HOUR => " strftime('%Y-%m-%d %H', Job.StartTime) ",
1361 STARTTIME_MONTH => " strftime('%Y-%m', Job.StartTime) ",
1362 STARTTIME_WEEK => " strftime('%Y-%W', Job.StartTime) ",
1363 STARTTIME_PHOUR=> " strftime('%H', Job.StartTime) ",
1364 STARTTIME_PDAY => " strftime('%d', Job.StartTime) ",
1365 STARTTIME_PMONTH => " strftime('%m', Job.StartTime) ",
1366 STARTTIME_PWEEK => " strftime('%W', Job.StartTime) ",
1367 DB_SIZE => " SELECT 0 ",
1368 CAT_POOL_TYPE => " MediaType || Pool.Name ",
1370 NOW => "strftime('%Y-%m-%d %H:%M:%S', 'now')",
1374 use Exporter 'import';
1375 our @EXPORT_OK = qw($btime);
1377 #our $btime = 1279144800;
1383 return $self->{info}->{dbi} =~ /dbi:mysql/i;
1389 return $self->{info}->{dbi} =~ /dbi:sqlite/i;
1395 return $self->{info}->{dbi} =~ /dbi:pg/i;
1402 $self->{dbh}->disconnect();
1407 sub dbh_selectall_arrayref
1409 my ($self, $query) = @_;
1410 $self->connect_db();
1411 $self->debug($query);
1412 return $self->{dbh}->selectall_arrayref($query);
1417 my ($self, @what) = @_;
1418 return join(',', $self->dbh_quote(@what)) ;
1423 my ($self, @what) = @_;
1425 $self->connect_db();
1427 return map { $self->{dbh}->quote($_) } @what;
1429 return $self->{dbh}->quote($what[0]) ;
1435 my ($self, $query) = @_ ;
1436 $self->connect_db();
1437 $self->debug($query);
1438 return $self->{dbh}->do($query);
1441 # For sqlite, convert UNIX_TIMESTAMP(a) to strftime('%s', a)
1444 my ($self, $query) = @_ ;
1445 if ($self->dbh_is_sqlite()) {
1446 $query =~ s/UNIX_TIMESTAMP\(([^)]+)\)/strftime('%s', $1)/gs;
1451 sub dbh_selectall_hashref
1453 my ($self, $query, $join) = @_;
1455 $self->connect_db();
1456 $self->debug($query);
1457 return $self->{dbh}->selectall_hashref($query, $join) ;
1460 sub dbh_selectrow_hashref
1462 my ($self, $query) = @_;
1464 $self->connect_db();
1465 $self->debug($query);
1466 return $self->{dbh}->selectrow_hashref($query) ;
1471 my ($self, @what) = @_;
1472 if ($self->dbh_is_mysql()) {
1473 return 'CONCAT(' . join(',', @what) . ')' ;
1475 return join(' || ', @what);
1481 my ($self, $query) = @_;
1482 $self->debug($query, up => 1);
1483 return $self->{dbh}->prepare($query);
1489 my @unit = qw(B KB MB GB TB);
1490 my $val = shift || 0;
1492 my $format = '%i %s';
1493 while ($val / 1024 > 1) {
1497 $format = ($i>0)?'%0.1f %s':'%i %s';
1498 return sprintf($format, $val, $unit[$i]);
1505 if ($val =~ /^(?:\d+(?:[ywdhms]\s*)?)+\s*$/) {
1520 # display Day, Hour, Year
1526 $val /= 60; # sec -> min
1528 if ($val / 60 <= 1) {
1532 $val /= 60; # min -> hour
1533 if ($val / 24 <= 1) {
1534 return "$val hours";
1537 $val /= 24; # hour -> day
1538 if ($val / 365 < 2) {
1542 $val /= 365 ; # day -> year
1544 return "$val years";
1550 my $val = shift || 0;
1552 if ($val eq '1' or $val eq "yes") {
1554 } elsif ($val eq '2' or $val eq "archived") {
1562 sub from_human_enabled
1564 my $val = shift || 0;
1566 if ($val eq '1' or $val eq "yes") {
1568 } elsif ($val eq '2' or $val eq "archived") {
1575 # get Day, Hour, Year
1581 unless ($val =~ /^\s*(\d+)\s*(\w)\w*\s*$/) {
1585 my %times = ( m => 60,
1591 my $mult = $times{$2} || 0;
1596 # get long term statistic table
1600 my $ret = $self->{info}->{stat_job_table} || 'JobHisto';
1601 if ($ret !~ m/^job$/i) {
1602 $ret = "(SELECT * FROM Job UNION SELECT * FROM $ret)";
1611 unless ($self->{dbh}) {
1613 $self->{dbh} = DBI->connect($self->{info}->{dbi},
1614 $self->{info}->{user},
1615 $self->{info}->{password});
1617 return $self->error("Can't connect to your database:\n$DBI::errstr\n")
1618 unless ($self->{dbh});
1620 $self->{dbh}->{FetchHashKeyName} = 'NAME_lc';
1622 if ($self->dbh_is_mysql()) {
1623 $self->{dbh}->do("SET group_concat_max_len=1000000");
1624 } elsif ($self->dbh_is_pg()) {
1625 $self->{dbh}->do("SET datestyle TO 'ISO, YMD'");
1632 my ($class, %arg) = @_;
1634 dbh => undef, # connect_db();
1636 dbi => '', # DBI:Pg:database=bacula;host=127.0.0.1
1642 map { $self->{lc($_)} = $arg{$_} } keys %arg ;
1644 if ($self->{info}->{dbi} =~ /DBI:(\w+):/i) {
1645 $self->{sql} = $sql_func{$1};
1648 $self->{loginname} = CGI::remote_user();
1649 $self->{debug} = $self->{info}->{debug};
1650 $self->{template_dir} = $self->{info}->{template_dir};
1652 my $args = $self->get_form('dir', 'lang');
1653 $self->set_lang($args->{lang});
1661 if ($self->{info}->{enable_security}) {
1662 $self->get_roles(); # get lang
1665 $self->display($self->{info}, "begin.tpl");
1671 $self->display($self->{info}, "end.tpl");
1677 my $arg = $self->get_form("qclient");
1678 my $f1 = $self->get_client_group_filter();
1679 my $f2 = $self->get_client_filter();
1681 # client_group_name | here
1682 #-------------------+-----
1687 SELECT client_group_name, max(here) AS here FROM (
1688 SELECT client_group_name, 1 AS here
1690 JOIN client_group_member USING (client_group_id)
1691 JOIN Client USING (ClientId) $f2
1692 WHERE Name = $arg->{qclient}
1694 SELECT client_group_name, 0
1695 FROM client_group $f1
1697 GROUP by client_group_name";
1699 my $all = $self->dbh_selectall_hashref($query, 'client_group_name');
1701 $self->display({ client_group => [ values %$all ], %$arg }, "client_edit.tpl");
1707 my $where=''; # by default
1709 my $arg = $self->get_form("client", "qre_client",
1710 "jclient_groups", "qnotingroup");
1712 if ($arg->{qre_client}) {
1713 $where = "WHERE Name $self->{sql}->{MATCH} $arg->{qre_client} ";
1714 } elsif ($arg->{client}) {
1715 $where = "WHERE Name = '$arg->{client}' ";
1716 } elsif ($arg->{jclient_groups}) {
1717 # $filter could already contains client_group_member
1719 JOIN client_group_member USING (ClientId)
1720 JOIN client_group USING (client_group_id)
1721 WHERE client_group_name IN ($arg->{jclient_groups}) ";
1722 } elsif ($arg->{qnotingroup}) {
1725 (SELECT 1 FROM client_group_member
1726 WHERE Client.ClientId = client_group_member.ClientId
1732 SELECT Name AS name,
1734 AutoPrune AS autoprune,
1735 FileRetention AS fileretention,
1736 JobRetention AS jobretention
1737 FROM Client " . $self->get_client_filter() .
1740 my $all = $self->dbh_selectall_hashref($query, 'name') ;
1742 my $dsp = { ID => $cur_id++,
1743 clients => [ values %$all] };
1745 $self->display($dsp, "client_list.tpl") ;
1750 my ($self, %arg) = @_;
1754 my $sql = $self->{sql};
1756 if ($arg{since} and $arg{age}) {
1757 my $d = strftime('%Y-%m-%d %H:%M:%S', localtime($btime + $arg{age}));
1759 AND StartTime > '$arg{since}'
1760 AND EndTime < '$d' ";
1762 $label .= "since $arg{since} and during " . human_sec($arg{age});
1764 } elsif ($arg{age}) {
1765 my $when = $btime - $arg{age};
1766 $limit .= "AND JobTDate > $when";
1768 $label = "last " . human_sec($arg{age});
1771 if ($arg{groupby}) {
1772 $limit .= " GROUP BY $arg{groupby} ";
1776 $limit .= " ORDER BY $arg{order} ";
1780 $limit .= " LIMIT $arg{limit} ";
1781 $label .= " limited to $arg{limit}";
1785 $limit .= " OFFSET $arg{offset} ";
1786 $label .= " with $arg{offset} offset ";
1790 $label = 'no filter';
1793 return ($limit, $label);
1798 my ($what, $default) = @_;
1799 my %opt_cookies = ( dir => 1 );
1801 my $ret = CGI::param($what);
1803 if ($opt_cookies{$what} && !$ret) {
1804 $ret = CGI::cookie($what);
1807 $ret = $ret || $default;
1814 $bweb->get_form(...) - Get useful stuff
1818 This function get and check parameters against regexp.
1820 If word begin with 'q', the return will be quoted or join quoted
1821 if it's end with 's'.
1826 $bweb->get_form('jobid', 'qclient', 'qpools') ;
1829 qclient => 'plume-fd',
1830 qpools => "'plume-fd', 'test-fd', '...'",
1837 my ($self, @what) = @_;
1838 my %what = map { $_ => 1 } @what;
1852 age => $self->{info}->{default_age},
1862 my %opt_ss =( # string with space
1871 my %opt_s = ( # default to ''
1891 my %opt_p = ( # option with path
1898 my %opt_r = (regexwhere => 1);
1899 my %opt_d = ( # option with date
1903 my %opt_t = (when => 2, # option with time
1904 begin => 1, # 1 hh:min are optionnal
1905 end => 1, # 2 hh:min are required
1908 foreach my $i (@what) {
1909 if (exists $opt_i{$i}) {# integer param
1910 my $value = get_item($i, $opt_i{$i}) ;
1911 if ($value =~ /^(\d+)$/) {
1913 } elsif ($i eq 'age' && # can have unit
1914 $value =~ /^(?:\d+(?:[ywdhms]\s*)?)+\s*$/) # 2y1h2m34s
1916 $ret{$i} = human_sec_unit($value);
1918 } elsif ($opt_s{$i}) { # simple string param
1919 my $value = get_item($i, '');
1920 if ($value =~ /^([\w\d\.-]+)$/) {
1923 } elsif ($opt_ss{$i}) { # simple string param (with space)
1924 my $value = get_item($i, '');
1925 if ($value =~ /^([\w\d\.\-\s]+)$/) {
1928 } elsif ($i =~ /^j(\w+)s$/) { # quote join args "'arg1', 'arg2'"
1929 my @value = grep { ! /^\s*$/ } CGI::param($1) ;
1931 $ret{$i} = $self->dbh_join(@value) ;
1934 } elsif ($i =~ /^q(\w+[^s])$/) { # 'arg1'
1935 my $value = CGI::param($1) ;
1937 $ret{$i} = $self->dbh_quote($value);
1940 } elsif ($i =~ /^q(\w+)s$/) { #[ 'arg1', 'arg2']
1941 $ret{$i} = [ map { { name => $self->dbh_quote($_) } }
1942 grep { ! /^\s*$/ } CGI::param($1) ];
1943 } elsif (exists $opt_p{$i}) {
1944 my $value = get_item($i, '');
1945 if ($value =~ /^([\w\d\.\/\s:\@\-]+)$/) {
1948 } elsif (exists $opt_r{$i}) {
1949 my $value = get_item($i, '');
1950 if ($value =~ /^([^'"']+)$/) {
1953 } elsif (exists $opt_d{$i}) {
1954 my $value = get_item($i, '');
1955 if ($value =~ /^\s*(\d+\s+\w+)$/) {
1958 } elsif (exists $opt_t{$i}) { # 1: hh:min optionnal, 2: hh:min required
1959 my $when = get_item($i, '');
1960 if ($when =~ /(\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?)/) {
1961 if ($opt_t{$i} == 1 or defined $2) {
1968 if ($what{comment}) {
1969 my $s = CGI::param('comment');
1971 $s =~ s/["\\'<>]/ /g; # strip some characters
1976 if ($what{storage_cmd}) {
1977 if (!grep {/^\Q$ret{storage_cmd}\E$/} ('mount', 'umount', 'release','status')) {
1978 delete $ret{storage_cmd};
1983 foreach my $s (CGI::param('slot')) {
1984 if ($s =~ /^(\d+)$/) {
1985 push @{$ret{slots}}, $s;
1991 my $age = $ret{age} || human_sec_unit($opt_i{age});
1992 my $since = CGI::param('since') || strftime('%F %T', localtime($btime - $age));
1993 if ($since =~ /^(\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?)$/) {
1999 my $lang = get_item('lang', 'en');
2000 if ($lang =~ /^(\w\w)$/) {
2005 if ($what{db_clients}) {
2007 if ($what{filter}) {
2008 # get security filter only if asked
2009 $filter = $self->get_client_filter();
2013 SELECT Client.Name as clientname
2017 my $clients = $self->dbh_selectall_hashref($query, 'clientname');
2018 $ret{db_clients} = [sort {$a->{clientname} cmp $b->{clientname} }
2022 if ($what{db_client_groups}) {
2024 if ($what{filter}) {
2025 # get security filter only if asked
2026 $filter = $self->get_client_group_filter();
2030 SELECT client_group_name AS name, comment AS comment
2031 FROM client_group $filter
2033 my $grps = $self->dbh_selectall_hashref($query, 'name');
2034 $ret{db_client_groups} = [sort {$a->{name} cmp $b->{name} }
2038 if ($what{db_usernames}) {
2040 SELECT username, comment
2043 my $users = $self->dbh_selectall_hashref($query, 'username');
2044 $ret{db_usernames} = [sort {$a->{username} cmp $b->{username} }
2048 if ($what{db_roles}) {
2050 SELECT rolename, comment
2053 my $r = $self->dbh_selectall_hashref($query, 'rolename');
2054 $ret{db_roles} = [sort {$a->{rolename} cmp $b->{rolename} }
2058 if ($what{db_mediatypes}) {
2060 SELECT MediaType as mediatype
2063 my $media = $self->dbh_selectall_hashref($query, 'mediatype');
2064 $ret{db_mediatypes} = [sort {$a->{mediatype} cmp $b->{mediatype} }
2068 if ($what{db_locations}) {
2070 SELECT Location as location, Cost as cost
2073 my $loc = $self->dbh_selectall_hashref($query, 'location');
2074 $ret{db_locations} = [ sort { $a->{location}
2080 if ($what{db_pools}) {
2081 my $query = "SELECT Name as name FROM Pool";
2083 my $all = $self->dbh_selectall_hashref($query, 'name') ;
2084 $ret{db_pools} = [ sort { $a->{name} cmp $b->{name} } values %$all ];
2087 if ($what{db_filesets}) {
2089 SELECT FileSet.FileSet AS fileset
2092 my $filesets = $self->dbh_selectall_hashref($query, 'fileset');
2094 $ret{db_filesets} = [sort {lc($a->{fileset}) cmp lc($b->{fileset}) }
2095 values %$filesets] ;
2098 if ($what{db_jobnames}) {
2100 if ($what{filter}) {
2101 $filter = " JOIN Client USING (ClientId) " . $self->get_client_filter();
2104 SELECT DISTINCT Job.Name AS jobname
2107 my $jobnames = $self->dbh_selectall_hashref($query, 'jobname');
2109 $ret{db_jobnames} = [sort {lc($a->{jobname}) cmp lc($b->{jobname}) }
2110 values %$jobnames] ;
2113 if ($what{db_devices}) {
2115 SELECT Device.Name AS name
2118 my $devices = $self->dbh_selectall_hashref($query, 'name');
2120 $ret{db_devices} = [sort {lc($a->{name}) cmp lc($b->{name}) }
2130 $self->can_do('r_view_stat');
2131 my $fields = $self->get_form(qw/age level status clients filesets
2132 graph gtype type filter db_clients
2133 limit db_filesets width height
2134 qclients qfilesets qjobnames db_jobnames/);
2136 my $url = CGI::url(-full => 0,
2139 $url =~ s/^.+?\?//; # http://path/to/bweb.pl?arg => arg
2141 # this organisation is to keep user choice between 2 click
2142 # TODO : fileset and client selection doesn't work
2149 if ($fields->{gtype} and $fields->{gtype} eq 'balloon') {
2150 system("./bgraph.pl");
2154 sub get_selected_media_location
2158 my $media = $self->get_form('jmedias');
2160 unless ($media->{jmedias}) {
2165 SELECT Media.VolumeName AS volumename, Location.Location AS location
2166 FROM Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2167 WHERE Media.VolumeName IN ($media->{jmedias})
2170 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2172 # { 'vol1' => { [volumename => 'vol1', location => 'ici'],
2181 my ($self, $in) = @_ ;
2182 $self->can_do('r_media_mgnt');
2183 my $media = $self->get_selected_media_location();
2189 my $elt = $self->get_form('db_locations');
2191 $self->display({ ID => $cur_id++,
2192 enabled => human_enabled($in),
2193 %$elt, # db_locations
2195 sort { $a->{volumename} cmp $b->{volumename} } values %$media
2204 $self->can_do('r_media_mgnt');
2206 my $elt = $self->get_form(qw/db_pools db_mediatypes db_locations/) ;
2208 $self->display($elt, "help_extern.tpl");
2211 sub help_extern_compute
2214 $self->can_do('r_media_mgnt');
2216 my $number = CGI::param('limit') || '' ;
2217 unless ($number =~ /^(\d+)$/) {
2218 return $self->error("Bad arg number : $number ");
2221 my ($sql, undef) = $self->get_param('pools',
2222 'locations', 'mediatypes');
2225 SELECT Media.VolumeName AS volumename,
2226 Media.VolStatus AS volstatus,
2227 Media.LastWritten AS lastwritten,
2228 Media.MediaType AS mediatype,
2229 Media.VolMounts AS volmounts,
2231 Media.Recycle AS recycle,
2232 $self->{sql}->{MEDIA_EXPIRE} AS expire
2234 INNER JOIN Pool ON (Pool.PoolId = Media.PoolId)
2235 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2237 WHERE Media.InChanger = 1
2238 AND Media.VolStatus IN ('Disabled', 'Error', 'Full', 'Used')
2240 ORDER BY expire DESC, recycle, Media.VolMounts DESC
2244 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2246 $self->display({ Media => [ values %$all ] },
2247 "help_extern_compute.tpl");
2253 $self->can_do('r_media_mgnt');
2255 my $param = $self->get_form(qw/db_locations db_pools db_mediatypes/) ;
2256 $self->display($param, "help_intern.tpl");
2259 sub help_intern_compute
2262 $self->can_do('r_media_mgnt');
2264 my $number = CGI::param('limit') || '' ;
2265 unless ($number =~ /^(\d+)$/) {
2266 return $self->error("Bad arg number : $number ");
2269 my ($sql, undef) = $self->get_param('pools', 'locations', 'mediatypes');
2271 if (CGI::param('expired')) {
2272 # we take only expired volumes or purged/recycle ones
2275 ( ($self->{sql}->{MEDIA_EXPIRE}) < $btime
2277 Media.VolStatus IN ('Purged', 'Recycle')
2284 SELECT Media.VolumeName AS volumename,
2285 Media.VolStatus AS volstatus,
2286 Media.LastWritten AS lastwritten,
2287 Media.MediaType AS mediatype,
2288 Media.VolMounts AS volmounts,
2290 $self->{sql}->{MEDIA_EXPIRE} AS expire
2292 INNER JOIN Pool ON (Pool.PoolId = Media.PoolId)
2293 LEFT JOIN Location ON (Location.LocationId = Media.LocationId)
2295 WHERE Media.InChanger <> 1
2296 AND Media.VolStatus IN ('Purged', 'Full', 'Append', 'Recycle')
2297 AND Media.Recycle = 1
2299 ORDER BY Media.VolUseDuration DESC, Media.VolMounts ASC, expire ASC
2303 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2305 $self->display({ Media => [ values %$all ] },
2306 "help_intern_compute.tpl");
2312 my ($self, %arg) = @_ ;
2314 my ($limit, $label) = $self->get_limit(%arg);
2315 my $filter = $self->get_client_filter();
2316 $filter = $filter? " JOIN Client USING (ClientId) $filter " : '';
2319 (SELECT count(Pool.PoolId) FROM Pool) AS nb_pool,
2320 (SELECT count(Media.MediaId) FROM Media) AS nb_media,
2321 (SELECT count(Job.JobId) FROM Job) AS nb_job,
2322 (SELECT sum(VolBytes) FROM Media) AS nb_bytes,
2323 ($self->{sql}->{DB_SIZE}) AS db_size,
2324 (SELECT count(Job.JobId)
2326 WHERE Job.JobStatus IN ('E','e','f','A')
2329 (SELECT count(Client.ClientId) FROM Client) AS nb_client
2332 my $row = $self->dbh_selectrow_hashref($query) ;
2334 $row->{nb_bytes} = human_size($row->{nb_bytes});
2336 $row->{db_size} = human_size($row->{db_size});
2337 $row->{label} = $label;
2338 $row->{age} = $arg{age};
2340 $self->display($row, "general.tpl");
2345 my ($self, @what) = @_ ;
2346 my %elt = map { $_ => 1 } @what;
2351 if ($elt{clients}) {
2352 my @clients = grep { ! /^\s*$/ } CGI::param('client');
2354 $ret{clients} = \@clients;
2355 my $str = $self->dbh_join(@clients);
2356 $limit .= "AND Client.Name IN ($str) ";
2360 if ($elt{client_groups}) {
2361 my @clients = grep { ! /^\s*$/ } CGI::param('client_group');
2363 $ret{client_groups} = \@clients;
2364 my $str = $self->dbh_join(@clients);
2365 $limit .= "AND client_group_name IN ($str) ";
2369 if ($elt{filesets}) {
2370 my @filesets = grep { ! /^\s*$/ } CGI::param('fileset');
2372 $ret{filesets} = \@filesets;
2373 my $str = $self->dbh_join(@filesets);
2374 $limit .= "AND FileSet.FileSet IN ($str) ";
2378 if ($elt{mediatypes}) {
2379 my @media = grep { ! /^\s*$/ } CGI::param('mediatype');
2381 $ret{mediatypes} = \@media;
2382 my $str = $self->dbh_join(@media);
2383 $limit .= "AND Media.MediaType IN ($str) ";
2388 my $client = CGI::param('client');
2390 $ret{client} = $client;
2391 $client = $self->dbh_quote($client);
2392 $limit .= "AND Client.Name = $client ";
2397 my $level = CGI::param('level') || '';
2398 if ($level =~ /^(\w)$/) {
2400 $limit .= "AND Job.Level = '$1' ";
2405 my $jobid = CGI::param('jobid') || '';
2407 if ($jobid =~ /^(\d+)$/) {
2409 $limit .= "AND Job.JobId = '$1' ";
2414 my $status = CGI::param('status') || '';
2415 if ($status =~ /^(\w)$/) {
2418 $limit .= "AND Job.JobStatus IN ('E','e','f','A') ";
2419 } elsif ($1 eq 'W') {
2420 $limit .= "AND Job.JobStatus IN ('T', 'W') OR Job.JobErrors > 0 ";
2422 $limit .= "AND Job.JobStatus = '$1' ";
2427 if ($elt{volstatus}) {
2428 my $status = CGI::param('volstatus') || '';
2429 if ($status =~ /^(\w+)$/) {
2431 $limit .= "AND Media.VolStatus = '$1' ";
2435 if ($elt{locations}) {
2436 my @location = grep { ! /^\s*$/ } CGI::param('location') ;
2438 $ret{locations} = \@location;
2439 my $str = $self->dbh_join(@location);
2440 $limit .= "AND Location.Location IN ($str) ";
2445 my @pool = grep { ! /^\s*$/ } CGI::param('pool') ;
2447 $ret{pools} = \@pool;
2448 my $str = $self->dbh_join(@pool);
2449 $limit .= "AND Pool.Name IN ($str) ";
2453 if ($elt{location}) {
2454 my $location = CGI::param('location') || '';
2456 $ret{location} = $location;
2457 $location = $self->dbh_quote($location);
2458 $limit .= "AND Location.Location = $location ";
2463 my $pool = CGI::param('pool') || '';
2466 $pool = $self->dbh_quote($pool);
2467 $limit .= "AND Pool.Name = $pool ";
2471 if ($elt{jobtype}) {
2472 my $jobtype = CGI::param('jobtype') || '';
2473 if ($jobtype =~ /^(\w)$/) {
2475 $limit .= "AND Job.Type = '$1' ";
2479 return ($limit, %ret);
2490 my ($self, %arg) = @_ ;
2491 return if $self->cant_do('r_view_job');
2493 $arg{order} = ' Job.JobId DESC ';
2495 my ($limit, $label) = $self->get_limit(%arg);
2496 my ($where, undef) = $self->get_param('clients',
2505 if (CGI::param('client_group')) {
2507 JOIN client_group_member USING (ClientId)
2508 JOIN client_group USING (client_group_id)
2511 my $filter = $self->get_client_filter();
2512 my $comment = $self->get_db_field('Comment');
2513 my $rb = $self->get_db_field('ReadBytes');
2515 SELECT Job.JobId AS jobid,
2516 Client.Name AS client,
2517 FileSet.FileSet AS fileset,
2518 Job.Name AS jobname,
2520 StartTime AS starttime,
2522 Pool.Name AS poolname,
2523 JobFiles AS jobfiles,
2524 JobBytes AS jobbytes,
2525 JobStatus AS jobstatus,
2528 $comment AS comment,
2529 $self->{sql}->{JOB_DURATION} AS duration,
2530 JobErrors AS joberrors
2532 FROM Client $filter $cgq,
2533 Job LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId)
2534 LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId)
2535 WHERE Client.ClientId=Job.ClientId
2536 AND Job.JobStatus NOT IN ('R', 'C')
2541 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
2543 $self->display({ Filter => $label,
2547 sort { $a->{jobid} <=> $b->{jobid} }
2554 # Adapt the code to the Schema version
2555 # TODO: can use the Version field
2558 my ($self, $what) = @_ ;
2560 my %feature = ('Comment' => 4, 'ReadBytes' => 4);
2561 my %replacement = ('Comment' => "''", 'ReadBytes' => 'JobBytes');
2563 if (!$self->{info}->{dir_ver} or
2564 $self->{info}->{dir_ver} >= $feature{$what})
2568 return $replacement{$what};
2572 # display job informations
2573 sub display_job_zoom
2575 my ($self, $jobid) = @_ ;
2576 $self->can_do('r_view_job');
2578 $jobid = $self->dbh_quote($jobid);
2580 # get security filter
2581 my $filter = $self->get_client_filter();
2582 my $comment = $self->get_db_field('Comment');
2583 my $rb = $self->get_db_field('ReadBytes');
2585 SELECT DISTINCT Job.JobId AS jobid,
2586 Client.Name AS client,
2587 Job.Name AS jobname,
2588 FileSet.FileSet AS fileset,
2590 Pool.Name AS poolname,
2591 StartTime AS starttime,
2592 JobFiles AS jobfiles,
2593 JobBytes AS jobbytes,
2594 JobStatus AS jobstatus,
2595 JobErrors AS joberrors,
2598 $comment AS comment,
2599 $self->{sql}->{JOB_DURATION} AS duration
2600 FROM Client $filter,
2601 Job LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId)
2602 LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId)
2603 WHERE Client.ClientId=Job.ClientId
2604 AND Job.JobId = $jobid
2607 my $row = $self->dbh_selectrow_hashref($query) ;
2609 # display all volumes associate with this job
2611 SELECT Media.VolumeName as volumename
2612 FROM Job,Media,JobMedia
2613 WHERE Job.JobId = $jobid
2614 AND JobMedia.JobId=Job.JobId
2615 AND JobMedia.MediaId=Media.MediaId
2618 my $all = $self->dbh_selectall_hashref($query, 'volumename');
2620 $row->{volumes} = [ values %$all ] ;
2621 $row->{wiki_url} = $self->{info}->{wiki_url};
2623 $self->display($row, "display_job_zoom.tpl");
2626 sub display_job_group
2628 my ($self, %arg) = @_;
2629 $self->can_do('r_view_job');
2631 my ($limit, $label) = $self->get_limit(groupby => 'client_group_name', %arg);
2633 my ($where, undef) = $self->get_param('client_groups',
2636 my $filter = $self->get_client_group_filter();
2639 SELECT client_group_name AS client_group_name,
2640 COALESCE(jobok.jobfiles,0) + COALESCE(joberr.jobfiles,0) AS jobfiles,
2641 COALESCE(jobok.jobbytes,0) + COALESCE(joberr.jobbytes,0) AS jobbytes,
2642 COALESCE(jobok.joberrors,0) + COALESCE(joberr.joberrors,0) AS joberrors,
2643 COALESCE(jobok.nbjobs,0) AS nbjobok,
2644 COALESCE(joberr.nbjobs,0) AS nbjoberr,
2645 COALESCE(jobok.duration, '0') AS duration
2647 FROM client_group $filter LEFT JOIN (
2648 SELECT client_group_name AS client_group_name, COUNT(1) AS nbjobs,
2649 SUM(JobFiles) AS jobfiles, SUM(JobBytes) AS jobbytes,
2650 SUM(JobErrors) AS joberrors,
2651 $self->{sql}->{JOB_DURATION} AS duration
2652 FROM Job JOIN client_group_member ON (Job.ClientId = client_group_member.ClientId)
2653 JOIN client_group USING (client_group_id)
2655 WHERE Type IN ('B', 'R') AND JobStatus IN ('T', 'W')
2658 ) AS jobok USING (client_group_name) LEFT JOIN
2661 SELECT client_group_name AS client_group_name, COUNT(1) AS nbjobs,
2662 SUM(JobFiles) AS jobfiles, SUM(JobBytes) AS jobbytes,
2663 SUM(JobErrors) AS joberrors
2664 FROM Job JOIN client_group_member ON (Job.ClientId = client_group_member.ClientId)
2665 JOIN client_group USING (client_group_id)
2667 WHERE Type IN ('B', 'R') AND JobStatus IN ('f','E', 'A')
2670 ) AS joberr USING (client_group_name)
2674 my $all = $self->dbh_selectall_hashref($query, 'client_group_name');
2676 my $rep = { groups => [ values %$all ], age => $arg{age}, filter => $label };
2679 $self->display($rep, "display_job_group.tpl");
2684 my ($self, %arg) = @_ ;
2685 $self->can_do('r_view_media');
2687 my ($limit, $label) = $self->get_limit(%arg);
2688 my ($where, %elt) = $self->get_param('pools',
2693 my $arg = $self->get_form('jmedias', 'qre_media', 'expired');
2695 if ($arg->{jmedias}) {
2696 $where = "AND Media.VolumeName IN ($arg->{jmedias}) $where";
2698 if ($arg->{qre_media}) {
2699 $where = "AND Media.VolumeName $self->{sql}->{MATCH} $arg->{qre_media} $where";
2701 if ($arg->{expired}) {
2703 AND VolStatus IN ('Full', 'Used')
2704 AND ( $self->{sql}->{MEDIA_EXPIRE} ) < $btime " . $where ;
2708 SELECT Media.VolumeName AS volumename,
2709 Media.VolBytes AS volbytes,
2710 Media.VolStatus AS volstatus,
2711 Media.MediaType AS mediatype,
2712 Media.InChanger AS online,
2713 Media.LastWritten AS lastwritten,
2714 Location.Location AS location,
2715 (volbytes*100/COALESCE(media_avg_size.size,-1)) AS volusage,
2716 Pool.Name AS poolname,
2717 $self->{sql}->{MEDIA_EXPIRE} AS expire
2719 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2720 LEFT JOIN (SELECT avg(Media.VolBytes) AS size,
2721 Media.MediaType AS MediaType
2723 WHERE Media.VolStatus = 'Full'
2724 GROUP BY Media.MediaType
2725 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
2727 WHERE Media.PoolId=Pool.PoolId
2731 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2733 $self->display({ ID => $cur_id++,
2735 Location => $elt{location},
2736 Media => [ values %$all ],
2738 "display_media.tpl");
2741 sub display_allmedia
2745 my $pool = $self->get_form('db_pools');
2747 foreach my $name (@{ $pool->{db_pools} }) {
2748 CGI::param('pool', $name->{name});
2749 $self->display_media();
2753 sub display_media_zoom
2757 my $media = $self->get_form('jmedias');
2759 unless ($media->{jmedias}) {
2760 return $self->error("Can't get media selection");
2764 SELECT InChanger AS online,
2765 Media.Enabled AS enabled,
2766 VolBytes AS nb_bytes,
2767 VolumeName AS volumename,
2768 VolStatus AS volstatus,
2769 VolMounts AS nb_mounts,
2770 Media.VolUseDuration AS voluseduration,
2771 Media.MaxVolJobs AS maxvoljobs,
2772 Media.MaxVolFiles AS maxvolfiles,
2773 Media.MaxVolBytes AS maxvolbytes,
2774 VolErrors AS nb_errors,
2775 Pool.Name AS poolname,
2776 Location.Location AS location,
2777 Media.Recycle AS recycle,
2778 Media.VolRetention AS volretention,
2779 Media.LastWritten AS lastwritten,
2780 Media.VolReadTime/1000000 AS volreadtime,
2781 Media.VolWriteTime/1000000 AS volwritetime,
2782 Media.RecycleCount AS recyclecount,
2783 Media.Comment AS comment,
2784 $self->{sql}->{MEDIA_EXPIRE} AS expire
2786 Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
2787 WHERE Pool.PoolId = Media.PoolId
2788 AND VolumeName IN ($media->{jmedias})
2791 my $all = $self->dbh_selectall_hashref($query, 'volumename') ;
2793 foreach my $media (values %$all) {
2794 my $mq = $self->dbh_quote($media->{volumename});
2797 SELECT DISTINCT Job.JobId AS jobid,
2799 Job.StartTime AS starttime,
2802 Job.JobFiles AS files,
2803 Job.JobBytes AS bytes,
2804 Job.jobstatus AS status
2805 FROM Media,JobMedia,Job
2806 WHERE Media.VolumeName=$mq
2807 AND Media.MediaId=JobMedia.MediaId
2808 AND JobMedia.JobId=Job.JobId
2811 my $jobs = $self->dbh_selectall_hashref($query, 'jobid') ;
2814 SELECT LocationLog.Date AS date,
2815 Location.Location AS location,
2816 LocationLog.Comment AS comment
2817 FROM Media,LocationLog INNER JOIN Location ON (LocationLog.LocationId = Location.LocationId)
2818 WHERE Media.MediaId = LocationLog.MediaId
2819 AND Media.VolumeName = $mq
2823 my $log = $self->dbh_selectall_arrayref($query) ;
2825 $logtxt = join("\n", map { ($_->[0] . ' ' . $_->[1] . ' ' . $_->[2])} @$log ) ;
2828 $self->display({ jobs => [ values %$jobs ],
2829 LocationLog => $logtxt,
2831 "display_media_zoom.tpl");
2838 $self->can_do('r_location_mgnt');
2840 my $loc = $self->get_form('qlocation');
2841 unless ($loc->{qlocation}) {
2842 return $self->error("Can't get location");
2846 SELECT Location.Location AS location,
2847 Location.Cost AS cost,
2848 Location.Enabled AS enabled
2850 WHERE Location.Location = $loc->{qlocation}
2853 my $row = $self->dbh_selectrow_hashref($query);
2854 $row->{enabled} = human_enabled($row->{enabled});
2855 $self->display({ ID => $cur_id++,
2856 %$row }, "location_edit.tpl") ;
2862 $self->can_do('r_location_mgnt');
2864 my $arg = $self->get_form(qw/qlocation qnewlocation cost enabled/) ;
2865 unless ($arg->{qlocation}) {
2866 return $self->error("Can't get location");
2868 unless ($arg->{qnewlocation}) {
2869 return $self->error("Can't get new location name");
2871 unless ($arg->{cost}) {
2872 return $self->error("Can't get new cost");
2875 my $enabled = from_human_enabled($arg->{enabled});
2878 UPDATE Location SET Cost = $arg->{cost},
2879 Location = $arg->{qnewlocation},
2881 WHERE Location.Location = $arg->{qlocation}
2884 $self->dbh_do($query);
2886 $self->location_display();
2892 $self->can_do('r_location_mgnt');
2894 my $arg = $self->get_form(qw/qlocation/) ;
2896 unless ($arg->{qlocation}) {
2897 return $self->error("Can't get location");
2901 SELECT count(Media.MediaId) AS nb
2902 FROM Media INNER JOIN Location USING (LocationID)
2903 WHERE Location = $arg->{qlocation}
2906 my $res = $self->dbh_selectrow_hashref($query);
2909 return $self->error("Sorry, the location must be empty");
2913 DELETE FROM Location WHERE Location = $arg->{qlocation}
2916 $self->dbh_do($query);
2918 $self->location_display();
2924 $self->can_do('r_location_mgnt');
2926 my $arg = $self->get_form(qw/qlocation cost/) ;
2928 unless ($arg->{qlocation}) {
2929 $self->display({}, "location_add.tpl");
2932 unless ($arg->{cost}) {
2933 return $self->error("Can't get new cost");
2936 my $enabled = CGI::param('enabled') || '';
2937 $enabled = from_human_enabled($enabled);
2940 INSERT INTO Location (Location, Cost, Enabled)
2941 VALUES ($arg->{qlocation}, $arg->{cost}, $enabled)
2944 $self->dbh_do($query);
2946 $self->location_display();
2949 sub location_display
2954 SELECT Location.Location AS location,
2955 Location.Cost AS cost,
2956 Location.Enabled AS enabled,
2957 (SELECT count(Media.MediaId)
2959 WHERE Media.LocationId = Location.LocationId
2964 my $location = $self->dbh_selectall_hashref($query, 'location');
2965 $self->display({ ID => $cur_id++,
2966 Locations => [ values %$location ] },
2967 "display_location.tpl");
2974 my $media = $self->get_selected_media_location();
2979 my $arg = $self->get_form('db_locations', 'qnewlocation');
2981 $self->display({ email => $self->{info}->{email_media},
2983 media => [ values %$media ],
2985 "update_location.tpl");
2988 ###########################################################
2993 my $arg = $self->get_form(qw/jclient_groups qclient/);
2995 unless ($arg->{qclient}) {
2996 return $self->error("Can't get client name");
2999 $self->can_do('r_group_mgnt');
3001 my $f1 = $self->get_client_filter();
3002 my $f2 = $self->get_client_group_filter();
3004 $self->{dbh}->begin_work();
3007 DELETE FROM client_group_member
3011 WHERE Client.Name = $arg->{qclient})
3013 $self->dbh_do($query);
3015 if ($arg->{jclient_groups}) {
3017 INSERT INTO client_group_member (client_group_id, ClientId)
3018 (SELECT client_group_id, (SELECT ClientId
3020 WHERE Name = $arg->{qclient})
3021 FROM client_group $f2 WHERE client_group_name IN ($arg->{jclient_groups})
3024 $self->dbh_do($query);
3027 $self->{dbh}->commit() or $self->error($self->{dbh}->errstr);
3029 $self->display_clients();
3035 my $grp = $self->get_form(qw/qclient_group db_clients/);
3037 unless ($grp->{qclient_group}) {
3038 $self->can_do('r_group_mgnt');
3039 $self->display({ ID => $cur_id++,
3040 client_group => "''",
3042 }, "groups_edit.tpl");
3046 unless ($self->cant_do('r_group_mgnt')) {
3047 $self->can_do('r_view_group');
3052 FROM Client JOIN client_group_member using (ClientId)
3053 JOIN client_group using (client_group_id)
3054 WHERE client_group_name = $grp->{qclient_group}
3057 my $row = $self->dbh_selectall_hashref($query, "name");
3059 $self->display({ ID => $cur_id++,
3060 client_group => $grp->{qclient_group},
3062 client_group_member => [ values %$row]},
3069 $self->can_do('r_group_mgnt');
3071 my $arg = $self->get_form(qw/qclient_group jclients qnewgroup qcomment/);
3072 if (!$arg->{qcomment}) {
3073 $arg->{qcomment} = "''";
3076 if (!$arg->{qclient_group} and $arg->{qnewgroup}) {
3078 INSERT INTO client_group (client_group_name, comment)
3079 VALUES ($arg->{qnewgroup}, $arg->{qcomment})
3081 $self->dbh_do($query);
3082 $arg->{qclient_group} = $arg->{qnewgroup};
3085 unless ($arg->{qclient_group}) {
3086 return $self->error("Can't get groups");
3089 $self->{dbh}->begin_work();
3092 DELETE FROM client_group_member
3093 WHERE client_group_id IN
3094 (SELECT client_group_id
3096 WHERE client_group_name = $arg->{qclient_group})
3098 $self->dbh_do($query);
3100 if ($arg->{jclients}) {
3102 INSERT INTO client_group_member (ClientId, client_group_id)
3104 (SELECT client_group_id
3106 WHERE client_group_name = $arg->{qclient_group})
3107 FROM Client WHERE Name IN ($arg->{jclients})
3110 $self->dbh_do($query);
3112 if ($arg->{qclient_group} ne $arg->{qnewgroup}) {
3115 SET client_group_name = $arg->{qnewgroup}, comment = $arg->{qcomment}
3116 WHERE client_group_name = $arg->{qclient_group}
3119 $self->dbh_do($query);
3122 $self->{dbh}->commit() or $self->error($self->{dbh}->errstr);
3124 $self->display_groups();
3130 $self->can_do('r_group_mgnt');
3132 my $arg = $self->get_form(qw/qclient_group/);
3134 unless ($arg->{qclient_group}) {
3135 return $self->error("Can't get groups");
3138 $self->{dbh}->begin_work();
3141 DELETE FROM client_group_member
3142 WHERE client_group_id IN
3143 (SELECT client_group_id
3145 WHERE client_group_name = $arg->{qclient_group})");
3148 DELETE FROM bweb_client_group_acl
3149 WHERE client_group_id IN
3150 (SELECT client_group_id
3152 WHERE client_group_name = $arg->{qclient_group})");
3155 DELETE FROM client_group
3156 WHERE client_group_name = $arg->{qclient_group}");
3158 $self->{dbh}->commit();
3159 $self->display_groups();
3167 if ($self->cant_do('r_group_mgnt')) {
3168 $arg = $self->get_form(qw/db_client_groups filter/) ;
3170 $arg = $self->get_form(qw/db_client_groups/) ;
3173 if ($self->{dbh}->errstr) {
3174 return $self->error("Can't use groups with bweb, read INSTALL to enable them");
3179 $self->display({ ID => $cur_id++,
3181 "display_groups.tpl");
3184 ###########################################################
3189 if (not $self->{info}->{enable_security}) {
3192 if (!$self->{loginname}) {
3193 $self->error("Can't get your login name");
3194 $self->display_end();
3197 # admin is a special user that can do everything
3198 if ($self->{loginname} eq 'admin') {
3202 if (defined $self->{security}) {
3205 $self->{security} = {};
3206 my $u = $self->dbh_quote($self->{loginname});
3209 SELECT use_acl, rolename, tpl
3211 JOIN bweb_role_member USING (userid)
3212 JOIN bweb_role USING (roleid)
3215 my $rows = $self->dbh_selectall_arrayref($query);
3216 # do cache with this role
3217 if (!$rows or !scalar(@$rows)) {
3218 $self->error("Can't get $self->{loginname}'s roles");
3219 $self->display_end();
3222 foreach my $r (@$rows) {
3223 $self->{security}->{$r->[1]}=1;
3225 $self->{security}->{use_acl} = $rows->[0]->[0];
3226 if ($rows->[0]->[2] =~ /^(\w\w)$/) {
3227 $self->set_lang($1);
3234 my ($self, $client) = @_;
3236 my $filter = $self->get_client_filter();
3240 my $cont = $self->dbh_selectrow_hashref("
3243 WHERE Name = '$client'
3245 return defined $cont;
3250 my ($self, $action) = @_;
3251 # is security enabled in configuration ?
3252 if (not $self->{info}->{enable_security}) {
3255 # admin is a special user that can do everything
3256 if ($self->{loginname} eq 'admin') {
3260 if (!$self->{loginname}) {
3261 $self->{error} = "Can't do $action, your are not logged. " .
3262 "Check security with your administrator";
3265 if (!$self->get_roles()) {
3268 if (!$self->{security}->{$action}) {
3270 "$self->{loginname} sorry, but this action ($action) " .
3271 "is not permited. " .
3272 "Check security with your administrator";
3278 # make like an assert (program die)
3281 my ($self, $action) = @_;
3282 if ($self->cant_do($action)) {
3283 $self->error($self->{error});
3284 $self->display_end();
3294 if (!$self->{info}->{enable_security} or
3295 !$self->{info}->{enable_security_acl})
3300 if ($self->get_roles()) {
3301 return $self->{security}->{use_acl};
3307 # JOIN Client USING (ClientId) " . $b->get_client_filter() . "
3308 sub get_client_filter
3310 my ($self, $login) = @_;
3313 $u = $self->dbh_quote($login);
3314 } elsif ($self->use_filter()) {
3315 $u = $self->dbh_quote($self->{loginname});
3320 JOIN (SELECT ClientId FROM client_group_member
3321 JOIN client_group USING (client_group_id)
3322 JOIN bweb_client_group_acl USING (client_group_id)
3323 JOIN bweb_user USING (userid)
3324 WHERE bweb_user.username = $u
3325 ) AS filter USING (ClientId)";
3328 #JOIN client_group USING (client_group_id)" . $b->get_client_group_filter()
3329 sub get_client_group_filter
3331 my ($self, $login) = @_;
3334 $u = $self->dbh_quote($login);
3335 } elsif ($self->use_filter()) {
3336 $u = $self->dbh_quote($self->{loginname});
3341 JOIN (SELECT client_group_id
3342 FROM bweb_client_group_acl
3343 JOIN bweb_user USING (userid)
3344 WHERE bweb_user.username = $u
3345 ) AS filter USING (client_group_id)";
3348 # role and username have to be quoted before
3349 # role and username can be a quoted list
3352 my ($self, $role, $username) = @_;
3353 $self->can_do("r_user_mgnt");
3355 my $nb = $self->dbh_do("
3356 DELETE FROM bweb_role_member
3357 WHERE roleid = (SELECT roleid FROM bweb_role
3358 WHERE rolename IN ($role))
3359 AND userid = (SELECT userid FROM bweb_user
3360 WHERE username IN ($username))");
3364 # role and username have to be quoted before
3365 # role and username can be a quoted list
3368 my ($self, $role, $username) = @_;
3369 $self->can_do("r_user_mgnt");
3371 my $nb = $self->dbh_do("
3372 INSERT INTO bweb_role_member (roleid, userid)
3373 SELECT roleid, userid FROM bweb_role, bweb_user
3374 WHERE rolename IN ($role)
3375 AND username IN ($username)
3380 # role and username have to be quoted before
3381 # role and username can be a quoted list
3384 my ($self, $copy, $user) = @_;
3385 $self->can_do("r_user_mgnt");
3387 my $nb = $self->dbh_do("
3388 INSERT INTO bweb_role_member (roleid, userid)
3389 SELECT roleid, a.userid
3390 FROM bweb_user AS a, bweb_role_member
3391 JOIN bweb_user USING (userid)
3392 WHERE bweb_user.username = $copy
3393 AND a.username = $user");
3397 # username can be a join quoted list of usernames
3400 my ($self, $username) = @_;
3401 $self->can_do("r_user_mgnt");
3404 DELETE FROM bweb_role_member
3408 WHERE username in ($username))");
3410 DELETE FROM bweb_client_group_acl
3414 WHERE username IN ($username))");
3421 $self->can_do("r_user_mgnt");
3423 my $arg = $self->get_form(qw/jusernames/);
3425 unless ($arg->{jusernames}) {
3426 return $self->error("Can't get user");
3429 $self->{dbh}->begin_work();
3431 $self->revoke_all($arg->{jusernames});
3433 DELETE FROM bweb_user WHERE username IN ($arg->{jusernames})");
3435 $self->{dbh}->commit();
3437 $self->display_users();
3443 $self->can_do("r_user_mgnt");
3445 # we don't quote username directly to check that it is conform
3446 my $arg = $self->get_form(qw/username qpasswd qcomment jrolenames qcreate
3447 lang qcopy_username jclient_groups/) ;
3449 if (not $arg->{qcreate}) {
3450 $arg = $self->get_form(qw/db_roles db_usernames db_client_groups/);
3451 $self->display($arg, "display_user.tpl");
3455 my $u = $self->dbh_quote($arg->{username});
3457 $arg->{use_acl}=(CGI::param('use_acl')?'true':'false');
3459 if (!$arg->{qpasswd}) {
3460 $arg->{qpasswd} = "''";
3462 if (!$arg->{qcomment}) {
3463 $arg->{qcomment} = "''";
3466 # will fail if user already exists
3467 # UPDATE with mysql dbi does not return if update is ok
3470 SET passwd=$arg->{qpasswd}, comment=$arg->{qcomment},
3471 use_acl=$arg->{use_acl}, tpl='$arg->{lang}'
3472 WHERE username = $u")
3473 # and (! $self->dbh_is_mysql() )
3476 INSERT INTO bweb_user (username, passwd, use_acl, comment, tpl)
3477 VALUES ($u, $arg->{qpasswd}, $arg->{use_acl},
3478 $arg->{qcomment}, '$arg->{lang}')");
3480 $self->{dbh}->begin_work();
3482 $self->revoke_all($u);
3484 if ($arg->{qcopy_username}) {
3485 $self->grant_like($arg->{qcopy_username}, $u);
3487 $self->grant($arg->{jrolenames}, $u);
3490 if ($arg->{jclient_groups}) {
3492 INSERT INTO bweb_client_group_acl (client_group_id, userid)
3493 SELECT client_group_id, userid
3494 FROM client_group, bweb_user
3495 WHERE client_group_name IN ($arg->{jclient_groups})
3500 $self->{dbh}->commit();
3502 $self->display_users();
3505 # TODO: we miss a matrix with all user/roles
3509 $self->can_do("r_user_mgnt");
3511 my $arg = $self->get_form(qw/db_usernames/) ;
3513 if ($self->{dbh}->errstr) {
3514 return $self->error("Can't use users with bweb, read INSTALL to enable them");
3517 $self->display({ ID => $cur_id++,
3519 "display_users.tpl");
3525 $self->can_do("r_user_mgnt");
3527 my $arg = $self->get_form('username');
3528 my $user = $self->dbh_quote($arg->{username});
3530 my $userp = $self->dbh_selectrow_hashref("
3531 SELECT username, passwd, comment, use_acl, tpl
3533 WHERE username = $user
3536 return $self->error("Can't find $user in catalog");
3538 my $filter = $self->get_client_group_filter($arg->{username});
3539 my $scg = $self->dbh_selectall_hashref("
3540 SELECT client_group_name AS name
3541 FROM client_group $filter
3545 #------------+--------
3550 my $role = $self->dbh_selectall_hashref("
3551 SELECT rolename, max(here) AS userid FROM (
3552 SELECT rolename, 1 AS here
3554 JOIN bweb_role_member USING (userid)
3555 JOIN bweb_role USING (roleid)
3556 WHERE username = $user
3561 GROUP by rolename", 'rolename');
3563 $arg = $self->get_form(qw/db_usernames db_client_groups/);
3566 db_usernames => $arg->{db_usernames},
3567 username => $userp->{username},
3568 comment => $userp->{comment},
3569 passwd => $userp->{passwd},
3570 lang => $userp->{tpl},
3571 use_acl => $userp->{use_acl},
3572 db_client_groups => $arg->{db_client_groups},
3573 client_group => [ values %$scg ],
3574 db_roles => [ values %$role],
3575 }, "display_user.tpl");
3579 ###########################################################
3581 sub get_media_max_size
3583 my ($self, $type) = @_;
3585 "SELECT avg(VolBytes) AS size
3587 WHERE Media.VolStatus = 'Full'
3588 AND Media.MediaType = '$type'
3591 my $res = $self->selectrow_hashref($query);
3594 return $res->{size};
3604 my $media = $self->get_form('qmedia');
3606 unless ($media->{qmedia}) {
3607 return $self->error("Can't get media");
3611 SELECT Media.Slot AS slot,
3612 PoolMedia.Name AS poolname,
3613 Media.VolStatus AS volstatus,
3614 Media.InChanger AS inchanger,
3615 Location.Location AS location,
3616 Media.VolumeName AS volumename,
3617 Media.MaxVolBytes AS maxvolbytes,
3618 Media.MaxVolJobs AS maxvoljobs,
3619 Media.MaxVolFiles AS maxvolfiles,
3620 Media.VolUseDuration AS voluseduration,
3621 Media.VolRetention AS volretention,
3622 Media.Comment AS comment,
3623 PoolRecycle.Name AS poolrecycle,
3624 Media.Enabled AS enabled
3626 FROM Media INNER JOIN Pool AS PoolMedia ON (Media.PoolId = PoolMedia.PoolId)
3627 LEFT JOIN Pool AS PoolRecycle ON (Media.RecyclePoolId = PoolRecycle.PoolId)
3628 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
3630 WHERE Media.VolumeName = $media->{qmedia}
3633 my $row = $self->dbh_selectrow_hashref($query);
3634 $row->{volretention} = human_sec($row->{volretention});
3635 $row->{voluseduration} = human_sec($row->{voluseduration});
3636 $row->{enabled} = human_enabled($row->{enabled});
3638 my $elt = $self->get_form(qw/db_pools db_locations/);
3643 }, "update_media.tpl");
3649 $self->can_do('r_media_mgnt');
3651 my $arg = $self->get_form('jmedias', 'qnewlocation') ;
3653 unless ($arg->{jmedias}) {
3654 return $self->error("Can't get selected media");
3657 unless ($arg->{qnewlocation}) {
3658 return $self->error("Can't get new location");
3663 SET LocationId = (SELECT LocationId
3665 WHERE Location = $arg->{qnewlocation})
3666 WHERE Media.VolumeName IN ($arg->{jmedias})
3669 my $nb = $self->dbh_do($query);
3671 print "$nb media updated, you may have to update your autochanger.";
3673 $self->display_media();
3679 $self->can_do('r_media_mgnt');
3681 my $media = $self->get_selected_media_location();
3683 return $self->error("Can't get media selection");
3685 my $newloc = CGI::param('newlocation');
3687 my $user = CGI::param('user') || 'unknown';
3688 my $comm = CGI::param('comment') || '';
3689 $comm = $self->dbh_quote("$user: $comm");
3691 my $arg = $self->get_form('enabled');
3692 my $en = from_human_enabled($arg->{enabled});
3693 my $b = $self->get_bconsole();
3696 foreach my $vol (keys %$media) {
3698 INSERT INTO LocationLog (Date,Comment,MediaId,LocationId,NewEnabled,NewVolStatus)
3699 SELECT $self->{sql}->{NOW}, $comm, Media.MediaId, Location.LocationId,
3701 FROM Media, Location
3702 WHERE Media.VolumeName = '$vol'
3703 AND Location.Location = '$media->{$vol}->{location}'
3705 $self->dbh_do($query);
3706 $self->debug($query);
3707 $b->send_cmd("update volume=\"$vol\" enabled=$en");
3712 $q->param('action', 'update_location');
3713 my $url = $q->url(-full => 1, -query=>1);
3715 $self->display({ email => $self->{info}->{email_media},
3717 newlocation => $newloc,
3718 # [ { volumename => 'vol1' }, { volumename => 'vol2'},..]
3719 media => [ values %$media ],
3721 "change_location.tpl");
3725 sub display_client_stats
3727 my ($self, %arg) = @_ ;
3728 $self->can_do('r_view_stat');
3730 my $client = $self->dbh_quote($arg{clientname});
3731 # get security filter
3732 my $filter = $self->get_client_filter();
3734 my ($limit, $label) = $self->get_limit(%arg);
3737 count(Job.JobId) AS nb_jobs,
3738 sum(Job.JobBytes) AS nb_bytes,
3739 sum(Job.JobErrors) AS nb_err,
3740 sum(Job.JobFiles) AS nb_files,
3741 Client.Name AS clientname
3742 FROM Job JOIN Client USING (ClientId) $filter
3744 Client.Name = $client
3746 GROUP BY Client.Name
3749 my $row = $self->dbh_selectrow_hashref($query);
3751 $row->{ID} = $cur_id++;
3752 $row->{label} = $label;
3753 $row->{grapharg} = "client";
3754 $row->{age} = $arg{age};
3756 $self->display($row, "display_client_stats.tpl");
3760 sub _display_group_stats
3762 my ($self, %arg) = @_ ;
3764 my $carg = $self->get_form(qw/qclient_group/);
3766 unless ($carg->{qclient_group}) {
3767 return $self->error("Can't get group");
3769 my $jobt = $self->get_stat_table();
3770 my ($limit, $label) = $self->get_limit(%arg);
3774 count(Job.JobId) AS nb_jobs,
3775 sum(Job.JobBytes) AS nb_bytes,
3776 sum(Job.JobErrors) AS nb_err,
3777 sum(Job.JobFiles) AS nb_files,
3778 client_group.client_group_name AS clientname
3780 JOIN Client USING (ClientId)
3781 JOIN client_group_member ON (Client.ClientId = client_group_member.ClientId)
3782 JOIN client_group USING (client_group_id)
3784 client_group.client_group_name = $carg->{qclient_group}
3786 GROUP BY client_group.client_group_name
3789 my $row = $self->dbh_selectrow_hashref($query);
3791 $row->{ID} = $cur_id++;
3792 $row->{label} = $label;
3793 $row->{grapharg} = "client_group";
3795 $self->display($row, "display_client_stats.tpl");
3798 # [ name, num, value, joberrors, nb_job ] =>
3800 # [ { name => 'ALL',
3801 # events => [ { num => 1, label => '2007-01',
3802 # value => 'T', title => 10 },
3803 # { num => 2, label => '2007-02',
3804 # value => 'R', title => 11 },
3807 # { name => 'Other',
3811 sub make_overview_tab
3813 my ($self, $q) = @_;
3814 my $ret = $self->dbh_selectall_arrayref($q);
3818 for my $elt (@$ret) {
3819 if ($cur_name and $cur_name ne $elt->[0]) { # order by name, num
3820 push @items, { name => $cur_name, events => $events};
3823 $cur_name = $elt->[0];
3825 { num => $elt->[1], status => $elt->[2],
3826 joberrors => $elt->[3], title => "$elt->[4] jobs", date => $elt->[5]};
3828 push @items, { name => $cur_name, events => $events};
3832 sub get_time_overview
3834 my ($self, $arg) = @_; # want since et age from get_form();
3835 my $type = $arg->{type} || 'day';
3836 if ($type =~ /^(day|week|hour|month)$/) {
3842 my $jobt = $self->get_stat_table();
3843 my $stime1 = $self->{sql}->{"STARTTIME_P" . $type}; # get 1, 2, 3, 4
3844 $stime1 =~ s/Job.StartTime/date/;
3845 my $stime2 = $self->{sql}->{"STARTTIME_" . $type}; # get 2007-01-03, 2007-01-23
3847 my ($limit, $label) = $self->get_limit('since' => $arg->{since},
3848 'age' => $arg->{age});
3849 return ($stime1, $stime2, $limit, $label, $jobt);
3852 # lu ma me je ve sa di
3853 # groupe1 v v x w v v v overview
3854 # |-- s1 v v v v v v v overview_zoom
3855 # |-- s2 v v x v v v v
3856 # `-- s3 v v v w v v v
3857 sub display_overview_zoom
3860 $self->can_do('r_view_stat');
3862 my $arg = $self->get_form(qw/jclient_groups age since type level/);
3863 $arg->{type} = $arg->{type} || 'day';
3865 if (!$arg->{jclient_groups}) {
3866 return $self->error("Can't get client_group selection");
3868 my ($filter2, undef) = $self->get_param(qw/level jobtype/);
3869 my ($stime1, $stime2, $limit, $label, $jobt) = $self->get_time_overview($arg);
3871 my $filter = $self->get_client_filter();
3873 SELECT name, $stime1 AS num,
3874 JobStatus AS value, joberrors, nb_job, date
3876 SELECT $stime2 AS date,
3877 Client.Name AS name,
3878 MAX(severity) AS severity,
3880 SUM(JobErrors) AS joberrors
3882 JOIN client_group_member USING (ClientId)
3883 JOIN client_group USING (client_group_id)
3884 JOIN Client USING (ClientId) $filter
3885 JOIN Status USING (JobStatus)
3886 WHERE client_group_name IN ($arg->{jclient_groups})
3887 AND JobStatus IN ('T', 'W', 'f', 'A', 'e', 'E')
3889 GROUP BY Client.Name, date
3890 ) AS sub JOIN Status USING (severity)
3893 my $items = $self->make_overview_tab($q);
3894 $self->display({label => $label,
3895 type => $arg->{type},
3896 action => "job;since=$arg->{since};level=$arg->{level};type=$arg->{type};age=$arg->{age};client=",
3897 items => $items}, "overview.tpl");
3900 sub display_overview
3903 $self->can_do('r_view_stat');
3905 my $arg = $self->get_form(qw/jclient_groups age since type level/);
3906 $arg->{type} = $arg->{type} || 'day';
3907 my ($filter2, undef) = $self->get_param(qw/client_groups level jobtype/);
3908 my $filter3 = $self->get_client_group_filter();
3909 my ($stime1, $stime2, $filter1, $label, $jobt) = $self->get_time_overview($arg);
3912 SELECT name, $stime1 AS num,
3913 Status.JobStatus AS value, joberrors, nb_job, date
3915 SELECT $stime2 AS date,
3916 client_group_name AS name,
3917 MAX(severity) AS severity,
3919 SUM(JobErrors) AS joberrors
3921 JOIN client_group_member USING (ClientId)
3922 JOIN client_group USING (client_group_id) $filter3
3923 JOIN Status USING (JobStatus)
3924 WHERE Job.JobStatus IN ('T', 'W', 'f', 'A', 'e', 'E')
3926 GROUP BY client_group_name, date
3927 ) AS sub JOIN Status USING (severity)
3930 my $items = $self->make_overview_tab($q);
3931 $self->display({label=>$label,
3932 type => $arg->{type},
3933 action => "overview_zoom;since=$arg->{since};level=$arg->{level};type=$arg->{type};age=$arg->{age};client_group=",
3934 items => $items}, "overview.tpl");
3938 # poolname can be undef
3941 my ($self, $poolname) = @_ ;
3942 $self->can_do('r_view_media');
3947 my $arg = $self->get_form('jmediatypes', 'qmediatypes');
3948 if ($arg->{jmediatypes}) {
3949 $whereW = "WHERE MediaType IN ($arg->{jmediatypes}) ";
3950 $whereA = "AND MediaType IN ($arg->{jmediatypes}) ";
3953 # TODO : afficher les tailles et les dates
3956 SELECT subq.volmax AS volmax,
3957 subq.volnum AS volnum,
3958 subq.voltotal AS voltotal,
3960 Pool.Recycle AS recycle,
3961 Pool.VolRetention AS volretention,
3962 Pool.VolUseDuration AS voluseduration,
3963 Pool.MaxVolJobs AS maxvoljobs,
3964 Pool.MaxVolFiles AS maxvolfiles,
3965 Pool.MaxVolBytes AS maxvolbytes,
3966 subq.PoolId AS PoolId,
3967 subq.MediaType AS mediatype,
3968 $self->{sql}->{CAT_POOL_TYPE} AS uniq
3971 SELECT COALESCE(media_avg_size.volavg,0) * count(Media.MediaId) AS volmax,
3972 count(Media.MediaId) AS volnum,
3973 sum(Media.VolBytes) AS voltotal,
3974 Media.PoolId AS PoolId,
3975 Media.MediaType AS MediaType
3977 LEFT JOIN (SELECT avg(Media.VolBytes) AS volavg,
3978 Media.MediaType AS MediaType
3980 WHERE Media.VolStatus = 'Full'
3981 GROUP BY Media.MediaType
3982 ) AS media_avg_size ON (Media.MediaType = media_avg_size.MediaType)
3983 GROUP BY Media.MediaType, Media.PoolId, media_avg_size.volavg
3985 LEFT JOIN Pool ON (Pool.PoolId = subq.PoolId)
3989 my $all = $self->dbh_selectall_hashref($query, 'uniq') ;
3992 SELECT Pool.Name AS name,
3993 sum(VolBytes) AS size
3994 FROM Media JOIN Pool ON (Media.PoolId = Pool.PoolId)
3995 WHERE Media.VolStatus IN ('Recycled', 'Purged')
3999 my $empty = $self->dbh_selectall_hashref($query, 'name');
4001 foreach my $p (values %$all) {
4002 if ($p->{volmax} > 0) { # mysql returns 0.0000
4003 # we remove Recycled/Purged media from pool usage
4004 if (defined $empty->{$p->{name}}) {
4005 $p->{voltotal} -= $empty->{$p->{name}}->{size};
4007 $p->{poolusage} = sprintf('%.2f', $p->{voltotal} * 100/ $p->{volmax}) ;
4009 $p->{poolusage} = 0;
4013 SELECT VolStatus AS volstatus, count(MediaId) AS nb
4015 WHERE PoolId=$p->{poolid}
4016 AND Media.MediaType = '$p->{mediatype}'
4020 my $content = $self->dbh_selectall_hashref($query, 'volstatus');
4021 foreach my $t (values %$content) {
4022 $p->{"nb_" . $t->{volstatus}} = $t->{nb} ;
4027 $self->display({ ID => $cur_id++,
4028 MediaType => $arg->{qmediatypes}, # [ { name => type1 } , { name => type2 } ]
4029 Pools => [ values %$all ]},
4030 "display_pool.tpl");
4033 # With this function, we get an estimation of next jobfiles/jobbytes count
4034 sub get_estimate_query
4036 my ($self, $mode, $job, $level) = @_;
4037 # get security filter
4038 my $filter = $self->get_client_filter();
4042 if ($self->dbh_is_mysql()) { # mysql doesn't have statistics functions
4044 SELECT jobname AS jobname,
4045 0.1 AS corr_jobbytes, AVG(jobbytes) AS jobbytes,
4046 COUNT(1) AS nb_jobbytes ";
4048 # postgresql have functions that permit to handle lineal regression
4050 # REGR_SLOPE(Y,X) = get x
4051 # REGR_INTERCEPT(Y,X) = get b
4052 # and we need y when x=now()
4053 # CORR gives the correlation
4054 # (TODO: display progress bar only if CORR > 0.8)
4057 SELECT temp.jobname AS jobname,
4058 COALESCE(CORR(jobbytes,jobtdate),0) AS corr_jobbytes,
4059 ($now*REGR_SLOPE(jobbytes,jobtdate)
4060 + REGR_INTERCEPT(jobbytes,jobtdate)) AS jobbytes,
4061 COUNT(1) AS nb_jobbytes ";
4063 # if it's a differential, we need to compare since the last full
4065 # F D D D F D D D F I I I I D I I I
4067 # | # # # # # # | # #
4068 # | # # # # # # # # | # # # # # # # # #
4069 # +----------------- +-------------------
4071 if ($level eq 'D') {
4073 AND Job.StartTime > (
4076 WHERE Job.Name = '$job'
4078 AND Job.JobStatus IN ('T', 'W')
4079 ORDER BY Job.StartTime DESC LIMIT 1
4086 SELECT Job.Name AS jobname,
4087 JobBytes AS jobbytes,
4088 JobTDate AS jobtdate
4089 FROM Job INNER JOIN Client USING (ClientId) $filter
4090 WHERE Job.Name = '$job'
4091 AND Job.Level = '$level'
4092 AND Job.JobStatus IN ('T', 'W')
4094 ORDER BY StartTime DESC
4096 ) AS temp GROUP BY temp.jobname
4099 if ($mode eq 'jobfiles') {
4100 $query =~ s/jobbytes/jobfiles/g;
4101 $query =~ s/JobBytes/JobFiles/g;
4106 sub display_running_job
4109 return if $self->cant_do('r_view_running_job');
4111 my $arg = $self->get_form('jobid');
4113 return $self->error("Can't get jobid") unless ($arg->{jobid});
4115 # get security filter
4116 my $filter = $self->get_client_filter();
4119 SELECT Client.Name AS name, Job.Name AS jobname,
4120 Job.Level AS level, Type AS type, JobStatus AS jobstatus
4121 FROM Job INNER JOIN Client USING (ClientId) $filter
4122 WHERE Job.JobId = $arg->{jobid}
4125 my $row = $self->dbh_selectrow_hashref($query);
4128 $arg->{client} = $row->{name};
4130 return $self->error("Can't get client");
4133 my $status = $row->{jobstatus};
4135 if ($status =~ /[TfAaEWD]/) {
4136 $self->display_job_zoom($arg->{jobid});
4137 $self->get_job_log();
4141 if ($row->{type} eq 'B') {
4142 # for jobfiles, we use only last Full backup. status client= returns
4143 # all files that have been checked
4144 my $query1 = $self->get_estimate_query('jobfiles', $row->{jobname}, 'F');
4145 my $query2 = $self->get_estimate_query('jobbytes',
4146 $row->{jobname}, $row->{level});
4148 # LEFT JOIN because we always have a previous Full
4150 SELECT corr_jobbytes, jobbytes, corr_jobfiles, jobfiles
4151 FROM ($query1) AS A LEFT JOIN ($query2) AS B USING (jobname)
4153 $row = $self->dbh_selectrow_hashref($query);
4156 $row->{jobbytes} = $row->{jobfiles} = 0;
4159 if ($status =~ /[RBSmMsjlL]/) {
4160 my $cli = new Bweb::Client(name => $arg->{client});
4161 $cli->display_running_job($self, $arg->{jobid}, $row);
4163 if ($arg->{jobid}) {
4164 $self->get_job_log();
4168 sub display_running_jobs
4170 my ($self, $display_action) = @_;
4171 return if $self->cant_do('r_view_running_job');
4173 # get security filter
4174 my $filter = $self->get_client_filter();
4177 SELECT Job.JobId AS jobid,
4178 Job.Name AS jobname,
4180 Job.StartTime AS starttime,
4181 Job.JobFiles AS jobfiles,
4182 Job.JobBytes AS jobbytes,
4183 Job.JobStatus AS jobstatus,
4184 $btime - Job.JobTDate AS duration,
4185 Client.Name AS clientname
4186 FROM Job INNER JOIN Client USING (ClientId) $filter
4188 JobStatus IN ('C','R','B','e','D','F','S','m','M','s','j','c','d','t','p')
4190 my $all = $self->dbh_selectall_hashref($query, 'jobid') ;
4192 $self->display({ ID => $cur_id++,
4193 display_action => $display_action,
4194 Jobs => [ values %$all ]},
4195 "running_job.tpl") ;
4198 sub display_group_stats
4201 my $arg = $self->get_form('age', 'since');
4202 return if $self->cant_do('r_view_stat');
4204 my $filter = $self->get_client_group_filter();
4206 my $jobt = $self->get_stat_table();
4208 my ($limit, $label) = $self->get_limit(%$arg);
4209 my ($where, undef) = $self->get_param('client_groups', 'level');
4212 SELECT client_group_name AS name, nb_byte, nb_file, nb_job, nb_err, nb_resto
4215 SELECT sum(JobBytes) AS nb_byte,
4216 sum(JobFiles) AS nb_file,
4217 count(1) AS nb_job, client_group_name
4218 FROM $jobt AS Job JOIN client_group_member USING (ClientId)
4219 JOIN client_group USING (client_group_id) $filter
4220 WHERE JobStatus IN ('T', 'W') AND Type IN ('M', 'B', 'g')
4222 GROUP BY client_group_name ORDER BY client_group_name
4226 SELECT count(1) AS nb_err, client_group_name
4227 FROM $jobt AS Job JOIN client_group_member USING (ClientId)
4228 JOIN client_group USING (client_group_id)
4229 WHERE JobStatus IN ('E','e','f','A') AND Type = 'B'
4231 GROUP BY client_group_name ORDER BY client_group_name
4233 ) AS T3 USING (client_group_name) LEFT JOIN (
4235 SELECT count(1) AS nb_resto, client_group_name
4236 FROM $jobt AS Job JOIN client_group_member USING (ClientId)
4237 JOIN client_group USING (client_group_id)
4238 WHERE JobStatus IN ('T','W') AND Type = 'R'
4240 GROUP BY client_group_name ORDER BY client_group_name
4242 ) AS T2 USING (client_group_name)
4244 $self->debug($query);
4245 my $all = $self->dbh_selectall_hashref($query, 'name') ;
4248 $self->display({ ID => $cur_id++,
4250 Stats => [ values %$all ]},
4251 "display_stats.tpl") ;
4254 # return the autochanger list to update
4258 $self->can_do('r_media_mgnt');
4261 my $arg = $self->get_form('jmedias');
4263 unless ($arg->{jmedias}) {
4264 return $self->error("Can't get media selection");
4268 SELECT Media.VolumeName AS volumename,
4269 Storage.Name AS storage,
4270 Location.Location AS location,
4272 FROM Media INNER JOIN Storage ON (Media.StorageId = Storage.StorageId)
4273 LEFT JOIN Location ON (Media.LocationId = Location.LocationId)
4274 WHERE Media.VolumeName IN ($arg->{jmedias})
4275 AND Media.InChanger = 1
4278 my $all = $self->dbh_selectall_hashref($query, 'volumename');
4280 foreach my $vol (values %$all) {
4281 my $a = $self->ach_get($vol->{location});
4283 $ret{$vol->{location}} = 1;
4285 unless ($a->{have_status}) {
4287 $a->{have_status} = 1;
4290 print "eject $vol->{volumename} from $vol->{storage} : ";
4291 if ($a->send_to_io($vol->{slot})) {
4292 print "<img src='/bweb/T.png' alt='ok'><br/>";
4294 print "<img src='/bweb/E.png' alt='err'><br/>";
4304 my ($to, $subject, $content) = (CGI::param('email'),
4305 CGI::param('subject'),
4306 CGI::param('content'));
4307 $to =~ s/[^\w\d\.\@<>,]//;
4308 $subject =~ s/[^\w\d\.\[\]]/ /;
4310 open(MAIL, "|mail -s '$subject' '$to'") ;
4311 print MAIL $content;
4321 my $arg = $self->get_form('jobid', 'client');
4323 print CGI::header('text/brestore');
4324 print "jobid=$arg->{jobid}\n" if ($arg->{jobid});
4325 print "client=$arg->{client}\n" if ($arg->{client});
4326 print "\n\nYou have to assign this mime type with /usr/bin/brestore.pl\n";
4330 # TODO : move this to Bweb::Autochanger ?
4331 # TODO : make this internal to not eject tape ?
4337 $self->can_do('r_view_job');
4339 my $arg = $self->get_form(qw/limit offset jobid/);
4340 if (!$arg->{jobid}) {
4341 return $self->error("Can't get jobid");
4345 title => "Content of JobId $arg->{jobid} ",
4346 name => "list files jobid=$arg->{jobid}",
4351 my $b = new Bconsole(pref => $self->{info},timeout => 60);
4354 $b->send_cmd("list files jobid=$arg->{jobid} limit=$arg->{limit}"); # TODO: add offset
4363 my ($self, $name) = @_;
4366 return $self->error("Can't get your autochanger name ach");
4369 unless ($self->{info}->{ach_list}) {
4370 return $self->error("Could not find any autochanger");
4373 my $a = $self->{info}->{ach_list}->{$name};
4376 $self->error("Can't get your autochanger $name from your ach_list");
4381 $a->{debug} = $self->{debug};
4388 my ($self, $ach) = @_;
4389 $self->can_do('r_configure');
4391 $self->{info}->{ach_list}->{$ach->{name}} = $ach;
4393 $self->{info}->save();
4401 $self->can_do('r_configure');
4403 my $arg = $self->get_form('ach');
4405 or !$self->{info}->{ach_list}
4406 or !$self->{info}->{ach_list}->{$arg->{ach}})
4408 return $self->error("Can't get autochanger name");
4411 my $ach = $self->{info}->{ach_list}->{$arg->{ach}};
4415 [ map { { name => $_, index => $i++ } } @{$ach->{drive_name}} ] ;
4417 my $b = $self->get_bconsole();
4419 my @storages = $b->list_storage() ;
4421 $ach->{devices} = [ map { { name => $_ } } @storages ];
4423 $self->display($ach, "ach_add.tpl");
4424 delete $ach->{drives};
4425 delete $ach->{devices};
4432 $self->can_do('r_configure');
4434 my $arg = $self->get_form('ach');
4437 or !$self->{info}->{ach_list}
4438 or !$self->{info}->{ach_list}->{$arg->{ach}})
4440 return $self->error("Can't get autochanger name");
4443 delete $self->{info}->{ach_list}->{$arg->{ach}} ;
4445 $self->{info}->save();
4446 $self->{info}->view();
4452 $self->can_do('r_configure');
4454 my $arg = $self->get_form('ach', 'mtxcmd', 'device', 'precmd');
4456 my $b = $self->get_bconsole();
4457 my @storages = $b->list_storage() ;
4459 unless ($arg->{ach}) {
4460 $arg->{devices} = [ map { { name => $_ } } @storages ];
4461 return $self->display($arg, "ach_add.tpl");
4465 foreach my $drive (CGI::param('drives'))
4467 unless (grep(/^$drive$/,@storages)) {
4468 return $self->error("Can't find $drive in storage list");
4471 my $index = CGI::param("index_$drive");
4472 unless (defined $index and $index =~ /^(\d+)$/) {
4473 return $self->error("Can't get $drive index");
4476 $drives[$index] = $drive;
4480 return $self->error("Can't get drives from Autochanger");
4483 my $a = new Bweb::Autochanger(name => $arg->{ach},
4484 precmd => $arg->{precmd},
4485 drive_name => \@drives,
4486 device => $arg->{device},
4487 mtxcmd => $arg->{mtxcmd});
4489 $self->ach_register($a) ;
4491 $self->{info}->view();
4497 $self->can_do('r_delete_job');
4499 my $arg = $self->get_form('jobid');
4501 if ($arg->{jobid}) {
4502 my $b = $self->get_bconsole();
4503 my $ret = $b->send_cmd("delete jobid=\"$arg->{jobid}\"");
4507 title => "Delete a job ",
4508 name => "delete jobid=$arg->{jobid}",
4517 $self->can_do('r_media_mgnt');
4519 my $arg = $self->get_form(qw/media volstatus inchanger pool
4520 slot volretention voluseduration
4521 maxvoljobs maxvolfiles maxvolbytes
4522 qcomment poolrecycle enabled
4525 unless ($arg->{media}) {
4526 return $self->error("Can't find media selection");
4529 my $update = "update volume=$arg->{media} ";
4531 if ($arg->{volstatus}) {
4532 $update .= " volstatus=$arg->{volstatus} ";
4535 if ($arg->{inchanger}) {
4536 $update .= " inchanger=yes " ;
4538 $update .= " slot=$arg->{slot} ";
4541 $update .= " slot=0 inchanger=no ";
4544 if ($arg->{enabled}) {
4545 $update .= " enabled=$arg->{enabled} ";
4549 $update .= " pool=$arg->{pool} " ;
4552 if (defined $arg->{volretention}) {
4553 $update .= " volretention=\"$arg->{volretention}\" " ;
4556 if (defined $arg->{voluseduration}) {
4557 $update .= " voluse=\"$arg->{voluseduration}\" " ;
4560 if (defined $arg->{maxvoljobs}) {
4561 $update .= " maxvoljobs=$arg->{maxvoljobs} " ;
4564 if (defined $arg->{maxvolfiles}) {
4565 $update .= " maxvolfiles=$arg->{maxvolfiles} " ;
4568 if (defined $arg->{maxvolbytes}) {
4569 $update .= " maxvolbytes=$arg->{maxvolbytes} " ;
4572 if (defined $arg->{poolrecycle}) {
4573 $update .= " recyclepool=\"$arg->{poolrecycle}\" " ;
4575 $update .= " recyclepool= " ;
4578 my $b = $self->get_bconsole();
4581 content => $b->send_cmd($update),
4582 title => "Update a volume ",
4590 my $media = $self->dbh_quote($arg->{media});
4592 my $loc = CGI::param('location') || '';
4594 $loc = $self->dbh_quote($loc); # is checked by db
4595 push @q, "LocationId=(SELECT LocationId FROM Location WHERE Location=$loc)";
4597 if (!$arg->{qcomment}) {
4598 $arg->{qcomment} = "''";
4600 push @q, "Comment=$arg->{qcomment}";
4605 SET " . join (',', @q) . "
4606 WHERE Media.VolumeName = $media
4608 $self->dbh_do($query);
4610 $self->update_media();
4616 $self->can_do('r_autochanger_mgnt');
4618 my $ach = CGI::param('ach') ;
4619 $ach = $self->ach_get($ach);
4621 return $self->error("Bad autochanger name");
4625 title => "Scanning autochanger content ",
4626 name => "update slots",
4630 my $b = new Bconsole(pref => $self->{info},timeout => 60,log_stdout => 1);
4631 $b->update_slots($ach->{name});
4641 $self->can_do('r_view_log');
4643 my $arg = $self->get_form('jobid', 'limit', 'offset');
4644 unless ($arg->{jobid}) {
4645 return $self->error("Can't get jobid");
4648 if ($arg->{limit} == 100) {
4649 $arg->{limit} = 1000;
4651 # get security filter
4652 my $filter = $self->get_client_filter();
4655 SELECT Job.Name as name, Client.Name as clientname
4656 FROM Job INNER JOIN Client USING (ClientId) $filter
4657 WHERE JobId = $arg->{jobid}
4660 my $row = $self->dbh_selectrow_hashref($query);
4663 return $self->error("Can't find $arg->{jobid} in catalog");
4666 # display only Error and Warning messages
4668 if (CGI::param('error')) {
4669 $filter = " AND LogText $self->{sql}->{MATCH} 'Error|Warning|ERR=' ";
4673 if (CGI::param('time') || $self->{info}->{display_log_time}) {
4674 $logtext = $self->dbh_strcat('Time', " ' ' ", 'LogText');
4676 $logtext = 'LogText';
4680 SELECT count(1) AS nbline,
4681 GROUP_CONCAT($logtext $self->{sql}->{CONCAT_SEP}) AS logtxt, id
4683 SELECT 1 AS id, Time, LogText
4685 WHERE ( Log.JobId = $arg->{jobid}
4687 AND Time >= (SELECT StartTime FROM Job WHERE JobId=$arg->{jobid})
4688 AND Time <= (SELECT COALESCE(EndTime,$self->{sql}->{NOW})
4689 FROM Job WHERE JobId=$arg->{jobid})
4693 OFFSET $arg->{offset}
4699 my $log = $self->dbh_selectrow_hashref($query);
4701 return $self->error("Can't get log for jobid $arg->{jobid}, check that
4702 your 'Messages' resources include 'catalog = all' and you loaded Bweb SQL
4703 functions in your Catalog.");
4705 $log->{logtxt} =~ s/(\0|\\,)//g;
4706 $self->display({ lines=> $log->{logtxt},
4707 nbline => $log->{nbline},
4708 jobid => $arg->{jobid},
4709 name => $row->{name},
4710 client => $row->{clientname},
4711 offset => $arg->{offset},
4712 limit => $arg->{limit},
4713 }, 'display_log.tpl');
4716 sub cancel_future_job
4719 $self->can_do('r_cancel_job');
4721 my $arg = $self->get_form(qw/job pool level client when/);
4723 if ( !$arg->{job} or !$arg->{pool} or !$arg->{level}
4724 or !$arg->{client} or !$arg->{when})
4726 return $self->error("Can't get enough information to mark this job as canceled");
4729 $arg->{level} =~ s/^(.).+/$1/; # we keep the first letter
4730 my $jobtable = $self->{info}->{stat_job_table} || 'JobHisto';
4732 if ($jobtable =~ /^Job$/i) {
4733 return $self->error("Can add records only in history table");
4735 my $jname = "$arg->{job}.$arg->{when}";
4738 my $found = $self->dbh_selectrow_hashref("
4743 AND Name = '$arg->{job}'
4746 return $self->error("$jname is already in history table");
4750 INSERT INTO $jobtable
4751 (JobId, Name, Job, Type, Level, JobStatus, SchedTime, StartTime, EndTime,
4752 RealEndTime, ClientId, PoolId)
4754 (0, '$arg->{job}', '$jname', 'B', '$arg->{level}', 'A',
4755 '$arg->{when}', '$arg->{when}', '$arg->{when}', '$arg->{when}',
4756 (SELECT ClientId FROM Client WHERE Name = '$arg->{client}'),
4757 (SELECT PoolId FROM Pool WHERE Name = '$arg->{pool}')
4760 $self->display({ Filter => "Dummy record for $jname",
4764 client => $arg->{client},
4765 jobname => $arg->{job},
4766 pool => $arg->{pool},
4767 level => $arg->{level},
4768 starttime => $arg->{when},
4782 $self->can_do('r_media_mgnt');
4783 my $arg = $self->get_form('storage', 'pool', 'nb', 'media', 'offset');
4784 my $b = $self->get_bconsole();
4786 if (!$arg->{storage} || !$arg->{pool} || not defined $arg->{nb} || !$arg->{media} || !$arg->{offset}) {
4787 CGI::param(offset => 0);
4788 $arg = $self->get_form('db_pools');
4789 $arg->{storage} = [ map { { name => $_ } }$b->list_storage()];
4790 $self->display($arg, 'add_media.tpl');
4794 $b->send("add pool=\"$arg->{pool}\" storage=\"$arg->{storage}\"\n");
4795 if ($arg->{nb} > 0) {
4796 $arg->{offset} = $arg->{offset}?$arg->{offset}:1;
4797 $b->send("$arg->{nb}\n");
4798 $b->send("$arg->{media}\n");
4799 $b->send("$arg->{offset}\n");
4803 $b->send("$arg->{media}\n");
4808 #$b->expect_it('-re','^[*]');
4810 CGI::param('media', '');
4811 CGI::param('re_media', $arg->{media});
4812 $self->display_media();
4818 $self->can_do('r_autochanger_mgnt');
4820 my $arg = $self->get_form('ach', 'slots', 'drive', 'pool');
4822 unless ($arg->{ach}) {
4823 return $self->error("Can't find autochanger name");
4826 my $a = $self->ach_get($arg->{ach});
4828 return $self->error("Can't find autochanger name in configuration");
4831 my $storage = $a->get_drive_name($arg->{drive});
4833 return $self->error("Can't get your drive name");
4839 if ($arg->{slots}) {
4840 $slots = join(",", @{ $arg->{slots} });
4841 $slots_sql = " AND Slot IN ($slots) ";
4842 $t += 60*scalar( @{ $arg->{slots} }) ;
4844 my $pool = $arg->{pool} || 'Scratch';
4845 my $b = new Bconsole(pref => $self->{info}, timeout => $t,log_stdout => 1);
4846 print "<h1>This command can take long time, be patient...</h1>";
4848 $b->label_barcodes(storage => $storage,
4849 drive => $arg->{drive},
4857 SET LocationId = (SELECT LocationId
4859 WHERE Location = '$arg->{ach}')
4861 WHERE (LocationId = 0 OR LocationId IS NULL)
4870 $self->can_do('r_purge');
4872 my @volume = CGI::param('media');
4875 return $self->error("Can't get media selection");
4878 my $b = $self->get_bconsole(timeout => 60);
4880 foreach my $v (@volume) {
4882 content => $b->purge_volume($v),
4883 title => "Purge media",
4884 name => "purge volume=$v",
4894 $self->can_do('r_prune');
4896 my @volume = CGI::param('media');
4898 return $self->error("Can't get media selection");
4901 my $b = $self->get_bconsole(timeout => 60);
4903 foreach my $v (@volume) {
4905 content => $b->prune_volume($v),
4906 title => "Prune volume",
4907 name => "prune volume=$v",
4917 $self->can_do('r_cancel_job');
4919 my $arg = $self->get_form('jobid');
4920 unless ($arg->{jobid}) {
4921 return $self->error("Can't get jobid");
4924 my $b = $self->get_bconsole();
4926 content => $b->cancel($arg->{jobid}),
4927 title => "Cancel job",
4928 name => "cancel jobid=$arg->{jobid}",
4935 # Warning, we display current fileset
4938 my $arg = $self->get_form('fileset');
4940 if ($arg->{fileset}) {
4941 my $b = $self->get_bconsole();
4942 my $ret = $b->get_fileset($arg->{fileset});
4943 $self->display({ fileset => $arg->{fileset},
4945 }, "fileset_view.tpl");
4947 $self->error("Can't get fileset name");
4951 sub director_show_sched
4954 $self->can_do('r_view_job');
4955 my $arg = $self->get_form('days');
4957 my $b = $self->get_bconsole();
4958 my $ret = $b->director_get_sched( $arg->{days} );
4963 }, "scheduled_job.tpl");
4966 sub enable_disable_job
4968 my ($self, $what) = @_ ;
4969 $self->can_do('r_run_job');
4971 my $arg = $self->get_form('job');
4973 return $self->error("Can't find job name");
4976 my $b = $self->get_bconsole();
4986 content => $b->send_cmd("$cmd job=\"$arg->{job}\""),
4987 title => "$cmd $arg->{job}",
4988 name => "$cmd job=\"$arg->{job}\"",
4995 my ($self, $lang) = @_;
4996 $self->{current_lang} = $lang;
5001 my ($self, @opts) = @_;
5002 return new Bconsole(pref => $self->{info}, @opts);
5008 $self->can_do('r_storage_mgnt');
5009 my $arg = $self->get_form(qw/storage storage_cmd drive slot/);
5010 my $b = $self->get_bconsole();
5012 if ($arg->{storage} and $arg->{storage_cmd}) {
5013 my $cmd = "$arg->{storage_cmd} storage=\"$arg->{storage}\" drive=$arg->{drive} slot=$arg->{slot}";
5014 my $ret = $b->send_cmd($cmd);
5018 title => "Storage ",
5023 my $storages= [ map { { name => $_ } } $b->list_storage()];
5024 $self->display({ storage => $storages}, "cmd_storage.tpl");
5031 $self->can_do('r_run_job');
5033 my $b = $self->get_bconsole();
5035 my $joblist = [ map { { name => $_ } } $b->list_backup() ];
5037 $self->display({ Jobs => $joblist }, "run_job.tpl");
5042 my ($self, $ouput) = @_;
5045 $self->debug($ouput);
5046 foreach my $l (split(/\r?\n/, $ouput)) {
5048 if ($l =~ /(\w+): name=([\w\d\.\s-]+?)(\s+\w+=.+)?$/) {
5054 if (my @l = $l =~ /(\w+)=([\w\d*]+)/g) {
5060 foreach my $k (keys %arg) {
5061 $lowcase{lc($k)} = $arg{$k} ;
5063 $self->debug(\%lowcase);
5070 $self->can_do('r_run_job');
5072 my $b = $self->get_bconsole();
5073 my $arg = $self->get_form(qw/pool level client fileset storage media job comment/);
5076 return $self->error("Can't get job name");
5079 # we take informations from director, and we overwrite with user wish
5080 my $info = $b->send_cmd("show job=\"$arg->{job}\"");
5081 my $attr = $self->run_parse_job($info);
5083 if (!$arg->{pool} and $arg->{media}) {
5084 my $r = $self->dbh_selectrow_hashref("
5085 SELECT Pool.Name AS name
5086 FROM Media JOIN Pool USING (PoolId)
5087 WHERE Media.VolumeName = '$arg->{media}'
5088 AND Pool.Name != 'Scratch'
5091 $arg->{pool} = $r->{name};
5095 my %job_opt = (%$attr, %$arg);
5097 my $jobs = [ map {{ name => $_ }} $b->list_backup() ];
5099 my $pools = [ map { { name => $_ } } $b->list_pool() ];
5100 my $clients = [ map { { name => $_ } }$b->list_client()];
5101 my $filesets= [ map { { name => $_ } }$b->list_fileset() ];
5102 my $storages= [ map { { name => $_ } }$b->list_storage()];
5107 clients => $clients,
5108 filesets => $filesets,
5109 storages => $storages,
5111 }, "run_job_mod.tpl");
5117 $self->can_do('r_run_job');
5119 my $b = $self->get_bconsole();
5121 my $jobs = [ map {{ name => $_ }} $b->list_backup() ];
5123 return $self->error("Bconsole returns an error, check your setup. ERR=$b->{error}");
5133 $self->can_do('r_run_job');
5135 my $b = $self->get_bconsole();
5137 # TODO: check input (don't use pool, level)
5139 my $arg = $self->get_form(qw/pool level client priority when
5140 fileset job storage comment/);
5142 return $self->error("Can't get your job name");
5145 my $jobid = $b->run(job => $arg->{job},
5146 client => $arg->{client},
5147 priority => $arg->{priority},
5148 level => $arg->{level},
5149 storage => $arg->{storage},
5150 pool => $arg->{pool},
5151 fileset => $arg->{fileset},
5152 when => $arg->{when},
5153 comment => $arg->{comment}
5158 print "<br>You can follow job (jobid=$jobid) execution <a href='?action=dsp_cur_job;client=$arg->{client};jobid=$jobid'> here </a><script type='text/javascript' language='JavaScript'>setTimeout(function() { window.location='?action=dsp_cur_job;client=$arg->{client};jobid=$jobid'},2000);</script>";
5161 sub display_next_job
5165 my $arg = $self->get_form(qw/job begin end/);
5167 return $self->error("Can't get job name");
5170 my $b = $self->get_bconsole();
5172 my $job = $b->send_cmd("show job=\"$arg->{job}\"");
5173 my $attr = $self->run_parse_job($job);
5175 if (!$attr->{schedule}) {
5176 return $self->error("Can't get $arg->{job} schedule");
5178 my $jpool=$attr->{pool} || '';
5180 my $sched = new Bweb::Sched(bconsole => $b, name => $attr->{schedule},
5181 begin => $arg->{begin}, end => $arg->{end});
5183 my $ss = $sched->get_scheds($attr->{schedule});
5186 foreach my $s (@$ss) {
5187 my $level = $sched->get_level($s);
5188 my $pool = $sched->get_pool($s) || $jpool;
5189 my $evt = $sched->get_event($s);
5190 push @ret, map { "$_ : $pool ($level)\n" } @$evt;
5193 print "<b>$arg->{job}:</b><pre>", sort @ret, "</pre><br>";
5196 # permit to verify for higher level backup
5197 # we attempt a Increment, we made a Full, that ok
5198 # TODO: Pool may have change
5199 sub get_higher_level
5201 my ($self, $level) = @_;
5202 if ($level eq 'F') {
5204 } elsif ($level eq 'D') {
5206 } elsif ($level eq 'I') {
5207 return "'F', 'D', 'I'";
5212 # check jobs against their schedule
5215 my ($self, $sched, $schedname, $job, $job_pool, $client, $type) = @_;
5216 return undef if (!$self->can_view_client($client));
5218 $self->debug("checking $job, $job_pool, $client, $type, $schedname");
5220 my $sch = $sched->get_scheds($schedname);
5221 return undef if (!$sch);
5224 foreach my $s (@$sch) {
5226 if ($type eq 'B') { # we take the pool only for backup job
5227 $pool = $sched->get_pool($s) || $job_pool;
5229 my $level = $sched->get_level($s);
5230 my ($l) = ($level =~ m/^(.)/); # we keep the first letter
5231 $l = $self->get_higher_level($l);
5232 my $evts = $sched->get_event($s);
5233 my $end = $sched->{end}; # this backup must have start before the next one
5234 foreach my $evt (reverse @$evts) {
5235 my $all = $self->dbh_selectrow_hashref("
5238 JOIN Client USING (ClientId) LEFT JOIN Pool USING (PoolId)
5239 WHERE Job.StartTime >= '$evt'
5240 AND Job.StartTime < '$end'
5241 AND Job.Name = '$job'
5242 AND Job.Type = '$type'
5243 AND Job.JobStatus IN ('T', 'W')
5244 AND Job.Level IN ($l)
5245 " . ($pool?" AND Pool.Name = '$pool' ":'') . "
5246 AND Client.Name = '$client'
5250 $self->debug("found job record for $job on $client");
5252 push @{$self->{tmp}}, {date => $evt, level => $level,
5253 type => 'Backup', name => $job,
5254 pool => $pool, volume => $pool,
5262 sub display_missing_job
5265 my $arg = $self->get_form(qw/begin end age/);
5267 if (!$arg->{begin}) { # TODO: change this
5268 $arg->{begin} = strftime('%F %T', localtime($btime - $arg->{age}));
5271 $arg->{end} = strftime('%F %T', localtime($btime));
5273 $self->{tmp} = []; # check_job use this for result
5275 my $bconsole = $self->get_bconsole();
5277 my $sched = new Bweb::Sched(bconsole => $bconsole,
5278 begin => $arg->{begin},
5279 end => $arg->{end});
5280 $self->debug($sched);
5281 my $job = $bconsole->send_cmd("show job");
5282 my ($jname, $jsched, $jclient, $jpool, $jtype);
5283 foreach my $j (split(/\r?\n/, $job)) {
5284 if ($j =~ /Job: name=([\w\d\-]+?) JobType=(\d+)/i) {
5285 if ($jname and $jsched) {
5286 $self->check_job($sched, $jsched, $jname,
5287 $jpool, $jclient, $jtype);
5291 $jclient = $jpool = $jsched = undef;
5292 } elsif ($j =~ /Client: name=(.+?) address=/i) {
5294 } elsif ($j =~ /Pool: name=([\w\d\-]+) PoolType=/i) {
5296 } elsif ($j =~ /Schedule: name=([\w\d\-]+)/i) {
5302 title => "Missing Jobs (from $arg->{begin} to $arg->{end})",
5303 list => $self->{tmp},
5304 wiki_url => $self->{info}->{wiki_url},
5306 }, "scheduled_job.tpl");
5308 delete $self->{tmp};