vagrant-aws-iam-decoder 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +19 -0
  5. data/CHANGELOG.md +96 -0
  6. data/Gemfile +12 -0
  7. data/LICENSE +8 -0
  8. data/README.md +326 -0
  9. data/Rakefile +22 -0
  10. data/dummy.box +0 -0
  11. data/example_box/README.md +13 -0
  12. data/example_box/metadata.json +3 -0
  13. data/lib/vagrant-aws-iam-decoder.rb +18 -0
  14. data/lib/vagrant-aws/action.rb +210 -0
  15. data/lib/vagrant-aws/action/connect_aws.rb +48 -0
  16. data/lib/vagrant-aws/action/elb_deregister_instance.rb +24 -0
  17. data/lib/vagrant-aws/action/elb_register_instance.rb +24 -0
  18. data/lib/vagrant-aws/action/is_created.rb +18 -0
  19. data/lib/vagrant-aws/action/is_stopped.rb +18 -0
  20. data/lib/vagrant-aws/action/message_already_created.rb +16 -0
  21. data/lib/vagrant-aws/action/message_not_created.rb +16 -0
  22. data/lib/vagrant-aws/action/message_will_not_destroy.rb +16 -0
  23. data/lib/vagrant-aws/action/package_instance.rb +192 -0
  24. data/lib/vagrant-aws/action/read_ssh_info.rb +53 -0
  25. data/lib/vagrant-aws/action/read_state.rb +38 -0
  26. data/lib/vagrant-aws/action/run_instance.rb +396 -0
  27. data/lib/vagrant-aws/action/start_instance.rb +81 -0
  28. data/lib/vagrant-aws/action/stop_instance.rb +28 -0
  29. data/lib/vagrant-aws/action/terminate_instance.rb +51 -0
  30. data/lib/vagrant-aws/action/timed_provision.rb +21 -0
  31. data/lib/vagrant-aws/action/wait_for_state.rb +41 -0
  32. data/lib/vagrant-aws/action/warn_networks.rb +19 -0
  33. data/lib/vagrant-aws/config.rb +601 -0
  34. data/lib/vagrant-aws/errors.rb +43 -0
  35. data/lib/vagrant-aws/plugin.rb +73 -0
  36. data/lib/vagrant-aws/provider.rb +50 -0
  37. data/lib/vagrant-aws/util/elb.rb +58 -0
  38. data/lib/vagrant-aws/util/timer.rb +17 -0
  39. data/lib/vagrant-aws/version.rb +5 -0
  40. data/locales/en.yml +161 -0
  41. data/spec/spec_helper.rb +1 -0
  42. data/spec/vagrant-aws/config_spec.rb +395 -0
  43. data/templates/metadata.json.erb +3 -0
  44. data/templates/vagrant-aws_package_Vagrantfile.erb +5 -0
  45. data/vagrant-aws-iam-decoder.gemspec +62 -0
  46. metadata +163 -0
