4 # burn manager for bacula CD image files
6 # Copyright (C) 2004 Kern Sibbald
8 # Thu Dec 09 2004 D. Scott Barninger <barninger at fairfieldcomputers.com>
9 # ASSIGNMENT OF COPYRIGHT
10 # FOR VALUE RECEIVED, D. Scott Barninger hereby sells, transfers and
11 # assigns unto Kern Sibbald, his successors, assigns and personal representatives,
12 # all right, title and interest in and to the copyright in this software.
13 # D. Scott Barninger warrants good title to said copyright, that it is
14 # free of all liens, encumbrances or any known claims against said copyright.
16 # This program is free software; you can redistribute it and/or
17 # modify it under the terms of the GNU General Public
18 # License version 2 as published by the Free Software Foundation.
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
23 # General Public License for more details.
25 # You should have received a copy of the GNU General Public
26 # License along with this program; if not, write to the Free
27 # Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
31 my $VERSION = "0.2.7";
33 require 5.000; use strict 'vars', 'refs', 'subs';
36 #-------------------------------------------------------------------#
37 # Program Configuration
39 ## web server configuration
41 # web server path to program from server root
42 my $prog_name = "/cgi-bin/bimagemgr.pl";
45 my $http_host="localhost";
47 # path to graphics from document root
48 my $logo_graphic = "/bimagemgr.gif";
49 my $spacer_graphic = "/clearpixel.gif";
50 my $burn_graphic = "/cdrom_spins.gif";
53 ## database configuration
56 my $database = "bacula";
67 # database driver selection - uncomment one set
69 my $db_driver = "mysql";
70 my $db_name_param = "database";
71 my $catalog_dump = "mysqldump --host=$host --user=$user --password=$password $database";
73 # my $db_driver = "Pg";
74 # my $db_name_param = "dbname";
75 # my $catalog_dump = "pg_dump --host=$host --username=$user --password=$password $database";
78 # path to backup files
79 my $image_path = "/mnt/backup/backup";
81 ## path to cdrecord and burner settings
82 my $cdrecord = "/usr/bin/cdrecord";
83 my $mkisofs = "/usr/bin/mkisofs";
84 my $cdburner = "1,0,0";
85 my $burner_speed = "40";
86 # burnfree option - uncomment one
87 #my $burnfree = "driveropts=noburnfree"; # no buffer underrun protection
88 my $burnfree = "driveropts=burnfree"; # with buffer underrun
92 my $tempfile="temp.html";
93 my $tempfile_path="/var/www/html/temp.html";
94 my $working_dir="/var/tmp";
96 # copyright info for page footer
97 my $copyright = "Copyright © 2004 The Bacula Team";
98 #-------------------------------------------------------------------#
100 my %input = &getcgivars;
101 my $action = $input{'action'};
102 my $vol = $input{'vol'};
107 #-------------------------------------------------------------------#
109 # Description: check requested action and call appropriate subroutine
113 # set default action & department
114 if (!$action) {$action = "display"};
116 if ($action eq "display") {
119 elsif ($action eq "burn") {
122 elsif ($action eq "reset") {
125 elsif ($action eq "version") {
127 print "<div align=\"center\">";
128 print "<br>Bacula CD Image Manager version $VERSION<br><br>";
129 print "Copyright © 2004 D. Scott Barninger<br>";
130 print "Licensed under the GNU GPL version 2.0";
132 print "</body></html>";
135 &HTMLdie("Unknown action $action","So sorry Kimosabe..");
140 #-------------------------------------------------------------------#
142 # Description: main page display routine
145 my ($MediaId,$VolumeName,$LastWritten,$VolWrites,$VolStatus,$data);
150 # connect to database
151 my $dbh = DBI->connect("DBI:$db_driver:$db_name_param=$database;host=$host","$user","$password",
152 {'RaiseError' => 1}) || &HTMLdie("Unable to connect to database.");
153 my $sth = $dbh->prepare("SELECT Media.VolumeName,Media.LastWritten,CDImages.LastBurn,
154 Media.VolWrites,Media.VolStatus
156 WHERE CDImages.MediaId = Media.MediaId");
158 print "<div align=\"center\">";
159 print "<table width=\"80%\" border=\"0\">";
161 print "<td colspan=\"6\"><b>";
162 print "<p>Backup files which need to be committed to CDR disk since their last write date are shown below with a Burn button. ";
163 print "Place a blank CDR disk in the drive and click the Burn button for the volume you wish to burn to disk.</p>";
164 print "<p>When CD recording is complete the popup window will display the output of cdrecord. ";
165 print "A successful burn is indicated by the last line showing that all bytes were successfully recorded.</p>";
166 print "<p>After the popup window indicates that the burn is complete, close the popup and <a href=\"$prog_name\">refresh this window</a>. ";
167 print "If the burn is not successful click the Reset link under the last burn date to restore the Burn button and <a href=\"$prog_name\">refresh this window</a>.</p>";
168 print "<p>To burn a copy of your catalog click the catalog Burn button at the bottom of the page. ";
169 print "Up to date copies of your catalog and all backup volumes ensure that your bacula server can be rebuilt in the event of a catastrophe.</p>";
173 print "<td colspan=\"6\"> </td>";
176 print "<table width=\"80%\" border=\"1\">";
178 print "<td align=\"center\" colspan=\"6\"><h3>Current Volume Information</h3>Make sure the backup file path $image_path is mounted.</td>";
181 print "<td align=\"center\"><b>Volume Name</b></td>";
182 print "<td align=\"center\"><b>Last Written</b></td>";
183 print "<td align=\"center\"><b>Last Burn</b></td>";
184 print "<td align=\"center\"><b>Writes</b></td>";
185 print "<td align=\"center\"><b>Status</b></td>";
186 print "<td align=\"center\"> </td>";
190 while ($data = $sth->fetchrow_arrayref) {
192 print "<td align=\"center\">$$data[0]</td>";
193 print "<td align=\"center\">$$data[1]</td>";
194 print "<td align=\"center\">$$data[2]<br><a href=\"$prog_name?action=reset&vol=$$data[0]\">Reset</a></td>";
195 print "<td align=\"center\">$$data[3]</td>";
196 print "<td align=\"center\">$$data[4]</td>";
197 if ($$data[1] gt $$data[2] && $$data[3] gt "0" && $$data[4] ne "Purged") {
198 print "<td align=\"center\"><form><input type=button value=\"Burn\" onClick=\"BurnWindow=window.open(\'$prog_name?action=burn&vol=$$data[0]\', \'BurnWindow\', \'scrollbars=yes,menubar=no,width=550,height=450,screenX=0,screenY=15\')\"></form></td>";
201 print "<td align=\"center\">No Burn</td>";
209 print "<td align=\"center\" colspan=\"6\"><img src=\"$spacer_graphic\" height=\"18\"><form><input type=button value=\"Burn Catalog\" onClick=\"BurnWindow=window.open(\'$prog_name?action=burn&vol=bacula.sql\', \'BurnWindow\', \'scrollbars=yes,menubar=no,width=550,height=450,screenX=0,screenY=15\')\"><img src=\"$spacer_graphic\" width=\"5\"><input type=button value=\"Blank CDRW\" onClick=\"BurnWindow=window.open(\'$prog_name?action=burn&vol=blank\', \'BurnWindow\', \'scrollbars=yes,menubar=no,width=550,height=450,screenX=0,screenY=15\')\"></form><img src=\"$spacer_graphic\" height=\"1\"></td>";
218 #-------------------------------------------------------------------#
220 # Description: burn cd images
224 my $Volume = "$image_path/$vol";
226 # check to see if this is a catalog request
227 if ($vol eq "bacula.sql") {
228 $Volume = "$working_dir/bacula.sql";
230 # check to see if this is a blanking request
231 if ($vol eq "blank") {
232 $Volume = "Blank CD/RW";
235 # open the burn results file and write header info
236 open(OUTF,">$tempfile_path") || &HTMLdie("Unable to open temporary output file.");
239 print OUTF "<title>Burning Volume $Volume</title>";
240 print OUTF "</head>";
242 print OUTF "<div align=\"center\">";
243 print OUTF "<table width=\"100%\">";
244 print OUTF "<tbody>";
246 print OUTF "<td align=\"center\"> <img src=\"$logo_graphic\"> </td>";
248 print OUTF "</tbody>";
249 print OUTF "</table>";
253 # now send to the burn status window that we are burning
254 print "Content-type: text/html\n\n";
257 print "<meta http-equiv=\"Refresh\" content=\"3; url=http://$http_host/$tempfile\">";
258 print "<title>Burning Volume $Volume</title>";
261 print "<div align=\"center\">";
262 print "<table width=\"100%\">";
265 print "<td align=\"center\"> <img src=\"$logo_graphic\"> </td>";
268 print "<td align=\"center\"> <img src=\"$spacer_graphic\" height=\"10\"> </td>";
271 print "<td align=\"center\"> <img src=\"$burn_graphic\"> </td>";
275 print "<p>Now burning $Volume ...</p>";
280 # check to see if this is a catalog request
281 if ($vol eq "bacula.sql") {
282 system("$catalog_dump > $working_dir/bacula.sql");
285 # check to see if this is a blanking request
286 if ($vol eq "blank") {
287 system("$cdrecord -eject speed=2 dev=$cdburner blank=fast >> $tempfile_path");
290 # burn the image and clean up
291 system("$mkisofs -o $working_dir/temp.iso -J -r -V $vol $Volume");
292 system("$cdrecord -eject $burnfree speed=$burner_speed dev=$cdburner $working_dir/temp.iso >> $tempfile_path");
293 system("rm -f $working_dir/temp.iso");
296 if ($vol eq "bacula.sql") {
297 system("rm -f $working_dir/bacula.sql");
300 # finish up the burn results file
301 open(OUTF,">>$tempfile_path") || &HTMLdie("Unable to open temporary output file.");
302 print OUTF "<table width=\"100%\">";
303 print OUTF "<tbody>";
305 print OUTF "<td><div align=\"center\">If you do not see successful output from cdrecord above the burn has failed.</div></td>";
308 print OUTF "<td><div align=\"center\">Please close this window and refresh the main window.</div></td>";
311 print OUTF "<td><div align=\"center\"><font size=\"-3\"> $copyright </font></div></td>";
313 print OUTF "</tbody>";
314 print OUTF "</table>";
316 print OUTF "</body>";
317 print OUTF "</html>";
320 # now pretty up the burn results file by replacing \n with <br>
321 open(INFILE, "$tempfile_path") || &HTMLdie("Unable to open input file $tempfile_path");
322 open(OUTFILE, ">$working_dir/bimagemgr-temp") || &HTMLdie("Unable to open output file bimagemgr-temp");
323 while(my $line = <INFILE>) {
324 $line =~ s/\n/<br>/g;
325 print OUTFILE ($line);
329 system("cp -f $working_dir/bimagemgr-temp $tempfile_path");
331 if ($vol ne "bacula.sql" && $vol ne "blank") {
332 ## update the burn date in the CDImages table
333 # get current timestamp
334 my($sysdate,$systime,$sysyear,$sysmon,$sysmday) = &SysDate;
335 my $burndate = "$sysdate $systime";
337 # connect to database
338 my $dbh = DBI->connect("DBI:$db_driver:$db_name_param=$database;host=$host","$user","$password",
339 {'RaiseError' => 1}) || &HTMLdie("Unable to connect to database.");
340 # get the MediaId for our volume
341 my $sth = $dbh->prepare("SELECT MediaId from Media WHERE VolumeName = \"$vol\"");
343 my $media_id = $sth->fetchrow_array;
346 $dbh->do("UPDATE CDImages SET LastBurn = \"$burndate\" WHERE MediaId = $media_id");
354 #-------------------------------------------------------------------#
355 # Function UpdateImageTable
356 # Description: update the CDImages table from the Media table
358 sub UpdateImageTable {
360 my ($data,@MediaId,$id,$exists,$sth1,$sth2);
362 # connect to database
363 my $dbh = DBI->connect("DBI:$db_driver:$db_name_param=$database;host=$host","$user","$password",
364 {'RaiseError' => 1}) || &HTMLdie("Unable to connect to database.");
366 # get the list of current MediaId
367 $sth1 = $dbh->prepare("SELECT MediaId from Media");
369 while ($data = $sth1->fetchrow_arrayref) {
370 push(@MediaId,$$data[0]);
374 # now check if we have a matching row in CDImages
375 # if not then insert a record
376 foreach $id (@MediaId) {
377 $sth2 = $dbh->prepare("SELECT MediaId from CDImages
378 WHERE MediaId = $id");
380 $exists = $sth2->fetchrow_array;
381 if ($exists ne $id) {
382 $dbh->do("INSERT into CDImages VALUES ($id,\"0000-00-00 00:00:00\")");
392 #-------------------------------------------------------------------#
394 # Description: reset the Last Burn date to 0
399 # connect to database
400 my $dbh = DBI->connect("DBI:$db_driver:$db_name_param=$database;host=$host","$user","$password",
401 {'RaiseError' => 1}) || &HTMLdie("Unable to connect to database.");
404 $sth = $dbh->prepare("SELECT MediaId FROM Media WHERE VolumeName=\"$vol\"");
406 $id = $sth->fetchrow_array;
410 $dbh->do("UPDATE CDImages SET LastBurn=\"0000-00-00 00:00:00\"
411 WHERE MediaId=$id") || &HTMLdie("Unable to update Last Burn Date.");
415 print "Location:http://$http_host$prog_name\n\n";
418 #-------------------------------------------------------------------#
419 # Function DisplayHeader
420 # Description: main page display header
424 $title || ($title= "Bacula CD Image Manager") ;
425 print "Content-type: text/html\n\n";
428 print "<title>$title</title>";
431 print "<div align=\"center\">";
432 print "<table width=\"100%\">";
435 print "<td align=\"center\"><a href=\"$prog_name?action=version\" alt=\"About bimagemgr\"><img src=\"$logo_graphic\" border=\"0\"></a></td>";
444 #-------------------------------------------------------------------#
445 # Function DisplayFooter
446 # Description: main page display footer
449 print "<table width=\"100%\">";
452 print "<td><div align=\"center\"><font size=\"-3\"> $copyright </font></div></td>";
462 #-------------------------------------------------------------------#
464 # Description: get current date/time
467 # usage: my($sysdate,$systime,$sysyear,$sysmon,$sysmday) = &SysDate;
469 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
470 if (length ($min) == 1) {$min = '0'.$min;}
471 if (length ($sec) == 1) {$sec = '0'.$sec;}
472 # since localtime returns the month as 0-11
474 if (length ($mon) == 1) {$mon = '0'.$mon;}
475 if (length ($mday) == 1) {$mday = '0'.$mday;}
476 # since localtime returns the year as the number of years since 1900
477 # ie year is 100 in the year 2000 (so is y2k OK)
478 $year = $year + 1900;
479 my $date = "$year-$mon-$mday";
480 my $time = "$hour:$min:$sec";
481 return($date,$time,$year,$mon,$mday);
484 #-------------------------------------------------------------------#
485 # Function getcgivars
486 # Read all CGI vars into an associative array.
487 # courtesy James Marshall james@jmarshall.com http://www.jmarshall.com/easy/cgi/
488 # If multiple input fields have the same name, they are concatenated into
489 # one array element and delimited with the \0 character (which fails if
490 # the input has any \0 characters, very unlikely but conceivably possible).
491 # Currently only supports Content-Type of application/x-www-form-urlencoded.
497 # First, read entire string of CGI vars into $in
498 if ( ($ENV{'REQUEST_METHOD'} eq 'GET') ||
499 ($ENV{'REQUEST_METHOD'} eq 'HEAD') ) {
500 $in= $ENV{'QUERY_STRING'} ;
502 } elsif ($ENV{'REQUEST_METHOD'} eq 'POST') {
503 if ($ENV{'CONTENT_TYPE'}=~ m#^application/x-www-form-urlencoded$#i) {
504 length($ENV{'CONTENT_LENGTH'})
505 || &HTMLdie("No Content-Length sent with the POST request.") ;
506 read(STDIN, $in, $ENV{'CONTENT_LENGTH'}) ;
509 &HTMLdie("Unsupported Content-Type: $ENV{'CONTENT_TYPE'}") ;
513 &HTMLdie("Script was called with unsupported REQUEST_METHOD.") ;
516 # Resolve and unencode name/value pairs into %in
517 foreach (split('&', $in)) {
519 ($name, $value)= split('=', $_, 2) ;
520 $name=~ s/%(..)/chr(hex($1))/ge ;
521 $value=~ s/%(..)/chr(hex($1))/ge ;
522 $in{$name}.= "\0" if defined($in{$name}) ; # concatenate multiple vars
523 $in{$name}.= $value ;
530 #-------------------------------------------------------------------#
532 # Description: Die, outputting HTML error page
533 # If no $title, use a default title
535 my ($msg,$title)= @_ ;
536 $title || ($title= "CGI Error") ;
537 print "Content-type: text/html\n\n";
540 print "<title>$title</title>";
543 print "<h1>$title</h1>";
544 print "<h3>$msg</h3>";
546 print "<input type=button name=\"BackButton\" value=\"<- Back\" id=\"Button1\" onClick=\"history.back()\">";
554 #-------------------------------------------------------------------#
558 # first functional version
561 # add configuration option for Postgresql driver
564 # add Reset subroutine and version display
567 # add burn of catalog
568 # add instructions to the main display
571 # correct equivalence operator in Burn function
574 # add blank of CD/RW disk
577 # add conditional in Burn() to prevent updating of CDImages
578 # for catalog or CD/RW blanking burns
581 # bug 461 - correct INSERT syntax in UpdateImageTable to
582 # work with PostgreSQL