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