Imported Upstream version 3.3.0
[debian/amanda] / server-src / amrmtape.pl
1 #!@PERL@
2 #
3 # Copyright (c) 2008,2009 Zmanda, Inc.  All Rights Reserved.
4 #
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License version 2 as published
7 # by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 # for more details.
13 #
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
17 #
18 # Contact information: Zmanda Inc, 465 S. Mathilda Ave., Suite 300
19 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
20 #
21 use lib '@amperldir@';
22 use strict;
23 use warnings;
24
25 use Amanda::Config qw( :init :getconf config_print_errors
26   config_dir_relative new_config_overrides add_config_override);
27 use Amanda::Changer;
28 use Amanda::Device qw( :constants );
29 use Amanda::Debug qw( :logging );
30 use Amanda::Disklist;
31 use Amanda::Paths;
32 use Amanda::MainLoop;
33 use Amanda::Tapelist;
34 use Amanda::Util qw( :constants );
35 use File::Copy;
36 use File::Basename;
37 use Getopt::Long;
38
39 my $amadmin = "$sbindir/amadmin";
40 my $amtrmidx = "$amlibexecdir/amtrmidx";
41 my $amtrmlog = "$amlibexecdir/amtrmlog";
42
43 my $dry_run;
44 my $cleanup;
45 my $erase;
46 my $changer_name;
47 my $keep_label;
48 my $verbose = 1;
49 my $help;
50 my $logdir;
51 my $log_file;
52 my $log_created = 0;
53
54 sub die_handler {
55     if ($log_created == 1) {
56         unlink $log_file;
57         $log_created = 0;
58     }
59 }
60 $SIG{__DIE__} = \&die_handler;
61
62 sub int_handler {
63     if ($log_created == 1) {
64         unlink $log_file;
65         $log_created = 0;
66     }
67     die "Interrupted\n";
68 }
69 $SIG{INT} = \&int_handler;
70
71 sub usage() {
72     print <<EOF
73 $0 [-n] [-v] [-q] [-d] [config-overwrites] <config> <label>
74 \t--changer changer-name
75 \t\tSpecify the name of the changer to use (for --erase).
76 \t--cleanup
77 \t\tRemove indexes and logs immediately
78 \t-n, --dryrun
79 \t\tDo nothing to original files, leave new ones in database directory.
80 \t--erase
81 \t\tErase the media, if possible
82 \t-h, --help
83 \t\tDisplay this message.
84 \t--keep-label
85 \t\tDo not remove label from the tapelist
86 \t-q, --quiet
87 \t\tQuiet, opposite of -v.
88 \t-v, --verbose
89 \t\tVerbose, list backups of hosts and disks that are being discarded.
90
91 This program allows you to invalidate the contents of an existing
92 backup tape within the Amanda current tape database.  This is meant as
93 a recovery mecanism for when a good backup is damaged either by faulty
94 hardware or user error, i.e. the tape is eaten by the tape drive, or
95 the tape has been overwritten.
96 EOF
97 }
98
99 sub vlog(@) {
100     foreach my $msg (@_) {
101         message($msg);
102         print "$0: $msg\n" if $verbose;
103     }
104 }
105
106 Amanda::Util::setup_application("amrmtape", "server", $CONTEXT_CMDLINE);
107
108 my $config_overrides = new_config_overrides( scalar(@ARGV) + 1 );
109
110 my $opts_ok = GetOptions(
111     "changer=s" => \$changer_name,
112     "cleanup" => \$cleanup,
113     "dryrun|n" => \$dry_run,
114     "erase" => \$erase,
115     "help|h" => \$help,
116     "keep-label" => \$keep_label,
117     'o=s' => sub { add_config_override_opt( $config_overrides, $_[1] ); },
118     "quiet|q" => sub { undef $verbose; },
119     "verbose|v" => \$verbose,
120 );
121
122 unless ($opts_ok && scalar(@ARGV) == 2) {
123     unless (scalar(@ARGV) == 2) {
124         print STDERR "Specify a configuration and label.\n";
125     }
126     usage();
127     exit 1;
128 }
129
130 if ($help) {
131     usage();
132     exit 0;
133 }
134
135 my ($config_name, $label) = @ARGV;
136
137 set_config_overrides($config_overrides);
138 my $cfg_ok = config_init( $CONFIG_INIT_EXPLICIT_NAME, $config_name );
139
140 my ($cfgerr_level, @cfgerr_errors) = config_errors();
141 if ($cfgerr_level >= $CFGERR_WARNINGS) {
142     config_print_errors();
143     if ($cfgerr_level >= $CFGERR_ERRORS) {
144         die "Errors processing config file";
145     }
146 }
147
148 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
149 $logdir = config_dir_relative(getconf($CNF_LOGDIR));
150 $log_file = "$logdir/log";
151
152 if ($erase) {
153     # Check for log file existance
154     if (-e $log_file) {
155         `amcleanup -p $config_name`;
156     }
157
158     if (-e $log_file) {
159         local *LOG;
160         open(LOG,  $log_file);
161         my $info_line = <LOG>;
162         close LOG;
163         $info_line =~ /^INFO (.*) .* pid .*$/;
164         my $process_name = $1;
165         print "$process_name is running, or you must run amcleanup\n";
166         exit 1;
167     }
168 }
169
170 # amadmin may later try to load this and will die if it has errors
171 # load it now to catch the problem sooner (before we might erase data)
172 my $diskfile = config_dir_relative(getconf($CNF_DISKFILE));
173 $cfgerr_level = Amanda::Disklist::read_disklist('filename' => $diskfile);
174 if ($cfgerr_level >= $CFGERR_ERRORS) {
175     die "Errors processing disklist";
176 }
177
178 my $tapelist_file = config_dir_relative(getconf($CNF_TAPELIST));
179 my $tapelist = Amanda::Tapelist->new($tapelist_file, !$dry_run);
180 unless ($tapelist) {
181     die "Could not read the tapelist";
182 }
183
184
185 my $scrub_db = sub {
186     my $t = $tapelist->lookup_tapelabel($label);
187     if ($keep_label) {
188         $t->{'datestamp'} = 0 if $t;
189     } elsif (!defined $t) {
190         print "label '$label' not found in $tapelist_file\n";
191         exit 0;
192     } else {
193         $tapelist->remove_tapelabel($label);
194     }
195
196     #take a copy in case we roolback
197     my $backup_tapelist_file = dirname($tapelist_file) . "-backup-amrmtape-" . time();
198     if (-x $tapelist_file) {
199         unless (copy($tapelist_file, $backup_tapelist_file)) {
200             die "Failed to copy/backup $tapelist_file to $backup_tapelist_file";
201         }
202     }
203
204     unless ($dry_run) {
205         $tapelist->write();
206     }
207
208     my $tmp_curinfo_file = "$AMANDA_TMPDIR/curinfo-amrmtape-" . time();
209     unless (open(AMADMIN, "$amadmin $config_name export |")) {
210         die "Failed to execute $amadmin: $! $?";
211     }
212     open(CURINFO, ">$tmp_curinfo_file");
213
214     sub info_line($) {
215         print CURINFO "$_[0]";
216     }
217
218     my $host;
219     my $disk;
220     my $dead_level = 10;
221     while(my $line = <AMADMIN>) {
222         my @parts = split(/\s+/, $line);
223         if ($parts[0] =~ /^CURINFO|#|(?:command|last_level|consecutive_runs|(?:full|incr)-(?:rate|comp)):$/) {
224             info_line $line;
225         } elsif ($parts[0] eq 'host:') {
226             $host = $parts[1];
227             info_line $line;
228         } elsif ($parts[0] eq 'disk:') {
229             $disk = $parts[1];
230             info_line $line;
231         } elsif ($parts[0] eq 'history:') {
232             info_line $line;
233         } elsif ($line eq "//\n") {
234             info_line $line;
235             $dead_level = 10;
236         } elsif ($parts[0] eq 'stats:') {
237             if (scalar(@parts) < 6 || scalar(@parts) > 8) {
238                 die "unexpected number of fields in \"stats\" entry for $host:$disk\n\t$line";
239             }
240             my $level = $parts[1];
241             my $cur_label = $parts[7];
242             if ($cur_label eq $label) {
243                 $dead_level = $level;
244                 vlog "Discarding Host: $host, Disk: $disk, Level: $level\n";
245             } elsif ( $level > $dead_level ) {
246                 vlog "Discarding Host: $host, Disk: $disk, Level: $level\n";
247             } else {
248                 info_line $line;
249             }
250         } else {
251             die "Error: unrecognized line of input:\n\t$line";
252         }
253     }
254
255     my $rollback_from_curinfo = sub {
256             unlink $tmp_curinfo_file;
257             return if $keep_label;
258             unless (move($backup_tapelist_file, $tapelist_file)) {
259                 printf STDERR "Failed to rollback new tapelist.\n";
260             }
261     };
262
263     close CURINFO;
264
265     unless (close AMADMIN) {
266         $rollback_from_curinfo->();
267         die "$amadmin exited with non-zero while exporting: $! $?";
268     }
269
270     unless ($dry_run) {
271         if (system("$amadmin $config_name import < $tmp_curinfo_file")) {
272             $rollback_from_curinfo->();
273             die "$amadmin exited with non-zero while importing: $! $?";
274         }
275     }
276
277     unlink $tmp_curinfo_file;
278     unlink $backup_tapelist_file;
279
280     if ($cleanup && !$dry_run) {
281         if (system($amtrmlog, $config_name)) {
282             die "$amtrmlog exited with non-zero while scrubbing logs: $! $?";
283         }
284         if (system($amtrmidx, $config_name)) {
285             die "$amtrmidx exited with non-zero while scrubbing indexes: $! $?";
286         }
287     }
288
289     Amanda::MainLoop::quit();
290 };
291
292 my $erase_volume = make_cb('erase_volume' => sub {
293     if ($erase) {
294         $log_created = 1;
295         local *LOG;
296         open(LOG, ">$log_file");
297         print LOG "INFO amrmtape amrmtape pid $$\n";
298         close LOG;
299         my $chg = Amanda::Changer->new($changer_name, tapelist => $tapelist);
300         $chg->load(
301             'label' => $label,
302             'res_cb' => sub {
303                 my ($err, $resv) = @_;
304                 die $err if $err;
305
306                 my $dev = $resv->{'device'};
307                 die "Can not erase $label because the device doesn't support this feature"
308                     unless $dev->property_get('full_deletion');
309                 if (!$dry_run) {
310                     $dev->erase()
311                         or die "Failed to erase volume";
312                     $dev->finish();
313
314                     # label the tape with the same label it had
315                     if ($keep_label) {
316                         $dev->start($ACCESS_WRITE, $label, undef)
317                             or die "Failed to write tape label";
318                     }
319                 }
320
321                 $resv->release(finished_cb => sub {
322                     my ($err) = @_;
323
324                     $chg->quit();
325                     die $err if $err;
326
327                     $scrub_db->();
328                 });
329             });
330     } else {
331         $scrub_db->();
332     }
333 });
334
335 # kick things off
336 $erase_volume->();
337 Amanda::MainLoop::run();
338
339 if ($log_created == 1) {
340     unlink $log_file;
341     $log_created = 0;
342 }
343
344 Amanda::Util::finish_application();