]> git.pld-linux.org Git - packages/amanda.git/blame - amlvm-snapshot.pl
- scipt to backup lvm snapshots
[packages/amanda.git] / amlvm-snapshot.pl
CommitLineData
9b104ea3
JR
1#!/usr/bin/perl
2# Copyright (c) 2010, Daniel Duvall. All Rights Reserved.
3#
4# This program is free software; you can redistribute it and/or modify it
5# under the terms of the GNU General Public License version 2 as published
6# by the Free Software Foundation.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11# for more details.
12#
13# You should have received a copy of the GNU General Public License along
14# with this program; if not, write to the Free Software Foundation, Inc.,
15# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16#
17# Author: Daniel Duvall <the.liberal.media@gmail.com>
18
19# PROPERTY:
20#
21# SNAPSHOT-SIZE
22#
23# LVCREATE-PATH
24# LVDISPLAY-PATH
25# LVREMOVE-PATH
26# VGDISPLAY-PATH
27#
28# SUDO
29#
30use lib '@@PERL_VENDORARCH@@';
31use strict;
32use Getopt::Long;
33
34package Amanda::Script::Amlvm_snapshot;
35use base qw(Amanda::Script);
36
37use Amanda::Config qw( :getconf :init );
38use Amanda::Debug qw( :logging );
39use Amanda::Util qw( :constants );
40use Amanda::Paths;
41use Amanda::Constants;
42
43use Config;
44use File::Temp qw(tempdir);
45use IPC::Open3;
46use Symbol;
47
48sub new {
49 my $class = shift;
50 my ($execute_where, $config, $host, $disk, $device, $level, $index,
51 $message, $collection, $record, $snapsize, $lvcreate, $lvdisplay,
52 $lvremove, $vgdisplay, $blkid, $sudo) = @_;
53 my $self = $class->SUPER::new($execute_where, $config);
54
55 $self->{execute_where} = $execute_where;
56 $self->{config} = $config;
57 $self->{host} = $host;
58 $self->{device} = $device;
59 $self->{disk} = $disk;
60 $self->{level} = [ @{$level} ]; # Copy the array
61 $self->{index} = $index;
62 $self->{message} = $message;
63 $self->{collection} = $collection;
64 $self->{record} = $record;
65
66 $self->{snapsize} = $snapsize;
67
68 $self->{lvcreate} = $lvcreate;
69 $self->{lvdisplay} = $lvdisplay;
70 $self->{lvremove} = $lvremove;
71 $self->{vgdisplay} = $vgdisplay;
72 $self->{blkid} = $blkid;
73
74 $self->{sudo} = $sudo;
75
76 $self->{volume_group} = undef;
77 $self->{fs_type} = undef;
78
79 return $self;
80}
81
82sub calculate_snapsize {
83 my $self = shift;
84
85 my $size;
86
87 # if a snapshot size isn't already set, use all available extents in the
88 # volume group
89 if (!defined $self->{snapsize}) {
90 foreach ($self->execute(1, "$self->{vgdisplay} -c")) {
91 my @parts = split(/:/);
92 my $group = $parts[0];
93 my $total = $parts[13];
94 my $alloc = $parts[14];
95
96 $group =~ s/^\s*//;
97 chomp($group);
98
99 if ($group eq $self->{volume_group}) {
100 $self->{snapsize} = $total - $alloc;
101 last;
102 }
103 }
104 }
105
106 # fallback to just 1 extent (though this might fail anyway)
107 $self->{snapsize} = 1 if (!defined $self->{snapsize});
108}
109
110sub create_snapshot {
111 my $self = shift;
112
113 # calculate default snapshot size
114 $self->calculate_snapsize();
115
116 debug("A snapshot of size `$self->{snapsize}' will be created.");
117
118 my @parts = split('/', $self->{device});
119 my $vg_name = $parts[2];
120 my $lv_name = $parts[3];
121
122 # create a new snapshot with lvcreate
123 $self->execute(1,
124 "$self->{lvcreate}", "--extents", $self->{snapsize},
125 "--snapshot", "--name", "amsnap-$vg_name-$lv_name", $self->{device}
126 );
127 my $snapshot_device = $self->get_snap_device(0);
128
129 debug("Created snapshot of `$self->{device}' at `$snapshot_device'.");
130}
131
132# Executes (safely) the given command and arguments. Optional execution
133# through sudo can be specified, but will only occur if the script was invoked
134# with the '--sudo' argument.
135sub execute {
136 my $self = shift;
137 my $sudo = shift;
138 my $cmd = shift;
139
140 # escape all given arguments
141 my @args = map(quotemeta, @_);
142
143 my ($in, $out, $err, $pid);
144 $err = Symbol::gensym;
145
146 my $full_cmd = ($sudo and $self->{sudo}) ? "sudo $cmd" : $cmd;
147
148 $full_cmd .= " @args";
149
150 $pid = open3($in, $out, $err, $full_cmd);
151
152 close($in);
153
154 my @output = <$out>;
155 my @errors = <$err>;
156
157 close($out);
158 close($err);
159
160 waitpid($pid, 0);
161
162 # NOTE There's an exception for readlink, as it's failure isn't critical.
163 if ($? > 0 and $cmd ne "readlink") {
164 my $err_str = join("", @errors);
165 chomp($err_str);
166
167 $self->print_to_server_and_die(
168 "Failed to execute (status $?) `$full_cmd': $err_str",
169 $Amanda::Script_App::ERROR
170 );
171 }
172
173 return @output;
174}
175
176# Returns the snapshot device path.
177sub get_snap_device {
178 my $self = shift;
179 my $mapper = shift;
180 my @parts = split('/', $self->{device});
181 my $vg_name = $parts[2];
182 my $lv_name = $parts[3];
183
184 if ($mapper) {
185 return "/dev/mapper/$self->{volume_group}-amsnap--$vg_name--$lv_name";
186 } else {
187 return "/dev/$self->{volume_group}/amsnap-$vg_name-$lv_name";
188 }
189}
190
191# Mounts the snapshot device at the configured directory.
192sub mount_snapshot {
193 my $self = shift;
194
195 # mount options
196 my @options = ('ro');
197
198 # special mount options for xfs
199 # XXX should this be left up to the user as an argument?
200 if ($self->{fs_type} eq 'xfs') {
201 push(@options, 'nouuid');
202 }
203
204 # create a temporary mount point and mount the snapshot volume
205 $self->{directory} = tempdir(CLEANUP => 0);
206 my $snapshot_device = $self->get_snap_device(0);
207 $self->execute(1,
208 "mount -o ", join(",", @options),
209 $snapshot_device, $self->{directory}
210 );
211
212 debug("Mounted snapshot `$snapshot_device' at `$self->{directory}'.");
213}
214
215# Readlink wrapper.
216sub readlink {
217 my $self = shift;
218 my $path = shift;
219
220 # NOTE: We don't use perl's readlink here, because it might need to be
221 # executed with elevated privileges (sudo).
222 my $real_path = join("", $self->execute(1, "readlink", $path));
223 chomp($real_path);
224 $real_path =~ s@\.\.@/dev@;
225
226 return ($real_path ne "") ? $real_path : $path;
227}
228
229# Removes the snapshot device.
230sub remove_snapshot {
231 my $self = shift;
232
233 # remove snapshot device with 'lvremove'
234 $self->execute(1, "$self->{lvremove} -f", $self->get_snap_device(0));
235
236 debug("Removed snapshot of `$self->{device}'.");
237}
238
239# Resolves the underlying device on which the configured directory resides.
240sub resolve_device {
241 my $self = shift;
242
243 # Search mtab for the mount point. Get the device path and filesystem type.
244 my $mnt_device = $self->scan_mtab(
245 sub { return $_[0] if ($_[1] eq $self->{disk}); }
246 );
247
248 my $fs_type = $self->scan_mtab(
249 sub { return $_[2] if ($_[1] eq $self->{disk}); }
250 );
251
252 if (!$mnt_device) {
253 if ($self->{disk} eq $self->{device}) {
254 $self->print_to_server_and_die(
255 "Failed to resolve a device from directory `$self->{disk}'. ",
256 $Amanda::Script_App::ERROR
257 );
258 } else {
259 $mnt_device = $self->{device};
260 $fs_type = join("", $self->execute(1, "$self->{blkid} -s TYPE -o value $self->{device}"));
261 chomp($fs_type);
262 }
263 }
264
265 # loop through the LVs to find the one that matches
266 foreach ($self->execute(1, "$self->{lvdisplay} -c")) {
267 my ($device, $group) = split(/:/);
268
269 $device =~ s/^\s*//;
270 chomp($device);
271
272 my $real_device = $self->readlink($device);
273 chomp($real_device);
274
275 if ($real_device eq $mnt_device) {
276 $self->{device} = $device;
277 $self->{volume_group} = $group;
278 $self->{fs_type} = $fs_type;
279
280 debug(
281 "Resolved device `$self->{device}' and volume group ".
282 "`$self->{volume_group}' from mount point `$self->{disk}'."
283 );
284
285 last;
286 }
287 }
288}
289
290# Iterates over lines in the system mtab and invokes the given anonymous
291# subroutine with entries from each record:
292# 1. Canonical device path (as resolved from readlink).
293# 2. Mount point directory.
294# 3. Filesystem type.
295sub scan_mtab {
296 my $self = shift;
297 my $sub = shift;
298
299 open(MTAB, "/etc/mtab");
300 my $line;
301 my $result;
302 while ($line = <MTAB>) {
303 chomp($line);
304 my ($device, $directory, $type) = split(/\s+/, $line);
305 $result = $sub->($self->readlink($device), $directory, $type);
306 last if ($result);
307 }
308 close MTAB;
309
310 return $result;
311}
312
313sub setup {
314 my $self = shift;
315
316 # can only be executed in client context
317 if ($self->{execute_where} ne "client") {
318 $self->print_to_server_and_die(
319 "Script must be run on the client",
320 $Amanda::Script_App::ERROR
321 );
322 }
323
324 # resolve paths, if not already provided.
325 if (!defined $self->{lvcreate}) {
326 chomp($self->{lvcreate} = `which lvcreate`);
327 $self->print_to_server_and_die(
328 "lvcreate wasn't found.",
329 $Amanda::Script_App::ERROR
330 ) if $?;
331 }
332
333 if (!defined $self->{lvdisplay}) {
334 chomp($self->{lvdisplay} = `which lvdisplay`);
335 $self->print_to_server_and_die(
336 "lvdisplay wasn't found.",
337 $Amanda::Script_App::ERROR
338 ) if $?;
339 }
340
341 if (!defined $self->{lvremove}) {
342 chomp($self->{lvremove} = `which lvremove`);
343 $self->print_to_server_and_die(
344 "lvremove wasn't found.",
345 $Amanda::Script_App::ERROR
346 ) if $?;
347 }
348
349 if (!defined $self->{vgdisplay}) {
350 chomp($self->{vgdisplay} = `which vgdisplay`);
351 $self->print_to_server_and_die(
352 "vgdisplay wasn't found.",
353 $Amanda::Script_App::ERROR
354 ) if $?;
355 }
356
357 if (!defined $self->{blkid}) {
358 chomp($self->{blkid} = `which blkid`);
359 $self->print_to_server_and_die(
360 "blkid wasn't found.",
361 $Amanda::Script_App::ERROR
362 ) if $?;
363 }
364
365 # resolve actual lvm device
366 $self->resolve_device();
367
368 if (!defined $self->{volume_group}) {
369 $self->print_to_server_and_die(
370 "Failed to resolve device path and volume group.",
371 $Amanda::Script_App::ERROR
372 );
373 }
374}
375
376sub umount_snapshot {
377 my $self = shift;
378 my $device = $self->readlink($self->get_snap_device(0));
379
380 $device =~ s@\.\.@/dev@;
381 debug("umount_snapshot $device");
382
383 my $mnt = $self->scan_mtab(sub { return $_[1] if ($_[0] eq $device); });
384 if (!$mnt) {
385 $device = $self->readlink($self->get_snap_device(1));
386 $mnt = $self->scan_mtab(sub { return $_[1] if ($_[0] eq $device); });
387 }
388
389 if (!$mnt) {
390 $self->print_to_server_and_die(
391 "Failed to get mount point for snapshot device `$device'.",
392 $Amanda::Script_App::ERROR
393 );
394 }
395
396 $self->execute(1, "umount", $mnt);
397
398 debug("Un-mounted snapshot device `$device' from `$mnt'.");
399 rmdir $mnt;
400 debug("Remove snapshot mount point rmdir `$mnt'.");
401}
402
403
404sub command_support {
405 my $self = shift;
406
407 print "CONFIG YES\n";
408 print "HOST YES\n";
409 print "DISK YES\n";
410 print "MESSAGE-LINE YES\n";
411 print "MESSAGE-XML NO\n";
412 print "EXECUTE-WHERE YES\n";
413}
414
415#define a execute_on_* function for every execute_on you want the script to do
416#something
417sub command_pre_dle_backup {
418 my $self = shift;
419
420 $self->setup();
421 $self->create_snapshot();
422 $self->mount_snapshot();
423
424 print "PROPERTY directory $self->{directory}\n";
425}
426
427sub command_post_dle_backup {
428 my $self = shift;
429
430 $self->setup();
431 $self->umount_snapshot();
432 $self->remove_snapshot();
433}
434
435sub command_pre_dle_amcheck {
436 my $self = shift;
437
438 $self->setup();
439 $self->create_snapshot();
440 $self->mount_snapshot();
441
442 print "PROPERTY directory $self->{directory}\n";
443}
444
445sub command_post_dle_amcheck {
446 my $self = shift;
447
448 $self->setup();
449 $self->umount_snapshot();
450 $self->remove_snapshot();
451}
452
453package main;
454
455sub usage {
456 print <<EOF;
457Usage: amlvm-snapshot <command> --execute-where=client --config=<config> --host=<host> --disk=<disk> --device=<device> --level=<level> --index=<yes|no> --message=<text> --collection=<no> --record=<yes|no> --snapshot-size=<lvm snapshot size> --lvcreate-path=<path> --lvdisplay-path=<path> --lvremove-path=<path> --vgdisplay-path=<path> --blkid-path=<path> --sudo=<0|1>.
458EOF
459 exit(1);
460}
461
462my $opt_execute_where;
463my $opt_config;
464my $opt_host;
465my $opt_disk;
466my $opt_device;
467my @opt_level;
468my $opt_index;
469my $opt_message;
470my $opt_collection;
471my $opt_record;
472
473my $opt_snapsize;
474my $opt_lvcreate;
475my $opt_lvdisplay;
476my $opt_lvremove;
477my $opt_vgdisplay;
478my $opt_blkid;
479my $opt_sudo;
480
481Getopt::Long::Configure(qw{bundling});
482GetOptions(
483 'execute-where=s' => \$opt_execute_where,
484 'config=s' => \$opt_config,
485 'host=s' => \$opt_host,
486 'disk=s' => \$opt_disk,
487 'device=s' => \$opt_device,
488 'level=s' => \@opt_level,
489 'index=s' => \$opt_index,
490 'message=s' => \$opt_message,
491 'collection=s' => \$opt_collection,
492 'record=s' => \$opt_record,
493 'snapshot-size=s' => \$opt_snapsize,
494 'lvcreate-path=s' => \$opt_lvcreate,
495 'lvdisplay-path=s' => \$opt_lvdisplay,
496 'lvremove-path=s' => \$opt_lvremove,
497 'vgdisplay-path=s' => \$opt_vgdisplay,
498 'blkid=s' => \$opt_blkid,
499 'sudo=s' => \$opt_sudo,
500) or usage();
501
502# add SBIN to PATH
503$ENV{'PATH'} = "/sbin:/usr/sbin:$ENV{'PATH'}:/usr/local/sbin";
504
505my $script = Amanda::Script::Amlvm_snapshot->new($opt_execute_where,
506 $opt_config, $opt_host, $opt_disk, $opt_device, \@opt_level, $opt_index,
507 $opt_message, $opt_collection, $opt_record, $opt_snapsize, $opt_lvcreate,
508 $opt_lvdisplay, $opt_lvremove, $opt_vgdisplay, $opt_blkid, $opt_sudo);
509$script->do($ARGV[0]);
510
511# vim: set et sts=4 sw=4 :
This page took 0.341609 seconds and 4 git commands to generate.