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>
30 use lib '@@PERL_VENDORARCH@@';
34 package Amanda::Script::Amlvm_snapshot;
35 use base qw(Amanda::Script);
37 use Amanda::Config qw( :getconf :init );
38 use Amanda::Debug qw( :logging );
39 use Amanda::Util qw( :constants );
41 use Amanda::Constants;
44 use File::Temp qw(tempdir);
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);
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;
66 $self->{snapsize} = $snapsize;
68 $self->{lvcreate} = $lvcreate;
69 $self->{lvdisplay} = $lvdisplay;
70 $self->{lvremove} = $lvremove;
71 $self->{vgdisplay} = $vgdisplay;
72 $self->{blkid} = $blkid;
74 $self->{sudo} = $sudo;
76 $self->{volume_group} = undef;
77 $self->{fs_type} = undef;
82 sub calculate_snapsize {
87 # if a snapshot size isn't already set, use all available extents in the
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];
99 if ($group eq $self->{volume_group}) {
100 $self->{snapsize} = $total - $alloc;
106 # fallback to just 1 extent (though this might fail anyway)
107 $self->{snapsize} = 1 if (!defined $self->{snapsize});
110 sub create_snapshot {
113 # calculate default snapshot size
114 $self->calculate_snapsize();
116 debug("A snapshot of size `$self->{snapsize}' will be created.");
118 my @parts = split('/', $self->{device});
119 my $vg_name = $parts[2];
120 my $lv_name = $parts[3];
122 # create a new snapshot with lvcreate
124 "$self->{lvcreate}", "--extents", $self->{snapsize},
125 "--snapshot", "--name", "amsnap-$vg_name-$lv_name", $self->{device}
127 my $snapshot_device = $self->get_snap_device(0);
129 debug("Created snapshot of `$self->{device}' at `$snapshot_device'.");
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.
140 # escape all given arguments
141 my @args = map(quotemeta, @_);
143 my ($in, $out, $err, $pid);
144 $err = Symbol::gensym;
146 my $full_cmd = ($sudo and $self->{sudo}) ? "sudo $cmd" : $cmd;
148 $full_cmd .= " @args";
150 $pid = open3($in, $out, $err, $full_cmd);
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);
167 $self->print_to_server_and_die(
168 "Failed to execute (status $?) `$full_cmd': $err_str",
169 $Amanda::Script_App::ERROR
176 # Returns the snapshot device path.
177 sub get_snap_device {
180 my @parts = split('/', $self->{device});
181 my $vg_name = $parts[2];
182 my $lv_name = $parts[3];
185 return "/dev/mapper/$self->{volume_group}-amsnap--$vg_name--$lv_name";
187 return "/dev/$self->{volume_group}/amsnap-$vg_name-$lv_name";
191 # Mounts the snapshot device at the configured directory.
196 my @options = ('ro');
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');
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);
208 "mount -o ", join(",", @options),
209 $snapshot_device, $self->{directory}
212 debug("Mounted snapshot `$snapshot_device' at `$self->{directory}'.");
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));
224 $real_path =~ s@\.\.@/dev@;
226 return ($real_path ne "") ? $real_path : $path;
229 # Removes the snapshot device.
230 sub remove_snapshot {
233 # remove snapshot device with 'lvremove'
234 $self->execute(1, "$self->{lvremove} -f", $self->get_snap_device(0));
236 debug("Removed snapshot of `$self->{device}'.");
239 # Resolves the underlying device on which the configured directory resides.
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}); }
248 my $fs_type = $self->scan_mtab(
249 sub { return $_[2] if ($_[1] eq $self->{disk}); }
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
259 $mnt_device = $self->{device};
260 $fs_type = join("", $self->execute(1, "$self->{blkid} -s TYPE -o value $self->{device}"));
265 # loop through the LVs to find the one that matches
266 foreach ($self->execute(1, "$self->{lvdisplay} -c")) {
267 my ($device, $group) = split(/:/);
272 my $real_device = $self->readlink($device);
275 if ($real_device eq $mnt_device) {
276 $self->{device} = $device;
277 $self->{volume_group} = $group;
278 $self->{fs_type} = $fs_type;
281 "Resolved device `$self->{device}' and volume group ".
282 "`$self->{volume_group}' from mount point `$self->{disk}'."
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.
299 open(MTAB, "/etc/mtab");
302 while ($line = <MTAB>) {
304 my ($device, $directory, $type) = split(/\s+/, $line);
305 $result = $sub->($self->readlink($device), $directory, $type);
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
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
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
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
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
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
365 # resolve actual lvm device
366 $self->resolve_device();
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
376 sub umount_snapshot {
378 my $device = $self->readlink($self->get_snap_device(0));
380 $device =~ s@\.\.@/dev@;
381 debug("umount_snapshot $device");
383 my $mnt = $self->scan_mtab(sub { return $_[1] if ($_[0] eq $device); });
385 $device = $self->readlink($self->get_snap_device(1));
386 $mnt = $self->scan_mtab(sub { return $_[1] if ($_[0] eq $device); });
390 $self->print_to_server_and_die(
391 "Failed to get mount point for snapshot device `$device'.",
392 $Amanda::Script_App::ERROR
396 $self->execute(1, "umount", $mnt);
398 debug("Un-mounted snapshot device `$device' from `$mnt'.");
400 debug("Remove snapshot mount point rmdir `$mnt'.");
404 sub command_support {
407 print "CONFIG YES\n";
410 print "MESSAGE-LINE YES\n";
411 print "MESSAGE-XML NO\n";
412 print "EXECUTE-WHERE YES\n";
415 #define a execute_on_* function for every execute_on you want the script to do
417 sub command_pre_dle_backup {
421 $self->create_snapshot();
422 $self->mount_snapshot();
424 print "PROPERTY directory $self->{directory}\n";
427 sub command_post_dle_backup {
431 $self->umount_snapshot();
432 $self->remove_snapshot();
435 sub command_pre_dle_amcheck {
439 $self->create_snapshot();
440 $self->mount_snapshot();
442 print "PROPERTY directory $self->{directory}\n";
445 sub command_post_dle_amcheck {
449 $self->umount_snapshot();
450 $self->remove_snapshot();
457 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> --sudo=<0|1>.
462 my $opt_execute_where;
481 Getopt::Long::Configure(qw{bundling});
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,
503 $ENV{'PATH'} = "/sbin:/usr/sbin:$ENV{'PATH'}:/usr/local/sbin";
505 my $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]);
511 # vim: set et sts=4 sw=4 :