1 # Copyright (c) 2008, 2009, 2010 Zmanda, Inc. All Rights Reserved.
3 # This program is free software; you can redistribute it and/or modify it
4 # under the terms of the GNU General Public License version 2 as published
5 # by the Free Software Foundation.
7 # This program is distributed in the hope that it will be useful, but
8 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
9 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 # You should have received a copy of the GNU General Public License along
13 # with this program; if not, write to the Free Software Foundation, Inc.,
14 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16 # Contact information: Zmanda Inc, 505 N Mathlida Ave, Suite 120
17 # Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
19 package Amanda::DB::Catalog;
23 Amanda::DB::Catalog - access to the Amanda catalog: where is that dump?
27 use Amanda::DB::Catalog;
29 # get all dump timestamps on record
30 my @timestamps = Amanda::DB::Catalog::get_timestamps();
32 # loop over those timestamps, printing dump info for each one
33 for my $timestamp (@timestamps) {
34 my @dumpfiles = Amanda::DB::Catalog::get_parts(
35 timestamp => $timestamp,
39 for my $dumpfile (@dumpfiles) {
40 print " ", $dumpfile->{hostname}, ":", $dumpfile->{diskname},
41 " level ", $dumpfile->{level}, "\n";
47 The Amanda catalog is modeled as a set of dumps comprised of parts. A dump is
48 a complete bytestream received from an application, and is uniquely identified
49 by the combination of C<hostname>, C<diskname>, C<dump_timestamp>, C<level>,
50 and C<write_timestamp>. A dump may be partial, or even a complete failure.
52 A part corresponds to a single file on a volume, containing a portion of the
53 data for a dump. A part, then, is completely specified by a volume label and a
54 file number (C<filenum>). Each part has, among other things, a part number
55 (C<partnum>) which gives its relative position within the dump. The bytestream
56 for a dump is recovered by concatenating all of the successful (C<status> = OK)
57 parts matching the dump.
59 Files in the holding disk are considered part of the catalog, and are
60 represented as single-part dumps (holding-disk chunking is ignored, as it is
61 distinct from split parts).
65 The dump table contains one row per dump. It has the following columns:
71 (string) -- timestamp of the run in which the dump was created
75 (string) -- timestamp of the run in which the part was written to this volume,
76 or C<"00000000000000"> for dumps in the holding disk.
80 (string) -- dump hostname
84 (string) -- dump diskname
88 (integer) -- dump level
92 (string) -- "OK", "PARTIAL", or "FAIL"
96 (string) -- reason for PARTIAL or FAIL status
100 (integer) -- number of successful parts in this dump
104 (integer) -- size (in kb) of this part
108 (integer) -- size (in kb) of the complete dump (uncompress and uncrypted).
112 (integer) -- time (in seconds) spent writing this part
116 (arrayref) -- array of parts, indexed by partnum (so C<< $parts->[0] >> is
117 always C<undef>). When multiple partial parts are available, the choice of the
118 partial that is included in this array is undefined.
122 A dump is represented as a hashref with these keys.
124 The C<write_timestamp> gives the time of the amanda run in which the part was
125 written to this volume. The C<write_timestamp> may differ from the
126 C<dump_timestamp> if, for example, I<amflush> wrote the part to tape after the
131 The parts table contains one row per part, and has the following columns:
137 (string) -- volume label (not present for holding files)
141 (integer) -- file on that volume (not present for holding files)
145 (string) -- fully-qualified pathname of the holding file (not present for
150 (object ref) -- a reference to the dump containing this part
154 (string) -- "OK", "PARTIAL" or some other descriptor
158 (integer) -- part number of a split part (1-based)
162 (integer) -- size (in kb) of this part
166 (integer) -- time (in seconds) spent writing this part
170 A part is represented as a hashref with these keys. The C<label> and
171 C<filenum> serve as a primary key.
173 Note that parts' C<dump> and dumps' C<parts> create a reference loop. This is
174 broken by making the C<parts> array's contents weak references in C<get_dumps>,
175 and the C<dump> reference weak in C<get_parts>.
179 All timestamps used in this module are full-length, in the format
180 C<YYYYMMDDHHMMSS>. If the underlying data contains only datestamps, they are
181 zero-extended into timestamps: C<YYYYMMDD000000>. A C<dump_timestamp> always
182 corresponds to the initiation of the I<original> dump run, while
183 C<write_timestamp> gives the time the file was written to the volume. When
184 parts are migrated from volume to volume (e.g., by I<amvault>), the
185 C<dump_timestamp> does not change.
187 In Amanda, the tuple (C<hostname>, C<diskname>, C<level>, C<dump_timestamp>)
188 serves as a unique identifier for a dump bytestream, but because the bytestream
189 may appear several times in the catalog (due to vaulting) the additional
190 C<write_timestamp> is required to identify a particular on-storage instance of
191 a dump. Note that the part sizes may differ between instances, so it is not
192 valid to concatenate parts from different dump instances.
198 The following functions provide summary data based on the contents of the
203 =item get_write_timestamps()
205 Get a list of all write timestamps, sorted in chronological order.
207 =item get_latest_write_timestamp()
209 Return the most recent write timestamp.
211 =item get_labels_written_at_timestamp($ts)
213 Return a list of labels for volumes written at the given timestamp.
221 =item get_parts(%parameters)
223 This function returns a sequence of parts. Values in C<%parameters> restrict
224 the set of parts that are returned. The hash can have any of the following
229 =item write_timestamp
231 restrict to parts written at this timestamp
233 =item write_timestamps
235 (arrayref) restrict to parts written at any of these timestamps (note that
236 holding-disk files have no C<write_timestamp>, so this option and the previous
241 restrict to parts with exactly this timestamp
243 =item dump_timestamps
245 (arrayref) restrict to parts with any of these timestamps
247 =item dump_timestamp_match
249 restrict to parts with timestamps matching this expression
253 if true, only return dumps on holding disk. If false, omit dumps on holding
258 restrict to parts with exactly this hostname
262 (arrayref) restrict to parts with any of these hostnames
266 restrict to parts with hostnames matching this expression
270 restrict to parts with exactly this diskname
274 (arrayref) restrict to parts with any of these disknames
278 restrict to parts with disknames matching this expression
282 restrict to parts with exactly this label
286 (arrayref) restrict to parts with any of these labels
290 restrict to parts with exactly this level
294 (arrayref) restrict to parts with any of these levels
298 restrict to parts with this status
302 (arrayref of dumpspecs) restruct to parts matching one or more of these dumpspecs
306 Match expressions are described in the amanda(8) manual page.
308 =item sort_parts([ $key1, $key2, .. ], @parts)
310 Given a list of parts, this function sorts that list by the requested keys.
311 The following keys are available:
319 =item write_timestamp
329 Note that this sorts labels I<lexically>, not necessarily in the order they were used!
337 Keys are processed from left to right: if two dumps have the same value for
338 C<$key1>, then C<$key2> is examined, and so on. Key names may be prefixed by a
339 dash (C<->) to reverse the order.
341 Note that some of these keys are dump keys; the function will automatically
342 access those values via the C<dump> attribute.
350 =item get_dumps(%parameters)
352 This function returns a sequence of dumps. Values in C<%parameters> restrict
353 the set of dumps that are returned. The same keys as are used for C<get_parts>
354 are available here, with the exception of C<label> and C<labels>. The
355 C<status> key applies to the dump status, not the status of its constituent
358 =item sort_dumps([ $key1, $key2 ], @dumps)
360 Like C<sort_parts>, this sorts a sequence of dumps generated by C<get_dumps>.
361 The same keys are available, with the exception of C<label>, C<filenum>, and
370 =item add_part($part)
372 Add the given part to the database. In terms of logfiles, this will either
373 create a new logfile (if the part's C<write_timestamp> has not been seen
374 before) or append to an existing logfile. Note that a new logfile will require
375 a corresponding new entry in the tapelist.
377 Note that no locking is performed: multiple simultaneous calls to this function
378 can result in a corrupted or incorrect logfile.
386 use Amanda::Logfile qw( :constants match_disk match_host
387 match_datestamp match_level );
388 use Amanda::Tapelist;
389 use Amanda::Config qw( :init :getconf config_dir_relative );
390 use Amanda::Util qw( quote_string weaken_ref );
395 my $tapelist = undef;
396 my $tapelist_filename = undef;
400 my ($timestamp) = @_;
401 if (length($timestamp) == 8) {
402 return $timestamp."000000";
407 sub get_write_timestamps {
410 # find_log assumes that the tapelist has been loaded, so load it now
413 for (Amanda::Logfile::find_log()) {
414 next unless (my ($timestamp) = /^log\.([0-9]+)(?:\.[0-9]+|\.amflush)?$/);
415 push @rv, zeropad($timestamp);
421 sub get_latest_write_timestamp {
422 # get all of the timestamps and select the last one
423 my @timestamps = get_write_timestamps();
426 return $timestamps[-1];
432 # this generic function implements the loop of scanning logfiles to find
433 # the requested data; get_parts and get_dumps then adjust the results to
434 # match what the user expects.
435 sub get_parts_and_dumps {
436 my $get_what = shift; # "parts" or "dumps"
438 my $logfile_dir = config_dir_relative(getconf($CNF_LOGDIR));
440 # find_log assumes that the tapelist has been loaded, so load it now
443 # pre-process params by appending all of the "singular" parameters to the "plurals"
444 push @{$params{'write_timestamps'}}, map { zeropad($_) } $params{'write_timestamp'}
445 if exists($params{'write_timestamp'});
446 push @{$params{'dump_timestamps'}}, map { zeropad($_) } $params{'dump_timestamp'}
447 if exists($params{'dump_timestamp'});
448 push @{$params{'hostnames'}}, $params{'hostname'}
449 if exists($params{'hostname'});
450 push @{$params{'disknames'}}, $params{'diskname'}
451 if exists($params{'diskname'});
452 push @{$params{'levels'}}, $params{'level'}
453 if exists($params{'level'});
454 if ($get_what eq 'parts') {
455 push @{$params{'labels'}}, $params{'label'}
456 if exists($params{'label'});
458 delete $params{'labels'};
461 # specifying write_timestamps implies we won't check holding files
462 if ($params{'write_timestamps'}) {
463 if (defined $params{'holding'} and $params{'holding'}) {
464 return [], []; # well, that's easy..
466 $params{'holding'} = 0;
469 # Since we're working from logfiles, we have to pick the logfiles we'll use first.
470 # Then we can use search_logfile.
472 if ($params{'holding'}) {
473 @logfiles = ( 'holding', );
474 } elsif (exists($params{'write_timestamps'})) {
475 # if we have specific write_timestamps, the job is pretty easy.
476 my %timestamps_hash = map { ($_, undef) } @{$params{'write_timestamps'}};
477 for my $logfile (Amanda::Logfile::find_log()) {
478 next unless (my ($timestamp) = $logfile =~ /^log\.([0-9]+)(?:\.[0-9]+|\.amflush)?$/);
479 next unless (exists($timestamps_hash{zeropad($timestamp)}));
480 push @logfiles, $logfile;
482 } elsif (exists($params{'dump_timestamps'})) {
483 # otherwise, we need only look in logfiles at or after the earliest dump timestamp
484 my @sorted_timestamps = sort @{$params{'dump_timestamps'}};
485 my $earliest_timestamp = $sorted_timestamps[0];
486 for my $logfile (Amanda::Logfile::find_log()) {
487 next unless (my ($timestamp) = $logfile =~ /^log\.([0-9]+)(?:\.[0-9]+|\.amflush)?$/);
488 next unless (zeropad($timestamp) ge $earliest_timestamp);
489 push @logfiles, $logfile;
492 # oh well -- it looks like we'll have to read all existing logfiles.
493 @logfiles = Amanda::Logfile::find_log();
496 # Set up some hash tables for speedy lookups of various attributes
497 my (%dump_timestamps_hash, %hostnames_hash, %disknames_hash, %levels_hash, %labels_hash);
498 %dump_timestamps_hash = map { ($_, undef) } @{$params{'dump_timestamps'}}
499 if (exists($params{'dump_timestamps'}));
500 %hostnames_hash = map { ($_, undef) } @{$params{'hostnames'}}
501 if (exists($params{'hostnames'}));
502 %disknames_hash = map { ($_, undef) } @{$params{'disknames'}}
503 if (exists($params{'disknames'}));
504 %levels_hash = map { ($_, undef) } @{$params{'levels'}}
505 if (exists($params{'levels'}));
506 %labels_hash = map { ($_, undef) } @{$params{'labels'}}
507 if (exists($params{'labels'}));
512 # *also* scan holding if the holding param wasn't specified
513 if (!exists $params{'holding'}) {
514 push @logfiles, 'holding';
517 # now loop over those logfiles and use search_logfile to load the dumpfiles
518 # from them, then process each entry from the logfile
519 for my $logfile (@logfiles) {
520 my (@find_results, $write_timestamp);
522 # get the raw contents from search_logfile, or use holding if
524 if ($logfile ne 'holding') {
525 @find_results = Amanda::Logfile::search_logfile(undef, undef,
526 "$logfile_dir/$logfile", 1);
527 # convert to dumpfile hashes, including the write_timestamp from the logfile name
528 my ($timestamp) = $logfile =~ /^log\.([0-9]+)(?:\.[0-9]+|\.amflush)?$/;
529 $write_timestamp = zeropad($timestamp);
532 @find_results = Amanda::Logfile::search_holding_disk();
533 $write_timestamp = '00000000000000';
536 # filter against *_match with dumps_match
537 @find_results = Amanda::Logfile::dumps_match([@find_results],
538 exists($params{'hostname_match'})? $params{'hostname_match'} : undef,
539 exists($params{'diskname_match'})? $params{'diskname_match'} : undef,
540 exists($params{'dump_timestamp_match'})? $params{'dump_timestamp_match'} : undef,
544 # loop over each entry in the logfile.
545 for my $find_result (@find_results) {
547 # filter out the non-dump error messages that find.c produces
548 next unless (defined $find_result->{'label'});
550 # bail out on this result early, if possible
551 next if (%dump_timestamps_hash
552 and !exists($dump_timestamps_hash{zeropad($find_result->{'timestamp'})}));
553 next if (%hostnames_hash
554 and !exists($hostnames_hash{$find_result->{'hostname'}}));
555 next if (%disknames_hash
556 and !exists($disknames_hash{$find_result->{'diskname'}}));
557 next if (%levels_hash
558 and !exists($levels_hash{$find_result->{'level'}}));
559 next if (%labels_hash
560 and !exists($labels_hash{$find_result->{'label'}}));
561 if ($get_what eq 'parts') {
562 next if (exists($params{'status'})
563 and $find_result->{'status'} ne $params{'status'});
566 # filter each result against dumpspecs, to avoid dumps_match_dumpspecs'
567 # tendency to produce duplicate results
568 next if ($params{'dumpspecs'}
569 and !Amanda::Logfile::dumps_match_dumpspecs([$find_result],
570 $params{'dumpspecs'}, 0));
572 my $dump_timestamp = zeropad($find_result->{'timestamp'});
574 my $dumpkey = join("\0", $find_result->{'hostname'}, $find_result->{'diskname'},
575 $write_timestamp, $find_result->{'level'});
576 my $dump = $dumps{$dumpkey};
577 if (!defined $dump) {
578 $dump = $dumps{$dumpkey} = {
579 dump_timestamp => $dump_timestamp,
580 write_timestamp => $write_timestamp,
581 hostname => $find_result->{'hostname'},
582 diskname => $find_result->{'diskname'},
583 level => $find_result->{'level'}+0,
584 orig_kb => $find_result->{'orig_kb'},
585 status => $find_result->{'dump_status'},
586 message => $find_result->{'message'},
587 # the rest of these params are unknown until we see a taper
588 # DONE, PARTIAL, or FAIL line, although we count nparts
589 # manually instead of relying on the logfile
596 # start setting up a part hash for this result
598 if ($logfile ne 'holding') {
601 label => $find_result->{'label'},
602 filenum => $find_result->{'filenum'},
604 status => $find_result->{'status'},
605 sec => $find_result->{'sec'},
606 kb => $find_result->{'kb'},
607 orig_kb => $find_result->{'orig_kb'},
608 partnum => $find_result->{'partnum'},
613 holding_file => $find_result->{'label'},
615 status => $find_result->{'status'},
617 kb => $find_result->{'kb'},
618 orig_kb => $find_result->{'orig_kb'},
621 # and fix up the dump, too
622 $dump->{'status'} = $find_result->{'status'};
623 $dump->{'kb'} = $find_result->{'kb'};
624 $dump->{'sec'} = $find_result->{'sec'};
627 # weaken the dump ref if we're returning dumps
628 weaken_ref($part{'dump'})
629 if ($get_what eq 'dumps');
631 # count the number of successful parts in the dump
632 $dump->{'nparts'}++ if $part{'status'} eq 'OK';
634 # and add a ref to the array of parts; if we're getting
635 # parts, then this is a weak ref
636 $dump->{'parts'}[$part{'partnum'}] = \%part;
637 weaken_ref($dump->{'parts'}[$part{'partnum'}])
638 if ($get_what eq 'parts');
643 # if these dumps were on the holding disk, then we're done
644 next if $logfile eq 'holding';
646 # re-read the logfile to extract dump-level info that's not captured by
648 my $logh = Amanda::Logfile::open_logfile("$logfile_dir/$logfile");
649 die "logfile '$logfile' not found" unless $logh;
650 while (my ($type, $prog, $str) = Amanda::Logfile::get_logline($logh)) {
651 next unless $prog == $P_TAPER;
653 if ($type == $L_DONE) {
655 } elsif ($type == $L_PARTIAL) {
657 } elsif ($type == $L_FAIL) {
663 # now extract the appropriate info; luckily these log lines have the same
664 # format, more or less
665 my ($hostname, $diskname, $dump_timestamp, $nparts, $level, $secs, $kb, $message);
666 ($hostname, $str) = Amanda::Util::skip_quoted_string($str);
667 ($diskname, $str) = Amanda::Util::skip_quoted_string($str);
668 ($dump_timestamp, $str) = Amanda::Util::skip_quoted_string($str);
669 if ($status ne 'FAIL') {
670 ($nparts, $str) = Amanda::Util::skip_quoted_string($str);
674 ($level, $str) = Amanda::Util::skip_quoted_string($str);
675 if ($status ne 'FAIL') {
677 ($secs, $kb, $str) = ($str =~ /^\[sec ([0-9.]+) kb (\d+) .*\] ?(.*)$/)
680 if ($status ne 'OK') {
686 $hostname = Amanda::Util::unquote_string($hostname);
687 $diskname = Amanda::Util::unquote_string($diskname);
688 $message = Amanda::Util::unquote_string($message) if $message;
690 # filter against dump criteria
691 next if ($params{'dump_timestamp_match'}
692 and !match_datestamp($params{'dump_timestamp_match'}, zeropad($dump_timestamp)));
693 next if (%dump_timestamps_hash
694 and !exists($dump_timestamps_hash{zeropad($dump_timestamp)}));
696 next if ($params{'hostname_match'}
697 and !match_host($params{'hostname_match'}, $hostname));
698 next if (%hostnames_hash
699 and !exists($hostnames_hash{$hostname}));
701 next if ($params{'diskname_match'}
702 and !match_disk($params{'diskname_match'}, $diskname));
703 next if (%disknames_hash
704 and !exists($disknames_hash{$diskname}));
706 next if (%levels_hash
707 and !exists($levels_hash{$level}));
708 # get_dumps filters on status
710 if ($params{'dumpspecs'}) {
712 for my $ds (@{$params{'dumpspecs'}}) {
713 # (the "". are for SWIG's benefit - SWIGged functions don't like
714 # strings generated by SWIG. Long story.)
715 next if (defined $ds->{'host'}
716 and !match_host("".$ds->{'host'}, $hostname));
717 next if (defined $ds->{'disk'}
718 and !match_disk("".$ds->{'disk'}, $diskname));
719 next if (defined $ds->{'datestamp'}
720 and !match_datestamp("".$ds->{'datestamp'}, $dump_timestamp));
721 next if (defined $ds->{'level'}
722 and !match_level("".$ds->{'level'}, $level));
730 my $dumpkey = join("\0", $hostname, $diskname, $write_timestamp, $level);
731 my $dump = $dumps{$dumpkey};
732 if (!defined $dump) {
733 # this will happen when a dump has no parts - a FAILed dump.
734 $dump = $dumps{$dumpkey} = {
735 dump_timestamp => $dump_timestamp,
736 write_timestamp => $write_timestamp,
737 hostname => $hostname,
738 diskname => $diskname,
740 nparts => $nparts, # hopefully 0?
744 $dump->{'message'} = $message;
745 if ($status eq 'FAIL') {
747 $dump->{'sec'} = 0.0;
749 $dump->{'kb'} = $kb+0;
750 $dump->{'sec'} = $secs+0.0;
753 Amanda::Logfile::close_logfile($logh);
756 return [ values %dumps], \@parts;
760 my ($dumps, $parts) = get_parts_and_dumps("parts", @_);
766 my ($dumps, $parts) = get_parts_and_dumps("dumps", @_);
769 if (exists $params{'status'}) {
770 @dumps = grep { $_->{'status'} eq $params{'status'} } @dumps;
777 my ($keys, @parts) = @_;
779 # TODO: make this more efficient by selecting the comparison
780 # functions once, in advance, and just applying them
783 for my $key (@$keys) {
784 my ($rev, $k) = ($key =~ /^(-?)(.*)$/);
786 if ($k =~ /^(partnum|filenum)$/) {
787 # compare part components numerically
788 $res = $a->{$k} <=> $b->{$k};
789 } elsif ($k =~ /^(nparts|level)$/) {
790 # compare dump components numerically
791 $res = $a->{'dump'}->{$k} <=> $b->{'dump'}->{$k};
792 } elsif ($k =~ /^(hostname|diskname|write_timestamp|dump_timestamp)$/) {
793 # compare dump components alphabetically
794 $res = $a->{'dump'}->{$k} cmp $b->{'dump'}->{$k};
796 # compare part components alphabetically
797 $res = $a->{$k} cmp $b->{$k};
799 $res = -$res if ($rev eq '-' and $res);
807 my ($keys, @dumps) = @_;
809 # TODO: make this more efficient by selecting the comparison
810 # functions once, in advance, and just applying them
813 for my $key (@$keys) {
814 my ($rev, $k) = ($key =~ /^(-?)(.*)$/);
816 if ($k =~ /^(nparts|level)$/) {
817 # compare dump components numerically
818 $res = $a->{$k} <=> $b->{$k};
819 } else { # ($k =~ /^(hostname|diskname|write_timestamp|dump_timestamp)$/)
820 # compare dump components alphabetically
821 $res = $a->{$k} cmp $b->{$k};
823 $res = -$res if ($rev eq '-' and $res);
830 # caches for add_part() to avoid repeatedly looking up the log
831 # filename for a particular write_timestamp.
832 my $add_part_last_label = undef;
833 my $add_part_last_write_timestamp = undef;
834 my $add_part_last_logfile = undef;
842 my $logdir = getconf($CNF_LOGDIR);
843 my ($last_filenum, $last_secs, $last_kbs);
845 # first order of business is to find out whether we need to make a new
847 my $write_timestamp = zeropad($dump->{'write_timestamp'});
848 die "dump has no 'write_timestamp'" unless defined $write_timestamp;
850 # consult our one-element cache for this label and write_timestamp
851 if (!defined $add_part_last_label
852 or $add_part_last_label ne $dump->{'label'}
853 or $add_part_last_write_timestamp ne $dump->{'write_timestamp'}) {
856 $add_part_last_logfile = undef;
858 for my $lf (Amanda::Logfile::find_log()) {
859 next unless (my ($log_timestamp) = $lf =~ /^log\.([0-9]+)(?:\.[0-9]+|\.amflush)?$/);
860 next unless (zeropad($log_timestamp) eq $write_timestamp);
862 # write timestamp matches; now check the label
864 for $find_result (Amanda::Logfile::search_logfile(undef, undef,
866 next unless (defined $find_result->{'label'});
868 if ($find_result->{'label'} eq $dump->{'label'}) {
869 $add_part_last_label = $dump->{'label'};
870 $add_part_last_write_timestamp = $dump->{'write_timestamp'};
871 $add_part_last_logfile = $lf;
877 $logfile = $add_part_last_logfile;
879 # truncate the write_timestamp if we're not using timestamps
880 if (!getconf($CNF_USETIMESTAMPS)) {
881 $write_timestamp = substr($write_timestamp, 0, 8);
884 # get the information on the last dump and part in this logfile, or create
885 # a new logfile if none exists, then open the logfile for writing.
886 if (defined $logfile) {
889 # NOTE: this depends on an implementation detail of search_logfile: it
890 # returns the results in the reverse order of appearance in the logfile.
891 # Since we're concerned with the last elements of this logfile that we
892 # will be appending to shortly, we simply reverse this list. As this
893 # package is rewritten to parse logfiles on its own (or access a relational
894 # database), this implementation detail will no longer be relevant.
895 my @find_results = reverse Amanda::Logfile::search_logfile(undef, undef,
896 "$logdir/$logfile", 1);
897 for $find_result (@find_results) {
898 # filter out the non-dump error messages that find.c produces
899 next unless (defined $find_result->{'label'});
901 $last_filenum = $find_result->{'filenum'};
903 # if this is part number 1, reset our secs and kbs counters on the
904 # assumption that this is the beginning of a new dump
905 if ($find_result->{'partnum'} == 1) {
906 $last_secs = $last_kbs = 0;
908 $last_secs += $find_result->{'sec'};
909 $last_kbs += $find_result->{'kb'};
912 open($logfh, ">>", "$logdir/$logfile");
918 # pick an unused log filename
921 $logfile = "log.$write_timestamp.$i";
922 last unless -f "$logdir/$logfile";
926 open($logfh, ">", "$logdir/$logfile")
927 or die("Could not write '$logdir/$logfile': $!");
930 "INFO taper This logfile was generated by Amanda::DB::Catalog\n";
933 "START taper datestamp $write_timestamp label $dump->{label} tape $i\n";
935 if (!defined $tapelist_filename) {
936 $tapelist_filename = config_dir_relative(getconf($CNF_TAPELIST));
939 # reload the tapelist immediately, in case it's been modified
940 $tapelist = Amanda::Tapelist::read_tapelist($tapelist_filename);
942 # see if we need to add an entry to the tapelist for this dump
943 if (!grep { $_->{'label'} eq $dump->{'label'}
944 and zeropad($_->{'datestamp'}) eq zeropad($dump->{'write_timestamp'})
946 $tapelist->add_tapelabel($write_timestamp, $dump->{'label'});
947 $tapelist->write($tapelist_filename);
951 if ($last_filenum >= 0 && $last_filenum+1 != $dump->{'filenum'}) {
952 warn "Discontinuity in filenums in $logfile: " .
953 "from $last_filenum to $dump->{filenum}";
956 my $kps = $dump->{'sec'}? (($dump->{'kb'} + 0.0) / $dump->{'sec'}) : 0.0;
958 my $part_line = "PART taper ";
959 $part_line .= "$dump->{label} ";
960 $part_line .= "$dump->{filenum} ";
961 $part_line .= quote_string($dump->{hostname}) . " ";
962 $part_line .= quote_string($dump->{diskname}) . " ";
963 $part_line .= "$dump->{dump_timestamp} ";
964 $part_line .= "$dump->{partnum}/$dump->{nparts} ";
965 $part_line .= "$dump->{level} ";
966 $part_line .= "[sec $dump->{sec} kb $dump->{kb} kps $kps]";
967 print $logfh "$part_line\n";
969 # TODO: we don't always know nparts when writing a part, so
970 # this is not always an effective way to detect a complete dump.
971 # However, it works for purposes of data vaulting.
972 if ($dump->{'partnum'} == $dump->{'nparts'}) {
973 my $secs = $last_secs + $dump->{'sec'};
974 my $kbs = $last_kbs + $dump->{'kb'};
975 $kps = $secs? ($kbs + 0.0) / $secs : 0.0;
977 my $done_line = "DONE taper ";
978 $done_line .= quote_string($dump->{hostname}) ." ";
979 $done_line .= quote_string($dump->{diskname}) ." ";
980 $done_line .= "$dump->{dump_timestamp} ";
981 $done_line .= "$dump->{nparts} ";
982 $done_line .= "$dump->{level} ";
983 $done_line .= "[sec $secs kb $kbs kps $kps]";
984 print $logfh "$done_line\n";
991 if (!defined $tapelist) {
992 $tapelist_filename = config_dir_relative(getconf($CNF_TAPELIST));
993 $tapelist = Amanda::Tapelist::read_tapelist($tapelist_filename);
997 sub _clear_cache { # (used by installcheck)
998 $tapelist = $tapelist_filename = undef;