]>
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 | # | |
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 : |