7c052a52216f16b1752d13d223157450d2779305
[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 debug("Arguments: " . join(' ', @ARGV));
111 Getopt::Long::Configure(qw{ bundling });
112 my $opts_ok = GetOptions(
113     'version' => \&Amanda::Util::version_opt,
114     "changer=s" => \$changer_name,
115     "cleanup" => \$cleanup,
116     "dryrun|n" => \$dry_run,
117     "erase" => \$erase,
118     "help|h" => \$help,
119     "keep-label" => \$keep_label,
120     'o=s' => sub { add_config_override_opt( $config_overrides, $_[1] ); },
121     "quiet|q" => sub { undef $verbose; },
122     "verbose|v" => \$verbose,
123 );
124
125 unless ($opts_ok && scalar(@ARGV) == 2) {
126     unless (scalar(@ARGV) == 2) {
127         print STDERR "Specify a configuration and label.\n";
128     }
129     usage();
130     exit 1;
131 }
132
133 if ($help) {
134     usage();
135     exit 0;
136 }
137
138 my ($config_name, $label) = @ARGV;
139
140 set_config_overrides($config_overrides);
141 my $cfg_ok = config_init( $CONFIG_INIT_EXPLICIT_NAME, $config_name );
142
143 my ($cfgerr_level, @cfgerr_errors) = config_errors();
144 if ($cfgerr_level >= $CFGERR_WARNINGS) {
145     config_print_errors();
146     if ($cfgerr_level >= $CFGERR_ERRORS) {
147         die "Errors processing config file";
148     }
149 }
150
151 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
152 $logdir = config_dir_relative(getconf($CNF_LOGDIR));
153 $log_file = "$logdir/log";
154
155 if ($erase) {
156     # Check for log file existance
157     if (-e $log_file) {
158         `amcleanup -p $config_name`;
159     }
160
161     if (-e $log_file) {
162         local *LOG;
163         open(LOG,  $log_file);
164         my $info_line = <LOG>;
165         close LOG;
166         $info_line =~ /^INFO (.*) .* pid .*$/;
167         my $process_name = $1;
168         print "$process_name is running, or you must run amcleanup\n";
169         exit 1;
170     }
171 }
172
173 # amadmin may later try to load this and will die if it has errors
174 # load it now to catch the problem sooner (before we might erase data)
175 my $diskfile = config_dir_relative(getconf($CNF_DISKFILE));
176 $cfgerr_level = Amanda::Disklist::read_disklist('filename' => $diskfile);
177 if ($cfgerr_level >= $CFGERR_ERRORS) {
178     die "Errors processing disklist";
179 }
180
181 my $tapelist_file = config_dir_relative(getconf($CNF_TAPELIST));
182 my $tapelist = Amanda::Tapelist->new($tapelist_file, !$dry_run);
183 unless ($tapelist) {
184     die "Could not read the tapelist";
185 }
186
187
188 my $scrub_db = sub {
189     my $t = $tapelist->lookup_tapelabel($label);
190     if ($keep_label) {
191         $t->{'datestamp'} = 0 if $t;
192     } elsif (!defined $t) {
193         print "label '$label' not found in $tapelist_file\n";
194         exit 0;
195     } else {
196         $tapelist->remove_tapelabel($label);
197     }
198
199     #take a copy in case we roolback
200     my $backup_tapelist_file = dirname($tapelist_file) . "-backup-amrmtape-" . time();
201     if (-x $tapelist_file) {
202         unless (copy($tapelist_file, $backup_tapelist_file)) {
203             die "Failed to copy/backup $tapelist_file to $backup_tapelist_file";
204         }
205     }
206
207     unless ($dry_run) {
208         $tapelist->write();
209     }
210
211     my $tmp_curinfo_file = "$AMANDA_TMPDIR/curinfo-amrmtape-" . time();
212     unless (open(AMADMIN, "$amadmin $config_name export |")) {
213         die "Failed to execute $amadmin: $! $?";
214     }
215     open(CURINFO, ">$tmp_curinfo_file");
216
217     sub info_line($) {
218         print CURINFO "$_[0]";
219     }
220
221     my $host;
222     my $disk;
223     my $dead_level = 10;
224     while(my $line = <AMADMIN>) {
225         my @parts = split(/\s+/, $line);
226         if ($parts[0] =~ /^CURINFO|#|(?:command|last_level|consecutive_runs|(?:full|incr)-(?:rate|comp)):$/) {
227             info_line $line;
228         } elsif ($parts[0] eq 'host:') {
229             $host = $parts[1];
230             info_line $line;
231         } elsif ($parts[0] eq 'disk:') {
232             $disk = $parts[1];
233             info_line $line;
234         } elsif ($parts[0] eq 'history:') {
235             info_line $line;
236         } elsif ($line eq "//\n") {
237             info_line $line;
238             $dead_level = 10;
239         } elsif ($parts[0] eq 'stats:') {
240             if (scalar(@parts) < 6 || scalar(@parts) > 8) {
241                 die "unexpected number of fields in \"stats\" entry for $host:$disk\n\t$line";
242             }
243             my $level = $parts[1];
244             my $cur_label = $parts[7];
245             if (defined $cur_label and $cur_label eq $label) {
246                 $dead_level = $level;
247                 vlog "Discarding Host: $host, Disk: $disk, Level: $level\n";
248             } elsif ( $level > $dead_level ) {
249                 vlog "Discarding Host: $host, Disk: $disk, Level: $level\n";
250             } else {
251                 info_line $line;
252             }
253         } else {
254             die "Error: unrecognized line of input:\n\t$line";
255         }
256     }
257
258     my $rollback_from_curinfo = sub {
259             unlink $tmp_curinfo_file;
260             return if $keep_label;
261             unless (move($backup_tapelist_file, $tapelist_file)) {
262                 printf STDERR "Failed to rollback new tapelist.\n";
263             }
264     };
265
266     close CURINFO;
267
268     unless (close AMADMIN) {
269         $rollback_from_curinfo->();
270         die "$amadmin exited with non-zero while exporting: $! $?";
271     }
272
273     unless ($dry_run) {
274         if (system("$amadmin $config_name import < $tmp_curinfo_file")) {
275             $rollback_from_curinfo->();
276             die "$amadmin exited with non-zero while importing: $! $?";
277         }
278     }
279
280     unlink $tmp_curinfo_file;
281     unlink $backup_tapelist_file;
282
283     if ($cleanup && !$dry_run) {
284         if (system($amtrmlog, $config_name)) {
285             die "$amtrmlog exited with non-zero while scrubbing logs: $! $?";
286         }
287         if (system($amtrmidx, $config_name)) {
288             die "$amtrmidx exited with non-zero while scrubbing indexes: $! $?";
289         }
290     }
291
292     Amanda::MainLoop::quit();
293 };
294
295 my $erase_volume = make_cb('erase_volume' => sub {
296     if ($erase) {
297         $log_created = 1;
298         local *LOG;
299         open(LOG, ">$log_file");
300         print LOG "INFO amrmtape amrmtape pid $$\n";
301         close LOG;
302         my $chg = Amanda::Changer->new($changer_name, tapelist => $tapelist);
303         $chg->load(
304             'label' => $label,
305             'res_cb' => sub {
306                 my ($err, $resv) = @_;
307                 die $err if $err;
308
309                 my $dev = $resv->{'device'};
310                 die "Can not erase $label because the device doesn't support this feature"
311                     unless $dev->property_get('full_deletion');
312
313                 my $rel_cb = make_cb('rel_cb' => sub {
314                     $resv->release(finished_cb => sub {
315                         my ($err) = @_;
316
317                         $chg->quit();
318                         die $err if $err;
319
320                         $scrub_db->();
321                     });
322                 });
323
324                 if (!$dry_run) {
325                     $dev->erase()
326                         or die "Failed to erase volume";
327                     $resv->set_label(finished_cb => sub {
328                         $dev->finish();
329
330                         # label the tape with the same label it had
331                         if ($keep_label) {
332                             $dev->start($ACCESS_WRITE, $label, undef)
333                                 or die "Failed to write tape label";
334                             return $resv->set_label(label => $label, finished_cb => $rel_cb);
335                         }
336                         $rel_cb->();
337                     });
338                 } else {
339                     $rel_cb->();
340                 }
341
342             });
343     } else {
344         $scrub_db->();
345     }
346 });
347
348 # kick things off
349 $erase_volume->();
350 Amanda::MainLoop::run();
351
352 if ($log_created == 1) {
353     unlink $log_file;
354     $log_created = 0;
355 }
356
357 Amanda::Util::finish_application();