File Coverage

lib/VM/EC2/Staging/Manager.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package VM::EC2::Staging::Manager;
2              
3             =head1 NAME
4              
5             VM::EC2::Staging::Manager - Automate VMs and volumes for moving data in and out of cloud.
6              
7             =head1 SYNOPSIS
8              
9             use VM::EC2::Staging::Manager;
10              
11             my $ec2 = VM::EC2->new(-region=>'us-east-1');
12             my $staging = $ec2->staging_manager(-on_exit => 'stop', # default, stop servers when process exists
13             -verbose => 1, # default, verbose progress messages
14             -scan => 1, # default, scan region for existing staging servers and volumes
15             -image_name => 'ubuntu-precise-12.04', # default server image
16             -user_name => 'ubuntu', # default server login name
17             );
18              
19             # Assuming an EBS image named ami-12345 is located in the US, copy it into
20             # the South American region, returning the AMI ID in South America
21             my $new_image = $staging->copy_image('ami-12345','sa-east-1');
22              
23             # provision a new server, using defaults. Name will be assigned automatically
24             my $server = $staging->provision_server(-availability_zone => 'us-east-1a');
25              
26             # retrieve a new server named "my_server", if one exists. If not, it creates one
27             # using the specified options
28             my $server = $staging->get_server(-name => 'my_server',
29             -availability_zone => 'us-east-1a',
30             -instance_type => 't1.micro');
31              
32             # open up an ssh session in an xterm
33             $server->shell;
34              
35             # run a command over ssh on the server. See VM::EC2::Staging::Server
36             $server->ssh('whoami');
37              
38             # run a command over ssh on the server, returning the result as an array of lines or a
39             # scalar string, similar to backticks (``)
40             my @password_lines = $server->scmd('cat /etc/passwd');
41              
42             # run a command on the server and read from it using a filehandle
43             my $fh = $server->scmd_read('ls -R /usr/lib');
44             while (<$fh>) { # do something }
45              
46             # run a command on the server and write to it using a filehandle
47             my $fh = $server->scmd_write('sudo -s "cat >>/etc/fstab"');
48             print $fh "/dev/sdf3 /mnt/demo ext3 0 2\n";
49             close $fh;
50              
51             # Provision a new volume named "Pictures". Will automatically be mounted to a staging server in
52             # the specified zone. Server will be created if needed.
53             my $volume = $staging->provision_volume(-name => 'Pictures',
54             -fstype => 'ext4',
55             -availability_zone => 'us-east-1a',
56             -size => 2) or die $staging->error_str;
57              
58             # gets an existing volume named "Pictures" if it exists. Otherwise provisions a new volume;
59             my $volume = $staging->get_volume(-name => 'Pictures',
60             -fstype => 'ext4',
61             -availability_zone => 'us-east-1a',
62             -size => 2) or die $staging->error_str;
63              
64             # copy contents of local directory /opt/test to remote volume $volume using rsync
65             # See VM::EC2::Staging::Volume
66             $volume->put('/opt/test/');
67              
68             # same thing, but first creating a subdirectory on the remote volume
69             $volume->put('/opt/test/' => './mirrors/');
70              
71             # copy contents of remote volume $volume to local directory /tmp/test using rsync
72             $volume->get('/tmp/test');
73              
74             # same thing, but from a subdirectory of the remote volume
75             $volume->get('./mirrors/' => '/tmp/test');
76              
77             # server to server transfer (works both within and between availability regions)
78             my $south_america = VM::EC2->new(-region=>'sa-east-1')->staging_manager; # create a staging manager in Sao Paolo
79             my $volume2 = $south_america->provision_volume(-name => 'Videos',
80             -availability_zone => 'sa-east-1a',
81             -size => 2);
82             $staging->rsync("$volume/mirrors" => "$volume2/us-east");
83              
84             $staging->stop_all_servers();
85             $staging->start_all_servers();
86             $staging->terminate_all_servers();
87             $staging->force_terminate_all_servers();
88              
89             =head1 DESCRIPTION
90              
91             VM::EC2::Staging::Manager manages a set of EC2 volumes and servers
92             in a single AWS region. It was primarily designed to simplify the
93             process of provisioning and populating volumes, but it also provides a
94             handy set of ssh commands that allow you to run remote commands
95             programmatically.
96              
97             The manager also allows you to copy EBS-backed AMIs and their attached
98             volumes from one region to another, something that is otherwise
99             difficult to do.
100              
101             The main classes are:
102              
103             VM::EC2::Staging::Manager -- A set of volume and server resources in
104             a single AWS region.
105              
106             VM::EC2::Staging::Server -- A staging server running somewhere in the
107             region. It is a VM::EC2::Instance
108             extended to provide remote command and
109             copy facilities.
110              
111             VM::EC2::Staging::Volume -- A staging disk volume running somewhere in the
112             region. It is a VM::EC2::Volume
113             extended to provide remote copy
114             facilities.
115              
116             Staging servers can provision volumes, format them, mount them, copy
117             data between local and remote (virtual) machines, and execute secure
118             shell commands. Staging volumes can mount themselves on servers, run a
119             variety of filesystem-oriented commands, and invoke commands on the
120             servers to copy data around locally and remotely.
121              
122             See L and L for
123             the full details.
124              
125             =head1 Constructors
126              
127             The following methods allow you to create new
128             VM::EC2::Staging::Manager instances. Be aware that only one manager is
129             allowed per EC2 region; attempting to create additional managers in
130             the same region will return the same one each time.
131              
132             =cut
133              
134 1     1   1361 use strict;
  1         1  
  1         33  
135 1     1   914 use VM::EC2 ':standard';
  0            
  0            
136             use Carp 'croak','longmess';
137             use File::Spec;
138             use File::Path 'make_path','remove_tree';
139             use File::Basename 'dirname','basename';
140             use Scalar::Util 'weaken';
141             use String::Approx 'adistr';
142             use File::Temp 'tempfile';
143              
144             use constant GB => 1_073_741_824;
145             use constant SERVER_STARTUP_TIMEOUT => 120;
146             use constant LOCK_TIMEOUT => 10;
147             use constant VERBOSE_DEBUG => 3;
148             use constant VERBOSE_INFO => 2;
149             use constant VERBOSE_WARN => 1;
150              
151             my (%Zones,%Instances,%Volumes,%Managers);
152             my $Verbose;
153             my ($LastHost,$LastMt);
154              
155             =head2 $manager = $ec2->staging_manager(@args)
156              
157             This is a simplified way to create a staging manager. First create the
158             EC2 object in the desired region, and then call its staging_manager()
159             method:
160              
161             $manager = VM::EC2->new(-region=>'us-west-2')->staging_manager()
162              
163             The staging_manager() method is only known to VM::EC2 objects if you
164             first "use" VM::EC2::Staging::Manager.
165              
166             =over 4
167              
168             =item Required Arguments
169              
170             None.
171              
172             =item Optional Arguments
173              
174             The optional arguments change the way that the manager creates new
175             servers and volumes.
176              
177             -on_exit What to do with running servers when the manager goes
178             out of scope or the script exits. One of 'run',
179             'stop' (default), or 'terminate'. "run" keeps all
180             created instances running, so beware!
181              
182             -architecture Architecture for newly-created server
183             instances (default "i386"). Can be overridden in calls to get_server()
184             and provision_server().
185              
186             -instance_type Type of newly-created servers (default "m1.small"). Can be overridden
187             in calls to get_server() and provision_server().
188              
189             -root_type Root type for newly-created servers (default depends
190             on the -on_exit behavior; "ebs" for exit behavior of
191             "stop" and "instance-store" for exit behavior of "run"
192             or "terminate".
193              
194             -image_name Name or ami ID of the AMI to use for creating the
195             instances of new servers. Defaults to 'ubuntu-precise-12.04'.
196             If the image name begins with "ami-", then it is
197             treated as an AMI ID. Otherwise it is treated as
198             a name pattern and will be used to search the AMI
199             name field using the wildcard search "*$name*".
200             Names work better than AMI ids here, because the
201             latter change from one region to another. If multiple
202             matching image candidates are found, then an alpha
203             sort on the name is used to find the image with the
204             highest alpha sort value, which happens to work with
205             Ubuntu images to find the latest release.
206              
207             -availability_zone Availability zone for newly-created
208             servers. Default is undef, in which case a random
209             zone is selected.
210              
211             -username Username to use for ssh connections. Defaults to
212             "ubuntu". Note that this user must be able to use
213             sudo on the instance without providing a password,
214             or functionality of this module will be limited.
215            
216             -verbose Integer level of verbosity. Level 1 prints warning
217             messages. Level 2 (the default) adds informational
218             messages as well. Level 3 adds verbose debugging
219             messages. Level 0 suppresses all messages.
220              
221             -quiet (deprecated) If true, turns off all verbose messages.
222              
223             -scan Boolean, default true. If true, scans region for
224             volumes and servers created by earlier manager
225             instances.
226              
227             -reuse_key Boolean, default true. If true, creates a single
228             ssh keypair for each region and reuses it. Note that
229             the private key is kept on the local computer in the
230             directory ~/.vm-ec2-staging, and so additional
231             keypairs may be created if you use this module on
232             multiple local machines. If this option is false,
233             then a new keypair will be created for every server
234             you partition.
235              
236             -reuse_volumes Boolean, default true. If this flag is true, then
237             calls to provision_volume() will return existing
238             volumes if they share the same name as the requested
239             volume. If no suitable existing volume exists, then
240             the most recent snapshot of this volume is used to
241             create it in the specified availability zone. Only
242             if no volume or snapshot exist will a new volume be
243             created from scratch.
244              
245             -dotdir Path to the directory that contains keyfiles and other
246             stable configuration information for this module.
247             Defaults to ~/.vm_ec2_staging. You may wish to change
248             this to, say, a private dropbox directory or an NFS-mount
249             in order to share keyfiles among machines. Be aware of
250             the security implications of sharing private key files.
251              
252             -server_class By default, staging server objects created by the manager
253             are of class type VM::EC2::Staging::Server. If you create
254             a custom server subclass, you need to let the manager know
255             about it by passing the class name to this argument.
256              
257             -volume_class By default, staging volume objects created by the manager
258             are of class type VM::EC2::Staging::Volume. If you create
259             a custom volume subclass, you need to let the manager know
260             about it by passing the class name to this argument.
261              
262             =back
263              
264             =head2 $manager = VM::EC2::Staging::Manager(-ec2 => $ec2,@args)
265              
266             This is a more traditional constructur for the staging manager.
267              
268             =over 4
269              
270             =item Required Arguments
271            
272             -ec2 A VM::EC2 object.
273              
274             =item Optional Arguments
275              
276             All of the arguments listed in the description of
277             VM::EC2->staging_manager().
278              
279             =back
280              
281             =cut
282              
283             sub VM::EC2::staging_manager {
284             my $self = shift;
285             return VM::EC2::Staging::Manager->new(@_,-ec2=>$self)
286             }
287              
288              
289             sub new {
290             my $self = shift;
291             my %args = @_;
292             $args{-ec2} ||= VM::EC2->new();
293              
294             if (my $manager = $self->find_manager($args{-ec2}->endpoint)) {
295             return $manager;
296             }
297              
298             $args{-on_exit} ||= $self->default_exit_behavior;
299             $args{-reuse_key} ||= $self->default_reuse_keys;
300             $args{-username} ||= $self->default_user_name;
301             $args{-architecture} ||= $self->default_architecture;
302             $args{-root_type} ||= $self->default_root_type;
303             $args{-instance_type} ||= $self->default_instance_type;
304             $args{-reuse_volumes} ||= $self->default_reuse_volumes;
305             $args{-image_name} ||= $self->default_image_name;
306             $args{-availability_zone} ||= undef;
307             $args{-verbose} = $self->default_verbosity unless exists $args{-verbose};
308             $args{-scan} = 1 unless exists $args{-scan};
309             $args{-pid} = $$;
310             $args{-dotdir} ||= $self->default_dot_directory_path;
311             $args{-volume_class} ||= $self->default_volume_class;
312             $args{-server_class} ||= $self->default_server_class;
313              
314             $args{-verbose} = 0 if $args{-quiet};
315              
316             # bring in classes
317             foreach ('-server_class','-volume_class') {
318             eval "use $args{$_};1" or croak "Can't use $args{$_}"
319             unless $args{$_}->can('new');
320             }
321              
322             # create accessors
323             my $class = ref $self || $self;
324             foreach (keys %args) {
325             (my $func_name = $_) =~ s/^-//;
326             next if $self->can($func_name);
327             eval <
328             sub ${class}::${func_name} {
329             my \$self = shift;
330             my \$d = \$self->{$_};
331             \$self->{$_} = shift if \@_;
332             return \$d;
333             }
334             END
335             die $@ if $@;
336             }
337              
338             $Verbose = $args{-verbose}; # package global, for a few edge cases
339             my $obj = bless \%args,ref $class || $class;
340             weaken($Managers{$obj->ec2->endpoint} = $obj);
341             if ($args{-scan}) {
342             $obj->info("Scanning for existing staging servers and volumes in ",$obj->ec2->endpoint,".\n");
343             $obj->scan_region;
344             }
345             return $obj;
346             }
347              
348              
349             # class method
350             # the point of this somewhat odd way of storing managers is to ensure that there is only one
351             # manager per endpoint, and to avoid circular references in the Server and Volume objects.
352             sub find_manager {
353             my $class = shift;
354             my $endpoint = shift;
355             return unless $endpoint;
356             return $Managers{$endpoint};
357             }
358              
359             =head1 Interzone Copying of AMIs and Snapshots
360              
361             This library provides convenience methods for copying whole AMIs as
362             well as individual snapshots from one zone to another. It does this by
363             gathering information about the AMI/snapshot in the source zone,
364             creating staging servers in the source and target zones, and then
365             copying the volume data from the source to the target. If an
366             AMI/snapshot does not use a recognized filesystem (e.g. it is part of
367             an LVM or RAID disk set), then block level copying of the entire
368             device is used. Otherwise, rsync() is used to minimize data transfer
369             fees.
370              
371             Note that interzone copying of instance-backed AMIs is B
372             supported. Only EBS-backed images can be copied in this way.
373              
374             See also the command-line script migrate-ebs-image.pl that comes with
375             this package.
376              
377             =head2 $new_image_id = $manager->copy_image($source_image,$destination_zone,@register_options)
378              
379             This method copies the AMI indicated by $source_image from the zone
380             that $manager belongs to, into the indicated $destination_zone, and
381             returns the AMI ID of the new image in the destination zone.
382              
383             $source_image may be an AMI ID, or a VM::EC2::Image object.
384              
385             $destination_zone may be a simple region name, such as "us-west-2", or
386             a VM::EC2::Region object (as returned by VM::EC2->describe_regions),
387             or a VM::EC2::Staging::Manager object that is associated with the
388             desired region. The latter form gives you control over the nature of
389             the staging instances created in the destination zone. For example, if
390             you wish to use 'm1.large' high-I/O instances in both the source and
391             destination reasons, you would proceed like this:
392              
393             my $source = VM::EC2->new(-region=>'us-east-1'
394             )->staging_manager(-instance_type=>'m1.large',
395             -on_exit =>'terminate');
396             my $destination = VM::EC2->new(-region=>'us-west-2'
397             )->staging_manager(-instance_type=>'m1.large',
398             -on_exit =>'terminate');
399             my $new_image = $source->copy_image('ami-123456' => $destination);
400              
401             If present, the named argument list @register_options will be passed
402             to register_image() and used to override options in the destination
403             image. This can be used to set ephemeral device mappings, which cannot
404             currently be detected and transferred automatically by copy_image():
405              
406             $new_image =$source->copy_image('ami-123456' => 'us-west-2',
407             -description => 'My AMI western style',
408             -block_devices => '/dev/sde=ephemeral0');
409              
410             =head2 $dest_kernel = $manager->match_kernel($src_kernel,$dest_zone)
411              
412             Find a kernel in $dest_zone that matches the $src_kernel in the
413             current zone. $dest_zone can be a VM::EC2::Staging manager object, a
414             region name, or a VM::EC2::Region object.
415              
416             =cut
417              
418             #############################################
419             # copying AMIs from one zone to another
420             #############################################
421             sub copy_image {
422             my $self = shift;
423             my ($imageId,$destination,@options) = @_;
424             my $ec2 = $self->ec2;
425              
426             my $image = ref $imageId && $imageId->isa('VM::EC2::Image') ? $imageId
427             : $ec2->describe_images($imageId);
428             $image
429             or croak "Unknown image '$imageId'";
430             $image->imageType eq 'machine'
431             or croak "$image is not an AMI";
432             # $image->platform eq 'windows'
433             # and croak "It is not currently possible to migrate Windows images between regions via this method";
434             $image->rootDeviceType eq 'ebs'
435             or croak "It is not currently possible to migrate instance-store backed images between regions via this method";
436            
437             my $dest_manager = $self->_parse_destination($destination);
438              
439             my $root_type = $image->rootDeviceType;
440             if ($root_type eq 'ebs') {
441             return $self->_copy_ebs_image($image,$dest_manager,\@options);
442             } else {
443             return $self->_copy_instance_image($image,$dest_manager,\@options);
444             }
445             }
446              
447             =head2 $new_snapshot_id = $manager->copy_snapshot($source_snapshot,$destination_zone)
448              
449             This method copies the EBS snapshot indicated by $source_snapshot from
450             the zone that $manager belongs to, into the indicated
451             $destination_zone, and returns the ID of the new snapshot in the
452             destination zone.
453              
454             $source_snapshot may be an string ID, or a VM::EC2::Snapshot object.
455              
456             $destination_zone may be a simple region name, such as "us-west-2", or
457             a VM::EC2::Region object (as returned by VM::EC2->describe_regions),
458             or a VM::EC2::Staging::Manager object that is associated with the
459             desired region.
460              
461             Note that this call uses the Amazon CopySnapshot API call that was
462             introduced in 2012-12-01 and no longer involves the creation of
463             staging servers in the source and destination regions.
464              
465             =cut
466              
467             sub copy_snapshot {
468             my $self = shift;
469             my ($snapId,$dest_manager) = @_;
470             my $snap = $self->ec2->describe_snapshots($snapId)
471             or croak "Couldn't find snapshot for $snapId";
472             my $description = "duplicate of $snap, created by ".__PACKAGE__." during snapshot copying";
473             my $dest_region = ref($dest_manager) && $dest_manager->can('ec2')
474             ? $dest_manager->ec2->region
475             : "$dest_manager";
476              
477             $self->info("Copying snapshot $snap from ",$self->ec2->region," to $dest_region...\n");
478             my $snapshot = $snap->copy(-region => $dest_region,
479             -description => $description);
480              
481             while (!eval{$snapshot->current_status}) {
482             sleep 1;
483             }
484             $self->info("...new snapshot = $snapshot; status = ",$snapshot->current_status,"\n");
485              
486             # copy snapshot tags
487             my $tags = $snap->tags;
488             $snapshot->add_tags($tags);
489              
490             return $snapshot;
491             }
492              
493             sub _copy_instance_image {
494             my $self = shift;
495             croak "This module is currently unable to copy instance-backed AMIs between regions.\n";
496             }
497              
498             sub _copy_ebs_image {
499             my $self = shift;
500             my ($image,$dest_manager,$options) = @_;
501              
502             # apply overrides
503             my %overrides = @$options if $options;
504              
505             # hashref with keys 'name', 'description','architecture','kernel','ramdisk','block_devices','root_device'
506             # 'is_public','authorized_users'
507             $self->info("Gathering information about image $image.\n");
508             my $info = $self->_gather_image_info($image);
509              
510             my $name = $info->{name};
511             my $description = $info->{description};
512             my $architecture = $info->{architecture};
513             my $root_device = $info->{root_device};
514             my $platform = $info->{platform};
515             my ($kernel,$ramdisk);
516              
517             # make sure we have a suitable image in the destination region
518             # if the virtualization type is HVM
519             my $is_hvm = $image->virtualization_type eq 'hvm';
520             if ($is_hvm) {
521             $self->_find_hvm_image($dest_manager->ec2,
522             $root_device,
523             $architecture,
524             $platform)
525             or croak "Destination region ",$dest_manager->ec2->region," does not currently support HVM images of this type";
526             }
527              
528             if ($info->{kernel} && !$overrides{-kernel}) {
529             $self->info("Searching for a suitable kernel in the destination region.\n");
530             $kernel = $self->_match_kernel($info->{kernel},$dest_manager,'kernel')
531             or croak "Could not find an equivalent kernel for $info->{kernel} in region ",$dest_manager->ec2->endpoint;
532             $self->info("Matched kernel $kernel\n");
533             }
534            
535             if ($info->{ramdisk} && !$overrides{-ramdisk}) {
536             $self->info("Searching for a suitable ramdisk in the destination region.\n");
537             $ramdisk = ( $self->_match_kernel($info->{ramdisk},$dest_manager,'ramdisk')
538             || $dest_manager->_guess_ramdisk($kernel)
539             ) or croak "Could not find an equivalent ramdisk for $info->{ramdisk} in region ",$dest_manager->ec2->endpoint;
540             $self->info("Matched ramdisk $ramdisk\n");
541             }
542              
543             my $block_devices = $info->{block_devices}; # format same as $image->blockDeviceMapping
544              
545             $self->info("Copying EBS volumes attached to this image (this may take a long time).\n");
546             my @bd = @$block_devices;
547             my %dest_snapshots = map {
548             $_->snapshotId
549             ? ($_->snapshotId => $self->copy_snapshot($_->snapshotId,$dest_manager))
550             : ()
551             } @bd;
552            
553             $self->info("Waiting for all snapshots to complete. This may take a long time.\n");
554             my $state = $dest_manager->ec2->wait_for_snapshots(values %dest_snapshots);
555             my @errored = grep {$state->{$_} eq 'error'} values %dest_snapshots;
556             croak ("Snapshot(s) @errored could not be completed due to an error")
557             if @errored;
558              
559             # create the new block device mapping
560             my @mappings;
561             for my $source_ebs (@$block_devices) {
562             my $dest = "$source_ebs"; # interpolates into correct format
563             $dest =~ s/=([\w-]+)/'='.($dest_snapshots{$1}||$1)/e; # replace source snap with dest snap
564             push @mappings,$dest;
565             }
566              
567             # ensure choose a unique name
568             if ($dest_manager->ec2->describe_images({name => $name})) {
569             print STDERR "An image named '$name' already exists in destination region. ";
570             $name = $self->_token($name);
571             print STDERR "Renamed to '$name'\n";
572             }
573              
574             # merge block device mappings if present
575             if (my $m = $overrides{-block_device_mapping}||$overrides{-block_devices}) {
576             push @mappings,(ref $m ? @$m : $m);
577             delete $overrides{-block_device_mapping};
578             delete $overrides{-block_devices};
579             }
580              
581             # helpful for recovering failed process
582             my $block_device_info_args = join ' ',map {"-b $_"} @mappings;
583              
584             my $img;
585              
586             if ($is_hvm) {
587             $self->info("Registering snapshot in destination with the equivalent of:\n");
588             $self->info("ec2-register -n '$name' -d '$description' -a $architecture --virtualization-type hvm --root-device-name $root_device $block_device_info_args\n");
589             $self->info("Note: this is a notional command line that can only be used by AWS development partners.\n");
590             $img = $self->_create_hvm_image(-ec2 => $dest_manager->ec2,
591             -name => $name,
592             -root_device_name => $root_device,
593             -block_device_mapping => \@mappings,
594             -description => $description,
595             -architecture => $architecture,
596             -platform => $image->platform,
597             %overrides);
598             }
599              
600             else {
601             $self->info("Registering snapshot in destination with the equivalent of:\n");
602             $self->info("ec2-register -n '$name' -d '$description' -a $architecture --kernel '$kernel' --ramdisk '$ramdisk' --root-device-name $root_device $block_device_info_args\n");
603             $img = $dest_manager->ec2->register_image(-name => $name,
604             -root_device_name => $root_device,
605             -block_device_mapping => \@mappings,
606             -description => $description,
607             -architecture => $architecture,
608             $kernel ? (-kernel_id => $kernel): (),
609             $ramdisk ? (-ramdisk_id => $ramdisk): (),
610             %overrides,
611             );
612             $img or croak "Could not register image: ",$dest_manager->ec2->error_str;
613             }
614            
615             # copy launch permissions
616             $img->make_public(1) if $info->{is_public};
617             $img->add_authorized_users(@{$info->{authorized_users}}) if @{$info->{authorized_users}};
618            
619             # copy tags
620             my $tags = $image->tags;
621             $img->add_tags($tags);
622              
623             # Improve the snapshot tags
624             my $source_region = $self->ec2->region;
625             my $dest_region = $dest_manager->ec2->region;
626             for (@mappings) {
627             my ($snap) = /(snap-[0=9a-f]+)/ or next;
628             $snap = $dest_manager->ec2->describe_snapshots($snap) or next;
629             $snap->add_tags(Name => "Copy image $image($source_region) to $img($dest_region)");
630             }
631              
632             return $img;
633             }
634              
635             # copying an HVM image requires us to:
636             # 1. Copy each of the snapshots to the destination region
637             # 2. Find a public HVM image in the destination region that matches the architecture, hypervisor type,
638             # and root device type of the source image. (note: platform must not be 'windows'
639             # 3. Run a cc2 instance: "cc2.8xlarge", but replace default block device mapping with the new snapshots.
640             # 4. Stop the image.
641             # 5. Detach the root volume
642             # 6. Initialize and attach a new root volume from the copied source root snapshot.
643             # 7. Run create_image() on the instance.
644             # 8. Terminate the instance and clean up.
645             sub _create_hvm_image {
646             my $self = shift;
647             my %args = @_;
648              
649             my $ec2 = $args{-ec2};
650              
651             # find a suitable image that we can run
652             $self->info("Searching for a suitable HVM image in destination region\n");
653             my $ami = $self->_find_hvm_image($ec2,$args{-root_device_name},$args{-architecture},$args{-platform});
654             $ami or croak "Could not find suitable HVM image in region ",$ec2->region;
655              
656             $self->info("...Found $ami (",$ami->name,")\n");
657              
658             # remove root device from the block device list
659             my $root = $args{-root_device_name};
660             my @nonroot_devices = grep {!/^$root/} @{$args{-block_device_mapping}};
661             my ($root_snapshot) = "@{$args{-block_device_mapping}}" =~ /$root=(snap-[0-9a-f]+)/;
662            
663             my $instance_type = $args{-platform} eq 'windows' ? 'm1.small' : 'cc2.8xlarge';
664             $self->info("Launching an HVM staging server in the target region. Heuristically choosing instance type of '$instance_type' for this type of HVM..\n");
665              
666             my $instance = $ec2->run_instances(-instance_type => $instance_type,
667             -image_id => $ami,
668             -block_devices => \@nonroot_devices)
669             or croak "Could not run HVM instance: ",$ec2->error_str;
670             $self->info("Waiting for instance to become ready.\n");
671             $ec2->wait_for_instances($instance);
672            
673             $self->info("Stopping instance temporarily to swap root volumes.\n");
674             $instance->stop(1);
675              
676             $self->info("Detaching original root volume...\n");
677             my $a = $instance->detach_volume($root) or croak "Could not detach $root: ", $ec2->error_str;
678             $ec2->wait_for_attachments($a);
679             $a->current_status eq 'detached' or croak "Could not detach $root, status = ",$a->current_status;
680             $ec2->delete_volume($a->volumeId) or croak "Could not delete original root volume: ",$ec2->error_str;
681              
682             $self->info("Creating and attaching new root volume..\n");
683             my $vol = $ec2->create_volume(-availability_zone => $instance->placement,
684             -snapshot_id => $root_snapshot)
685             or croak "Could not create volume from root snapshot $root_snapshot: ",$ec2->error_str;
686             $ec2->wait_for_volumes($vol);
687             $vol->current_status eq 'available' or croak "Volume creation failed, status = ",$vol->current_status;
688              
689             $a = $instance->attach_volume($vol,$root) or croak "Could not attach new root volume: ",$ec2->error_str;
690             $ec2->wait_for_attachments($a);
691             $a->current_status eq 'attached' or croak "Attach failed, status = ",$a->current_status;
692             $a->deleteOnTermination(1);
693              
694             $self->info("Creating image in destination region...\n");
695             my $img = $instance->create_image($args{-name},$args{-description});
696              
697             # get rid of the original copied snapshots - we no longer need them
698             foreach (@{$args{-block_device_mapping}}) {
699             my ($snapshot) = /(snap-[0-9a-f]+)/ or next;
700             $ec2->delete_snapshot($snapshot)
701             or $self->warn("Could not delete unneeded snapshot $snapshot; please delete manually: ",$ec2->error_str)
702             }
703              
704             # terminate the staging server.
705             $self->info("Terminating the staging server\n");
706             $instance->terminate; # this will delete the volume as well because of deleteOnTermination
707              
708             return $img;
709             }
710              
711             sub _find_hvm_image {
712             my $self = shift;
713             my ($ec2,$root_device_name,$architecture,$platform) = @_;
714              
715             my $cache_key = join (';',@_);
716             return $self->{_hvm_image}{$cache_key} if exists $self->{_hvm_image}{$cache_key};
717              
718             my @i = $ec2->describe_images(-executable_by=> 'all',
719             -owner => 'amazon',
720             -filter => {
721             'virtualization-type' => 'hvm',
722             'root-device-type' => 'ebs',
723             'root-device-name' => $root_device_name,
724             'architecture' => $architecture,
725             });
726             @i = grep {$_->platform eq $platform} @i;
727             return $self->{_hvm_image}{$cache_key} = $i[0];
728             }
729              
730              
731             =head1 Instance Methods for Managing Staging Servers
732              
733             These methods allow you to create and interrogate staging
734             servers. They each return one or more VM::EC2::Staging::Server
735             objects. See L for more information about
736             what you can do with these servers once they are running.
737              
738             =head2 $server = $manager->provision_server(%options)
739              
740             Create a new VM::EC2::Staging::Server object according to the passed
741             options, which override the default options provided by the Manager
742             object.
743              
744             -name Name for this server, which can be used to retrieve
745             it later with a call to get_server().
746              
747             -architecture Architecture for the newly-created server
748             instances (e.g. "i386"). If not specified, then defaults
749             to the default_architecture() value. If explicitly
750             specified as undef, then the architecture of the matching
751             image will be used.
752              
753             -instance_type Type of the newly-created server (e.g. "m1.small").
754              
755             -root_type Root type for the server ("ebs" or "instance-store").
756              
757             -image_name Name or ami ID of the AMI to use for creating the
758             instance for the server. If the image name begins with
759             "ami-", then it is treated as an AMI ID. Otherwise it
760             is treated as a name pattern and will be used to
761             search the AMI name field using the wildcard search
762             "*$name*". Names work better than AMI ids here,
763             because the latter change from one region to
764             another. If multiple matching image candidates are
765             found, then an alpha sort on the name is used to find
766             the image with the highest alpha sort value, which
767             happens to work with Ubuntu images to find the latest
768             release.
769              
770             -availability_zone Availability zone for the server, or undef to
771             choose an availability zone randomly.
772              
773             -username Username to use for ssh connections. Defaults to
774             "ubuntu". Note that this user must be able to use
775             sudo on the instance without providing a password,
776             or functionality of this server will be limited.
777              
778             In addition, you may use any of the options recognized by
779             VM::EC2->run_instances() (e.g. -block_devices).
780              
781             =cut
782              
783             sub provision_server {
784             my $self = shift;
785             my @args = @_;
786              
787             # let subroutine arguments override manager's args
788             my %args = ($self->_run_instance_args,@args);
789              
790             # fix possible gotcha -- instance store is not allowed for micro instances.
791             $args{-root_type} = 'ebs' if $args{-instance_type} eq 't1.micro';
792             $args{-name} ||= $self->new_server_name;
793              
794             my ($keyname,$keyfile) = $self->_security_key;
795             my $security_group = $self->_security_group;
796             my $image = $self->_search_for_image(%args) or croak "No suitable image found";
797             $args{-architecture} = $image->architecture;
798              
799             my ($instance) = $self->ec2->run_instances(
800             -image_id => $image,
801             -security_group_id => $security_group,
802             -key_name => $keyname,
803             %args,
804             );
805             $instance or croak $self->ec2->error_str;
806              
807             my $success;
808             while (!$success) {
809             # race condition...
810             $success = eval{ $instance->add_tags(StagingRole => 'StagingInstance',
811             Name => "Staging server $args{-name} created by ".__PACKAGE__,
812             StagingUsername => $self->username,
813             StagingName => $args{-name});
814             }
815             }
816              
817             my $class = $args{-server_class} || $self->server_class;
818            
819             my $server = $class->new(
820             -keyfile => $keyfile,
821             -username => $self->username,
822             -instance => $instance,
823             -manager => $self,
824             -name => $args{-name},
825             @args,
826             );
827             eval {
828             local $SIG{ALRM} = sub {die 'timeout'};
829             alarm(SERVER_STARTUP_TIMEOUT);
830             $self->wait_for_servers($server);
831             };
832             alarm(0);
833             croak "server did not start after ",SERVER_STARTUP_TIMEOUT," seconds"
834             if $@ =~ /timeout/;
835             $self->register_server($server);
836             return $server;
837             }
838              
839             sub _run_instance_args {
840             my $self = shift;
841             my @args;
842             for my $arg (qw(instance_type availability_zone architecture image_name root_type)) {
843             push @args,("-${arg}" => $self->$arg);
844             }
845             return @args;
846             }
847              
848             =head2 $server = $manager->get_server(-name=>$name,%other_options)
849              
850             =head2 $server = $manager->get_server($name)
851              
852             Return an existing VM::EC2::Staging::Server object having the
853             indicated symbolic name, or create a new server if one with this name
854             does not already exist. The server's instance characteristics will be
855             configured according to the options passed to the manager at create
856             time (e.g. -availability_zone, -instance_type). These options can be
857             overridden by %other_args. See provision_volume() for details.
858              
859             =cut
860              
861             sub get_server {
862             my $self = shift;
863             unshift @_,'-name' if @_ == 1;
864              
865             my %args = @_;
866             $args{-name} ||= $self->new_server_name;
867              
868             # find servers of same name
869             local $^W = 0; # prevent an uninitialized value warning
870             my %servers = map {$_->name => $_} $self->servers;
871             my $server = $servers{$args{-name}} || $self->provision_server(%args);
872              
873             # this information needs to be renewed each time
874             $server->username($args{-username}) if $args{-username};
875             bless $server,$args{-server_class} if $args{-server_class};
876              
877             $server->start unless $server->ping;
878             return $server;
879             }
880              
881             =head2 $server = $manager->get_server_in_zone(-zone=>$availability_zone,%other_options)
882              
883             =head2 $server = $manager->get_server_in_zone($availability_zone)
884              
885             Return an existing VM::EC2::Staging::Server running in the indicated
886             symbolic name, or create a new server if one with this name does not
887             already exist. The server's instance characteristics will be
888             configured according to the options passed to the manager at create
889             time (e.g. -availability_zone, -instance_type). These options can be
890             overridden by %other_args. See provision_server() for details.
891              
892             =cut
893              
894             sub get_server_in_zone {
895             my $self = shift;
896             unshift @_,'-availability_zone' if @_ == 1;
897             my %args = @_;
898             my $zone = $args{-availability_zone};
899             if ($zone && (my $servers = $Zones{$zone}{Servers})) {
900             my $server = (values %{$servers})[0];
901             $server->start unless $server->is_up;
902             return $server;
903             }
904             else {
905             return $self->provision_server(%args);
906             }
907             }
908              
909             =head2 $server = $manager->find_server_by_instance($instance_id)
910              
911             Given an EC2 instanceId, return the corresponding
912             VM::EC2::Staging::Server, if any.
913              
914             =cut
915              
916             sub find_server_by_instance {
917             my $self = shift;
918             my $server = shift;
919             return $Instances{$server};
920             }
921              
922             =head2 @servers $manager->servers
923              
924             Return all registered VM::EC2::Staging::Servers in the zone managed by
925             the manager.
926              
927             =cut
928              
929             sub servers {
930             my $self = shift;
931             my $endpoint = $self->ec2->endpoint;
932             return $self->_servers($endpoint);
933             }
934              
935             =head2 $manager->start_all_servers
936              
937             Start all VM::EC2::Staging::Servers that are currently in the "stop"
938             state.
939              
940             =cut
941              
942             sub start_all_servers {
943             my $self = shift;
944             my @servers = $self->servers;
945             my @need_starting = grep {$_->current_status eq 'stopped'} @servers;
946             return unless @need_starting;
947             eval {
948             local $SIG{ALRM} = sub {die 'timeout'};
949             alarm(SERVER_STARTUP_TIMEOUT);
950             $self->_start_instances(@need_starting);
951             };
952             alarm(0);
953             croak "some servers did not start after ",SERVER_STARTUP_TIMEOUT," seconds"
954             if $@ =~ /timeout/;
955             }
956              
957             =head2 $manager->stop_all_servers
958              
959             Stop all VM::EC2::Staging::Servers that are currently in the "running"
960             state.
961              
962             =cut
963              
964             sub stop_all_servers {
965             my $self = shift;
966             my $ec2 = $self->ec2;
967             my @servers = grep {$_->ec2 eq $ec2} $self->servers;
968             @servers or return;
969             $self->info("Stopping servers @servers.\n");
970             $self->ec2->stop_instances(@servers);
971             $self->ec2->wait_for_instances(@servers);
972             }
973              
974             =head2 $manager->terminate_all_servers
975              
976             Terminate all VM::EC2::Staging::Servers and unregister them.
977              
978             =cut
979              
980             sub terminate_all_servers {
981             my $self = shift;
982             my $ec2 = $self->ec2 or return;
983             my @servers = $self->servers or return;
984             $self->_terminate_servers(@servers);
985             }
986              
987             =head2 $manager->force_terminate_all_servers
988              
989             Force termination of all VM::EC2::Staging::Servers, even if the
990             internal registration system indicates that some may be in use by
991             other Manager instances.
992              
993             =cut
994              
995             sub force_terminate_all_servers {
996             my $self = shift;
997             my $ec2 = $self->ec2 or return;
998             my @servers = $self->servers or return;
999             $ec2->terminate_instances(@servers) or warn $self->ec2->error_str;
1000             $ec2->wait_for_instances(@servers);
1001             }
1002              
1003             sub _terminate_servers {
1004             my $self = shift;
1005             my @servers = @_;
1006             my $ec2 = $self->ec2 or return;
1007              
1008             my @terminate;
1009             foreach (@servers) {
1010             my $in_use = $self->unregister_server($_);
1011             if ($in_use) {
1012             $self->warn("$_ is still in use. Will not terminate.\n");
1013             next;
1014             }
1015             push @terminate,$_;
1016             }
1017            
1018             if (@terminate) {
1019             $self->info("Terminating servers @terminate.\n");
1020             $ec2->terminate_instances(@terminate) or warn $self->ec2->error_str;
1021             $ec2->wait_for_instances(@terminate);
1022             }
1023              
1024             unless ($self->reuse_key) {
1025             $ec2->delete_key_pair($_) foreach $ec2->describe_key_pairs(-filter=>{'key-name' => 'staging-key-*'});
1026             }
1027             }
1028              
1029             =head2 $manager->wait_for_servers(@servers)
1030              
1031             Wait until all the servers on the list @servers are up and able to
1032             accept ssh commands. You may wish to wrap this in an eval{} and
1033             timeout in order to avoid waiting indefinitely.
1034              
1035             =cut
1036              
1037             sub wait_for_servers {
1038             my $self = shift;
1039             my @instances = @_;
1040             my $status = $self->ec2->wait_for_instances(@instances);
1041             my %pending = map {$_=>$_} grep {$_->current_status eq 'running'} @instances;
1042             $self->info("Waiting for ssh daemon on @instances.\n") if %pending;
1043             while (%pending) {
1044             for my $s (values %pending) {
1045             unless ($s->ping) {
1046             sleep 5;
1047             next;
1048             }
1049             delete $pending{$s};
1050             }
1051             }
1052             return $status;
1053             }
1054              
1055             sub _start_instances {
1056             my $self = shift;
1057             my @need_starting = @_;
1058             $self->info("starting instances: @need_starting.\n");
1059             $self->ec2->start_instances(@need_starting);
1060             $self->wait_for_servers(@need_starting);
1061             }
1062              
1063             =head1 Instance Methods for Managing Staging Volumes
1064              
1065             These methods allow you to create and interrogate staging
1066             volumes. They each return one or more VM::EC2::Staging::Volume
1067             objects. See L for more information about
1068             what you can do with these staging volume objects.
1069              
1070             =head2 $volume = $manager->provision_volume(%options)
1071              
1072             Create and register a new VM::EC2::Staging::Volume and mount it on a
1073             staging server in the appropriate availability zone. A new staging
1074             server will be created for this purpose if one does not already
1075             exist.
1076              
1077             If you provide a symbolic name for the volume and the manager has
1078             previously snapshotted a volume by the same name, then the snapshot
1079             will be used to create the volume (this behavior can be suppressed by
1080             passing -reuse=>0). This allows for the following pattern for
1081             efficiently updating a snapshotted volume:
1082              
1083             my $vol = $manager->provision_volume(-name=>'MyPictures',
1084             -size=>10);
1085             $vol->put('/usr/local/my_pictures/'); # will do an rsync from local directory
1086             $vol->create_snapshot; # write out to a snapshot
1087             $vol->delete;
1088              
1089             You may also explicitly specify a volumeId or snapshotId. The former
1090             allows you to place an existing volume under management of
1091             VM::EC2::Staging::Manager and returns a corresponding staging volume
1092             object. The latter creates the staging volume from the indicated
1093             snapshot, irregardless of whether the snapshot was created by the
1094             staging manager at an earlier time.
1095              
1096             Newly-created staging volumes are automatically formatted as ext4
1097             filesystems and mounted on the staging server under
1098             /mnt/Staging/$name, where $name is the staging volume's symbolic
1099             name. The filesystem type and the mountpoint can be modified with the
1100             -fstype and -mount arguments, respectively. In addition, you may
1101             specify an -fstype of "raw", in which case the volume will be attached
1102             to a staging server (creating the server first if necessary) but not
1103             formatted or mounted. This is useful when creating multi-volume RAID
1104             or LVM setups.
1105              
1106             Options:
1107              
1108             -name Name of the staging volume. A fatal error issues if a staging
1109             volume by this name already exists (use get_volume() to
1110             avoid this). If no name is provided, then a random
1111             unique one is chosen for you.
1112              
1113             -availability_zone
1114             Availability zone in which to create this
1115             volume. If none is specified, then a zone is chosen that
1116             reuses an existing staging server, if any.
1117              
1118             -size Size of the desired volume, in GB.
1119              
1120             -fstype Filesystem type for the volume, ext4 by default. Supported
1121             types are ext2, ext3, ext4, xfs, reiserfs, jfs, hfs,
1122             ntfs, vfat, msdos, and raw.
1123              
1124             -mount Mount point for this volume on the staging server (e.g. /opt/bin).
1125             Use with care, as there are no checks to prevent you from mounting
1126             two staging volumes on top of each other or mounting over essential
1127             operating system paths.
1128              
1129             -label Volume label. Only applies to filesystems that support labels
1130             (all except hfs, vfat, msdos and raw).
1131              
1132             -volume_id Create the staging volume from an existing EBS volume with
1133             the specified ID. Most other options are ignored in this
1134             case.
1135              
1136             -snapshot_id
1137             Create the staging volume from an existing EBS
1138             snapshot. If a size is specified that is larger than the
1139             snapshot, then the volume and its filesystem will be
1140             automatically extended (this only works for ext volumes
1141             at the moment). Shrinking of volumes is not currently
1142             supported.
1143              
1144             -reuse If true, then the most recent snapshot created from a staging
1145             volume of the same name is used to create the
1146             volume. This is the default. Pass 0 to disable this
1147             behavior.
1148              
1149             The B<-reuse> argument is intended to support the following use case
1150             in which you wish to rsync a directory on a host system somewhere to
1151             an EBS snapshot, without maintaining a live server and volume on EC2:
1152              
1153             my $volume = $manager->provision_volume(-name=>'backup_1',
1154             -reuse => 1,
1155             -fstype => 'ext3',
1156             -size => 10);
1157             $volume->put('fred@gw.harvard.edu:my_music');
1158             $volume->create_snapshot('Music Backup '.localtime);
1159             $volume->delete;
1160              
1161             The next time this script is run, the "backup_1" volume will be
1162             recreated from the most recent snapshot, minimizing copying. A new
1163             snapshot is created, and the staging volume is deleted.
1164              
1165             =cut
1166              
1167             sub provision_volume {
1168             my $self = shift;
1169             my %args = @_;
1170              
1171             $args{-name} ||= $self->new_volume_name;
1172             $args{-size} ||= 1 unless $args{-snapshot_id} || $args{-volume_id};
1173             $args{-volume_id} ||= undef;
1174             $args{-snapshot_id} ||= undef;
1175             $args{-reuse} = $self->reuse_volumes unless defined $args{-reuse};
1176             $args{-mount} ||= '/mnt/Staging/'.$args{-name}; # BUG: "/mnt/Staging" is hardcoded in multiple places
1177             $args{-fstype} ||= 'ext4';
1178             $args{-availability_zone} ||= $self->_select_used_zone;
1179             $args{-label} ||= $args{-name};
1180              
1181             $self->find_volume_by_name($args{-name}) &&
1182             croak "There is already a volume named $args{-name} in this region";
1183            
1184             if ($args{-snapshot_id}) {
1185             $self->info("Provisioning volume from snapshot $args{-snapshot_id}.\n");
1186             } elsif ($args{-volume_id}) {
1187             $self->info("Provisioning volume from volume $args{-volume_id}.\n");
1188             my $v = $self->ec2->describe_volumes($args{-volume_id});
1189             $args{-availability_zone} = $v->availabilityZone if $v;
1190             $args{-size} = $v->size if $v;
1191             } else {
1192             $self->info("Provisioning a new $args{-size} GB $args{-fstype} volume.\n");
1193             }
1194              
1195             $args{-availability_zone} ? $self->info("Obtaining a staging server in zone $args{-availability_zone}.\n")
1196             : $self->info("Obtaining a staging server.\n");
1197             my $server = $self->get_server_in_zone($args{-availability_zone});
1198             $server->start unless $server->ping;
1199             my $volume = $server->provision_volume(%args);
1200             $self->register_volume($volume);
1201             return $volume;
1202             }
1203              
1204             =head2 $volume = $manager->get_volume(-name=>$name,%other_options)
1205              
1206             =head2 $volume = $manager->get_volume($name)
1207              
1208             Return an existing VM::EC2::Staging::Volume object with the indicated
1209             symbolic name, or else create a new volume if one with this name does
1210             not already exist. The volume's characteristics will be configured
1211             according to the options in %other_args. See provision_volume() for
1212             details. If called with no arguments, this method returns Volume
1213             object with default characteristics and a randomly-assigned name.
1214              
1215             =cut
1216              
1217             sub get_volume {
1218             my $self = shift;
1219              
1220             unshift @_,'-name' if @_ == 1;
1221             my %args = @_;
1222             $args{-name} ||= $self->new_volume_name;
1223              
1224             # find volume of same name
1225             my %vols = map {$_->name => $_} $self->volumes;
1226             my $vol = $vols{$args{-name}} || $self->provision_volume(%args);
1227             return $vol;
1228             }
1229              
1230             =head2 $result = $manager->rsync($src1,$src2,$src3...,$dest)
1231              
1232             This method provides remote synchronization (rsync) file-level copying
1233             between one or more source locations and a destination location via an
1234             ssh tunnel. Copying among arbitrary combinations of local and remote
1235             filesystems is supported, with the caveat that the remote filesystems
1236             must be contained on volumes and servers managed by this module (see
1237             below for a workaround).
1238              
1239             You may provide two or more directory paths. The last path will be
1240             treated as the copy destination, and the source paths will be treated
1241             as copy sources. All copying is performed using the -avz options,
1242             which activates recursive directory copying in which ownership,
1243             modification times and permissions are preserved, and compresses the
1244             data to reduce network usage. Verbosity is set so that the names of
1245             copied files are printed to STDERR. If you do not wish this, then use
1246             call the manager's quiet() method with a true value.
1247              
1248             Source paths can be formatted in one of several ways:
1249              
1250             /absolute/path
1251             Copy the contents of the directory /absolute/path located on the
1252             local machine to the destination. This will create a
1253             subdirectory named "path" on the destination disk. Add a slash
1254             to the end of the path (i.e. "/absolute/path/") in order to
1255             avoid creating this subdirectory on the destination disk.
1256              
1257             ./relative/path
1258             Relative paths work the way you expect, and depend on the current
1259             working directory. The terminating slash rule applies.
1260              
1261             $staging_volume
1262             Pass a VM::EC2::Staging::Volume to copy the contents of the
1263             volume to the destination disk starting at the root of the
1264             volume. Note that you do *not* need to have any knowledge of the
1265             mount point for this volume in order to copy its contents.
1266              
1267             $staging_volume:/absolute/path
1268             $staging_volume:absolute/path
1269             $staging_volume/absolute/path
1270             All these syntaxes accomplish the same thing, which is to
1271             copy a subdirectory of a staging volume to the destination disk.
1272             The root of the volume is its top level, regardless of where it
1273             is mounted on the staging server. Because of string
1274             interpolation magic, you can enclose staging volume object names
1275             in quotes in order to construct the path, as in
1276             "$picture_volume:/family/vacations/". As in local paths, a
1277             terminating slash indicates that the contents of the last
1278             directory in the path are to be copied without creating the
1279             enclosing directory on the desetination. Note that you do *not*
1280             need to have any knowledge of the mount point for this volume in
1281             order to copy its contents.
1282              
1283             $staging_server:/absolute/path
1284             Pass a staging server object and absolute path to copy the contents
1285             of this path to the destination disk. Because of string interpolation
1286             you can include server objects in quotes: "$my_server:/opt"
1287              
1288             $staging_server:relative/path
1289             This form will copy data from paths relative to the remote user's home
1290             directory on the staging server. Typically not very useful, but supported.
1291              
1292             The same syntax is supported for destination paths, except that it
1293             makes no difference whether a path has a trailing slash or not.
1294              
1295             As with the rsync command, if you proceed a path with a single colon
1296             (:/my/path), it is a short hand to use the previous server/volume/host
1297             in the source list.
1298              
1299             When specifying multiple source directories, all source directories must
1300             reside on the same local or remote machine. This is legal:
1301              
1302             $manager->rsync("$picture_volume:/family/vacations",
1303             "$picture_volume:/family/picnics"
1304             => "$backup_volume:/recent_backups");
1305              
1306             This is not:
1307              
1308             $manager->rsync("$picture_volume:/family/vacations",
1309             "$audio_volume:/beethoven"
1310             => "$backup_volume:/recent_backups");
1311              
1312             When specifying multiple sources, you may give the volume or server
1313             once for the first source and then start additional source paths with
1314             a ":" to indicate the same volume or server is to be used:
1315              
1316             $manager->rsync("$picture_volume:/family/vacations",
1317             ":/family/picnics"
1318             => "$backup_volume:/recent_backups");
1319              
1320             When copying to/from the local machine, the rsync process will run as
1321             the user that the script was launched by. However, on remote servers
1322             managed by the staging manager, the rsync process will run as
1323             superuser.
1324              
1325             The rsync() method will also accept regular remote DNS names and IP
1326             addresses, optionally preceded by a username:
1327              
1328             $manager->rsync("$picture_volume:/family/vacations" => 'fred@gw.harvard.edu:/tmp')
1329              
1330             When called in this way, the method does what it can to avoid
1331             prompting for a password or passphrase on the non-managed host
1332             (gw.harvard.edu in the above example). This includes turning off
1333             strict host checking and forwarding the user agent information from
1334             the local machine.
1335              
1336             =head2 $result = $manager->rsync(\@options,$src1,$src2,$src3...,$dest)
1337              
1338             This is a variant of the rsync command in which extra options can be
1339             passed to rsync by providing an array reference as the first argument.
1340             For example:
1341              
1342             $manager->rsync(['--exclude' => '*~'],
1343             '/usr/local/backups',
1344             "$my_server:/usr/local");
1345              
1346             =cut
1347              
1348             # most general form
1349             #
1350             sub rsync {
1351             my $self = shift;
1352             croak "usage: VM::EC2::Staging::Manager->rsync(\$source_path1,\$source_path2\...,\$dest_path)"
1353             unless @_ >= 2;
1354              
1355             my @p = @_;
1356             my @user_args = ($p[0] && ref($p[0]) eq 'ARRAY')
1357             ? @{shift @p}
1358             : ();
1359              
1360             undef $LastHost;
1361             undef $LastMt;
1362             my @paths = map {$self->_resolve_path($_)} @p;
1363              
1364             my $dest = pop @paths;
1365             my @source = @paths;
1366              
1367             my %hosts;
1368             local $^W=0; # avoid uninit value errors
1369             foreach (@source) {
1370             $hosts{$_->[0]} = $_->[0];
1371             }
1372             croak "More than one source host specified" if keys %hosts > 1;
1373             my ($source_host) = values %hosts;
1374             my $dest_host = $dest->[0];
1375              
1376             my @source_paths = map {$_->[1]} @source;
1377             my $dest_path = $dest->[1];
1378              
1379             my $rsync_args = $self->_rsync_args;
1380             my $dots;
1381              
1382             if ($self->verbosity == VERBOSE_INFO) {
1383             $rsync_args .= 'v'; # print a line for each file
1384             $dots = '2>&1|/tmp/dots.pl t';
1385             }
1386             $rsync_args .= ' '.join ' ', map {_quote_shell($_)} @user_args if @user_args;
1387              
1388             my $src_is_server = $source_host && UNIVERSAL::isa($source_host,'VM::EC2::Staging::Server');
1389             my $dest_is_server = $dest_host && UNIVERSAL::isa($dest_host,'VM::EC2::Staging::Server');
1390              
1391             # this is true when one of the paths contains a ":", indicating an rsync
1392             # path that contains a hostname, but not a managed server
1393             my $remote_path = "@source_paths $dest_path" =~ /:/;
1394              
1395             # remote rsync on either src or dest server
1396             if ($remote_path && ($src_is_server || $dest_is_server)) {
1397             my $server = $source_host || $dest_host;
1398             $self->_upload_dots_script($server) if $dots;
1399             return $server->ssh(['-t','-A'],"sudo -E rsync -e 'ssh -o \"CheckHostIP no\" -o \"StrictHostKeyChecking no\"' $rsync_args @source_paths $dest_path $dots");
1400             }
1401              
1402             # localhost => localhost
1403             if (!($source_host || $dest_host)) {
1404             my $dots_cmd = $self->_dots_cmd;
1405             return system("rsync @source $dest $dots_cmd") == 0;
1406             }
1407              
1408             # localhost => DataTransferServer
1409             if ($dest_is_server && !$src_is_server) {
1410             return $dest_host->_rsync_put($rsync_args,@source_paths,$dest_path);
1411             }
1412              
1413             # DataTransferServer => localhost
1414             if ($src_is_server && !$dest_is_server) {
1415             return $source_host->_rsync_get($rsync_args,@source_paths,$dest_path);
1416             }
1417              
1418             if ($source_host eq $dest_host) {
1419             $self->info("Beginning rsync @source_paths $dest_path...\n");
1420             my $result = $source_host->ssh('sudo','rsync',$rsync_args,@source_paths,$dest_path);
1421             $self->info("...rsync done.\n");
1422             return $result;
1423             }
1424              
1425             # DataTransferServer1 => DataTransferServer2
1426             # this one is slightly more difficult because datatransferserver1 has to
1427             # ssh authenticate against datatransferserver2.
1428             my $keyname = $self->_authorize($source_host => $dest_host);
1429              
1430             my $dest_ip = $dest_host->instance->dnsName;
1431             my $ssh_args = $source_host->_ssh_escaped_args;
1432             my $keyfile = $source_host->keyfile;
1433             $ssh_args =~ s/$keyfile/$keyname/; # because keyfile is embedded among args
1434             $self->info("Beginning rsync @source_paths $dest_ip:$dest_path...\n");
1435             $self->_upload_dots_script($source_host) if $dots;
1436             my $result = $source_host->ssh('sudo','rsync',$rsync_args,
1437             '-e',"'ssh $ssh_args'",
1438             "--rsync-path='sudo rsync'",
1439             @source_paths,"$dest_ip:$dest_path",$dots);
1440             $self->info("...rsync done.\n");
1441             return $result;
1442             }
1443              
1444             sub _quote_shell {
1445             my $thing = shift;
1446             $thing =~ s/\s/\ /;
1447             $thing =~ s/(['"])/\\($1)/;
1448             $thing;
1449             }
1450              
1451             =head2 $manager->dd($source_vol=>$dest_vol)
1452              
1453             This method performs block-level copying of the contents of
1454             $source_vol to $dest_vol by using dd over an SSH tunnel, where both
1455             source and destination volumes are VM::EC2::Staging::Volume
1456             objects. The volumes must be attached to a server but not
1457             mounted. Everything in the volume, including its partition table, is
1458             copied, allowing you to make an exact image of a disk.
1459              
1460             The volumes do B actually need to reside on this server, but can
1461             be attached to any staging server in the zone.
1462              
1463             =cut
1464              
1465             # for this to work, we have to create the concept of a "raw" staging volume
1466             # that is attached, but not mounted
1467             sub dd {
1468             my $self = shift;
1469              
1470             @_==2 or croak "usage: VM::EC2::Staging::Manager->dd(\$source_vol=>\$dest_vol)";
1471              
1472             my ($vol1,$vol2) = @_;
1473             my ($server1,$device1) = ($vol1->server,$vol1->mtdev);
1474             my ($server2,$device2) = ($vol2->server,$vol2->mtdev);
1475             my $hush = $self->verbosity < VERBOSE_INFO ? '2>/dev/null' : '';
1476             my $use_pv = $self->verbosity >= VERBOSE_WARN;
1477             my $gigs = $vol1->size;
1478              
1479             if ($use_pv) {
1480             $self->info("Configuring PV to show dd progress...\n");
1481             $server1->ssh("if [ ! -e /usr/bin/pv ]; then sudo apt-get -qq update >/dev/null 2>&1; sudo apt-get -y -qq install pv >/dev/null 2>&1; fi");
1482             }
1483              
1484             if ($server1 eq $server2) {
1485             if ($use_pv) {
1486             print STDERR "\n";
1487             $server1->ssh(['-t'], "sudo dd if=$device1 2>/dev/null | pv -f -s ${gigs}G -petr | sudo dd of=$device2 2>/dev/null");
1488             } else {
1489             $server1->ssh("sudo dd if=$device1 of=$device2 $hush");
1490             }
1491             } else {
1492             my $keyname = $self->_authorize($server1,$server2);
1493             my $dest_ip = $server2->instance->dnsName;
1494             my $ssh_args = $server1->_ssh_escaped_args;
1495             my $keyfile = $server1->keyfile;
1496             $ssh_args =~ s/$keyfile/$keyname/; # because keyfile is embedded among args
1497             my $pv = $use_pv ? "2>/dev/null | pv -s ${gigs}G -petr" : '';
1498             $server1->ssh(['-t'], "sudo dd if=$device1 $hush $pv | gzip -1 - | ssh $ssh_args $dest_ip 'gunzip -1 - | sudo dd of=$device2'");
1499             }
1500             }
1501              
1502             # take real or symbolic name and turn it into a two element
1503             # list consisting of server object and mount point
1504             # possible forms:
1505             # /local/path
1506             # vol-12345/relative/path
1507             # vol-12345:/relative/path
1508             # vol-12345:relative/path
1509             # $server:/absolute/path
1510             # $server:relative/path
1511             #
1512             # treat path as symbolic if it does not start with a slash
1513             # or dot characters
1514             sub _resolve_path {
1515             my $self = shift;
1516             my $vpath = shift;
1517              
1518             my ($servername,$pathname);
1519             if ($vpath =~ /^(vol-[0-9a-f]+):?(.*)/ &&
1520             (my $vol = VM::EC2::Staging::Manager->find_volume_by_volid($1))) {
1521             my $path = $2 || '/';
1522             $path = "/$path" if $path && $path !~ m!^/!;
1523             $vol->_spin_up;
1524             $servername = $LastHost = $vol->server;
1525             my $mtpt = $LastMt = $vol->mtpt;
1526             $pathname = $mtpt;
1527             $pathname .= $path if $path;
1528             } elsif ($vpath =~ /^(i-[0-9a-f]{8}):(.+)$/ &&
1529             (my $server = VM::EC2::Staging::Manager->find_server_by_instance($1))) {
1530             $servername = $LastHost = $server;
1531             $pathname = $2;
1532             } elsif ($vpath =~ /^:(.+)$/) {
1533             $servername = $LastHost if $LastHost;
1534             $pathname = $LastHost && $LastMt ? "$LastMt/$2" : $2;
1535             } else {
1536             return [undef,$vpath]; # localhost
1537             }
1538             return [$servername,$pathname];
1539             }
1540              
1541             sub _rsync_args {
1542             my $self = shift;
1543             my $verbosity = $self->verbosity;
1544             return $verbosity < VERBOSE_WARN ? '-azq'
1545             :$verbosity < VERBOSE_INFO ? '-azh'
1546             :$verbosity < VERBOSE_DEBUG ? '-azh'
1547             : '-azhv'
1548             }
1549              
1550             sub _authorize {
1551             my $self = shift;
1552             my ($source_host,$dest_host) = @_;
1553             my $keyname = "/tmp/${source_host}_to_${dest_host}";
1554             unless ($source_host->has_key($keyname)) {
1555             $source_host->info("creating ssh key for server to server data transfer.\n");
1556             $source_host->ssh("ssh-keygen -t dsa -q -f $keyname/dev/null");
1557             $source_host->has_key($keyname=>1);
1558             }
1559             unless ($dest_host->accepts_key($keyname)) {
1560             my $key_stuff = $source_host->scmd("cat ${keyname}.pub");
1561             chomp($key_stuff);
1562             $dest_host->ssh("mkdir -p .ssh; chmod 0700 .ssh; (echo '$key_stuff' && cat .ssh/authorized_keys) | sort | uniq > .ssh/authorized_keys.tmp; mv .ssh/authorized_keys.tmp .ssh/authorized_keys; chmod 0600 .ssh/authorized_keys");
1563             $dest_host->accepts_key($keyname=>1);
1564             }
1565              
1566             return $keyname;
1567             }
1568              
1569             =head2 $volume = $manager->find_volume_by_volid($volume_id)
1570              
1571             Given an EC2 volumeId, return the corresponding
1572             VM::EC2::Staging::Volume, if any.
1573              
1574             =cut
1575              
1576             sub find_volume_by_volid {
1577             my $self = shift;
1578             my $volid = shift;
1579             return $Volumes{$volid};
1580             }
1581              
1582             =head2 $volume = $manager->find_volume_by_name($name)
1583              
1584             Given a staging name (assigned at volume creation time), return the
1585             corresponding VM::EC2::Staging::Volume, if any.
1586              
1587             =cut
1588              
1589             sub find_volume_by_name {
1590             my $self = shift;
1591             my $name = shift;
1592             my %volumes = map {$_->name => $_} $self->volumes;
1593             return $volumes{$name};
1594             }
1595              
1596             =head2 @volumes = $manager->volumes
1597              
1598             Return all VM::EC2::Staging::Volumes managed in this zone.
1599              
1600             =cut
1601              
1602             sub volumes {
1603             my $self = shift;
1604             return grep {$_->ec2->endpoint eq $self->ec2->endpoint} values %Volumes;
1605             }
1606              
1607             =head1 Instance Methods for Accessing Configuration Options
1608              
1609             This section documents accessor methods that allow you to examine or
1610             change configuration options that were set at create time. Called with
1611             an argument, the accessor changes the option and returns the option's
1612             previous value. Called without an argument, the accessor returns the
1613             option's current value.
1614              
1615             =head2 $on_exit = $manager->on_exit([$new_behavior])
1616              
1617             Get or set the "on_exit" option, which specifies what to do with
1618             existing staging servers when the staging manager is destroyed. Valid
1619             values are "terminate", "stop" and "run".
1620              
1621             =head2 $reuse_key = $manager->reuse_key([$boolean])
1622              
1623             Get or set the "reuse_key" option, which if true uses the same
1624             internally-generated ssh keypair for all running instances. If false,
1625             then a new keypair will be created for each staging server. The
1626             keypair will be destroyed automatically when the staging server
1627             terminates (but only if the staging manager initiates the termination
1628             itself).
1629              
1630             =head2 $username = $manager->username([$new_username])
1631              
1632             Get or set the username used to log into staging servers.
1633              
1634             =head2 $architecture = $manager->architecture([$new_architecture])
1635              
1636             Get or set the architecture (i386, x86_64) to use for launching
1637             new staging servers.
1638              
1639             =head2 $root_type = $manager->root_type([$new_type])
1640              
1641             Get or set the instance root type for new staging servers
1642             ("instance-store", "ebs").
1643              
1644             =head2 $instance_type = $manager->instance_type([$new_type])
1645              
1646             Get or set the instance type to use for new staging servers
1647             (e.g. "t1.micro"). I recommend that you use "m1.small" (the default)
1648             or larger instance types because of the extremely slow I/O of the
1649             micro instance. In addition, micro instances running Ubuntu have a
1650             known bug that prevents them from unmounting and remounting EBS
1651             volumes repeatedly on the same block device. This can lead to hangs
1652             when the staging manager tries to create volumes.
1653              
1654             =head2 $reuse_volumes = $manager->reuse_volumes([$new_boolean])
1655              
1656             This gets or sets the "reuse_volumes" option, which if true causes the
1657             provision_volumes() call to create staging volumes from existing EBS
1658             volumes and snapshots that share the same staging manager symbolic
1659             name. See the discussion under VM::EC2->staging_manager(), and
1660             VM::EC2::Staging::Manager->provision_volume().
1661              
1662             =head2 $name = $manager->image_name([$new_name])
1663              
1664             This gets or sets the "image_name" option, which is the AMI ID or AMI
1665             name to use when creating new staging servers. Names beginning with
1666             "ami-" are treated as AMI IDs, and everything else is treated as a
1667             pattern match on the AMI name.
1668              
1669             =head2 $zone = $manager->availability_zone([$new_zone])
1670              
1671             Get or set the default availability zone to use when creating new
1672             servers and volumes. An undef value allows the staging manager to
1673             choose the zone in a way that minimizes resources.
1674              
1675             =head2 $class_name = $manager->volume_class([$new_class])
1676              
1677             Get or set the name of the perl package that implements staging
1678             volumes, VM::EC2::Staging::Volume by default. Staging volumes created
1679             by the manager will have this class type.
1680              
1681             =head2 $class_name = $manager->server_class([$new_class])
1682              
1683             Get or set the name of the perl package that implements staging
1684             servers, VM::EC2::Staging::Server by default. Staging servers created
1685             by the manager will have this class type.
1686              
1687             =head2 $boolean = $manager->scan([$boolean])
1688              
1689             Get or set the "scan" flag, which if true will cause the zone to be
1690             scanned quickly for existing managed servers and volumes when the
1691             manager is first created.
1692              
1693             =head2 $path = $manager->dot_directory([$new_directory])
1694              
1695             Get or set the dot directory which holds private key files.
1696              
1697             =cut
1698              
1699             sub dot_directory {
1700             my $self = shift;
1701             my $dir = $self->dotdir;
1702             unless (-e $dir && -d $dir) {
1703             mkdir $dir or croak "mkdir $dir: $!";
1704             chmod 0700,$dir or croak "chmod 0700 $dir: $!";
1705             }
1706             return $dir;
1707             }
1708              
1709             =head1 Internal Methods
1710              
1711             This section documents internal methods that are not normally called
1712             by end-user scripts but may be useful in subclasses. In addition,
1713             there are a number of undocumented internal methods that begin with
1714             the "_" character. Explore the source code to learn about these.
1715              
1716             =head2 $ok = $manager->environment_ok
1717              
1718             This performs a check on the environment in which the module is
1719             running. For this module to work properly, the ssh, rsync and dd
1720             programs must be found in the PATH. If all three programs are found,
1721             then this method returns true.
1722              
1723             This method can be called as an instance method or class method.
1724              
1725             =cut
1726              
1727             sub environment_ok {
1728             my $self = shift;
1729             foreach (qw(dd ssh rsync)) {
1730             chomp (my $path = `which $_`);
1731             return unless $path;
1732             }
1733             return 1;
1734             }
1735              
1736             =head2 $name = $manager->default_verbosity
1737              
1738             Returns the default verbosity level (2: warning+informational messages). This
1739             is overridden using -verbose at create time.
1740              
1741             =cut
1742              
1743             sub default_verbosity { VERBOSE_INFO }
1744              
1745             =head2 $name = $manager->default_exit_behavior
1746              
1747             Return the default exit behavior ("stop") when the manager terminates.
1748             Intended to be overridden in subclasses.
1749              
1750             =cut
1751              
1752             sub default_exit_behavior { 'stop' }
1753              
1754             =head2 $name = $manager->default_image_name
1755              
1756             Return the default image name ('ubuntu-precise-12.04') for use in
1757             creating new instances. Intended to be overridden in subclasses.
1758              
1759             =cut
1760              
1761             sub default_image_name { 'ubuntu-precise-12.04' }; # launches faster than precise
1762              
1763             =head2 $name = $manager->default_user_name
1764              
1765             Return the default user name ('ubuntu') for use in creating new
1766             instances. Intended to be overridden in subclasses.
1767              
1768             =cut
1769              
1770             sub default_user_name { 'ubuntu' }
1771              
1772             =head2 $name = $manager->default_architecture
1773              
1774             Return the default instance architecture ('i386') for use in creating
1775             new instances. Intended to be overridden in subclasses.
1776              
1777             =cut
1778              
1779             sub default_architecture { 'i386' }
1780              
1781             =head2 $name = $manager->default_root_type
1782              
1783             Return the default instance root type ('instance-store') for use in
1784             creating new instances. Intended to be overridden in subclasses. Note
1785             that this value is ignored if the exit behavior is "stop", in which case an
1786             ebs-backed instance will be used. Also, the m1.micro instance type
1787             does not come in an instance-store form, so ebs will be used in this
1788             case as well.
1789              
1790             =cut
1791              
1792             sub default_root_type { 'instance-store'}
1793              
1794             =head2 $name = $manager->default_instance_type
1795              
1796             Return the default instance type ('m1.small') for use in
1797             creating new instances. Intended to be overridden in subclasses. We default
1798             to m1.small rather than a micro instance because the I/O in m1.small
1799             is far faster than in t1.micro.
1800              
1801             =cut
1802              
1803             sub default_instance_type { 'm1.small' }
1804              
1805             =head2 $name = $manager->default_reuse_keys
1806              
1807             Return the default value of the -reuse_keys argument ('true'). This
1808             value allows the manager to create an ssh keypair once, and use the
1809             same one for all servers it creates over time. If false, then a new
1810             keypair is created for each server and then discarded when the server
1811             terminates.
1812              
1813             =cut
1814              
1815             sub default_reuse_keys { 1 }
1816              
1817             =head2 $name = $manager->default_reuse_volumes
1818              
1819             Return the default value of the -reuse_volumes argument ('true'). This
1820             value instructs the manager to use the symbolic name of the volume to
1821             return an existing volume whenever a request is made to provision a
1822             new one of the same name.
1823              
1824             =cut
1825              
1826             sub default_reuse_volumes { 1 }
1827              
1828             =head2 $path = $manager->default_dot_directory_path
1829              
1830             Return the default value of the -dotdir argument
1831             ("$ENV{HOME}/.vm-ec2-staging"). This value instructs the manager to
1832             use the symbolic name of the volume to return an existing volume
1833             whenever a request is made to provision a new one of the same name.
1834              
1835             =cut
1836              
1837             sub default_dot_directory_path {
1838             my $class = shift;
1839             my $dir = File::Spec->catfile($ENV{HOME},'.vm-ec2-staging');
1840             return $dir;
1841             }
1842              
1843             =head2 $class_name = $manager->default_volume_class
1844              
1845             Return the class name for staging volumes created by the manager,
1846             VM::EC2::Staging::Volume by default. If you wish a subclass of
1847             VM::EC2::Staging::Manager to create a different type of volume,
1848             override this method.
1849              
1850             =cut
1851              
1852             sub default_volume_class {
1853             return 'VM::EC2::Staging::Volume';
1854             }
1855              
1856             =head2 $class_name = $manager->default_server_class
1857              
1858             Return the class name for staging servers created by the manager,
1859             VM::EC2::Staging::Server by default. If you wish a subclass of
1860             VM::EC2::Staging::Manager to create a different type of volume,
1861             override this method.
1862              
1863             =cut
1864              
1865             sub default_server_class {
1866             return 'VM::EC2::Staging::Server';
1867             }
1868              
1869             =head2 $server = $manager->register_server($server)
1870              
1871             Register a VM::EC2::Staging::Server object. Usually called
1872             internally.
1873              
1874             =cut
1875              
1876             sub register_server {
1877             my $self = shift;
1878             my $server = shift;
1879             sleep 1; # AWS lag bugs
1880             my $zone = $server->placement;
1881             $Zones{$zone}{Servers}{$server} = $server;
1882             $Instances{$server->instance} = $server;
1883             return $self->_increment_usage_count($server);
1884             }
1885              
1886             =head2 $manager->unregister_server($server)
1887              
1888             Forget about the existence of VM::EC2::Staging::Server. Usually called
1889             internally.
1890              
1891             =cut
1892              
1893             sub unregister_server {
1894             my $self = shift;
1895             my $server = shift;
1896             my $zone = eval{$server->placement} or return; # avoids problems at global destruction
1897             delete $Zones{$zone}{Servers}{$server};
1898             delete $Instances{$server->instance};
1899             return $self->_decrement_usage_count($server);
1900             }
1901              
1902             =head2 $manager->register_volume($volume)
1903              
1904             Register a VM::EC2::Staging::Volume object. Usually called
1905             internally.
1906              
1907             =cut
1908              
1909             sub register_volume {
1910             my $self = shift;
1911             my $vol = shift;
1912             $self->_increment_usage_count($vol);
1913             $Zones{$vol->availabilityZone}{Volumes}{$vol} = $vol;
1914             $Volumes{$vol->volumeId} = $vol;
1915             }
1916              
1917             =head2 $manager->unregister_volume($volume)
1918              
1919             Forget about a VM::EC2::Staging::Volume object. Usually called
1920             internally.
1921              
1922             =cut
1923              
1924             sub unregister_volume {
1925             my $self = shift;
1926             my $vol = shift;
1927             my $zone = $vol->availabilityZone;
1928             $self->_decrement_usage_count($vol);
1929             delete $Zones{$zone}{$vol};
1930             delete $Volumes{$vol->volumeId};
1931             }
1932              
1933             =head2 $pid = $manager->pid([$new_pid])
1934              
1935             Get or set the process ID of the script that is running the
1936             manager. This is used internally to detect the case in which the
1937             script has forked, in which case we do not want to invoke the manager
1938             class's destructor in the child process (because it may stop or
1939             terminate servers still in use by the parent process).
1940              
1941             =head2 $path = $manager->dotdir([$new_dotdir])
1942              
1943             Low-level version of dot_directory(), differing only in the fact that
1944             dot_directory will automatically create the path, including subdirectories.
1945              
1946             =cut
1947              
1948             =head2 $manager->scan_region
1949              
1950             Synchronize internal list of managed servers and volumes with the EC2
1951             region. Called automatically during new() and needed only if servers &
1952             volumes are changed from outside the module while it is running.
1953              
1954             =cut
1955              
1956             # scan for staging instances in current region and cache them
1957             # into memory
1958             # status should be...
1959             # -on_exit => {'terminate','stop','run'}
1960             sub scan_region {
1961             my $self = shift;
1962             my $ec2 = shift || $self->ec2;
1963             $self->_scan_instances($ec2);
1964             $self->_scan_volumes($ec2);
1965             }
1966              
1967             sub _scan_instances {
1968             my $self = shift;
1969             my $ec2 = shift;
1970             my @instances = $ec2->describe_instances({'tag:StagingRole' => 'StagingInstance',
1971             'instance-state-name' => ['running','stopped']});
1972             for my $instance (@instances) {
1973             my $keyname = $instance->keyName or next;
1974             my $keyfile = $self->_check_keyfile($keyname) or next;
1975             my $username = $instance->tags->{'StagingUsername'} or next;
1976             my $name = $instance->tags->{StagingName} || $self->new_server_name;
1977             my $server = $self->server_class()->new(
1978             -name => $name,
1979             -keyfile => $keyfile,
1980             -username => $username,
1981             -instance => $instance,
1982             -manager => $self,
1983             );
1984             $self->register_server($server);
1985             }
1986             }
1987              
1988             sub _scan_volumes {
1989             my $self = shift;
1990             my $ec2 = shift;
1991              
1992             # now the volumes
1993             my @volumes = $ec2->describe_volumes(-filter=>{'tag:StagingRole' => 'StagingVolume',
1994             'status' => ['available','in-use']});
1995             for my $volume (@volumes) {
1996             my $status = $volume->status;
1997             my $zone = $volume->availabilityZone;
1998              
1999             my %args;
2000             $args{-endpoint} = $self->ec2->endpoint;
2001             $args{-volume} = $volume;
2002             $args{-name} = $volume->tags->{StagingName};
2003             $args{-fstype} = $volume->tags->{StagingFsType};
2004             $args{-mtpt} = $volume->tags->{StagingMtPt};
2005             my $mounted;
2006              
2007             if (my $attachment = $volume->attachment) {
2008             my $server = $self->find_server_by_instance($attachment->instance);
2009             $args{-server} = $server;
2010             ($args{-mtdev},$mounted) = $server->ping &&
2011             $server->_find_mount($attachment->device);
2012             }
2013              
2014             my $vol = $self->volume_class()->new(%args);
2015             $vol->mounted(1) if $mounted;
2016             $self->register_volume($vol);
2017             }
2018             }
2019              
2020             =head2 $group = $manager->security_group
2021              
2022             Returns or creates a security group with the permissions needed used
2023             to manage staging servers. Usually called internally.
2024              
2025             =cut
2026              
2027             sub security_group {
2028             my $self = shift;
2029             return $self->{security_group} ||= $self->_security_group();
2030             }
2031              
2032             =head2 $keypair = $manager->keypair
2033              
2034             Returns or creates the ssh keypair used internally by the manager to
2035             to access staging servers. Usually called internally.
2036              
2037             =cut
2038              
2039             sub keypair {
2040             my $self = shift;
2041             return $self->{keypair} ||= $self->_new_keypair();
2042             }
2043              
2044             sub _security_key {
2045             my $self = shift;
2046             my $ec2 = $self->ec2;
2047             if ($self->reuse_key) {
2048             my @candidates = $ec2->describe_key_pairs(-filter=>{'key-name' => 'staging-key-*'});
2049             for my $c (@candidates) {
2050             my $name = $c->keyName;
2051             my $keyfile = $self->_key_path($name);
2052             return ($c,$keyfile) if -e $keyfile;
2053             }
2054             }
2055             my $name = $self->_token('staging-key');
2056             $self->info("Creating keypair $name.\n");
2057             my $kp = $ec2->create_key_pair($name) or die $ec2->error_str;
2058             my $keyfile = $self->_key_path($name);
2059             my $private_key = $kp->privateKey;
2060             open my $k,'>',$keyfile or die "Couldn't create $keyfile: $!";
2061             chmod 0600,$keyfile or die "Couldn't chmod $keyfile: $!";
2062             print $k $private_key;
2063             close $k;
2064             return ($kp,$keyfile);
2065             }
2066              
2067             sub _security_group {
2068             my $self = shift;
2069             my $ec2 = $self->ec2;
2070             my @groups = $ec2->describe_security_groups(-filter=>{'tag:StagingRole' => 'StagingGroup'});
2071             return $groups[0] if @groups;
2072             my $name = $self->_token('ssh');
2073             $self->info("Creating staging security group $name.\n");
2074             my $sg = $ec2->create_security_group(-name => $name,
2075             -description => "SSH security group created by ".__PACKAGE__
2076             ) or die $ec2->error_str;
2077             $sg->authorize_incoming(-protocol => 'tcp',
2078             -port => 'ssh');
2079             $sg->update or die $ec2->error_str;
2080             $sg->add_tag(StagingRole => 'StagingGroup');
2081             return $sg;
2082              
2083             }
2084              
2085             =head2 $name = $manager->new_volume_name
2086              
2087             Returns a new random name for volumes provisioned without a -name
2088             argument. Currently names are in of the format "volume-12345678",
2089             where the numeric part are 8 random hex digits. Although no attempt is
2090             made to prevent naming collisions, the large number of possible names
2091             makes this unlikely.
2092              
2093             =cut
2094              
2095             sub new_volume_name {
2096             return shift->_token('volume');
2097             }
2098              
2099             =head2 $name = $manager->new_server_name
2100              
2101             Returns a new random name for server provisioned without a -name
2102             argument. Currently names are in of the format "server-12345678",
2103             where the numeric part are 8 random hex digits. Although no attempt
2104             is made to prevent naming collisions, the large number of possible
2105             names makes this unlikely.
2106              
2107             =cut
2108              
2109             sub new_server_name {
2110             return shift->_token('server');
2111             }
2112              
2113             sub _token {
2114             my $self = shift;
2115             my $base = shift or croak "usage: _token(\$basename)";
2116             return sprintf("$base-%08x",1+int(rand(0xFFFFFFFF)));
2117             }
2118              
2119             =head2 $description = $manager->volume_description($volume)
2120              
2121             This method is called to assign a description to newly-created
2122             volumes. The current format is "Staging volume for Foo created by
2123             VM::EC2::Staging::Manager", where Foo is the volume's symbolic name.
2124              
2125             =cut
2126              
2127             sub volume_description {
2128             my $self = shift;
2129             my $vol = shift;
2130             my $name = ref $vol ? $vol->name : $vol;
2131             return "Staging volume for $name created by ".__PACKAGE__;
2132             }
2133              
2134             =head2 $manager->debug("Debugging message\n")
2135              
2136             =head2 $manager->info("Informational message\n")
2137              
2138             =head2 $manager->warn("Warning message\n")
2139              
2140             Prints an informational message to standard error if current
2141             verbosity() level allows.
2142              
2143             =cut
2144              
2145             sub info {
2146             my $self = shift;
2147             return if $self->verbosity < VERBOSE_INFO;
2148             my @lines = split "\n",longmess();
2149             my $stack_count = grep /VM::EC2::Staging::Manager/,@lines;
2150             print STDERR '[info] ',' ' x (($stack_count-1)*3),@_;
2151             }
2152              
2153             sub warn {
2154             my $self = shift;
2155             return if $self->verbosity < VERBOSE_WARN;
2156             my @lines = split "\n",longmess();
2157             my $stack_count = grep /VM::EC2::Staging::Manager/,@lines;
2158             print STDERR '[warn] ',' ' x (($stack_count-1)*3),@_;
2159             }
2160              
2161             sub debug {
2162             my $self = shift;
2163             return if $self->verbosity < VERBOSE_DEBUG;
2164             my @lines = split "\n",longmess();
2165             my $stack_count = grep /VM::EC2::Staging::Manager/,@lines;
2166             print STDERR '[debug] ',' ' x (($stack_count-1)*3),@_;
2167             }
2168              
2169             =head2 $verbosity = $manager->verbosity([$new_value])
2170              
2171             The verbosity() method get/sets a flag that sets the level of
2172             informational messages.
2173              
2174             =cut
2175              
2176             sub verbosity {
2177             my $self = shift;
2178             my $d = ref $self ? $self->verbose : $Verbose;
2179             if (@_) {
2180             $Verbose = shift;
2181             $self->verbose($Verbose) if ref $self;
2182             }
2183             return $d;
2184             }
2185              
2186              
2187             sub _search_for_image {
2188             my $self = shift;
2189             my %args = @_;
2190             my $name = $args{-image_name};
2191              
2192             $self->info("Searching for a staging image...\n");
2193              
2194             my $root_type = $self->on_exit eq 'stop' ? 'ebs' : $args{-root_type};
2195             my @arch = $args{-architecture} ? ('architecture' => $args{-architecture}) : ();
2196              
2197             my @candidates = $name =~ /^ami-[0-9a-f]+/ ? $self->ec2->describe_images($name)
2198             : $self->ec2->describe_images({'name' => "*$args{-image_name}*",
2199             'root-device-type' => $root_type,
2200             @arch});
2201             return unless @candidates;
2202             # this assumes that the name has some sort of timestamp in it, which is true
2203             # of ubuntu images, but probably not others
2204             my ($most_recent) = sort {$b->name cmp $a->name} @candidates;
2205             $self->info("...found $most_recent: ",$most_recent->name,".\n");
2206             return $most_recent;
2207             }
2208              
2209             sub _gather_image_info {
2210             my $self = shift;
2211             my $image = shift;
2212             return {
2213             name => $image->name,
2214             description => $image->description,
2215             architecture => $image->architecture,
2216             kernel => $image->kernelId || undef,
2217             ramdisk => $image->ramdiskId || undef,
2218             root_device => $image->rootDeviceName,
2219             block_devices=> [$image->blockDeviceMapping],
2220             is_public => $image->isPublic,
2221             platform => $image->platform,
2222             virtualizationType => $image->virtualizationType,
2223             hypervisor => $image->hypervisor,
2224             authorized_users => [$image->authorized_users],
2225             };
2226             }
2227              
2228             sub _parse_destination {
2229             my $self = shift;
2230             my $destination = shift;
2231              
2232             my $ec2 = $self->ec2;
2233             my $dest_manager;
2234             if (ref $destination && $destination->isa('VM::EC2::Staging::Manager')) {
2235             $dest_manager = $destination;
2236             } else {
2237             my $dest_region = ref $destination && $destination->isa('VM::EC2::Region')
2238             ? $destination
2239             : $ec2->describe_regions($destination);
2240             $dest_region
2241             or croak "Invalid EC2 Region '$dest_region'; usage VM::EC2::Staging::Manager->copy_image(\$image,\$dest_region)";
2242             my $dest_endpoint = $dest_region->regionEndpoint;
2243             my $dest_ec2 = VM::EC2->new(-endpoint => $dest_endpoint,
2244             -access_key => $ec2->access_key,
2245             -secret_key => $ec2->secret)
2246             or croak "Could not create new VM::EC2 in $dest_region";
2247              
2248             $dest_manager = $self->new(-ec2 => $dest_ec2,
2249             -scan => $self->scan,
2250             -on_exit => 'destroy',
2251             -instance_type => $self->instance_type);
2252             }
2253              
2254             return $dest_manager;
2255             }
2256              
2257             sub match_kernel {
2258             my $self = shift;
2259             my ($src_kernel,$dest) = @_;
2260             my $dest_manager = $self->_parse_destination($dest) or croak "could not create destination manager for $dest";
2261             return $self->_match_kernel($src_kernel,$dest_manager,'kernel');
2262             }
2263              
2264             sub _match_kernel {
2265             my $self = shift;
2266             my ($imageId,$dest_manager) = @_;
2267             my $home_ec2 = $self->ec2;
2268             my $dest_ec2 = $dest_manager->ec2; # different endpoints!
2269             my $image = $home_ec2->describe_images($imageId) or return;
2270             my $type = $image->imageType;
2271             my @candidates;
2272              
2273             if (my $name = $image->name) { # will sometimes have a name
2274             @candidates = $dest_ec2->describe_images({'name' => $name,
2275             'image-type' => $type,
2276             });
2277             }
2278             unless (@candidates) {
2279             my $location = $image->imageLocation; # will always have a location
2280             my @path = split '/',$location;
2281             $location = $path[-1];
2282             @candidates = $dest_ec2->describe_images(-filter=>{'image-type'=>'kernel',
2283             'manifest-location'=>"*/$location"},
2284             -executable_by=>['all','self']);
2285             }
2286             unless (@candidates) { # go to approximate match
2287             my $location = $image->imageLocation;
2288             my @path = split '/',$location;
2289             my @kernels = $dest_ec2->describe_images(-filter=>{'image-type'=>'kernel',
2290             'manifest-location'=>"*/*"},
2291             -executable_by=>['all','self']);
2292             my %k = map {$_=>$_} @kernels;
2293             my %locations = map {my $l = $_->imageLocation;
2294             my @path = split '/',$l;
2295             $_ => \@path} @kernels;
2296              
2297             my %level0 = map {$_ => abs(adistr($path[0],$locations{$_}[0]))} keys %locations;
2298             my %level1 = map {$_ => abs(adistr($path[1],$locations{$_}[1]))} keys %locations;
2299             @candidates = sort {$level0{$a}<=>$level0{$b} || $level1{$a}<=>$level1{$b}} keys %locations;
2300             @candidates = map {$k{$_}} @candidates;
2301             }
2302             return $candidates[0];
2303             }
2304              
2305             # find the most likely ramdisk for a kernel based on preponderant configuration of public images
2306             sub _guess_ramdisk {
2307             my $self = shift;
2308             my $kernel = shift;
2309             my $ec2 = $self->ec2;
2310             my @images = $ec2->describe_images({'image-type' => 'machine',
2311             'kernel-id' => $kernel});
2312             my %ramdisks;
2313              
2314             foreach (@images) {
2315             $ramdisks{$_->ramdiskId}++;
2316             }
2317              
2318             my ($highest) = sort {$ramdisks{$b}<=>$ramdisks{$a}} keys %ramdisks;
2319             return $highest;
2320             }
2321              
2322             sub _check_keyfile {
2323             my $self = shift;
2324             my $keyname = shift;
2325             my $dotpath = $self->dot_directory;
2326             opendir my $d,$dotpath or die "Can't opendir $dotpath: $!";
2327             while (my $file = readdir($d)) {
2328             if ($file =~ /^$keyname.pem/) {
2329             return $1,$self->_key_path($keyname,$1);
2330             }
2331             }
2332             closedir $d;
2333             return;
2334             }
2335              
2336             sub _select_server_by_zone {
2337             my $self = shift;
2338             my $zone = shift;
2339             my @servers = values %{$Zones{$zone}{Servers}};
2340             return $servers[0];
2341             }
2342              
2343             sub _select_used_zone {
2344             my $self = shift;
2345             if (my @servers = $self->servers) {
2346             my @up = grep {$_->ping} @servers;
2347             my $server = $up[0] || $servers[0];
2348             return $server->placement;
2349             } elsif (my $zone = $self->availability_zone) {
2350             return $zone;
2351             } else {
2352             return;
2353             }
2354             }
2355              
2356             sub _key_path {
2357             my $self = shift;
2358             my $keyname = shift;
2359             return File::Spec->catfile($self->dot_directory,"${keyname}.pem")
2360             }
2361              
2362             # can be called as a class method
2363             sub _find_server_in_zone {
2364             my $self = shift;
2365             my $zone = shift;
2366             my @servers = sort {$a->ping cmp $b->ping} values %{$Zones{$zone}{Servers}};
2367             return unless @servers;
2368             return $servers[-1];
2369             }
2370              
2371             sub _servers {
2372             my $self = shift;
2373             my $endpoint = shift; # optional
2374             my @servers = values %Instances;
2375             return @servers unless $endpoint;
2376             return grep {$_->ec2->endpoint eq $endpoint} @servers;
2377             }
2378              
2379             sub _lock {
2380             my $self = shift;
2381             my ($resource,$lock_type) = @_;
2382             $lock_type eq 'SHARED' || $lock_type eq 'EXCLUSIVE'
2383             or croak "Usage: _lock(\$resource,'SHARED'|'EXCLUSIVE')";
2384              
2385             $resource->refresh;
2386             my $tags = $resource->tags;
2387             if (my $value = $tags->{StagingLock}) {
2388             my ($type,$pid) = split /\s+/,$value;
2389              
2390             if ($pid eq $$) { # we've already got lock
2391             $resource->add_tags(StagingLock=>"$lock_type $$")
2392             unless $type eq $lock_type;
2393             return 1;
2394             }
2395            
2396             if ($lock_type eq 'SHARED' && $type eq 'SHARED') {
2397             return 1;
2398             }
2399              
2400             # wait for lock
2401             eval {
2402             local $SIG{ALRM} = sub {die 'timeout'};
2403             alarm(LOCK_TIMEOUT); # we get lock eventually one way or another
2404             while (1) {
2405             $resource->refresh;
2406             last unless $resource->tags->{StagingLock};
2407             sleep 1;
2408             }
2409             };
2410             alarm(0);
2411             }
2412             $resource->add_tags(StagingLock=>"$lock_type $$");
2413             return 1;
2414             }
2415              
2416             sub _unlock {
2417             my $self = shift;
2418             my $resource = shift;
2419             $resource->refresh;
2420             my $sl = $resource->tags->{StagingLock} or return;
2421             my ($type,$pid) = split /\s+/,$sl;
2422             return unless $pid eq $$;
2423             $resource->delete_tags('StagingLock');
2424             }
2425              
2426             sub _safe_update_tag {
2427             my $self = shift;
2428             my ($resource,$tag,$value) = @_;
2429             $self->_lock($resource,'EXCLUSIVE');
2430             $resource->add_tag($tag => $value);
2431             $self->_unlock($resource);
2432             }
2433              
2434             sub _safe_read_tag {
2435             my $self = shift;
2436             my ($resource,$tag) = @_;
2437             $self->_lock($resource,'SHARED');
2438             my $value = $resource->tags->{$tag};
2439             $self->_unlock($resource);
2440             return $value;
2441             }
2442              
2443              
2444             sub _increment_usage_count {
2445             my $self = shift;
2446             my $resource = shift;
2447             $self->_lock($resource,'EXCLUSIVE');
2448             my $in_use = $resource->tags->{'StagingInUse'} || 0;
2449             $resource->add_tags(StagingInUse=>$in_use+1);
2450             $self->_unlock($resource);
2451             $in_use+1;
2452             }
2453              
2454             sub _decrement_usage_count {
2455             my $self = shift;
2456             my $resource = shift;
2457              
2458             $self->_lock($resource,'EXCLUSIVE');
2459             my $in_use = $resource->tags->{'StagingInUse'} || 0;
2460             $in_use--;
2461             if ($in_use > 0) {
2462             $resource->add_tags(StagingInUse=>$in_use);
2463             } else {
2464             $resource->delete_tags('StagingInUse');
2465             $in_use = 0;
2466             }
2467             $self->_unlock($resource);
2468             return $in_use;
2469             }
2470              
2471             sub _dots_cmd {
2472             my $self = shift;
2473             return '' unless $self->verbosity == VERBOSE_INFO;
2474             my ($fh,$dots_script) = tempfile('dots_XXXXXXX',SUFFIX=>'.pl',UNLINK=>1,TMPDIR=>1);
2475             print $fh $self->_dots_script;
2476             close $fh;
2477             chmod 0755,$dots_script;
2478             return "2>&1|$dots_script t";
2479             }
2480              
2481             sub _upload_dots_script {
2482             my $self = shift;
2483             my $server = shift;
2484             my $fh = $server->scmd_write('cat >/tmp/dots.pl');
2485             print $fh $self->_dots_script;
2486             close $fh;
2487             $server->ssh('chmod +x /tmp/dots.pl');
2488             }
2489              
2490             sub _dots_script {
2491             my $self = shift;
2492             my @lines = split "\n",longmess();
2493             my $stack_count = grep /VM::EC2::Staging::Manager/,@lines;
2494             my $spaces = ' ' x (($stack_count-1)*3);
2495             return <
2496             #!/usr/bin/perl
2497             my \$mode = shift || 'b';
2498             print STDERR "[info] ${spaces}One dot equals ",(\$mode eq 'b'?'100 Mb':'100 files'),': ';
2499             my \$b;
2500             READ:
2501             while (1) {
2502             do {read(STDIN,\$b,1e5) || last READ for 1..1000} if \$mode eq 'b';
2503             do {<> || last READ for 1.. 100} if \$mode eq 't';
2504             print STDERR '.';
2505             }
2506             print STDERR ".\n";
2507             END
2508             }
2509              
2510             sub DESTROY {
2511             my $self = shift;
2512             if ($$ == $self->pid) {
2513             my $action = $self->on_exit;
2514             $self->terminate_all_servers if $action eq 'terminate';
2515             $self->stop_all_servers if $action eq 'stop';
2516             }
2517             delete $Managers{$self->ec2->endpoint};
2518             }
2519              
2520              
2521              
2522             1;
2523              
2524              
2525             =head1 SEE ALSO
2526              
2527             L
2528             L
2529             L
2530             L
2531              
2532             =head1 AUTHOR
2533              
2534             Lincoln Stein Elincoln.stein@gmail.comE.
2535              
2536             Copyright (c) 2012 Ontario Institute for Cancer Research
2537              
2538             This package and its accompanying libraries is free software; you can
2539             redistribute it and/or modify it under the terms of the GPL (either
2540             version 1, or at your option, any later version) or the Artistic
2541             License 2.0. Refer to LICENSE for the full license text. In addition,
2542             please see DISCLAIMER.txt for disclaimers of warranty.
2543              
2544             =cut
2545