]> git.pld-linux.org Git - packages/amanda.git/blob - amlvm-snapshot.pl
perl 5.38.0 rebuild
[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 #    STABLE-MOUNTPOINT
29 #    SUDO
30 #
31 use lib '@@PERL_VENDORARCH@@';
32 use strict;
33 use Getopt::Long;
34
35 package Amanda::Script::Amlvm_snapshot;
36 use base qw(Amanda::Script);
37
38 use Amanda::Config qw( :getconf :init );
39 use Amanda::Debug qw( :logging );
40 use Amanda::Util qw( :constants );
41 use Amanda::Paths;
42 use Amanda::Constants;
43
44 use Config;
45 use File::Temp qw(tempdir);
46 use File::Path qw(make_path);
47 use IPC::Open3;
48 use Symbol;
49 use Digest::MD5 qw(md5_hex);
50
51 sub new {
52     my $class = shift;
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);
57
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;
68
69     $self->{snapsize}       = $snapsize;
70
71     $self->{lvcreate}       = $lvcreate;
72     $self->{lvdisplay}      = $lvdisplay;
73     $self->{lvremove}       = $lvremove;
74     $self->{vgdisplay}      = $vgdisplay;
75     $self->{blkid}          = $blkid;
76
77     $self->{stablemount}    = $stablemount;
78     $self->{sudo}           = $sudo;
79
80     $self->{volume_group}   = undef;
81     $self->{fs_type}        = undef;
82
83     return $self;
84 }
85
86 sub calculate_snapsize {
87     my $self = shift;
88
89     my $size;
90
91     # if a snapshot size isn't already set, use all available extents in the
92     # volume group
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];
99
100             $group =~ s/^\s*//;
101             chomp($group);
102
103             if ($group eq $self->{volume_group}) {
104                 $self->{snapsize} = $total - $alloc;
105                 last;
106             }
107         }
108     }
109
110     # fallback to just 1 extent (though this might fail anyway)
111     $self->{snapsize} = 1 if (!defined $self->{snapsize});
112 }
113
114 sub create_snapshot {
115     my $self = shift;
116
117     # calculate default snapshot size
118     $self->calculate_snapsize();
119
120     debug("A snapshot of size `$self->{snapsize}' will be created.");
121
122     my @parts = split('/', $self->{device});
123     my $vg_name = $parts[2];
124     my $lv_name = $parts[3];
125
126     # create a new snapshot with lvcreate
127     $self->execute(1,
128         "$self->{lvcreate}", "--extents", $self->{snapsize},
129         "--snapshot", "--name", "amsnap-$vg_name-$lv_name", $self->{device}
130     );
131     my $snapshot_device = $self->get_snap_device(0);
132
133     debug("Created snapshot of `$self->{device}' at `$snapshot_device'.");
134 }
135
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.
139 sub execute {
140     my $self = shift;
141     my $sudo = shift;
142     my $cmd = shift;
143
144     # escape all given arguments
145     my @args = map(quotemeta, @_);
146
147     my ($in, $out, $err, $pid);
148     $err = Symbol::gensym;
149
150     my $full_cmd = ($sudo and $self->{sudo}) ? "sudo $cmd" : $cmd;
151
152     $full_cmd .= " @args";
153
154     $pid = open3($in, $out, $err, $full_cmd);
155
156     close($in);
157
158     my @output = <$out>;
159     my @errors = <$err>;
160
161     close($out);
162     close($err);
163
164     waitpid($pid, 0);
165
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);
169         chomp($err_str);
170
171         $self->print_to_server_and_die(
172             "Failed to execute (status $?) `$full_cmd': $err_str",
173             $Amanda::Script_App::ERROR
174         );
175     }
176
177     return @output;
178 }
179
180 # Returns the snapshot device path.
181 sub get_snap_device {
182     my $self = shift;
183     my $mapper = shift;
184     my @parts = split('/', $self->{device});
185     my $vg_name = $parts[2];
186     my $lv_name = $parts[3];
187
188     if ($mapper) {
189         return "/dev/mapper/$self->{volume_group}-amsnap--$vg_name--$lv_name";
190     } else {
191         return "/dev/$self->{volume_group}/amsnap-$vg_name-$lv_name";
192     }
193 }
194
195 # Mounts the snapshot device at the configured directory.
196 sub mount_snapshot {
197     my $self = shift;
198
199     # mount options
200     my @options = ('ro');
201
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');
206     }
207
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});
212     } else {
213         $self->{directory} = tempdir(CLEANUP => 0);
214     }
215     my $snapshot_device = $self->get_snap_device(0);
216     $self->execute(1,
217         "mount -o ", join(",", @options),
218         $snapshot_device, $self->{directory}
219     );
220
221     debug("Mounted snapshot `$snapshot_device' at `$self->{directory}'.");
222 }
223
224 # Readlink wrapper.
225 sub readlink {
226     my $self = shift;
227     my $path = shift;
228
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));
232     chomp($real_path);
233     $real_path =~ s@\.\.@/dev@;
234
235     return ($real_path ne "") ? $real_path : $path;
236 }
237
238 # Removes the snapshot device.
239 sub remove_snapshot {
240     my $self = shift;
241
242     # remove snapshot device with 'lvremove'
243     $self->execute(1, "$self->{lvremove} -f", $self->get_snap_device(0));
244
245     debug("Removed snapshot of `$self->{device}'.");
246 }
247
248 # Resolves the underlying device on which the configured directory resides.
249 sub resolve_device {
250     my $self = shift;
251
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}); }
255     );
256
257     my $fs_type = $self->scan_mtab(
258         sub { return $_[2] if ($_[1] eq $self->{disk}); }
259     );
260
261     if (!$mnt_device) {
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
266             );
267         } else {
268             $mnt_device = $self->{device};
269             $fs_type = join("", $self->execute(1, "$self->{blkid} -s TYPE -o value $self->{device}"));
270             chomp($fs_type);
271         }
272     }
273
274     # loop through the LVs to find the one that matches
275     foreach ($self->execute(1, "$self->{lvdisplay} -c")) {
276         my ($device, $group) = split(/:/);
277
278         $device =~ s/^\s*//;
279         chomp($device);
280
281         my $real_device = $self->readlink($device);
282         chomp($real_device);
283
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;
288
289             debug(
290                 "Resolved device `$self->{device}' and volume group ".
291                 "`$self->{volume_group}' from mount point `$self->{disk}'."
292             );
293
294             last;
295         }
296     }
297 }
298
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.
304 sub scan_mtab {
305     my $self = shift;
306     my $sub = shift;
307
308     open(MTAB, "/etc/mtab");
309     my $line;
310     my $result;
311     while ($line = <MTAB>) {
312         chomp($line);
313         my ($device, $directory, $type) = split(/\s+/, $line);
314         $result = $sub->($self->readlink($device), $directory, $type);
315         last if ($result);
316     }
317     close MTAB;
318
319     return $result;
320 }
321
322 sub setup {
323     my $self = shift;
324
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
330         );
331     }
332
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
339         ) if $?;
340     }
341
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
347         ) if $?;
348     }
349
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
355         ) if $?;
356     }
357
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
363         ) if $?;
364     }
365
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
371         ) if $?;
372     }
373
374     # resolve actual lvm device
375     $self->resolve_device();
376
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
381         );
382     }
383 }
384
385 sub umount_snapshot {
386     my $self = shift;
387     my $device = $self->readlink($self->get_snap_device(0));
388
389     $device =~ s@\.\.@/dev@;
390     debug("umount_snapshot $device");
391
392     my $mnt = $self->scan_mtab(sub { return $_[1] if ($_[0] eq $device); });
393     if (!$mnt) {
394         $device = $self->readlink($self->get_snap_device(1));
395         $mnt = $self->scan_mtab(sub { return $_[1] if ($_[0] eq $device); });
396     }
397
398     if (!$mnt) {
399         $self->print_to_server_and_die(
400             "Failed to get mount point for snapshot device `$device'.",
401             $Amanda::Script_App::ERROR
402         );
403     }
404
405     $self->execute(1, "umount", $mnt);
406
407     debug("Un-mounted snapshot device `$device' from `$mnt'.");
408     rmdir $mnt;
409     debug("Remove snapshot mount point rmdir `$mnt'.");
410 }
411
412
413 sub command_support {
414     my $self = shift;
415
416     print "CONFIG YES\n";
417     print "HOST YES\n";
418     print "DISK YES\n";
419     print "MESSAGE-LINE YES\n";
420     print "MESSAGE-XML NO\n";
421     print "EXECUTE-WHERE YES\n";
422 }
423
424 #define a execute_on_* function for every execute_on you want the script to do
425 #something
426 sub command_pre_dle_backup {
427     my $self = shift;
428
429     $self->setup();
430     $self->create_snapshot();
431     $self->mount_snapshot();
432
433     print "PROPERTY directory $self->{directory}\n";
434 }
435
436 sub command_post_dle_backup {
437     my $self = shift;
438
439     $self->setup();
440     $self->umount_snapshot();
441     $self->remove_snapshot();
442 }
443
444 sub command_pre_dle_amcheck {
445     my $self = shift;
446
447     $self->setup();
448     $self->create_snapshot();
449     $self->mount_snapshot();
450
451     print "PROPERTY directory $self->{directory}\n";
452 }
453
454 sub command_post_dle_amcheck {
455     my $self = shift;
456
457     $self->setup();
458     $self->umount_snapshot();
459     $self->remove_snapshot();
460 }
461
462 package main;
463
464 sub usage {
465     print <<EOF;
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>.
467 EOF
468     exit(1);
469 }
470
471 my $opt_execute_where;
472 my $opt_config;
473 my $opt_host;
474 my $opt_disk;
475 my $opt_device;
476 my @opt_level;
477 my $opt_index;
478 my $opt_message;
479 my $opt_collection;
480 my $opt_record;
481
482 my $opt_snapsize;
483 my $opt_lvcreate;
484 my $opt_lvdisplay;
485 my $opt_lvremove;
486 my $opt_vgdisplay;
487 my $opt_blkid;
488 my $opt_stablemount;
489 my $opt_sudo;
490
491 Getopt::Long::Configure(qw{bundling});
492 GetOptions(
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,
511 ) or usage();
512
513 # add SBIN to PATH
514 $ENV{'PATH'} = "/sbin:/usr/sbin:$ENV{'PATH'}:/usr/local/sbin";
515
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]);
521
522 # vim: set et sts=4 sw=4 :
This page took 0.072034 seconds and 3 git commands to generate.