Imported Upstream version 3.2.0
[debian/amanda] / installcheck / Installcheck / Catalogs.pm
1 # Copyright (c) 2010 Zmanda, Inc.  All Rights Reserved.
2 #
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.
6 #
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
10 # for more details.
11 #
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
15 #
16 # Contact information: Zmanda Inc, 465 S. Mathilda Ave., Suite 300
17 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
18
19 package Installcheck::Catalogs;
20
21 =head1 NAME
22
23 Installcheck::Catalogs - manage catalog info that can be used to test
24 tools that do not need access to actual vtapes
25
26 =head1 SYNOPSIS
27
28   use Installcheck::Catalogs;
29   my $cat = Installcheck::Catalogs::load("skipped");
30   $cat->install();
31   my @tags = $cat->get_tags();
32
33 =head1 DESCRIPTION
34
35 The C<load> method loads a named set of catalog information from catalog files.
36
37 The resulting object just decodes the catalog information into a perl
38 structure.  To actually write the catalog to disk, use the C<install> method of
39 the resulting object.
40
41 Note that many test catalogs require a configuration to be loaded; this package
42 does not handle loading configurations.  However, the C<install> method does
43 take care of erasing the C<logs> subdirectory of the configuration directory as
44 well as any stray holding-disk files.
45
46 A catalog can have multiple, named snippets of text attached, as well.  These
47 are accessed via the C<get_text($name)> method.
48
49 =head2 Database Results
50
51 The C<%H>, C<%P>, and C<%D> directives set up a "shadow database" of dumps and
52 parts that are represented by the catalog.  These are available in two hashes,
53 one for dumps and one for parts, available from methods C<get_dumps> and
54 C<get_parts>.  The hashes are keyed by "tags", which are arbitrary strings.
55 The dumps and parts are built to look like those produced by
56 L<Amanda::DB::Catalog>; in particular, a dump has keys
57
58   parts (list of parts indexed by partnum)
59   dump_timestamp
60   hostname
61   diskname
62   level
63   status
64   kb
65   orig_kb
66   write_timestamp
67   message
68   nparts
69   sec
70
71 while a part has keys
72
73   dump (points to the parent dump)
74   status
75   sec
76   kb
77   orig_kb
78   partnum
79
80 a part will also have a C<holding_file> key if it is, indeed, a holding
81 file.  The C<holding_filename($tag)> method will return the filename of a
82 holding file.
83
84 =head2 Catalog Files
85
86 Each file in C<installcheck/catalogs> with the suffix C<.cat> represents a
87 cached catalog.  Since the Amanda catalog consists of many files (curinfo,
88 trace logs, index, disklist, tapelist, etc.), each catalog acts as a
89 container for several other named files.  The file is parsed in a line-based
90 fashion, with the following conventions:
91
92 =over 4
93
94 =item A line beginning with C<#> is a comment, and is ignored
95
96 =item A line beginning with C<%F> begins a new output file, with the rest of
97 the line (after whitespace) interpreted as a filename relative to the TESTCONF
98 configuration directory.  Any intervening directories required will be created.
99
100 =item A line beginning with C<%T> begins a new text section.  This is simliar
101 to C<%F>, but instead of a filename, the rest of the line specifies a text
102 handle.  The text will not be written to the filesystem on C<install>.
103
104 =item A line beginning with C<%H> specifies a holding-disk file.  The rest of
105 the line is a space-separated list:
106
107   %H tag datestamp hostname pathname level status size
108
109 A single-chunk holding-disk file of the appropriate size will be created,
110 filled with garbage, and the corresponding entries will be made in the dump and
111 part hashes.
112
113 =item A line beginning with C<%D> specifies a dump.  The format, all on one line, is:
114
115   %D tag dump_timestamp write_timestamp hostname diskname level status
116     message nparts sec kb orig_kb
117
118 =item A line beginning with C<%P> specifies a part.  The format, again all on
119 one line, is:
120
121   %P tag dumptag label filenum partnum status sec kb orig_kb
122
123 where C<dumptag> is the tag of the dump of which this is a part.
124
125 =item A line beginning with C<%%> is a custom tag, intended for use by scripts
126 to define their expectations of the logfile.  The results are available from
127 the C<get_tags> method.
128
129 =item A line beginning with C<\> is copied literally into the current output
130 file, without the leading C<\>.
131
132 =item Blank lines are ignored.
133
134 =back
135
136 =cut
137
138 sub load {
139     my ($name) = @_;
140
141     return Installcheck::Catalogs::Catalog->new($name);
142 }
143
144 package Installcheck::Catalogs::Catalog;
145
146 use warnings;
147 use strict;
148
149 use Installcheck;
150 use Amanda::Util;
151 use Amanda::Paths;
152 use Amanda::Xfer qw( :constants );
153 use File::Path qw( mkpath rmtree );
154
155 my $holdingdir = "$Installcheck::TMP/holding";
156
157 sub new {
158     my $class = shift;
159     my ($name) = @_;
160
161     my $filename = "$srcdir/catalogs/$name.cat";
162     die "no catalog file '$filename'" unless -f $filename;
163
164     my $self = bless {
165         files => {},
166         texts => {},
167         tags => [],
168         holding_files => {},
169         dumps => {},
170         parts => {},
171     }, $class;
172
173     $self->_parse($filename);
174
175     return $self;
176 }
177
178 sub _parse {
179     my $self = shift;
180     my ($filename) = @_;
181     my $write_timestamp;
182     my $fileref;
183
184     open(my $fh, "<", $filename) or die "could not open '$filename'";
185     while (<$fh>) {
186         ## comment or blank
187         if (/^#/ or /^$/) {
188             next;
189
190         ## new output file
191         } elsif (/^(%[TF])\s*(.*)$/) {
192             my $cur_filename = $2;
193             my $kind = ($1 eq '%F')? 'files' : 'texts';
194             die "duplicate file '$cur_filename'"
195                 if exists $self->{$kind}{$cur_filename};
196             $self->{$kind}{$cur_filename} = '';
197             $fileref = \$self->{$kind}{$cur_filename};
198
199         # holding file
200         } elsif (/^%H (\S+) (\S+) (\S+) (\S+) (\d+) (\S+) (\d+)$/) {
201
202             die "dump tag $1 already exists" if exists $self->{'dumps'}{$1};
203             die "part tag $1 already exists" if exists $self->{'parts'}{$1};
204
205             my $safe_disk = $4;
206             $safe_disk =~ tr{/}{_};
207             my $hfile = "$holdingdir/$2/$3.$safe_disk";
208
209             $self->{'holding_files'}->{$1} = [ $hfile, $2, $3, $4, $5, $6, $7 ];
210
211             my $dump = $self->{'dumps'}{$1} = {
212                 dump_timestamp => $2,
213                 hostname => $3,
214                 diskname => $4,
215                 level => $5+0,
216                 status => $6,
217                 kb => $7,
218                 orig_kb => 0,
219                 write_timestamp => '00000000000000',
220                 message => '',
221                 nparts => 1,
222                 sec => 0.0,
223             };
224             my $part = $self->{'parts'}{$1} = {
225                 holding_file => $hfile,
226                 dump => $dump,
227                 status => $dump->{'status'},
228                 sec => 0.0,
229                 kb => $dump->{'kb'},
230                 orig_kb => 0,
231                 partnum => 1,
232             };
233             $dump->{'parts'} = [ undef, $part ];
234
235         # dump
236         } elsif (/^%D (\S+) (\d+) (\d+) (\S+) (\S+) (\d+) (\S+) (\S+) (\d+) (\S+) (\d+) (\d+)/) {
237             die "dump tag $1 already exists" if exists $self->{'dumps'}{$1};
238             my $dump = $self->{'dumps'}{$1} = {
239                 dump_timestamp => $2,
240                 write_timestamp => $3,
241                 hostname => $4,
242                 diskname => $5,
243                 level => $6+0,
244                 status => $7,
245                 message => $8,
246                 nparts => $9,
247                 sec => $10+0.0,
248                 kb => $11,
249                 orig_kb => $12,
250                 parts => [ undef ],
251             };
252             # translate "" to an empty string
253             $dump->{'message'} = '' if $dump->{'message'} eq '""';
254
255         # part
256         } elsif (/^%P (\S+) (\S+) (\S+) (\d+) (\d+) (\S+) (\S+) (\d+) (\d+)/) {
257             die "part tag $1 already exists" if exists $self->{'parts'}{$1};
258             die "dump tag $2 does not exist" unless exists $self->{'dumps'}{$2};
259
260             my $part = $self->{'parts'}{$1} = {
261                 dump => $self->{dumps}{$2},
262                 label => $3,
263                 filenum => $4,
264                 partnum => $5,
265                 status => $6,
266                 sec => $7+0.0,
267                 kb => $8,
268                 orig_kb => $9
269             };
270             $self->{'dumps'}->{$2}->{'parts'}->[$5] = $part;
271
272         # processing tag
273         } elsif (/^%%\s*(.*)$/) {
274             push @{$self->{'tags'}}, $1;
275
276         # bogus directive
277         } elsif (/^%/) {
278             chomp;
279             die "invalid processing instruction '$_'";
280
281         # contents of the file (\-escaped)
282         } elsif (/^\\/) {
283             s/^\\//;
284             $$fileref .= $_;
285
286         # contents of the file (copy)
287         } else {
288             $$fileref .= $_;
289         }
290     }
291 }
292
293 sub _make_holding_file {
294     my ($filename, $datestamp, $hostname, $diskname, $level, $status, $size) = @_;
295
296     # make the parent dir
297     my $dir = $filename;
298     $dir =~ s{/[^/]*$}{};
299     mkpath($dir);
300
301     # (note that multi-chunk holding files are not used at this point)
302     my $hdr = Amanda::Header->new();
303     $hdr->{'type'} = $Amanda::Header::F_DUMPFILE;
304     $hdr->{'datestamp'} = $datestamp;
305     $hdr->{'dumplevel'} = $level+0;
306     $hdr->{'name'} = $hostname;
307     $hdr->{'disk'} = $diskname;
308     $hdr->{'program'} = "INSTALLCHECK";
309     $hdr->{'is_partial'} = ($status ne 'OK');
310
311     open(my $fh, ">", $filename) or die("opening '$filename': $!");
312     $fh->syswrite($hdr->to_string(32768,32768));
313
314     # transfer some data to that file
315     my $xfer = Amanda::Xfer->new([
316         Amanda::Xfer::Source::Pattern->new(1024*$size, "+-+-+-+-"),
317         Amanda::Xfer::Dest::Fd->new($fh),
318     ]);
319
320     $xfer->start(sub {
321         my ($src, $msg, $xfer) = @_;
322         if ($msg->{type} == $XMSG_ERROR) {
323             die $msg->{elt} . " failed: " . $msg->{message};
324         } elsif ($msg->{'type'} == $XMSG_DONE) {
325             $src->remove();
326             Amanda::MainLoop::quit();
327         }
328     });
329     Amanda::MainLoop::run();
330     close($fh);
331 }
332
333 sub install {
334     my $self = shift;
335
336     # first, remove the logdir
337     my $logdir = "$Amanda::Paths::CONFIG_DIR/TESTCONF/log";
338     rmtree($logdir) if -e $logdir;
339
340     # write the new config files
341     for my $filename (keys %{$self->{'files'}}) {
342         my $pathname = "$Amanda::Paths::CONFIG_DIR/TESTCONF/$filename";
343         my $dirname = $pathname;
344         $dirname =~ s{/[^/]+$}{};
345
346         mkpath($dirname) unless -d $dirname;
347         Amanda::Util::burp($pathname, $self->{'files'}{$filename});
348     }
349
350     # erase holding and create some new holding files
351     rmtree($holdingdir);
352     for my $hldinfo (values %{$self->{'holding_files'}}) {
353         _make_holding_file(@$hldinfo);
354     }
355 }
356
357 sub get_tags {
358     my $self = shift;
359     return @{$self->{'tags'}};
360 }
361
362 sub get_dumps {
363     my $self = shift;
364     return %{$self->{'dumps'}};
365 }
366
367 sub get_parts {
368     my $self = shift;
369     return %{$self->{'parts'}};
370 }
371
372 sub get_text {
373     my $self = shift;
374     my ($name) = @_;
375
376     return $self->{'texts'}->{$name};
377 }
378
379 sub get_file {
380     my $self = shift;
381     my ($name) = @_;
382
383     return $self->{'files'}->{$name};
384 }
385
386 sub holding_filename {
387     my $self = shift;
388     my ($tag) = @_;
389
390     my $fn = $self->{'holding_files'}{$tag}[0];
391     return $fn;
392 }
393
394 1;