From: Jan Rękorajski Date: Sat, 12 May 2012 10:23:21 +0000 (+0000) Subject: - scipt to backup lvm snapshots X-Git-Tag: auto/th/amanda-3_2_3-6~1 X-Git-Url: http://git.pld-linux.org/?p=packages%2Famanda.git;a=commitdiff_plain;h=9b104ea36f1996130e35327ff50ccf80f51e036d - scipt to backup lvm snapshots Changed files: amlvm-snapshot.README -> 1.1 amlvm-snapshot.conf -> 1.1 amlvm-snapshot.pl -> 1.1 --- diff --git a/amlvm-snapshot.README b/amlvm-snapshot.README new file mode 100644 index 0000000..2f0e615 --- /dev/null +++ b/amlvm-snapshot.README @@ -0,0 +1,98 @@ +Amanda LVM-snapshot Plugin +========================== + +This plugin provides support for LVM snapshots in Amanda dumps. It interfaces +with Amanda through the [Script API][1]. + +Install +------- + +Sorry, there's no Makefile yet. Simply copy the amlvm-snapshot.pl script into +Amanda's application directory. + +For example: + + cp amlvm-snapshot.pl /usr/libexec/amanda/application/amlvm-snapshot + +You may need to edit the location of Amanda's Perl libraries in the script +itself, the following line. + + use lib '/usr/local/share/perl/5.8.4'; + +Configure Amanda +---------------- + +Somewhere in your Amanda config you must define a `script-tool` that loads the +plugin. You can simply include the provided `lvm-snapshot.conf` file if you like. + + cp lvm-snapshot.conf /etc/amanda/DailySet1/lvm-snapshot.conf + echo 'includefile "lvm-snapshot.conf"' >> /etc/amanda/DailySet1/amanda.conf + +Once you have the `lvm-snapshot` `script-tool` defined, you can include it in +a `dumptype` definition. Note, however, that your dumptype _must use an +application-tool program_: Only application-tool programs can handle the +alternate mount point—of the snapshot device—that the script defines. + + define dumptype lvm-comp-amgtar { + comment "LVM snapshot dumped with amgtar" + global + program "APPLICATION" + application "app_amgtar" + script "lvm-snapshot" + compress client fast + index + } + +To allow amanda to dump more than one lvm-snapshot in parallel, +lvm-snapshot.conf has to have something like: + + property "SNAPSHOT-SIZE" "250" + +(the unit is PEs, for LVM PE Size 4,00 MiB it results in 1G snapshot-size) + +otherwise the first snapshot uses up all extents and the second snapshot fails. + +Configure Permissions +--------------------- + +This plugin requires elevated permissions in order to create and remove LVM +devices. There are two ways to provide access: setting setuid on the plugin +script itself, or by configuring `sudo` to allow execution of the LVM +programs. + +### setuid + +NOTE: I'm currently having trouble getting this to work right, as Amanda's +Perl libraries don't seem to play nice with setuid scripts. + +For setuid, simply configure the ownership and mode on `amlvm-snapshot`. In +this example, `disk` is the group that Amanda runs under. + + chown root:disk /usr/libexec/amanda/application/amlvm-snapshot + chmod 4750 /usr/libexec/amanda/application/amlvm-snapshot + +This will require that you have a version of Perl installed that was compiled +with `ENABLE_SUIDPERL`. + +### sudo + +For sudo, add the following to the `/etc/sudoers` file where "amandabackup" is +the name of your Amanda user. + + amandabackup ALL=(ALL) NOPASSWD: /sbin/lvcreate, /sbin/lvdisplay, /sbin/lvremove, /sbin/vgdisplay, /bin/readlink, /bin/mount, /bin/umount, /sbin/blkid + +The commands listed are those used by `amlvm-snapshot` to interact with the +LVM volumes. + +Remember to enable the `SUDO` property. This is already included in the +example `lvm-snapshot.conf` file. + + define script-tool lvm-snapshot { + # ... + property "SUDO" "1" + } + +Enjoy, +Daniel + +[1]: http://wiki.zmanda.com/index.php/Script_API diff --git a/amlvm-snapshot.conf b/amlvm-snapshot.conf new file mode 100644 index 0000000..5958cc0 --- /dev/null +++ b/amlvm-snapshot.conf @@ -0,0 +1,7 @@ +define script-tool lvm-snapshot { + comment "LVM snapshot" + plugin "amlvm-snapshot" + execute-where client + execute-on pre-dle-backup, post-dle-backup, pre-dle-amcheck, post-dle-amcheck + property "SUDO" "1" +} diff --git a/amlvm-snapshot.pl b/amlvm-snapshot.pl new file mode 100644 index 0000000..2ee2128 --- /dev/null +++ b/amlvm-snapshot.pl @@ -0,0 +1,511 @@ +#!/usr/bin/perl +# Copyright (c) 2010, Daniel Duvall. All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Author: Daniel Duvall + +# PROPERTY: +# +# SNAPSHOT-SIZE +# +# LVCREATE-PATH +# LVDISPLAY-PATH +# LVREMOVE-PATH +# VGDISPLAY-PATH +# +# SUDO +# +use lib '@@PERL_VENDORARCH@@'; +use strict; +use Getopt::Long; + +package Amanda::Script::Amlvm_snapshot; +use base qw(Amanda::Script); + +use Amanda::Config qw( :getconf :init ); +use Amanda::Debug qw( :logging ); +use Amanda::Util qw( :constants ); +use Amanda::Paths; +use Amanda::Constants; + +use Config; +use File::Temp qw(tempdir); +use IPC::Open3; +use Symbol; + +sub new { + my $class = shift; + my ($execute_where, $config, $host, $disk, $device, $level, $index, + $message, $collection, $record, $snapsize, $lvcreate, $lvdisplay, + $lvremove, $vgdisplay, $blkid, $sudo) = @_; + my $self = $class->SUPER::new($execute_where, $config); + + $self->{execute_where} = $execute_where; + $self->{config} = $config; + $self->{host} = $host; + $self->{device} = $device; + $self->{disk} = $disk; + $self->{level} = [ @{$level} ]; # Copy the array + $self->{index} = $index; + $self->{message} = $message; + $self->{collection} = $collection; + $self->{record} = $record; + + $self->{snapsize} = $snapsize; + + $self->{lvcreate} = $lvcreate; + $self->{lvdisplay} = $lvdisplay; + $self->{lvremove} = $lvremove; + $self->{vgdisplay} = $vgdisplay; + $self->{blkid} = $blkid; + + $self->{sudo} = $sudo; + + $self->{volume_group} = undef; + $self->{fs_type} = undef; + + return $self; +} + +sub calculate_snapsize { + my $self = shift; + + my $size; + + # if a snapshot size isn't already set, use all available extents in the + # volume group + if (!defined $self->{snapsize}) { + foreach ($self->execute(1, "$self->{vgdisplay} -c")) { + my @parts = split(/:/); + my $group = $parts[0]; + my $total = $parts[13]; + my $alloc = $parts[14]; + + $group =~ s/^\s*//; + chomp($group); + + if ($group eq $self->{volume_group}) { + $self->{snapsize} = $total - $alloc; + last; + } + } + } + + # fallback to just 1 extent (though this might fail anyway) + $self->{snapsize} = 1 if (!defined $self->{snapsize}); +} + +sub create_snapshot { + my $self = shift; + + # calculate default snapshot size + $self->calculate_snapsize(); + + debug("A snapshot of size `$self->{snapsize}' will be created."); + + my @parts = split('/', $self->{device}); + my $vg_name = $parts[2]; + my $lv_name = $parts[3]; + + # create a new snapshot with lvcreate + $self->execute(1, + "$self->{lvcreate}", "--extents", $self->{snapsize}, + "--snapshot", "--name", "amsnap-$vg_name-$lv_name", $self->{device} + ); + my $snapshot_device = $self->get_snap_device(0); + + debug("Created snapshot of `$self->{device}' at `$snapshot_device'."); +} + +# Executes (safely) the given command and arguments. Optional execution +# through sudo can be specified, but will only occur if the script was invoked +# with the '--sudo' argument. +sub execute { + my $self = shift; + my $sudo = shift; + my $cmd = shift; + + # escape all given arguments + my @args = map(quotemeta, @_); + + my ($in, $out, $err, $pid); + $err = Symbol::gensym; + + my $full_cmd = ($sudo and $self->{sudo}) ? "sudo $cmd" : $cmd; + + $full_cmd .= " @args"; + + $pid = open3($in, $out, $err, $full_cmd); + + close($in); + + my @output = <$out>; + my @errors = <$err>; + + close($out); + close($err); + + waitpid($pid, 0); + + # NOTE There's an exception for readlink, as it's failure isn't critical. + if ($? > 0 and $cmd ne "readlink") { + my $err_str = join("", @errors); + chomp($err_str); + + $self->print_to_server_and_die( + "Failed to execute (status $?) `$full_cmd': $err_str", + $Amanda::Script_App::ERROR + ); + } + + return @output; +} + +# Returns the snapshot device path. +sub get_snap_device { + my $self = shift; + my $mapper = shift; + my @parts = split('/', $self->{device}); + my $vg_name = $parts[2]; + my $lv_name = $parts[3]; + + if ($mapper) { + return "/dev/mapper/$self->{volume_group}-amsnap--$vg_name--$lv_name"; + } else { + return "/dev/$self->{volume_group}/amsnap-$vg_name-$lv_name"; + } +} + +# Mounts the snapshot device at the configured directory. +sub mount_snapshot { + my $self = shift; + + # mount options + my @options = ('ro'); + + # special mount options for xfs + # XXX should this be left up to the user as an argument? + if ($self->{fs_type} eq 'xfs') { + push(@options, 'nouuid'); + } + + # create a temporary mount point and mount the snapshot volume + $self->{directory} = tempdir(CLEANUP => 0); + my $snapshot_device = $self->get_snap_device(0); + $self->execute(1, + "mount -o ", join(",", @options), + $snapshot_device, $self->{directory} + ); + + debug("Mounted snapshot `$snapshot_device' at `$self->{directory}'."); +} + +# Readlink wrapper. +sub readlink { + my $self = shift; + my $path = shift; + + # NOTE: We don't use perl's readlink here, because it might need to be + # executed with elevated privileges (sudo). + my $real_path = join("", $self->execute(1, "readlink", $path)); + chomp($real_path); + $real_path =~ s@\.\.@/dev@; + + return ($real_path ne "") ? $real_path : $path; +} + +# Removes the snapshot device. +sub remove_snapshot { + my $self = shift; + + # remove snapshot device with 'lvremove' + $self->execute(1, "$self->{lvremove} -f", $self->get_snap_device(0)); + + debug("Removed snapshot of `$self->{device}'."); +} + +# Resolves the underlying device on which the configured directory resides. +sub resolve_device { + my $self = shift; + + # Search mtab for the mount point. Get the device path and filesystem type. + my $mnt_device = $self->scan_mtab( + sub { return $_[0] if ($_[1] eq $self->{disk}); } + ); + + my $fs_type = $self->scan_mtab( + sub { return $_[2] if ($_[1] eq $self->{disk}); } + ); + + if (!$mnt_device) { + if ($self->{disk} eq $self->{device}) { + $self->print_to_server_and_die( + "Failed to resolve a device from directory `$self->{disk}'. ", + $Amanda::Script_App::ERROR + ); + } else { + $mnt_device = $self->{device}; + $fs_type = join("", $self->execute(1, "$self->{blkid} -s TYPE -o value $self->{device}")); + chomp($fs_type); + } + } + + # loop through the LVs to find the one that matches + foreach ($self->execute(1, "$self->{lvdisplay} -c")) { + my ($device, $group) = split(/:/); + + $device =~ s/^\s*//; + chomp($device); + + my $real_device = $self->readlink($device); + chomp($real_device); + + if ($real_device eq $mnt_device) { + $self->{device} = $device; + $self->{volume_group} = $group; + $self->{fs_type} = $fs_type; + + debug( + "Resolved device `$self->{device}' and volume group ". + "`$self->{volume_group}' from mount point `$self->{disk}'." + ); + + last; + } + } +} + +# Iterates over lines in the system mtab and invokes the given anonymous +# subroutine with entries from each record: +# 1. Canonical device path (as resolved from readlink). +# 2. Mount point directory. +# 3. Filesystem type. +sub scan_mtab { + my $self = shift; + my $sub = shift; + + open(MTAB, "/etc/mtab"); + my $line; + my $result; + while ($line = ) { + chomp($line); + my ($device, $directory, $type) = split(/\s+/, $line); + $result = $sub->($self->readlink($device), $directory, $type); + last if ($result); + } + close MTAB; + + return $result; +} + +sub setup { + my $self = shift; + + # can only be executed in client context + if ($self->{execute_where} ne "client") { + $self->print_to_server_and_die( + "Script must be run on the client", + $Amanda::Script_App::ERROR + ); + } + + # resolve paths, if not already provided. + if (!defined $self->{lvcreate}) { + chomp($self->{lvcreate} = `which lvcreate`); + $self->print_to_server_and_die( + "lvcreate wasn't found.", + $Amanda::Script_App::ERROR + ) if $?; + } + + if (!defined $self->{lvdisplay}) { + chomp($self->{lvdisplay} = `which lvdisplay`); + $self->print_to_server_and_die( + "lvdisplay wasn't found.", + $Amanda::Script_App::ERROR + ) if $?; + } + + if (!defined $self->{lvremove}) { + chomp($self->{lvremove} = `which lvremove`); + $self->print_to_server_and_die( + "lvremove wasn't found.", + $Amanda::Script_App::ERROR + ) if $?; + } + + if (!defined $self->{vgdisplay}) { + chomp($self->{vgdisplay} = `which vgdisplay`); + $self->print_to_server_and_die( + "vgdisplay wasn't found.", + $Amanda::Script_App::ERROR + ) if $?; + } + + if (!defined $self->{blkid}) { + chomp($self->{blkid} = `which blkid`); + $self->print_to_server_and_die( + "blkid wasn't found.", + $Amanda::Script_App::ERROR + ) if $?; + } + + # resolve actual lvm device + $self->resolve_device(); + + if (!defined $self->{volume_group}) { + $self->print_to_server_and_die( + "Failed to resolve device path and volume group.", + $Amanda::Script_App::ERROR + ); + } +} + +sub umount_snapshot { + my $self = shift; + my $device = $self->readlink($self->get_snap_device(0)); + + $device =~ s@\.\.@/dev@; + debug("umount_snapshot $device"); + + my $mnt = $self->scan_mtab(sub { return $_[1] if ($_[0] eq $device); }); + if (!$mnt) { + $device = $self->readlink($self->get_snap_device(1)); + $mnt = $self->scan_mtab(sub { return $_[1] if ($_[0] eq $device); }); + } + + if (!$mnt) { + $self->print_to_server_and_die( + "Failed to get mount point for snapshot device `$device'.", + $Amanda::Script_App::ERROR + ); + } + + $self->execute(1, "umount", $mnt); + + debug("Un-mounted snapshot device `$device' from `$mnt'."); + rmdir $mnt; + debug("Remove snapshot mount point rmdir `$mnt'."); +} + + +sub command_support { + my $self = shift; + + print "CONFIG YES\n"; + print "HOST YES\n"; + print "DISK YES\n"; + print "MESSAGE-LINE YES\n"; + print "MESSAGE-XML NO\n"; + print "EXECUTE-WHERE YES\n"; +} + +#define a execute_on_* function for every execute_on you want the script to do +#something +sub command_pre_dle_backup { + my $self = shift; + + $self->setup(); + $self->create_snapshot(); + $self->mount_snapshot(); + + print "PROPERTY directory $self->{directory}\n"; +} + +sub command_post_dle_backup { + my $self = shift; + + $self->setup(); + $self->umount_snapshot(); + $self->remove_snapshot(); +} + +sub command_pre_dle_amcheck { + my $self = shift; + + $self->setup(); + $self->create_snapshot(); + $self->mount_snapshot(); + + print "PROPERTY directory $self->{directory}\n"; +} + +sub command_post_dle_amcheck { + my $self = shift; + + $self->setup(); + $self->umount_snapshot(); + $self->remove_snapshot(); +} + +package main; + +sub usage { + print < --execute-where=client --config= --host= --disk= --device= --level= --index= --message= --collection= --record= --snapshot-size= --lvcreate-path= --lvdisplay-path= --lvremove-path= --vgdisplay-path= --blkid-path= --sudo=<0|1>. +EOF + exit(1); +} + +my $opt_execute_where; +my $opt_config; +my $opt_host; +my $opt_disk; +my $opt_device; +my @opt_level; +my $opt_index; +my $opt_message; +my $opt_collection; +my $opt_record; + +my $opt_snapsize; +my $opt_lvcreate; +my $opt_lvdisplay; +my $opt_lvremove; +my $opt_vgdisplay; +my $opt_blkid; +my $opt_sudo; + +Getopt::Long::Configure(qw{bundling}); +GetOptions( + 'execute-where=s' => \$opt_execute_where, + 'config=s' => \$opt_config, + 'host=s' => \$opt_host, + 'disk=s' => \$opt_disk, + 'device=s' => \$opt_device, + 'level=s' => \@opt_level, + 'index=s' => \$opt_index, + 'message=s' => \$opt_message, + 'collection=s' => \$opt_collection, + 'record=s' => \$opt_record, + 'snapshot-size=s' => \$opt_snapsize, + 'lvcreate-path=s' => \$opt_lvcreate, + 'lvdisplay-path=s' => \$opt_lvdisplay, + 'lvremove-path=s' => \$opt_lvremove, + 'vgdisplay-path=s' => \$opt_vgdisplay, + 'blkid=s' => \$opt_blkid, + 'sudo=s' => \$opt_sudo, +) or usage(); + +# add SBIN to PATH +$ENV{'PATH'} = "/sbin:/usr/sbin:$ENV{'PATH'}:/usr/local/sbin"; + +my $script = Amanda::Script::Amlvm_snapshot->new($opt_execute_where, + $opt_config, $opt_host, $opt_disk, $opt_device, \@opt_level, $opt_index, + $opt_message, $opt_collection, $opt_record, $opt_snapsize, $opt_lvcreate, + $opt_lvdisplay, $opt_lvremove, $opt_vgdisplay, $opt_blkid, $opt_sudo); +$script->do($ARGV[0]); + +# vim: set et sts=4 sw=4 :