2 # Copyright (c) 2010, Daniel Duvall. All Rights Reserved.
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.
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
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
17 # Author: Daniel Duvall <the.liberal.media@gmail.com>
31 use lib '@@PERL_VENDORARCH@@';
35 package Amanda::Script::Amlvm_snapshot;
36 use base qw(Amanda::Script);
38 use Amanda::Config qw( :getconf :init );
39 use Amanda::Debug qw( :logging );
40 use Amanda::Util qw( :constants );
42 use Amanda::Constants;
45 use File::Temp qw(tempdir);
46 use File::Path qw(make_path);
49 use Digest::MD5 qw(md5_hex);
53 my ($execute_where, $config, $host, $disk, $device, $level, $index,
54 $message, $collection, $record, $snapsize, $lvcreate, $lvdisplay,
55 $lvremove, $vgdisplay, $blkid, $stablemount, $sudo) = @_;
56 my $self = $class->SUPER::new($execute_where, $config);
58 $self->{execute_where} = $execute_where;
59 $self->{config} = $config;
60 $self->{host} = $host;
61 $self->{device} = $device;
62 $self->{disk} = $disk;
63 $self->{level} = [ @{$level} ]; # Copy the array
64 $self->{index} = $index;
65 $self->{message} = $message;
66 $self->{collection} = $collection;
67 $self->{record} = $record;
69 $self->{snapsize} = $snapsize;
71 $self->{lvcreate} = $lvcreate;
72 $self->{lvdisplay} = $lvdisplay;
73 $self->{lvremove} = $lvremove;
74 $self->{vgdisplay} = $vgdisplay;
75 $self->{blkid} = $blkid;
77 $self->{stablemount} = $stablemount;
78 $self->{sudo} = $sudo;
80 $self->{volume_group} = undef;
81 $self->{fs_type} = undef;
86 sub calculate_snapsize {
91 # if a snapshot size isn't already set, use all available extents in the
93 if (!defined $self->{snapsize}) {
94 foreach ($self->execute(1, "$self->{vgdisplay} -c")) {
95 my @parts = split(/:/);
96 my $group = $parts[0];
97 my $total = $parts[13];
98 my $alloc = $parts[14];
103 if ($group eq $self->{volume_group}) {
104 $self->{snapsize} = $total - $alloc;
110 # fallback to just 1 extent (though this might fail anyway)
111 $self->{snapsize} = 1 if (!defined $self->{snapsize});
114 sub create_snapshot {
117 # calculate default snapshot size
118 $self->calculate_snapsize();
120 debug("A snapshot of size `$self->{snapsize}' will be created.");
122 my @parts = split('/', $self->{device});
123 my $vg_name = $parts[2];
124 my $lv_name = $parts[3];
126 # create a new snapshot with lvcreate
128 "$self->{lvcreate}", "--extents", $self->{snapsize},
129 "--snapshot", "--name", "amsnap-$vg_name-$lv_name", $self->{device}
131 my $snapshot_device = $self->get_snap_device(0);
133 debug("Created snapshot of `$self->{device}' at `$snapshot_device'.");
136 # Executes (safely) the given command and arguments. Optional execution
137 # through sudo can be specified, but will only occur if the script was invoked
138 # with the '--sudo' argument.
144 # escape all given arguments
145 my @args = map(quotemeta, @_);
147 my ($in, $out, $err, $pid);
148 $err = Symbol::gensym;
150 my $full_cmd = ($sudo and $self->{sudo}) ? "sudo $cmd" : $cmd;
152 $full_cmd .= " @args";
154 $pid = open3($in, $out, $err, $full_cmd);
166 # NOTE There's an exception for readlink, as it's failure isn't critical.
167 if ($? > 0 and $cmd ne "readlink") {
168 my $err_str = join("", @errors);
171 $self->print_to_server_and_die(
172 "Failed to execute (status $?) `$full_cmd': $err_str",
173 $Amanda::Script_App::ERROR
180 # Returns the snapshot device path.
181 sub get_snap_device {
184 my @parts = split('/', $self->{device});
185 my $vg_name = $parts[2];
186 my $lv_name = $parts[3];
189 return "/dev/mapper/$self->{volume_group}-amsnap--$vg_name--$lv_name";
191 return "/dev/$self->{volume_group}/amsnap-$vg_name-$lv_name";
195 # Mounts the snapshot device at the configured directory.
200 my @options = ('ro');
202 # special mount options for xfs
203 # XXX should this be left up to the user as an argument?
204 if ($self->{fs_type} eq 'xfs') {
205 push(@options, 'nouuid');
208 # create a temporary mount point and mount the snapshot volume
209 if ($self->{stablemount}) {
210 $self->{directory} = File::Spec->tmpdir . "/" . md5_hex($self->{disk});
211 make_path($self->{directory});
213 $self->{directory} = tempdir(CLEANUP => 0);
215 my $snapshot_device = $self->get_snap_device(0);
217 "mount -o ", join(",", @options),
218 $snapshot_device, $self->{directory}
221 debug("Mounted snapshot `$snapshot_device' at `$self->{directory}'.");
229 # NOTE: We don't use perl's readlink here, because it might need to be
230 # executed with elevated privileges (sudo).
231 my $real_path = join("", $self->execute(1, "readlink", $path));
233 $real_path =~ s@\.\.@/dev@;
235 return ($real_path ne "") ? $real_path : $path;
238 # Removes the snapshot device.
239 sub remove_snapshot {
242 # remove snapshot device with 'lvremove'
243 $self->execute(1, "$self->{lvremove} -f", $self->get_snap_device(0));
245 debug("Removed snapshot of `$self->{device}'.");
248 # Resolves the underlying device on which the configured directory resides.
252 # Search mtab for the mount point. Get the device path and filesystem type.
253 my $mnt_device = $self->scan_mtab(
254 sub { return $_[0] if ($_[1] eq $self->{disk}); }
257 my $fs_type = $self->scan_mtab(
258 sub { return $_[2] if ($_[1] eq $self->{disk}); }
262 if ($self->{disk} eq $self->{device}) {
263 $self->print_to_server_and_die(
264 "Failed to resolve a device from directory `$self->{disk}'. ",
265 $Amanda::Script_App::ERROR
268 $mnt_device = $self->{device};
269 $fs_type = join("", $self->execute(1, "$self->{blkid} -s TYPE -o value $self->{device}"));
274 # loop through the LVs to find the one that matches
275 foreach ($self->execute(1, "$self->{lvdisplay} -c")) {
276 my ($device, $group) = split(/:/);
281 my $real_device = $self->readlink($device);
284 if (($device eq $mnt_device) || ($real_device eq $mnt_device)) {
285 $self->{device} = $device;
286 $self->{volume_group} = $group;
287 $self->{fs_type} = $fs_type;
290 "Resolved device `$self->{device}' and volume group ".
291 "`$self->{volume_group}' from mount point `$self->{disk}'."
299 # Iterates over lines in the system mtab and invokes the given anonymous
300 # subroutine with entries from each record:
301 # 1. Canonical device path (as resolved from readlink).
302 # 2. Mount point directory.
303 # 3. Filesystem type.
308 open(MTAB, "/etc/mtab");
311 while ($line = <MTAB>) {
313 my ($device, $directory, $type) = split(/\s+/, $line);
314 $result = $sub->($self->readlink($device), $directory, $type);
325 # can only be executed in client context
326 if ($self->{execute_where} ne "client") {
327 $self->print_to_server_and_die(
328 "Script must be run on the client",
329 $Amanda::Script_App::ERROR
333 # resolve paths, if not already provided.
334 if (!defined $self->{lvcreate}) {
335 chomp($self->{lvcreate} = `which lvcreate`);
336 $self->print_to_server_and_die(
337 "lvcreate wasn't found.",
338 $Amanda::Script_App::ERROR
342 if (!defined $self->{lvdisplay}) {
343 chomp($self->{lvdisplay} = `which lvdisplay`);
344 $self->print_to_server_and_die(
345 "lvdisplay wasn't found.",
346 $Amanda::Script_App::ERROR
350 if (!defined $self->{lvremove}) {
351 chomp($self->{lvremove} = `which lvremove`);
352 $self->print_to_server_and_die(
353 "lvremove wasn't found.",
354 $Amanda::Script_App::ERROR
358 if (!defined $self->{vgdisplay}) {
359 chomp($self->{vgdisplay} = `which vgdisplay`);
360 $self->print_to_server_and_die(
361 "vgdisplay wasn't found.",
362 $Amanda::Script_App::ERROR
366 if (!defined $self->{blkid}) {
367 chomp($self->{blkid} = `which blkid`);
368 $self->print_to_server_and_die(
369 "blkid wasn't found.",
370 $Amanda::Script_App::ERROR
374 # resolve actual lvm device
375 $self->resolve_device();
377 if (!defined $self->{volume_group}) {
378 $self->print_to_server_and_die(
379 "Failed to resolve device path and volume group.",
380 $Amanda::Script_App::ERROR
385 sub umount_snapshot {
387 my $device = $self->readlink($self->get_snap_device(0));
389 $device =~ s@\.\.@/dev@;
390 debug("umount_snapshot $device");
392 my $mnt = $self->scan_mtab(sub { return $_[1] if ($_[0] eq $device); });
394 $device = $self->readlink($self->get_snap_device(1));
395 $mnt = $self->scan_mtab(sub { return $_[1] if ($_[0] eq $device); });
399 $self->print_to_server_and_die(
400 "Failed to get mount point for snapshot device `$device'.",
401 $Amanda::Script_App::ERROR
405 $self->execute(1, "umount", $mnt);
407 debug("Un-mounted snapshot device `$device' from `$mnt'.");
409 debug("Remove snapshot mount point rmdir `$mnt'.");
413 sub command_support {
416 print "CONFIG YES\n";
419 print "MESSAGE-LINE YES\n";
420 print "MESSAGE-XML NO\n";
421 print "EXECUTE-WHERE YES\n";
424 #define a execute_on_* function for every execute_on you want the script to do
426 sub command_pre_dle_backup {
430 $self->create_snapshot();
431 $self->mount_snapshot();
433 print "PROPERTY directory $self->{directory}\n";
436 sub command_post_dle_backup {
440 $self->umount_snapshot();
441 $self->remove_snapshot();
444 sub command_pre_dle_amcheck {
448 $self->create_snapshot();
449 $self->mount_snapshot();
451 print "PROPERTY directory $self->{directory}\n";
454 sub command_post_dle_amcheck {
458 $self->umount_snapshot();
459 $self->remove_snapshot();
466 Usage: 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> --stable-mountpoint=<0|1> --sudo=<0|1>.
471 my $opt_execute_where;
491 Getopt::Long::Configure(qw{bundling});
493 'execute-where=s' => \$opt_execute_where,
494 'config=s' => \$opt_config,
495 'host=s' => \$opt_host,
496 'disk=s' => \$opt_disk,
497 'device=s' => \$opt_device,
498 'level=s' => \@opt_level,
499 'index=s' => \$opt_index,
500 'message=s' => \$opt_message,
501 'collection=s' => \$opt_collection,
502 'record=s' => \$opt_record,
503 'snapshot-size=s' => \$opt_snapsize,
504 'lvcreate-path=s' => \$opt_lvcreate,
505 'lvdisplay-path=s' => \$opt_lvdisplay,
506 'lvremove-path=s' => \$opt_lvremove,
507 'vgdisplay-path=s' => \$opt_vgdisplay,
508 'blkid=s' => \$opt_blkid,
509 'stable-mountpoint=s' => \$opt_stablemount,
510 'sudo=s' => \$opt_sudo,
514 $ENV{'PATH'} = "/sbin:/usr/sbin:$ENV{'PATH'}:/usr/local/sbin";
516 my $script = Amanda::Script::Amlvm_snapshot->new($opt_execute_where,
517 $opt_config, $opt_host, $opt_disk, $opt_device, \@opt_level, $opt_index,
518 $opt_message, $opt_collection, $opt_record, $opt_snapsize, $opt_lvcreate,
519 $opt_lvdisplay, $opt_lvremove, $opt_vgdisplay, $opt_blkid, $opt_stablemount, $opt_sudo);
520 $script->do($ARGV[0]);
522 # vim: set et sts=4 sw=4 :