]> git.pld-linux.org Git - packages/amanda.git/blob - amlvm-snapshot.pl
2ee2128a7300631815ef98fc7c0fbb66015ae40c
[packages/amanda.git] / amlvm-snapshot.pl
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 #
30 use lib '@@PERL_VENDORARCH@@';
31 use strict;
32 use Getopt::Long;
33
34 package Amanda::Script::Amlvm_snapshot;
35 use base qw(Amanda::Script);
36
37 use Amanda::Config qw( :getconf :init );
38 use Amanda::Debug qw( :logging );
39 use Amanda::Util qw( :constants );
40 use Amanda::Paths;
41 use Amanda::Constants;
42
43 use Config;
44 use File::Temp qw(tempdir);
45 use IPC::Open3;
46 use Symbol;
47
48 sub 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
82 sub 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
110 sub 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.
135 sub 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.
177 sub 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.
192 sub 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.
216 sub 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.
230 sub 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.
240 sub 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.
295 sub 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
313 sub 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
376 sub 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
404 sub 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
417 sub 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
427 sub command_post_dle_backup {
428     my $self = shift;
429
430     $self->setup();
431     $self->umount_snapshot();
432     $self->remove_snapshot();
433 }
434
435 sub 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
445 sub command_post_dle_amcheck {
446     my $self = shift;
447
448     $self->setup();
449     $self->umount_snapshot();
450     $self->remove_snapshot();
451 }
452
453 package main;
454
455 sub usage {
456     print <<EOF;
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>.
458 EOF
459     exit(1);
460 }
461
462 my $opt_execute_where;
463 my $opt_config;
464 my $opt_host;
465 my $opt_disk;
466 my $opt_device;
467 my @opt_level;
468 my $opt_index;
469 my $opt_message;
470 my $opt_collection;
471 my $opt_record;
472
473 my $opt_snapsize;
474 my $opt_lvcreate;
475 my $opt_lvdisplay;
476 my $opt_lvremove;
477 my $opt_vgdisplay;
478 my $opt_blkid;
479 my $opt_sudo;
480
481 Getopt::Long::Configure(qw{bundling});
482 GetOptions(
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
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]);
510
511 # vim: set et sts=4 sw=4 :
This page took 0.057411 seconds and 2 git commands to generate.