@@ -0,0 +1,16 @@
1
+ module VagrantPlugins
2
+ module AWS
3
+ module Action
4
+ class MessageWillNotDestroy
5
+ def initialize(app, env)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ env[:ui].info(I18n.t("vagrant_aws.will_not_destroy", name: env[:machine].name))
11
+ @app.call(env)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,192 @@
1
+ require "log4r"
2
+ require 'vagrant/util/template_renderer'
3
+ require 'vagrant-aws/util/timer'
4
+ require 'vagrant/action/general/package'
5
+
6
+ module VagrantPlugins
7
+ module AWS
8
+ module Action
9
+ # This action packages a running aws-based server into an
10
+ # aws-based vagrant box. It does so by burning the associated
11
+ # vagrant-aws server instance, into an AMI via fog. Upon
12
+ # successful AMI burning, the action will create a .box tarball
13
+ # writing a Vagrantfile with the fresh AMI id into it.
14
+
15
+ # Vagrant itself comes with a general package action, which
16
+ # this plugin action does call. The general action provides
17
+ # the actual packaging as well as other options such as
18
+ # --include for including additional files and --vagrantfile
19
+ # which is pretty much not useful here anyway.
20
+
21
+ # The virtualbox package plugin action was loosely used
22
+ # as a model for this class.
23
+
24
+ class PackageInstance < Vagrant::Action::General::Package
25
+ include Vagrant::Util::Retryable
26
+
27
+ def initialize(app, env)
28
+ super
29
+ @logger = Log4r::Logger.new("vagrant_aws::action::package_instance")
30
+ env["package.include"] ||= []
31
+ end
32
+
33
+ alias_method :general_call, :call
34
+ def call(env)
35
+ # Initialize metrics if they haven't been
36
+ env[:metrics] ||= {}
37
+
38
+ # This block attempts to burn the server instance into an AMI
39
+ begin
40
+ # Get the Fog server object for given machine
41
+ server = env[:aws_compute].servers.get(env[:machine].id)
42
+
43
+ env[:ui].info(I18n.t("vagrant_aws.packaging_instance", :instance_id => server.id))
44
+
45
+ # Make the request to AWS to create an AMI from machine's instance
46
+ ami_response = server.service.create_image server.id, "#{server.tags["Name"]} Package - #{Time.now.strftime("%Y%m%d-%H%M%S")}", ""
47
+
48
+ # Find ami id
49
+ @ami_id = ami_response.data[:body]["imageId"]
50
+
51
+ # Attempt to burn the aws instance into an AMI within timeout
52
+ env[:metrics]["instance_ready_time"] = Util::Timer.time do
53
+
54
+ # Get the config, to set the ami burn timeout
55
+ region = env[:machine].provider_config.region
56
+ region_config = env[:machine].provider_config.get_region_config(region)
57
+ tries = region_config.instance_package_timeout / 2
58
+
59
+ env[:ui].info(I18n.t("vagrant_aws.burning_ami", :ami_id => @ami_id))
60
+ if !region_config.package_tags.empty?
61
+ server.service.create_tags(@ami_id, region_config.package_tags)
62
+ end
63
+
64
+ # Check the status of the AMI every 2 seconds until the ami burn timeout has been reached
65
+ begin
66
+ retryable(:on => Fog::Errors::TimeoutError, :tries => tries) do
67
+ # If we're interrupted don't worry about waiting
68
+ next if env[:interrupted]
69
+
70
+ # Need to update the ami_obj on each cycle
71
+ ami_obj = server.service.images.get(@ami_id)
72
+
73
+ # Wait for the server to be ready, raise error if timeout reached
74
+ server.wait_for(2) {
75
+ if ami_obj.state == "failed"
76
+ raise Errors::InstancePackageError,
77
+ ami_id: ami_obj.id,
78
+ err: ami_obj.state
79
+ end
80
+
81
+ ami_obj.ready?
82
+ }
83
+ end
84
+ rescue Fog::Errors::TimeoutError
85
+ # Notify the user upon timeout
86
+ raise Errors::InstancePackageTimeout,
87
+ timeout: region_config.instance_package_timeout
88
+ end
89
+ end
90
+ env[:ui].info(I18n.t("vagrant_aws.packaging_instance_complete", :time_seconds => env[:metrics]["instance_ready_time"].to_i))
91
+ rescue Fog::Compute::AWS::Error => e
92
+ raise Errors::FogError, :message => e.message
93
+ end
94
+
95
+ # Handles inclusions from --include and --vagrantfile options
96
+ setup_package_files(env)
97
+
98
+ # Setup the temporary directory for the tarball files
99
+ @temp_dir = env[:tmp_path].join(Time.now.to_i.to_s)
100
+ env["export.temp_dir"] = @temp_dir
101
+ FileUtils.mkpath(env["export.temp_dir"])
102
+
103
+ # Create the Vagrantfile and metadata.json files from templates to go in the box
104
+ create_vagrantfile(env)
105
+ create_metadata_file(env)
106
+
107
+ # Just match up a couple environmental variables so that
108
+ # the superclass will do the right thing. Then, call the
109
+ # superclass to actually create the tarball (.box file)
110
+ env["package.directory"] = env["export.temp_dir"]
111
+ general_call(env)
112
+
113
+ # Always call recover to clean up the temp dir
114
+ clean_temp_dir
115
+ end
116
+
117
+ protected
118
+
119
+ # Cleanup temp dir and files
120
+ def clean_temp_dir
121
+ if @temp_dir && File.exist?(@temp_dir)
122
+ FileUtils.rm_rf(@temp_dir)
123
+ end
124
+ end
125
+
126
+ # This method generates the Vagrantfile at the root of the box. Taken from
127
+ # VagrantPlugins::ProviderVirtualBox::Action::PackageVagrantfile
128
+ def create_vagrantfile env
129
+ File.open(File.join(env["export.temp_dir"], "Vagrantfile"), "w") do |f|
130
+ f.write(TemplateRenderer.render("vagrant-aws_package_Vagrantfile", {
131
+ region: env[:machine].provider_config.region,
132
+ ami: @ami_id,
133
+ template_root: template_root
134
+ }))
135
+ end
136
+ end
137
+
138
+ # This method generates the metadata.json file at the root of the box.
139
+ def create_metadata_file env
140
+ File.open(File.join(env["export.temp_dir"], "metadata.json"), "w") do |f|
141
+ f.write(TemplateRenderer.render("metadata.json", {
142
+ template_root: template_root
143
+ }))
144
+ end
145
+ end
146
+
147
+ # Sets up --include and --vagrantfile files which may be added as optional
148
+ # parameters. Taken from VagrantPlugins::ProviderVirtualBox::Action::SetupPackageFiles
149
+ def setup_package_files(env)
150
+ files = {}
151
+ env["package.include"].each do |file|
152
+ source = Pathname.new(file)
153
+ dest = nil
154
+
155
+ # If the source is relative then we add the file as-is to the include
156
+ # directory. Otherwise, we copy only the file into the root of the
157
+ # include directory. Kind of strange, but seems to match what people
158
+ # expect based on history.
159
+ if source.relative?
160
+ dest = source
161
+ else
162
+ dest = source.basename
163
+ end
164
+
165
+ # Assign the mapping
166
+ files[file] = dest
167
+ end
168
+
169
+ if env["package.vagrantfile"]
170
+ # Vagrantfiles are treated special and mapped to a specific file
171
+ files[env["package.vagrantfile"]] = "_Vagrantfile"
172
+ end
173
+
174
+ # Verify the mapping
175
+ files.each do |from, _|
176
+ raise Vagrant::Errors::PackageIncludeMissing,
177
+ file: from if !File.exist?(from)
178
+ end
179
+
180
+ # Save the mapping
181
+ env["package.files"] = files
182
+ end
183
+
184
+ # Used to find the base location of aws-vagrant templates
185
+ def template_root
186
+ AWS.source_root.join("templates")
187
+ end
188
+
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,53 @@
1
+ require "log4r"
2
+
3
+ module VagrantPlugins
4
+ module AWS
5
+ module Action
6
+ # This action reads the SSH info for the machine and puts it into the
7
+ # `:machine_ssh_info` key in the environment.
8
+ class ReadSSHInfo
9
+ def initialize(app, env)
10
+ @app = app
11
+ @logger = Log4r::Logger.new("vagrant_aws::action::read_ssh_info")
12
+ end
13
+
14
+ def call(env)
15
+ env[:machine_ssh_info] = read_ssh_info(env[:aws_compute], env[:machine])
16
+
17
+ @app.call(env)
18
+ end
19
+
20
+ def read_ssh_info(aws, machine)
21
+ return nil if machine.id.nil?
22
+
23
+ # Find the machine
24
+ server = aws.servers.get(machine.id)
25
+ if server.nil?
26
+ # The machine can't be found
27
+ @logger.info("Machine couldn't be found, assuming it got destroyed.")
28
+ machine.id = nil
29
+ return nil
30
+ end
31
+
32
+ # read attribute override
33
+ ssh_host_attribute = machine.provider_config.
34
+ get_region_config(machine.provider_config.region).ssh_host_attribute
35
+ # default host attributes to try. NOTE: Order matters!
36
+ ssh_attrs = [:dns_name, :public_ip_address, :private_ip_address]
37
+ ssh_attrs = (Array(ssh_host_attribute) + ssh_attrs).uniq if ssh_host_attribute
38
+ # try each attribute, get out on first value
39
+ host_value = nil
40
+ while !host_value and attr_name = ssh_attrs.shift
41
+ begin
42
+ host_value = server.send(attr_name)
43
+ rescue NoMethodError
44
+ @logger.info("SSH host attribute not found #{attr_name}")
45
+ end
46
+ end
47
+
48
+ return { :host => host_value, :port => 22 }
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,38 @@
1
+ require "log4r"
2
+
3
+ module VagrantPlugins
4
+ module AWS
5
+ module Action
6
+ # This action reads the state of the machine and puts it in the
7
+ # `:machine_state_id` key in the environment.
8
+ class ReadState
9
+ def initialize(app, env)
10
+ @app = app
11
+ @logger = Log4r::Logger.new("vagrant_aws::action::read_state")
12
+ end
13
+
14
+ def call(env)
15
+ env[:machine_state_id] = read_state(env[:aws_compute], env[:machine])
16
+
17
+ @app.call(env)
18
+ end
19
+
20
+ def read_state(aws, machine)
21
+ return :not_created if machine.id.nil?
22
+
23
+ # Find the machine
24
+ server = aws.servers.get(machine.id)
25
+ if server.nil? || [:"shutting-down", :terminated].include?(server.state.to_sym)
26
+ # The machine can't be found
27
+ @logger.info("Machine not found or terminated, assuming it got destroyed.")
28
+ machine.id = nil
29
+ return :not_created
30
+ end
31
+
32
+ # Return the state
33
+ return server.state.to_sym
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,396 @@
1
+ require "log4r"
2
+ require 'json'
3
+
4
+ require 'vagrant/util/retryable'
5
+
6
+ require 'vagrant-aws/util/timer'
7
+
8
+ module VagrantPlugins
9
+ module AWS
10
+ module Action
11
+ # This runs the configured instance.
12
+ class RunInstance
13
+ include Vagrant::Util::Retryable
14
+
15
+ def initialize(app, env)
16
+ @app = app
17
+ @logger = Log4r::Logger.new("vagrant_aws::action::run_instance")
18
+ end
19
+
20
+ def call(env)
21
+ # Initialize metrics if they haven't been
22
+ env[:metrics] ||= {}
23
+
24
+ # Get the region we're going to booting up in
25
+ region = env[:machine].provider_config.region
26
+
27
+ # Get the configs
28
+ region_config = env[:machine].provider_config.get_region_config(region)
29
+ ami = region_config.ami
30
+ availability_zone = region_config.availability_zone
31
+ instance_type = region_config.instance_type
32
+ keypair = region_config.keypair_name
33
+ private_ip_address = region_config.private_ip_address
34
+ security_groups = region_config.security_groups
35
+ subnet_id = region_config.subnet_id
36
+ tags = region_config.tags
37
+ user_data = region_config.user_data
38
+ block_device_mapping = region_config.block_device_mapping
39
+ elastic_ip = region_config.elastic_ip
40
+ terminate_on_shutdown = region_config.terminate_on_shutdown
41
+ iam_instance_profile_arn = region_config.iam_instance_profile_arn
42
+ iam_instance_profile_name = region_config.iam_instance_profile_name
43
+ monitoring = region_config.monitoring
44
+ ebs_optimized = region_config.ebs_optimized
45
+ source_dest_check = region_config.source_dest_check
46
+ associate_public_ip = region_config.associate_public_ip
47
+ kernel_id = region_config.kernel_id
48
+ tenancy = region_config.tenancy
49
+
50
+ # If there is no keypair then warn the user
51
+ if !keypair
52
+ env[:ui].warn(I18n.t("vagrant_aws.launch_no_keypair"))
53
+ end
54
+
55
+ # If there is a subnet ID then warn the user
56
+ if subnet_id && !elastic_ip
57
+ env[:ui].warn(I18n.t("vagrant_aws.launch_vpc_warning"))
58
+ end
59
+
60
+ # Launch!
61
+ env[:ui].info(I18n.t("vagrant_aws.launching_instance"))
62
+ env[:ui].info(" -- Type: #{instance_type}")
63
+ env[:ui].info(" -- AMI: #{ami}")
64
+ env[:ui].info(" -- Region: #{region}")
65
+ env[:ui].info(" -- Availability Zone: #{availability_zone}") if availability_zone
66
+ env[:ui].info(" -- Keypair: #{keypair}") if keypair
67
+ env[:ui].info(" -- Subnet ID: #{subnet_id}") if subnet_id
68
+ env[:ui].info(" -- IAM Instance Profile ARN: #{iam_instance_profile_arn}") if iam_instance_profile_arn
69
+ env[:ui].info(" -- IAM Instance Profile Name: #{iam_instance_profile_name}") if iam_instance_profile_name
70
+ env[:ui].info(" -- Private IP: #{private_ip_address}") if private_ip_address
71
+ env[:ui].info(" -- Elastic IP: #{elastic_ip}") if elastic_ip
72
+ env[:ui].info(" -- User Data: yes") if user_data
73
+ env[:ui].info(" -- Security Groups: #{security_groups.inspect}") if !security_groups.empty?
74
+ env[:ui].info(" -- User Data: #{user_data}") if user_data
75
+ env[:ui].info(" -- Block Device Mapping: #{block_device_mapping}") if block_device_mapping
76
+ env[:ui].info(" -- Terminate On Shutdown: #{terminate_on_shutdown}")
77
+ env[:ui].info(" -- Monitoring: #{monitoring}")
78
+ env[:ui].info(" -- EBS optimized: #{ebs_optimized}")
79
+ env[:ui].info(" -- Source Destination check: #{source_dest_check}")
80
+ env[:ui].info(" -- Assigning a public IP address in a VPC: #{associate_public_ip}")
81
+ env[:ui].info(" -- VPC tenancy specification: #{tenancy}")
82
+
83
+ options = {
84
+ :availability_zone => availability_zone,
85
+ :flavor_id => instance_type,
86
+ :image_id => ami,
87
+ :key_name => keypair,
88
+ :private_ip_address => private_ip_address,
89
+ :subnet_id => subnet_id,
90
+ :iam_instance_profile_arn => iam_instance_profile_arn,
91
+ :iam_instance_profile_name => iam_instance_profile_name,
92
+ :tags => tags,
93
+ :user_data => user_data,
94
+ :block_device_mapping => block_device_mapping,
95
+ :instance_initiated_shutdown_behavior => terminate_on_shutdown == true ? "terminate" : nil,
96
+ :monitoring => monitoring,
97
+ :ebs_optimized => ebs_optimized,
98
+ :associate_public_ip => associate_public_ip,
99
+ :kernel_id => kernel_id,
100
+ :tenancy => tenancy
101
+ }
102
+
103
+ if !security_groups.empty?
104
+ security_group_key = options[:subnet_id].nil? ? :groups : :security_group_ids
105
+ options[security_group_key] = security_groups
106
+ env[:ui].warn(I18n.t("vagrant_aws.warn_ssh_access")) unless allows_ssh_port?(env, security_groups, subnet_id)
107
+ end
108
+
109
+ begin
110
+ server = if region_config.spot_instance
111
+ server_from_spot_request(env, region_config, options)
112
+ else
113
+ env[:aws_compute].servers.create(options)
114
+ end
115
+ rescue Fog::Compute::AWS::NotFound => e
116
+ # Invalid subnet doesn't have its own error so we catch and
117
+ # check the error message here.
118
+ if e.message =~ /subnet ID/
119
+ raise Errors::FogError,
120
+ :message => "Subnet ID not found: #{subnet_id}"
121
+ end
122
+
123
+ raise
124
+ rescue Fog::Compute::AWS::Error => e
125
+ raise Errors::FogError, :message => e.message
126
+ rescue Excon::Errors::HTTPStatusError => e
127
+ raise Errors::InternalFogError,
128
+ :error => e.message,
129
+ :response => e.response.body
130
+ end
131
+
132
+ # Immediately save the ID since it is created at this point.
133
+ env[:machine].id = server.id
134
+
135
+ # Wait for the instance to be ready first
136
+ env[:metrics]["instance_ready_time"] = Util::Timer.time do
137
+ tries = region_config.instance_ready_timeout / 2
138
+
139
+ env[:ui].info(I18n.t("vagrant_aws.waiting_for_ready"))
140
+ begin
141
+ retryable(:on => Fog::Errors::TimeoutError, :tries => tries) do
142
+ # If we're interrupted don't worry about waiting
143
+ next if env[:interrupted]
144
+
145
+ # Wait for the server to be ready
146
+ server.wait_for(2, region_config.instance_check_interval) { ready? }
147
+ end
148
+ rescue Fog::Errors::TimeoutError
149
+ # Delete the instance
150
+ terminate(env)
151
+
152
+ # Notify the user
153
+ raise Errors::InstanceReadyTimeout,
154
+ timeout: region_config.instance_ready_timeout
155
+ end
156
+ end
157
+
158
+ @logger.info("Time to instance ready: #{env[:metrics]["instance_ready_time"]}")
159
+
160
+ # Allocate and associate an elastic IP if requested
161
+ if elastic_ip
162
+ domain = subnet_id ? 'vpc' : 'standard'
163
+ do_elastic_ip(env, domain, server, elastic_ip)
164
+ end
165
+
166
+ # Set the source destination checks
167
+ if !source_dest_check.nil?
168
+ if server.vpc_id.nil?
169
+ env[:ui].warn(I18n.t("vagrant_aws.source_dest_checks_no_vpc"))
170
+ else
171
+ begin
172
+ attrs = {
173
+ "SourceDestCheck.Value" => source_dest_check
174
+ }
175
+ env[:aws_compute].modify_instance_attribute(server.id, attrs)
176
+ rescue Fog::Compute::AWS::Error => e
177
+ raise Errors::FogError, :message => e.message
178
+ end
179
+ end
180
+ end
181
+
182
+ if !env[:interrupted]
183
+ env[:metrics]["instance_ssh_time"] = Util::Timer.time do
184
+ # Wait for SSH to be ready.
185
+ env[:ui].info(I18n.t("vagrant_aws.waiting_for_ssh"))
186
+ network_ready_retries = 0
187
+ network_ready_retries_max = 10
188
+ while true
189
+ # If we're interrupted then just back out
190
+ break if env[:interrupted]
191
+ # When an ec2 instance comes up, it's networking may not be ready
192
+ # by the time we connect.
193
+ begin
194
+ break if env[:machine].communicate.ready?
195
+ rescue Exception => e
196
+ if network_ready_retries < network_ready_retries_max then
197
+ network_ready_retries += 1
198
+ @logger.warn(I18n.t("vagrant_aws.waiting_for_ssh, retrying"))
199
+ else
200
+ raise e
201
+ end
202
+ end
203
+ sleep 2
204
+ end
205
+ end
206
+
207
+ @logger.info("Time for SSH ready: #{env[:metrics]["instance_ssh_time"]}")
208
+
209
+ # Ready and booted!
210
+ env[:ui].info(I18n.t("vagrant_aws.ready"))
211
+ end
212
+
213
+ # Terminate the instance if we were interrupted
214
+ terminate(env) if env[:interrupted]
215
+
216
+ @app.call(env)
217
+ end
218
+
219
+ # returns a fog server or nil
220
+ def server_from_spot_request(env, config, options)
221
+ if config.spot_max_price.nil?
222
+ spot_price_current = env[:aws_compute].describe_spot_price_history({
223
+ 'StartTime' => Time.now.iso8601,
224
+ 'EndTime' => Time.now.iso8601,
225
+ 'InstanceType' => [config.instance_type],
226
+ 'ProductDescription' => [config.spot_price_product_description.nil? ? 'Linux/UNIX' : config.spot_price_product_description]
227
+ })
228
+
229
+ spot_price_current.body['spotPriceHistorySet'].each do |set|
230
+ (@price_set ||= []) << set['spotPrice'].to_f
231
+ end
232
+
233
+ if @price_set.nil?
234
+ raise Errors::FogError,
235
+ :message => "Could not find any history spot prices."
236
+ end
237
+
238
+ avg_price = @price_set.inject(0.0) { |sum, el| sum + el } / @price_set.size
239
+
240
+ # make the bid 10% higher than the average
241
+ price = (avg_price * 1.1).round(4)
242
+ else
243
+ price = config.spot_max_price
244
+ end
245
+
246
+ options.merge!({
247
+ :price => price,
248
+ :valid_until => config.spot_valid_until
249
+ })
250
+
251
+ env[:ui].info(I18n.t("vagrant_aws.launching_spot_instance"))
252
+ env[:ui].info(" -- Price: #{price}")
253
+ env[:ui].info(" -- Valid until: #{config.spot_valid_until}") if config.spot_valid_until
254
+
255
+ # create the spot instance
256
+ spot_req = env[:aws_compute].spot_requests.create(options)
257
+
258
+ @logger.info("Spot request ID: #{spot_req.id}")
259
+
260
+ # initialize state
261
+ status_code = ""
262
+ while true
263
+ sleep 5
264
+
265
+ spot_req.reload()
266
+
267
+ # display something whenever the status code changes
268
+ if status_code != spot_req.state
269
+ env[:ui].info(spot_req.fault)
270
+ status_code = spot_req.state
271
+ end
272
+ spot_state = spot_req.state.to_sym
273
+ case spot_state
274
+ when :not_created, :open
275
+ @logger.debug("Spot request #{spot_state} #{status_code}, waiting")
276
+ when :active
277
+ break; # :)
278
+ when :closed, :cancelled, :failed
279
+ msg = "Spot request #{spot_state} #{status_code}, aborting"
280
+ @logger.error(msg)
281
+ raise Errors::FogError, :message => msg
282
+ else
283
+ @logger.debug("Unknown spot state #{spot_state} #{status_code}, waiting")
284
+ end
285
+ end
286
+ # cancel the spot request but let the server go thru
287
+ spot_req.destroy()
288
+
289
+ server = env[:aws_compute].servers.get(spot_req.instance_id)
290
+
291
+ # Spot Instances don't support tagging arguments on creation
292
+ # Retrospectively tag the server to handle this
293
+ if !config.tags.empty?
294
+ env[:aws_compute].create_tags(server.identity, config.tags)
295
+ end
296
+
297
+ server
298
+ end
299
+
300
+ def recover(env)
301
+ return if env["vagrant.error"].is_a?(Vagrant::Errors::VagrantError)
302
+
303
+ if env[:machine].provider.state.id != :not_created
304
+ # Undo the import
305
+ terminate(env)
306
+ end
307
+ end
308
+
309
+ def allows_ssh_port?(env, test_sec_groups, is_vpc)
310
+ port = 22 # TODO get ssh_info port
311
+ test_sec_groups = [ "default" ] if test_sec_groups.empty? # AWS default security group
312
+ # filter groups by name or group_id (vpc)
313
+ groups = test_sec_groups.map do |tsg|
314
+ env[:aws_compute].security_groups.all.select { |sg| tsg == (is_vpc ? sg.group_id : sg.name) }
315
+ end.flatten
316
+ # filter TCP rules
317
+ rules = groups.map { |sg| sg.ip_permissions.select { |r| r["ipProtocol"] == "tcp" } }.flatten
318
+ # test if any range includes port
319
+ !rules.select { |r| (r["fromPort"]..r["toPort"]).include?(port) }.empty?
320
+ end
321
+
322
+ def do_elastic_ip(env, domain, server, elastic_ip)
323
+ if elastic_ip =~ /\d+\.\d+\.\d+\.\d+/
324
+ begin
325
+ address = env[:aws_compute].addresses.get(elastic_ip)
326
+ rescue
327
+ handle_elastic_ip_error(env, "Could not retrieve Elastic IP: #{elastic_ip}")
328
+ end
329
+ if address.nil?
330
+ handle_elastic_ip_error(env, "Elastic IP not available: #{elastic_ip}")
331
+ end
332
+ @logger.debug("Public IP #{address.public_ip}")
333
+ else
334
+ begin
335
+ allocation = env[:aws_compute].allocate_address(domain)
336
+ rescue
337
+ handle_elastic_ip_error(env, "Could not allocate Elastic IP.")
338
+ end
339
+ @logger.debug("Public IP #{allocation.body['publicIp']}")
340
+ end
341
+
342
+ # Associate the address and save the metadata to a hash
343
+ h = nil
344
+ if domain == 'vpc'
345
+ # VPC requires an allocation ID to assign an IP
346
+ if address
347
+ association = env[:aws_compute].associate_address(server.id, nil, nil, address.allocation_id)
348
+ else
349
+ association = env[:aws_compute].associate_address(server.id, nil, nil, allocation.body['allocationId'])
350
+ # Only store release data for an allocated address
351
+ h = { :allocation_id => allocation.body['allocationId'], :association_id => association.body['associationId'], :public_ip => allocation.body['publicIp'] }
352
+ end
353
+ else
354
+ # Standard EC2 instances only need the allocated IP address
355
+ if address
356
+ association = env[:aws_compute].associate_address(server.id, address.public_ip)
357
+ else
358
+ association = env[:aws_compute].associate_address(server.id, allocation.body['publicIp'])
359
+ h = { :public_ip => allocation.body['publicIp'] }
360
+ end
361
+ end
362
+
363
+ unless association.body['return']
364
+ @logger.debug("Could not associate Elastic IP.")
365
+ terminate(env)
366
+ raise Errors::FogError,
367
+ :message => "Could not allocate Elastic IP."
368
+ end
369
+
370
+ # Save this IP to the data dir so it can be released when the instance is destroyed
371
+ if h
372
+ ip_file = env[:machine].data_dir.join('elastic_ip')
373
+ ip_file.open('w+') do |f|
374
+ f.write(h.to_json)
375
+ end
376
+ end
377
+ end
378
+
379
+ def handle_elastic_ip_error(env, message)
380
+ @logger.debug(message)
381
+ terminate(env)
382
+ raise Errors::FogError,
383
+ :message => message
384
+ end
385
+
386
+ def terminate(env)
387
+ destroy_env = env.dup
388
+ destroy_env.delete(:interrupted)
389
+ destroy_env[:config_validate] = false
390
+ destroy_env[:force_confirm_destroy] = true
391
+ env[:action_runner].run(Action.action_destroy, destroy_env)
392
+ end
393
+ end
394
+ end
395
+ end
396
+ end