Imported Upstream version 3.2.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 $scrub_db = sub {
179     my $tapelist_file = config_dir_relative(getconf($CNF_TAPELIST));
180     my $tapelist = Amanda::Tapelist->new($tapelist_file, !$dry_run);
181     unless ($tapelist) {
182         die "Could not read the tapelist";
183     }
184
185     my $t = $tapelist->lookup_tapelabel($label);
186     if ($keep_label) {
187         $t->{'datestamp'} = 0 if $t;
188     } elsif (!defined $t) {
189         print "label '$label' not found in $tapelist_file\n";
190         exit 0;
191     } else {
192         $tapelist->remove_tapelabel($label);
193     }
194
195     #take a copy in case we roolback
196     my $backup_tapelist_file = dirname($tapelist_file) . "-backup-amrmtape-" . time();
197     if (-x $tapelist_file) {
198         unless (copy($tapelist_file, $backup_tapelist_file)) {
199             die "Failed to copy/backup $tapelist_file to $backup_tapelist_file";
200         }
201     }
202
203     unless ($dry_run) {
204         $tapelist->write();
205     }
206
207     my $tmp_curinfo_file = "$AMANDA_TMPDIR/curinfo-amrmtape-" . time();
208     unless (open(AMADMIN, "$amadmin $config_name export |")) {
209         die "Failed to execute $amadmin: $! $?";
210     }
211     open(CURINFO, ">$tmp_curinfo_file");
212
213     sub info_line($) {
214         print CURINFO "$_[0]";
215     }
216
217     my $host;
218     my $disk;
219     my $dead_level = 10;
220     while(my $line = <AMADMIN>) {
221         my @parts = split(/\s+/, $line);
222         if ($parts[0] =~ /^CURINFO|#|(?:command|last_level|consecutive_runs|(?:full|incr)-(?:rate|comp)):$/) {
223             info_line $line;
224         } elsif ($parts[0] eq 'host:') {
225             $host = $parts[1];
226             info_line $line;
227         } elsif ($parts[0] eq 'disk:') {
228             $disk = $parts[1];
229             info_line $line;
230         } elsif ($parts[0] eq 'history:') {
231             info_line $line;
232         } elsif ($line eq "//\n") {
233             info_line $line;
234             $dead_level = 10;
235         } elsif ($parts[0] eq 'stats:') {
236             if (scalar(@parts) < 6 || scalar(@parts) > 8) {
237                 die "unexpected number of fields in \"stats\" entry for $host:$disk\n\t$line";
238             }
239             my $level = $parts[1];
240             my $cur_label = $parts[7];
241             if ($cur_label eq $label) {
242                 $dead_level = $level;
243                 vlog "Discarding Host: $host, Disk: $disk, Level: $level\n";
244             } elsif ( $level > $dead_level ) {
245                 vlog "Discarding Host: $host, Disk: $disk, Level: $level\n";
246             } else {
247                 info_line $line;
248             }
249         } else {
250             die "Error: unrecognized line of input:\n\t$line";
251         }
252     }
253
254     my $rollback_from_curinfo = sub {
255             unlink $tmp_curinfo_file;
256             return if $keep_label;
257             unless (move($backup_tapelist_file, $tapelist_file)) {
258                 printf STDERR "Failed to rollback new tapelist.\n";
259             }
260     };
261
262     close CURINFO;
263
264     unless (close AMADMIN) {
265         $rollback_from_curinfo->();
266         die "$amadmin exited with non-zero while exporting: $! $?";
267     }
268
269     unless ($dry_run) {
270         if (system("$amadmin $config_name import < $tmp_curinfo_file")) {
271             $rollback_from_curinfo->();
272             die "$amadmin exited with non-zero while importing: $! $?";
273         }
274     }
275
276     unlink $tmp_curinfo_file;
277     unlink $backup_tapelist_file;
278
279     if ($cleanup && !$dry_run) {
280         if (system($amtrmlog, $config_name)) {
281             die "$amtrmlog exited with non-zero while scrubbing logs: $! $?";
282         }
283         if (system($amtrmidx, $config_name)) {
284             die "$amtrmidx exited with non-zero while scrubbing indexes: $! $?";
285         }
286     }
287
288     Amanda::MainLoop::quit();
289 };
290
291 my $erase_volume = make_cb('erase_volume' => sub {
292     if ($erase) {
293         $log_created = 1;
294         local *LOG;
295         open(LOG, ">$log_file");
296         print LOG "INFO amrmtape amrmtape pid $$\n";
297         close LOG;
298         my $chg = Amanda::Changer->new($changer_name);
299         $chg->load(
300             'label' => $label,
301             'res_cb' => sub {
302                 my ($err, $resv) = @_;
303                 die $err if $err;
304
305                 my $dev = $resv->{'device'};
306                 die "Can not erase $label because the device doesn't support this feature"
307                     unless $dev->property_get('full_deletion');
308                 if (!$dry_run) {
309                     $dev->erase()
310                         or die "Failed to erase volume";
311                     $dev->finish();
312
313                     # label the tape with the same label it had
314                     if ($keep_label) {
315                         $dev->start($ACCESS_WRITE, $label, undef)
316                             or die "Failed to write tape label";
317                     }
318                 }
319
320                 $resv->release(finished_cb => sub {
321                     my ($err) = @_;
322                     die $err if $err;
323
324                     $scrub_db->();
325                 });
326             });
327     } else {
328         $scrub_db->();
329     }
330 });
331
332 # kick things off
333 $erase_volume->();
334 Amanda::MainLoop::run();
335
336 if ($log_created == 1) {
337     unlink $log_file;
338     $log_created = 0;
339 }
340
341 Amanda::Util::finish_application();