]>
Commit | Line | Data |
---|---|---|
9b104ea3 JR |
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 | # | |
3275a137 | 28 | # STABLE-MOUNTPOINT |
9b104ea3 JR |
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); | |
3275a137 | 46 | use File::Path qw(make_path); |
9b104ea3 JR |
47 | use IPC::Open3; |
48 | use Symbol; | |
3275a137 | 49 | use Digest::MD5 qw(md5_hex); |
9b104ea3 JR |
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, | |
3275a137 | 55 | $lvremove, $vgdisplay, $blkid, $stablemount, $sudo) = @_; |
9b104ea3 JR |
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 | ||
3275a137 | 77 | $self->{stablemount} = $stablemount; |
9b104ea3 JR |
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 | |
3275a137 JR |
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 | } | |
9b104ea3 JR |
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 | ||
62014f87 | 284 | if (($device eq $mnt_device) || ($real_device eq $mnt_device)) { |
9b104ea3 JR |
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; | |
3275a137 | 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>. |
9b104ea3 JR |
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; | |
3275a137 | 488 | my $opt_stablemount; |
9b104ea3 JR |
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, | |
3275a137 | 509 | 'stable-mountpoint=s' => \$opt_stablemount, |
9b104ea3 JR |
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, | |
3275a137 | 519 | $opt_lvdisplay, $opt_lvremove, $opt_vgdisplay, $opt_blkid, $opt_stablemount, $opt_sudo); |
9b104ea3 JR |
520 | $script->do($ARGV[0]); |
521 | ||
522 | # vim: set et sts=4 sw=4 : |