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