diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dff4432..d42b189c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ * Request confirmation on `vagrant destroy`, like normal VirtualBox + Vagrant. * If user data is configured, output is shown on "vagrant up" that it is being set. +* Add EIP support (GH #65) +* Add block device mapping support (GH #93) +* README improvements (GH #120) +* Fix missing locale message (GH #73) +* SyncFolders creates hostpath if it doesn't exist and `:create` option is set (GH #17) +* Add IAM Instance Profile support (GH #68) +* Add shutdown behavior support (GH #125) # 0.2.2 (April 18, 2013) diff --git a/Gemfile b/Gemfile index 4211d5ce..11f61e8e 100644 --- a/Gemfile +++ b/Gemfile @@ -6,5 +6,5 @@ group :development do # We depend on Vagrant for development, but we don't add it as a # gem dependency because we expect to be installed within the # Vagrant environment itself using `vagrant plugin`. - gem "vagrant", :git => "git://github.com/mitchellh/vagrant.git" + gem "vagrant", :git => "git://github.com/mitchellh/vagrant.git", :tag => "v1.2.7" end diff --git a/README.md b/README.md index 951e882d..36cd8e23 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,10 @@ This provider exposes quite a few provider-specific configuration options: * `security_groups` - An array of security groups for the instance. If this instance will be launched in VPC, this must be a list of security group IDs. +* `iam_instance_profile_arn` - The Amazon resource name (ARN) of the IAM Instance + Profile to associate with the instance +* `iam_instance_profile_name` - The name of the IAM Instance Profile to associate + with the instance * `subnet_id` - The subnet to boot the instance into, for VPC. * `tags` - A hash of tags to set on the machine. * `use_iam_profile` - If true, will use [IAM profiles](http://docs.aws.amazon.com/IAM/latest/UserGuide/instance-profiles.html) diff --git a/lib/vagrant-aws/action/read_ssh_info.rb b/lib/vagrant-aws/action/read_ssh_info.rb index f8452527..91fa17d6 100644 --- a/lib/vagrant-aws/action/read_ssh_info.rb +++ b/lib/vagrant-aws/action/read_ssh_info.rb @@ -31,7 +31,7 @@ def read_ssh_info(aws, machine) # Read the DNS info return { - :host => server.dns_name || server.private_ip_address, + :host => server.public_ip_address || server.dns_name || server.private_ip_address, :port => 22 } end diff --git a/lib/vagrant-aws/action/run_instance.rb b/lib/vagrant-aws/action/run_instance.rb index 86bef97d..8d97debb 100644 --- a/lib/vagrant-aws/action/run_instance.rb +++ b/lib/vagrant-aws/action/run_instance.rb @@ -1,4 +1,5 @@ require "log4r" +require 'json' require 'vagrant/util/retryable' @@ -35,6 +36,10 @@ def call(env) tags = region_config.tags user_data = region_config.user_data block_device_mapping = region_config.block_device_mapping + elastic_ip = region_config.elastic_ip + shutdown_behavior = region_config.shutdown_behavior + iam_instance_profile_arn = region_config.iam_instance_profile_arn + iam_instance_profile_name = region_config.iam_instance_profile_name # If there is no keypair then warn the user if !keypair @@ -42,7 +47,7 @@ def call(env) end # If there is a subnet ID then warn the user - if subnet_id + if subnet_id and !elastic_ip env[:ui].warn(I18n.t("vagrant_aws.launch_vpc_warning")) end @@ -54,11 +59,15 @@ def call(env) env[:ui].info(" -- Availability Zone: #{availability_zone}") if availability_zone env[:ui].info(" -- Keypair: #{keypair}") if keypair env[:ui].info(" -- Subnet ID: #{subnet_id}") if subnet_id + env[:ui].info(" -- IAM Instance Profile ARN: #{iam_instance_profile_arn}") if iam_instance_profile_arn + env[:ui].info(" -- IAM Instance Profile Name: #{iam_instance_profile_name}") if iam_instance_profile_name env[:ui].info(" -- Private IP: #{private_ip_address}") if private_ip_address + env[:ui].info(" -- Elastic IP: #{elastic_ip}") if elastic_ip env[:ui].info(" -- User Data: yes") if user_data env[:ui].info(" -- Security Groups: #{security_groups.inspect}") if !security_groups.empty? env[:ui].info(" -- User Data: #{user_data}") if user_data env[:ui].info(" -- Block Device Mapping: #{block_device_mapping}") if block_device_mapping + env[:ui].info(" -- Shutdown behavior: #{shutdown_behavior}") if shutdown_behavior begin options = { @@ -68,10 +77,15 @@ def call(env) :key_name => keypair, :private_ip_address => private_ip_address, :subnet_id => subnet_id, + :iam_instance_profile_arn => iam_instance_profile_arn, + :iam_instance_profile_name => iam_instance_profile_name, :tags => tags, :user_data => user_data, - :block_device_mapping => block_device_mapping + :block_device_mapping => block_device_mapping, } + + options[:instance_initiated_shutdown_behavior] = shutdown_behavior if env[:aws_compute].images.get(ami).root_device_type == "ebs" or shutdown_be + havior == "terminate" if !security_groups.empty? security_group_key = options[:subnet_id].nil? ? :groups : :security_group_ids @@ -90,6 +104,10 @@ def call(env) raise rescue Fog::Compute::AWS::Error => e raise Errors::FogError, :message => e.message + rescue Excon::Errors::HTTPStatusError => e + raise Errors::InternalFogError, + :error => e.message, + :response => e.response.body end # Immediately save the ID since it is created at this point. @@ -120,6 +138,12 @@ def call(env) @logger.info("Time to instance ready: #{env[:metrics]["instance_ready_time"]}") + # Allocate and associate an elastic IP if requested + if elastic_ip + domain = subnet_id ? 'vpc' : 'standard' + do_elastic_ip(env, domain, server) + end + if !env[:interrupted] env[:metrics]["instance_ssh_time"] = Util::Timer.time do # Wait for SSH to be ready. @@ -153,6 +177,37 @@ def recover(env) end end + def do_elastic_ip(env, domain, server) + allocation = env[:aws_compute].allocate_address(domain) + if allocation.body['publicIp'].nil? + @logger.debug("Could not allocate Elastic IP.") + return nil + end + @logger.debug("Public IP #{allocation.body['publicIp']}") + + # Associate the address and save the metadata to a hash + if domain == 'vpc' + # VPC requires an allocation ID to assign an IP + association = env[:aws_compute].associate_address(server.id, nil, nil, allocation.body['allocationId']) + h = { :allocation_id => allocation.body['allocationId'], :association_id => association.body['associationId'], :public_ip => allocation.body['publicIp'] } + else + # Standard EC2 instances only need the allocated IP address + association = env[:aws_compute].associate_address(server.id, allocation.body['publicIp']) + h = { :public_ip => allocation.body['publicIp'] } + end + + unless association.body['return'] + @logger.debug("Could not associate Elastic IP.") + return nil + end + + # Save this IP to the data dir so it can be released when the instance is destroyed + ip_file = env[:machine].data_dir.join('elastic_ip') + ip_file.open('w+') do |f| + f.write(h.to_json) + end + end + def terminate(env) destroy_env = env.dup destroy_env.delete(:interrupted) @@ -160,6 +215,7 @@ def terminate(env) destroy_env[:force_confirm_destroy] = true env[:action_runner].run(Action.action_destroy, destroy_env) end + end end end diff --git a/lib/vagrant-aws/action/sync_folders.rb b/lib/vagrant-aws/action/sync_folders.rb index 6023958e..bb9c529f 100644 --- a/lib/vagrant-aws/action/sync_folders.rb +++ b/lib/vagrant-aws/action/sync_folders.rb @@ -39,6 +39,17 @@ def call(env) :hostpath => hostpath, :guestpath => guestpath)) + # Create the host path if it doesn't exist and option flag is set + if data[:create] + begin + FileUtils::mkdir_p(hostpath) + rescue => err + raise Errors::MkdirError, + :hostpath => hostpath, + :err => err + end + end + # Create the guest path env[:machine].communicate.sudo("mkdir -p '#{guestpath}'") env[:machine].communicate.sudo( diff --git a/lib/vagrant-aws/action/terminate_instance.rb b/lib/vagrant-aws/action/terminate_instance.rb index 85609685..70bb53c4 100644 --- a/lib/vagrant-aws/action/terminate_instance.rb +++ b/lib/vagrant-aws/action/terminate_instance.rb @@ -1,4 +1,5 @@ require "log4r" +require "json" module VagrantPlugins module AWS @@ -16,10 +17,36 @@ def call(env) # Destroy the server and remove the tracking ID env[:ui].info(I18n.t("vagrant_aws.terminating")) server.destroy + # Deallocate Elastic IP if allocated + elastic_ip = env[:aws_compute].describe_addresses('public-ip' => server.public_ip_address) + if !elastic_ip[:body]["addressesSet"].empty? + env[:aws_compute].disassociate_address(nil,elastic_ip[:body]["addressesSet"][0]["associationId"]) + env[:ui].info(I18n.t("vagrant_aws.elastic_ip_deallocated")) + end env[:machine].id = nil + + # Release the elastic IP + ip_file = env[:machine].data_dir.join('elastic_ip') + if ip_file.file? + release_address(env,ip_file.read) + ip_file.delete + end @app.call(env) end + + # Release an elastic IP address + def release_address(env,eip) + h = JSON.parse(eip) + # Use association_id and allocation_id for VPC, use public IP for EC2 + if h['association_id'] + env[:aws_compute].disassociate_address(nil,h['association_id']) + env[:aws_compute].release_address(h['allocation_id']) + else + env[:aws_compute].disassociate_address(h['public_ip']) + env[:aws_compute].release_address(h['public_ip']) + end + end end end end diff --git a/lib/vagrant-aws/config.rb b/lib/vagrant-aws/config.rb index adcf9f74..08952809 100644 --- a/lib/vagrant-aws/config.rb +++ b/lib/vagrant-aws/config.rb @@ -39,6 +39,11 @@ class Config < Vagrant.plugin("2", :config) # @return [String] attr_accessor :private_ip_address + # Acquire and attach an elastic IP address (VPC). + # + # @return [Boolean] + attr_accessor :elastic_ip + # The name of the AWS region in which to create the instance. # # @return [String] @@ -65,6 +70,18 @@ class Config < Vagrant.plugin("2", :config) # @return [Array] attr_accessor :security_groups + # The Amazon resource name (ARN) of the IAM Instance Profile + # to associate with the instance. + # + # @return [String] + attr_accessor :iam_instance_profile_arn + + # The name of the IAM Instance Profile to associate with + # the instance. + # + # @return [String] + attr_accessor :iam_instance_profile_name + # The subnet ID to launch the machine into (VPC). # # @return [String] @@ -88,6 +105,11 @@ class Config < Vagrant.plugin("2", :config) attr_accessor :block_device_mapping + # Indicates whether an instance stops or terminates when you initiate shutdown from the instance + # + # @return [String] + attr_accessor :shutdown_behavior + def initialize(region_specific=false) @access_key_id = UNSET_VALUE @ami = UNSET_VALUE @@ -106,6 +128,10 @@ def initialize(region_specific=false) @user_data = UNSET_VALUE @use_iam_profile = UNSET_VALUE @block_device_mapping = {} + @elastic_ip = UNSET_VALUE + @iam_instance_profile_arn = UNSET_VALUE + @iam_instance_profile_name = UNSET_VALUE + @shutdown_behavior = UNSET_VALUE # Internal state (prefix with __ so they aren't automatically # merged) @@ -196,6 +222,9 @@ def finalize! # Default the private IP to nil since VPC is not default @private_ip_address = nil if @private_ip_address == UNSET_VALUE + # Acquire an elastic IP if requested + @elastic_ip = nil if @elastic_ip == UNSET_VALUE + # Default region is us-east-1. This is sensible because AWS # generally defaults to this as well. @region = "us-east-1" if @region == UNSET_VALUE @@ -209,12 +238,19 @@ def finalize! # Subnet is nil by default otherwise we'd launch into VPC. @subnet_id = nil if @subnet_id == UNSET_VALUE + # IAM Instance profile arn/name is nil by default. + @iam_instance_profile_arn = nil if @iam_instance_profile_arn == UNSET_VALUE + @iam_instance_profile_name = nil if @iam_instance_profile_name == UNSET_VALUE + # By default we don't use an IAM profile @use_iam_profile = false if @use_iam_profile == UNSET_VALUE # User Data is nil by default @user_data = nil if @user_data == UNSET_VALUE + # default stop + @shutdown_behavior = "stop" if @shutdown_behavior == UNSET_VALUE + # Compile our region specific configurations only within # NON-REGION-SPECIFIC configurations. if !@__region_specific @@ -256,7 +292,6 @@ def validate(machine) errors << I18n.t("vagrant_aws.config.secret_access_key_required") if \ config.secret_access_key.nil? end - errors << I18n.t("vagrant_aws.config.ami_required") if config.ami.nil? end diff --git a/lib/vagrant-aws/errors.rb b/lib/vagrant-aws/errors.rb index 7e128489..0b23999b 100644 --- a/lib/vagrant-aws/errors.rb +++ b/lib/vagrant-aws/errors.rb @@ -11,6 +11,10 @@ class FogError < VagrantAWSError error_key(:fog_error) end + class InternalFogError < VagrantAWSError + error_key(:internal_fog_error) + end + class InstanceReadyTimeout < VagrantAWSError error_key(:instance_ready_timeout) end @@ -18,6 +22,10 @@ class InstanceReadyTimeout < VagrantAWSError class RsyncError < VagrantAWSError error_key(:rsync_error) end + + class MkdirError < VagrantAWSError + error_key(:mkdir_error) + end end end end diff --git a/locales/en.yml b/locales/en.yml index 37214e31..f45ce852 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -20,6 +20,10 @@ en: Rsyncing folder: %{hostpath} => %{guestpath} terminating: |- Terminating the instance... + elastic_ip_allocated: |- + Allocated Elastic IP... + elastic_ip_deallocated: |- + Deallocated Elastic IP... waiting_for_ready: |- Waiting for instance to become "ready"... waiting_for_ssh: |- @@ -43,6 +47,8 @@ en: A region must be specified via "region" secret_access_key_required: |- A secret access key is required via "secret_access_key" + allocate_elastic_ip: |- + Allocate_elastic_ip must be set to either "standard" or "vpc" errors: fog_error: |- @@ -50,18 +56,30 @@ en: below: %{message} + internal_fog_error: |- + There was an error talking to AWS. The error message is shown + below: + + Error: %{error} + Response: %{response} instance_ready_timeout: |- The instance never became "ready" in AWS. The timeout currently set waiting for the instance to become ready is %{timeout} seconds. Please verify that the machine properly boots. If you need more time set the `instance_ready_timeout` configuration on the AWS provider. rsync_error: |- - There was an error when attemping to rsync a share folder. + There was an error when attempting to rsync a share folder. Please inspect the error message below for more info. Host path: %{hostpath} Guest path: %{guestpath} Error: %{stderr} + mkdir_error: |- + There was an error when attempting to create a shared host folder. + Please inspect the error message below for more info. + + Host path: %{hostpath} + Error: %{err} states: short_not_created: |- diff --git a/spec/vagrant-aws/config_spec.rb b/spec/vagrant-aws/config_spec.rb index dd6175b1..ece48a3c 100644 --- a/spec/vagrant-aws/config_spec.rb +++ b/spec/vagrant-aws/config_spec.rb @@ -26,10 +26,14 @@ its("secret_access_key") { should be_nil } its("security_groups") { should == [] } its("subnet_id") { should be_nil } + its("iam_instance_profile_arn") { should be_nil } + its("iam_instance_profile_name") { should be_nil } its("tags") { should == {} } its("user_data") { should be_nil } its("use_iam_profile") { should be_false } its("block_device_mapping") {should == {} } + its("elastic_ip") { should be_nil } + its("shutdown_behavior") { should == "stop" } end describe "overriding defaults" do @@ -40,7 +44,8 @@ [:access_key_id, :ami, :availability_zone, :instance_ready_timeout, :instance_type, :keypair_name, :region, :secret_access_key, :security_groups, - :subnet_id, :tags, + :subnet_id, :tags, :elastic_ip, :shutdown_behavior, + :iam_instance_profile_arn, :iam_instance_profile_name, :use_iam_profile, :user_data, :block_device_mapping].each do |attribute| it "should not default #{attribute} if overridden" do