vagrant-aws-iam-decoder 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
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