chef-provisioning-fog 0.14.0 → 0.15.0

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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +201 -201
  3. data/README.md +3 -3
  4. data/Rakefile +6 -6
  5. data/lib/chef/provider/fog_key_pair.rb +266 -266
  6. data/lib/chef/provisioning/driver_init/fog.rb +3 -3
  7. data/lib/chef/provisioning/fog_driver/driver.rb +736 -709
  8. data/lib/chef/provisioning/fog_driver/providers/aws.rb +492 -492
  9. data/lib/chef/provisioning/fog_driver/providers/aws/credentials.rb +115 -115
  10. data/lib/chef/provisioning/fog_driver/providers/cloudstack.rb +44 -44
  11. data/lib/chef/provisioning/fog_driver/providers/digitalocean.rb +136 -136
  12. data/lib/chef/provisioning/fog_driver/providers/google.rb +85 -84
  13. data/lib/chef/provisioning/fog_driver/providers/joyent.rb +63 -59
  14. data/lib/chef/provisioning/fog_driver/providers/openstack.rb +117 -41
  15. data/lib/chef/provisioning/fog_driver/providers/rackspace.rb +42 -42
  16. data/lib/chef/provisioning/fog_driver/providers/softlayer.rb +36 -36
  17. data/lib/chef/provisioning/fog_driver/providers/vcair.rb +409 -376
  18. data/lib/chef/provisioning/fog_driver/providers/xenserver.rb +210 -0
  19. data/lib/chef/provisioning/fog_driver/recipe_dsl.rb +32 -32
  20. data/lib/chef/provisioning/fog_driver/version.rb +7 -7
  21. data/lib/chef/resource/fog_key_pair.rb +34 -34
  22. data/spec/spec_helper.rb +18 -18
  23. data/spec/support/aws/config-file.csv +2 -2
  24. data/spec/support/aws/ini-file.ini +10 -10
  25. data/spec/support/chef_metal_fog/providers/testdriver.rb +16 -16
  26. data/spec/unit/chef/provisioning/fog_driver/driver_spec.rb +71 -0
  27. data/spec/unit/fog_driver_spec.rb +32 -32
  28. data/spec/unit/providers/aws/credentials_spec.rb +45 -45
  29. data/spec/unit/providers/rackspace_spec.rb +16 -16
  30. metadata +5 -3
@@ -1,492 +1,492 @@
1
- require 'chef/log'
2
- require 'fog/aws'
3
- require 'uri'
4
- require 'base64'
5
- require 'openssl'
6
- require 'pathname'
7
- require 'chef/provisioning/transport/winrm'
8
-
9
- # fog:AWS:<account_id>:<region>
10
- # fog:AWS:<profile_name>
11
- # fog:AWS:<profile_name>:<region>
12
- class Chef
13
- module Provisioning
14
- module FogDriver
15
- module Providers
16
- class AWS < FogDriver::Driver
17
-
18
- require_relative 'aws/credentials'
19
-
20
- Driver.register_provider_class('AWS', FogDriver::Providers::AWS)
21
-
22
- def creator
23
- driver_options[:aws_account_info][:aws_username]
24
- end
25
-
26
- def default_ssh_username
27
- 'ubuntu'
28
- end
29
-
30
- # Create a WinRM transport for an AWS instance
31
- # @param [Hash] machine_spec Machine-spec hash
32
- # @param [Hash] machine_options Machine options (from the recipe)
33
- # @param [Fog::Compute::Server] server A Fog mapping to the AWS instance
34
- # @return [ChefMetal::Transport::WinRM] A WinRM Transport object to talk to the server
35
- def create_winrm_transport(machine_spec, machine_options, server)
36
- remote_host = if machine_spec.location['use_private_ip_for_ssh']
37
- server.private_ip_address
38
- elsif !server.public_ip_address
39
- Chef::Log.warn("Server #{machine_spec.name} has no public IP address. Using private IP '#{server.private_ip_address}'. Set driver option 'use_private_ip_for_ssh' => true if this will always be the case ...")
40
- server.private_ip_address
41
- elsif server.public_ip_address
42
- server.public_ip_address
43
- else
44
- fail "Server #{server.id} has no private or public IP address!"
45
- end
46
-
47
- port = machine_spec.location['winrm_port'] || 5985
48
- endpoint = "http://#{remote_host}:#{port}/wsman"
49
- type = :plaintext
50
- pem_bytes = private_key_for(machine_spec, machine_options, server)
51
- encrypted_admin_password = wait_for_admin_password(machine_spec)
52
- decoded = Base64.decode64(encrypted_admin_password)
53
- private_key = OpenSSL::PKey::RSA.new(pem_bytes)
54
- decrypted_password = private_key.private_decrypt decoded
55
-
56
- # Use basic HTTP auth - this is required for the WinRM setup we
57
- # are using
58
- # TODO: Improve that.
59
- options = {
60
- :user => machine_spec.location['winrm.username'] || 'Administrator',
61
- :pass => decrypted_password,
62
- :disable_sspi => true,
63
- :basic_auth_only => true
64
- }
65
-
66
- Chef::Provisioning::Transport::WinRM.new(endpoint, type, options, {})
67
- end
68
-
69
- def allocate_image(action_handler, image_spec, image_options, machine_spec)
70
- if image_spec.location
71
- image = compute.images.get(image_spec.location['image_id'])
72
- if image
73
- raise "The image already exists, why are you asking me to create it? I can't do that, Dave."
74
- end
75
- end
76
- action_handler.perform_action "Create image #{image_spec.name} from machine #{machine_spec.name} with options #{image_options.inspect}" do
77
- opt = image_options.dup
78
- response = compute.create_image(machine_spec.location['server_id'],
79
- image_spec.name,
80
- opt.delete(:description) || "The image formerly and currently named '#{image_spec.name}'",
81
- opt.delete(:no_reboot) || false,
82
- opt)
83
- image_spec.location = {
84
- 'driver_url' => driver_url,
85
- 'driver_version' => FogDriver::VERSION,
86
- 'image_id' => response.body['imageId'],
87
- 'creator' => creator,
88
- 'allocated_at' => Time.now.to_i
89
- }
90
-
91
- image_spec.machine_options ||= {}
92
- image_spec.machine_options.merge!({
93
- :bootstrap_options => {
94
- :image_id => image_spec.location['image_id']
95
- }
96
- })
97
-
98
- end
99
- end
100
-
101
- def ready_image(action_handler, image_spec, image_options)
102
- if !image_spec.location
103
- raise "Cannot ready an image that does not exist"
104
- end
105
- image = compute.images.get(image_spec.location['image_id'])
106
- if !image.ready?
107
- action_handler.report_progress "Waiting for image to be ready ..."
108
- # TODO timeout
109
- image.wait_for { ready? }
110
- action_handler.report_progress "Image is ready!"
111
- end
112
- end
113
-
114
- def destroy_image(action_handler, image_spec, image_options)
115
- if !image_spec.location
116
- return
117
- end
118
- image = compute.images.get(image_spec.location['image_id'])
119
- if !image
120
- return
121
- end
122
- delete_snapshots = image_options[:delete_snapshots]
123
- delete_snapshots = true if delete_snapshots.nil?
124
- image.deregister(delete_snapshots)
125
- end
126
-
127
- def bootstrap_options_for(action_handler, machine_spec, machine_options)
128
- bootstrap_options = symbolize_keys(machine_options[:bootstrap_options] || {})
129
-
130
- if !bootstrap_options[:key_name]
131
- bootstrap_options[:key_name] = overwrite_default_key_willy_nilly(action_handler, machine_spec)
132
- end
133
-
134
- if machine_options[:is_windows]
135
- Chef::Log.debug('Attaching WinRM data for user data.')
136
- # Enable WinRM basic auth, HTTP and open the firewall
137
- bootstrap_options[:user_data] = user_data
138
- end
139
- bootstrap_options.delete(:tags) # we handle these separately for performance reasons
140
- bootstrap_options
141
- end
142
-
143
- def create_servers(action_handler, specs_and_options, parallelizer)
144
- super(action_handler, specs_and_options, parallelizer) do |machine_spec, server|
145
- yield machine_spec, server if block_given?
146
-
147
- machine_options = specs_and_options[machine_spec]
148
- bootstrap_options = symbolize_keys(machine_options[:bootstrap_options] || {})
149
- tags = default_tags(machine_spec, bootstrap_options[:tags] || {})
150
-
151
- # Right now, not doing that in case manual tagging is going on
152
- server_tags = server.tags || {}
153
- extra_tags = tags.keys.select { |tag_name| !server_tags.has_key?(tag_name) }.to_a
154
- different_tags = server_tags.select { |tag_name, tag_value| tags.has_key?(tag_name) && tags[tag_name] != tag_value }.to_a
155
- if extra_tags.size > 0 || different_tags.size > 0
156
- tags_description = [ "Update tags for #{machine_spec.name} on #{driver_url}" ]
157
- tags_description += extra_tags.map { |tag| " Add #{tag} = #{tags[tag].inspect}" }
158
- tags_description += different_tags.map { |tag_name, tag_value| " Update #{tag_name} from #{tag_value.inspect} to #{tags[tag_name].inspect}"}
159
- action_handler.perform_action tags_description do
160
- # TODO should we narrow this down to just extra/different tags or
161
- # is it OK to just pass 'em all? Certainly easier to do the
162
- # latter, and I can't think of a consequence for doing so offhand.
163
- compute.create_tags(server.identity, tags)
164
- end
165
- end
166
- end
167
- end
168
-
169
- def convergence_strategy_for(machine_spec, machine_options)
170
- machine_options = Cheffish::MergedConfig.new(machine_options, {
171
- :convergence_options => {:ohai_hints => {'ec2' => ''}}
172
- })
173
- super(machine_spec, machine_options)
174
- end
175
-
176
- # Attach given IP to machine
177
- def attach_ip(server, ip)
178
- Chef::Log.info "Attaching floating IP <#{ip}>"
179
- compute.associate_address(:instance_id => server.id,
180
- :allocation_id => option_for(machine_options, :allocation_id),
181
- :public_ip => ip)
182
- end
183
-
184
- def self.get_aws_profile(driver_options, aws_account_id)
185
- aws_credentials = get_aws_credentials(driver_options)
186
- compute_options = driver_options[:compute_options] || {}
187
-
188
- # Order of operations:
189
- # compute_options[:aws_access_key_id] / compute_options[:aws_secret_access_key] / compute_options[:aws_security_token] / compute_options[:region]
190
- # compute_options[:aws_profile]
191
- # ENV['AWS_ACCESS_KEY_ID'] / ENV['AWS_SECRET_ACCESS_KEY'] / ENV['AWS_SECURITY_TOKEN'] / ENV['AWS_DEFAULT_REGION']
192
- # ENV['AWS_PROFILE']
193
- # ENV['DEFAULT_PROFILE']
194
- # 'default'
195
- if compute_options[:aws_access_key_id]
196
- Chef::Log.debug("Using AWS driver access key options")
197
- aws_profile = {
198
- :aws_access_key_id => compute_options[:aws_access_key_id],
199
- :aws_secret_access_key => compute_options[:aws_secret_access_key],
200
- :aws_security_token => compute_options[:aws_session_token],
201
- :region => compute_options[:region]
202
- }
203
- elsif driver_options[:aws_profile]
204
- Chef::Log.debug("Using AWS profile #{driver_options[:aws_profile]}")
205
- aws_profile = aws_credentials[driver_options[:aws_profile]]
206
- elsif ENV['AWS_ACCESS_KEY_ID'] || ENV['AWS_ACCESS_KEY']
207
- Chef::Log.debug("Using AWS environment variable access keys")
208
- aws_profile = {
209
- :aws_access_key_id => ENV['AWS_ACCESS_KEY_ID'] || ENV['AWS_ACCESS_KEY'],
210
- :aws_secret_access_key => ENV['AWS_SECRET_ACCESS_KEY'] || ENV['AWS_SECRET_KEY'],
211
- :aws_security_token => ENV['AWS_SECURITY_TOKEN'],
212
- :region => ENV['AWS_DEFAULT_REGION'] || ENV['AWS_REGION'] || ENV['EC2_REGION']
213
- }
214
- elsif ENV['AWS_PROFILE']
215
- Chef::Log.debug("Using AWS profile #{ENV['AWS_PROFILE']} from AWS_PROFILE environment variable")
216
- aws_profile = aws_credentials[ENV['AWS_PROFILE']]
217
- if !aws_profile
218
- raise "Environment variable AWS_PROFILE is set to #{ENV['AWS_PROFILE'].inspect} but your AWS config file does not contain that profile!"
219
- end
220
- else
221
- Chef::Log.debug("Using AWS default profile")
222
- aws_profile = aws_credentials.default
223
- end
224
-
225
- default_ec2_endpoint = compute_options[:ec2_endpoint] || ENV['EC2_URL']
226
- default_iam_endpoint = compute_options[:iam_endpoint] || ENV['AWS_IAM_URL']
227
-
228
- # Merge in account info for profile
229
- if aws_profile
230
- aws_profile = aws_profile.merge(aws_account_info_for(aws_profile, default_iam_endpoint))
231
- end
232
-
233
- # If no profile is found (or the profile is not the right account), search
234
- # for a profile that matches the given account ID
235
- if aws_account_id && (!aws_profile || aws_profile[:aws_account_id] != aws_account_id)
236
- aws_profile = find_aws_profile_for_account_id(aws_credentials, aws_account_id, default_iam_endpoint)
237
- end
238
-
239
- fail 'No AWS profile specified! Are you missing something in the Chef or AWS config?' unless aws_profile
240
-
241
- aws_profile[:ec2_endpoint] ||= default_ec2_endpoint
242
- aws_profile[:iam_endpoint] ||= default_iam_endpoint
243
-
244
- aws_profile.delete_if { |key, value| value.nil? }
245
- aws_profile
246
- end
247
-
248
- def self.find_aws_profile_for_account_id(aws_credentials, aws_account_id, default_iam_endpoint=nil)
249
- aws_profile = nil
250
- aws_credentials.each do |profile_name, profile|
251
- begin
252
- aws_account_info = aws_account_info_for(profile, default_iam_endpoint)
253
- rescue
254
- Chef::Log.warn("Could not connect to AWS profile #{aws_credentials[:name]}: #{$!}")
255
- Chef::Log.debug($!.backtrace.join("\n"))
256
- next
257
- end
258
- if aws_account_info[:aws_account_id] == aws_account_id
259
- aws_profile = profile
260
- aws_profile[:name] = profile_name
261
- aws_profile = aws_profile.merge(aws_account_info)
262
- break
263
- end
264
- end
265
- if aws_profile
266
- Chef::Log.info("Discovered AWS profile #{aws_profile[:name]} pointing at account #{aws_account_id}. Using ...")
267
- else
268
- raise "No AWS profile leads to account #{aws_account_id}. Do you need to add profiles to the AWS config?"
269
- end
270
- aws_profile
271
- end
272
-
273
- def self.aws_account_info_for(aws_profile, default_iam_endpoint = nil)
274
- iam_endpoint = aws_profile[:iam_endpoint] || default_iam_endpoint
275
-
276
- @@aws_account_info ||= {}
277
- @@aws_account_info[aws_profile[:aws_access_key_id]] ||= begin
278
- options = {
279
- # Endpoint configuration
280
- :aws_access_key_id => aws_profile[:aws_access_key_id],
281
- :aws_secret_access_key => aws_profile[:aws_secret_access_key],
282
- :aws_session_token => aws_profile[:aws_security_token]
283
- }
284
- if iam_endpoint
285
- options[:host] = URI(iam_endpoint).host
286
- options[:scheme] = URI(iam_endpoint).scheme
287
- options[:port] = URI(iam_endpoint).port
288
- options[:path] = URI(iam_endpoint).path
289
- end
290
- options.delete_if { |key, value| value.nil? }
291
-
292
- iam = Fog::AWS::IAM.new(options)
293
- arn = begin
294
- # TODO it would be nice if Fog let you do this normally ...
295
- iam.send(:request, {
296
- 'Action' => 'GetUser',
297
- :parser => Fog::Parsers::AWS::IAM::GetUser.new
298
- }).body['User']['Arn']
299
- rescue Fog::AWS::IAM::Error
300
- # TODO Someone tell me there is a better way to find out your current
301
- # user ID than this! This is what happens when you use an IAM user
302
- # with default privileges.
303
- if $!.message =~ /AccessDenied.+(arn:aws:iam::\d+:\S+)/
304
- arn = $1
305
- else
306
- raise
307
- end
308
- end
309
- arn_split = arn.split(':', 6)
310
- {
311
- :aws_account_id => arn_split[4],
312
- :aws_username => arn_split[5],
313
- :aws_user_arn => arn
314
- }
315
- end
316
- end
317
-
318
- def self.get_aws_credentials(driver_options)
319
- # Grab the list of possible credentials
320
- if driver_options[:aws_credentials]
321
- aws_credentials = driver_options[:aws_credentials]
322
- else
323
- aws_credentials = Credentials.new
324
- if driver_options[:aws_config_file]
325
- aws_credentials.load_ini(driver_options[:aws_config_file])
326
- elsif driver_options[:aws_csv_file]
327
- aws_credentials.load_csv(driver_options[:aws_csv_file])
328
- else
329
- aws_credentials.load_default
330
- end
331
- end
332
- aws_credentials
333
- end
334
-
335
- def self.compute_options_for(provider, id, config)
336
- new_compute_options = {}
337
- new_compute_options[:provider] = provider
338
- new_config = { :driver_options => { :compute_options => new_compute_options }}
339
- new_defaults = {
340
- :driver_options => { :compute_options => {} },
341
- :machine_options => { :bootstrap_options => {} }
342
- }
343
- result = Cheffish::MergedConfig.new(new_config, config, new_defaults)
344
-
345
- if id && id != ''
346
- # AWS canonical URLs are of the form fog:AWS:
347
- if id =~ /^(\d{12}|IAM)(:(.+))?$/
348
- if $2
349
- id = $1
350
- new_compute_options[:region] = $3
351
- else
352
- Chef::Log.warn("Old-style AWS URL #{id} from an early beta of chef-provisioning (before chef-metal 0.11-final) found. If you have servers in multiple regions on this account, you may see odd behavior like servers being recreated. To fix, edit any nodes with attribute chef_provisioning.location.driver_url to include the region like so: fog:AWS:#{id}:<region> (e.g. us-east-1)")
353
- end
354
- else
355
- # Assume it is a profile name, and set that.
356
- aws_profile, region = id.split(':', 2)
357
- new_config[:driver_options][:aws_profile] = aws_profile
358
- new_compute_options[:region] = region
359
- id = nil
360
- end
361
- end
362
- if id == 'IAM'
363
- id = "IAM:#{result[:driver_options][:compute_options][:region]}"
364
- new_config[:driver_options][:aws_account_info] = { aws_username: 'IAM' }
365
- new_compute_options[:use_iam_profile] = true
366
- else
367
- aws_profile = get_aws_profile(result[:driver_options], id)
368
- new_compute_options[:aws_access_key_id] = aws_profile[:aws_access_key_id]
369
- new_compute_options[:aws_secret_access_key] = aws_profile[:aws_secret_access_key]
370
- new_compute_options[:aws_session_token] = aws_profile[:aws_security_token]
371
- new_defaults[:driver_options][:compute_options][:region] = aws_profile[:region]
372
- new_defaults[:driver_options][:compute_options][:endpoint] = aws_profile[:ec2_endpoint]
373
-
374
- account_info = aws_account_info_for(result[:driver_options][:compute_options])
375
- new_config[:driver_options][:aws_account_info] = account_info
376
- id = "#{account_info[:aws_account_id]}:#{result[:driver_options][:compute_options][:region]}"
377
- end
378
-
379
- # Make sure we're using a reasonable default AMI, for now this is Ubuntu 14.04 LTS
380
- result[:machine_options][:bootstrap_options][:image_id] ||=
381
- default_ami_for_region(result[:driver_options][:compute_options][:region])
382
-
383
- [result, id]
384
- end
385
-
386
- def create_many_servers(num_servers, bootstrap_options, parallelizer)
387
- # Create all the servers in one request if we have a version of Fog that can do that
388
- if compute.servers.respond_to?(:create_many)
389
- servers = compute.servers.create_many(num_servers, num_servers, bootstrap_options)
390
- if block_given?
391
- parallelizer.parallelize(servers) do |server|
392
- yield server
393
- end.to_a
394
- end
395
- servers
396
- else
397
- super
398
- end
399
- end
400
-
401
- def servers_for(machine_specs)
402
- # Grab all the servers in one request
403
- instance_ids = machine_specs.map { |machine_spec| (machine_spec.location || {})['server_id'] }.select { |id| !id.nil? }
404
- servers = compute.servers.all('instance-id' => instance_ids)
405
- result = {}
406
- machine_specs.each do |machine_spec|
407
- if machine_spec.location
408
- result[machine_spec] = servers.select { |s| s.id == machine_spec.location['server_id'] }.first
409
- else
410
- result[machine_spec] = nil
411
- end
412
- end
413
- result
414
- end
415
-
416
- private
417
- def user_data
418
- # TODO: Make this use HTTPS at some point.
419
- <<EOD
420
- <powershell>
421
- winrm quickconfig -q
422
- winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="300"}'
423
- winrm set winrm/config '@{MaxTimeoutms="1800000"}'
424
- winrm set winrm/config/service '@{AllowUnencrypted="true"}'
425
- winrm set winrm/config/service/auth '@{Basic="true"}'
426
-
427
- netsh advfirewall firewall add rule name="WinRM 5985" protocol=TCP dir=in localport=5985 action=allow
428
- netsh advfirewall firewall add rule name="WinRM 5986" protocol=TCP dir=in localport=5986 action=allow
429
-
430
- net stop winrm
431
- sc config winrm start=auto
432
- net start winrm
433
- </powershell>
434
-
435
- EOD
436
- end
437
-
438
- def self.default_ami_for_region(region)
439
- Chef::Log.debug("Choosing default AMI for region '#{region}'")
440
-
441
- case region
442
- when 'ap-northeast-1'
443
- 'ami-c786dcc6'
444
- when 'ap-southeast-1'
445
- 'ami-eefca7bc'
446
- when 'ap-southeast-2'
447
- 'ami-996706a3'
448
- when 'eu-west-1'
449
- 'ami-4ab46b3d'
450
- when 'sa-east-1'
451
- 'ami-6770d87a'
452
- when 'us-east-1'
453
- 'ami-d2ff23ba'
454
- when 'us-west-1'
455
- 'ami-73717d36'
456
- when 'us-west-2'
457
- 'ami-f1ce8bc1'
458
- end
459
- end
460
-
461
- # Wait for the Windows Admin password to become available
462
- # @param [Hash] machine_spec Machine spec data
463
- # @return [String] encrypted admin password
464
- def wait_for_admin_password(machine_spec)
465
- time_elapsed = 0
466
- sleep_time = 10
467
- max_wait_time = 900 # 15 minutes
468
- encrypted_admin_password = nil
469
- instance_id = machine_spec.location['server_id']
470
-
471
-
472
- Chef::Log.info "waiting for #{machine_spec.name}'s admin password to be available..."
473
- while time_elapsed < max_wait_time && encrypted_admin_password.nil?
474
- response = compute.get_password_data(instance_id)
475
- encrypted_admin_password = response.body['passwordData']
476
- if encrypted_admin_password.nil?
477
- Chef::Log.info "#{time_elapsed}/#{max_wait_time}s elapsed -- sleeping #{sleep_time} seconds for #{machine_spec.name}'s admin password."
478
- sleep(sleep_time)
479
- time_elapsed += sleep_time
480
- end
481
- end
482
-
483
- Chef::Log.info "#{machine_spec.name}'s admin password is available!'"
484
-
485
- encrypted_admin_password
486
- end
487
-
488
- end
489
- end
490
- end
491
- end
492
- end
1
+ require 'chef/log'
2
+ require 'fog/aws'
3
+ require 'uri'
4
+ require 'base64'
5
+ require 'openssl'
6
+ require 'pathname'
7
+ require 'chef/provisioning/transport/winrm'
8
+
9
+ # fog:AWS:<account_id>:<region>
10
+ # fog:AWS:<profile_name>
11
+ # fog:AWS:<profile_name>:<region>
12
+ class Chef
13
+ module Provisioning
14
+ module FogDriver
15
+ module Providers
16
+ class AWS < FogDriver::Driver
17
+
18
+ require_relative 'aws/credentials'
19
+
20
+ Driver.register_provider_class('AWS', FogDriver::Providers::AWS)
21
+
22
+ def creator
23
+ driver_options[:aws_account_info][:aws_username]
24
+ end
25
+
26
+ def default_ssh_username
27
+ 'ubuntu'
28
+ end
29
+
30
+ # Create a WinRM transport for an AWS instance
31
+ # @param [Hash] machine_spec Machine-spec hash
32
+ # @param [Hash] machine_options Machine options (from the recipe)
33
+ # @param [Fog::Compute::Server] server A Fog mapping to the AWS instance
34
+ # @return [ChefMetal::Transport::WinRM] A WinRM Transport object to talk to the server
35
+ def create_winrm_transport(machine_spec, machine_options, server)
36
+ remote_host = if machine_spec.location['use_private_ip_for_ssh']
37
+ server.private_ip_address
38
+ elsif !server.public_ip_address
39
+ Chef::Log.warn("Server #{machine_spec.name} has no public IP address. Using private IP '#{server.private_ip_address}'. Set driver option 'use_private_ip_for_ssh' => true if this will always be the case ...")
40
+ server.private_ip_address
41
+ elsif server.public_ip_address
42
+ server.public_ip_address
43
+ else
44
+ fail "Server #{server.id} has no private or public IP address!"
45
+ end
46
+
47
+ port = machine_spec.location['winrm_port'] || 5985
48
+ endpoint = "http://#{remote_host}:#{port}/wsman"
49
+ type = :plaintext
50
+ pem_bytes = private_key_for(machine_spec, machine_options, server)
51
+ encrypted_admin_password = wait_for_admin_password(machine_spec)
52
+ decoded = Base64.decode64(encrypted_admin_password)
53
+ private_key = OpenSSL::PKey::RSA.new(pem_bytes)
54
+ decrypted_password = private_key.private_decrypt decoded
55
+
56
+ # Use basic HTTP auth - this is required for the WinRM setup we
57
+ # are using
58
+ # TODO: Improve that.
59
+ options = {
60
+ :user => machine_spec.location['winrm.username'] || 'Administrator',
61
+ :pass => decrypted_password,
62
+ :disable_sspi => true,
63
+ :basic_auth_only => true
64
+ }
65
+
66
+ Chef::Provisioning::Transport::WinRM.new(endpoint, type, options, {})
67
+ end
68
+
69
+ def allocate_image(action_handler, image_spec, image_options, machine_spec)
70
+ if image_spec.location
71
+ image = compute.images.get(image_spec.location['image_id'])
72
+ if image
73
+ raise "The image already exists, why are you asking me to create it? I can't do that, Dave."
74
+ end
75
+ end
76
+ action_handler.perform_action "Create image #{image_spec.name} from machine #{machine_spec.name} with options #{image_options.inspect}" do
77
+ opt = image_options.dup
78
+ response = compute.create_image(machine_spec.location['server_id'],
79
+ image_spec.name,
80
+ opt.delete(:description) || "The image formerly and currently named '#{image_spec.name}'",
81
+ opt.delete(:no_reboot) || false,
82
+ opt)
83
+ image_spec.location = {
84
+ 'driver_url' => driver_url,
85
+ 'driver_version' => FogDriver::VERSION,
86
+ 'image_id' => response.body['imageId'],
87
+ 'creator' => creator,
88
+ 'allocated_at' => Time.now.to_i
89
+ }
90
+
91
+ image_spec.machine_options ||= {}
92
+ image_spec.machine_options.merge!({
93
+ :bootstrap_options => {
94
+ :image_id => image_spec.location['image_id']
95
+ }
96
+ })
97
+
98
+ end
99
+ end
100
+
101
+ def ready_image(action_handler, image_spec, image_options)
102
+ if !image_spec.location
103
+ raise "Cannot ready an image that does not exist"
104
+ end
105
+ image = compute.images.get(image_spec.location['image_id'])
106
+ if !image.ready?
107
+ action_handler.report_progress "Waiting for image to be ready ..."
108
+ # TODO timeout
109
+ image.wait_for { ready? }
110
+ action_handler.report_progress "Image is ready!"
111
+ end
112
+ end
113
+
114
+ def destroy_image(action_handler, image_spec, image_options)
115
+ if !image_spec.location
116
+ return
117
+ end
118
+ image = compute.images.get(image_spec.location['image_id'])
119
+ if !image
120
+ return
121
+ end
122
+ delete_snapshots = image_options[:delete_snapshots]
123
+ delete_snapshots = true if delete_snapshots.nil?
124
+ image.deregister(delete_snapshots)
125
+ end
126
+
127
+ def bootstrap_options_for(action_handler, machine_spec, machine_options)
128
+ bootstrap_options = symbolize_keys(machine_options[:bootstrap_options] || {})
129
+
130
+ if !bootstrap_options[:key_name]
131
+ bootstrap_options[:key_name] = overwrite_default_key_willy_nilly(action_handler, machine_spec)
132
+ end
133
+
134
+ if machine_options[:is_windows]
135
+ Chef::Log.debug('Attaching WinRM data for user data.')
136
+ # Enable WinRM basic auth, HTTP and open the firewall
137
+ bootstrap_options[:user_data] = user_data
138
+ end
139
+ bootstrap_options.delete(:tags) # we handle these separately for performance reasons
140
+ bootstrap_options
141
+ end
142
+
143
+ def create_servers(action_handler, specs_and_options, parallelizer)
144
+ super(action_handler, specs_and_options, parallelizer) do |machine_spec, server|
145
+ yield machine_spec, server if block_given?
146
+
147
+ machine_options = specs_and_options[machine_spec]
148
+ bootstrap_options = symbolize_keys(machine_options[:bootstrap_options] || {})
149
+ tags = default_tags(machine_spec, bootstrap_options[:tags] || {})
150
+
151
+ # Right now, not doing that in case manual tagging is going on
152
+ server_tags = server.tags || {}
153
+ extra_tags = tags.keys.select { |tag_name| !server_tags.has_key?(tag_name) }.to_a
154
+ different_tags = server_tags.select { |tag_name, tag_value| tags.has_key?(tag_name) && tags[tag_name] != tag_value }.to_a
155
+ if extra_tags.size > 0 || different_tags.size > 0
156
+ tags_description = [ "Update tags for #{machine_spec.name} on #{driver_url}" ]
157
+ tags_description += extra_tags.map { |tag| " Add #{tag} = #{tags[tag].inspect}" }
158
+ tags_description += different_tags.map { |tag_name, tag_value| " Update #{tag_name} from #{tag_value.inspect} to #{tags[tag_name].inspect}"}
159
+ action_handler.perform_action tags_description do
160
+ # TODO should we narrow this down to just extra/different tags or
161
+ # is it OK to just pass 'em all? Certainly easier to do the
162
+ # latter, and I can't think of a consequence for doing so offhand.
163
+ compute.create_tags(server.identity, tags)
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ def convergence_strategy_for(machine_spec, machine_options)
170
+ machine_options = Cheffish::MergedConfig.new(machine_options, {
171
+ :convergence_options => {:ohai_hints => {'ec2' => ''}}
172
+ })
173
+ super(machine_spec, machine_options)
174
+ end
175
+
176
+ # Attach given IP to machine
177
+ def attach_ip(server, ip)
178
+ Chef::Log.info "Attaching floating IP <#{ip}>"
179
+ compute.associate_address(:instance_id => server.id,
180
+ :allocation_id => option_for(machine_options, :allocation_id),
181
+ :public_ip => ip)
182
+ end
183
+
184
+ def self.get_aws_profile(driver_options, aws_account_id)
185
+ aws_credentials = get_aws_credentials(driver_options)
186
+ compute_options = driver_options[:compute_options] || {}
187
+
188
+ # Order of operations:
189
+ # compute_options[:aws_access_key_id] / compute_options[:aws_secret_access_key] / compute_options[:aws_security_token] / compute_options[:region]
190
+ # compute_options[:aws_profile]
191
+ # ENV['AWS_ACCESS_KEY_ID'] / ENV['AWS_SECRET_ACCESS_KEY'] / ENV['AWS_SECURITY_TOKEN'] / ENV['AWS_DEFAULT_REGION']
192
+ # ENV['AWS_PROFILE']
193
+ # ENV['DEFAULT_PROFILE']
194
+ # 'default'
195
+ if compute_options[:aws_access_key_id]
196
+ Chef::Log.debug("Using AWS driver access key options")
197
+ aws_profile = {
198
+ :aws_access_key_id => compute_options[:aws_access_key_id],
199
+ :aws_secret_access_key => compute_options[:aws_secret_access_key],
200
+ :aws_security_token => compute_options[:aws_session_token],
201
+ :region => compute_options[:region]
202
+ }
203
+ elsif driver_options[:aws_profile]
204
+ Chef::Log.debug("Using AWS profile #{driver_options[:aws_profile]}")
205
+ aws_profile = aws_credentials[driver_options[:aws_profile]]
206
+ elsif ENV['AWS_ACCESS_KEY_ID'] || ENV['AWS_ACCESS_KEY']
207
+ Chef::Log.debug("Using AWS environment variable access keys")
208
+ aws_profile = {
209
+ :aws_access_key_id => ENV['AWS_ACCESS_KEY_ID'] || ENV['AWS_ACCESS_KEY'],
210
+ :aws_secret_access_key => ENV['AWS_SECRET_ACCESS_KEY'] || ENV['AWS_SECRET_KEY'],
211
+ :aws_security_token => ENV['AWS_SECURITY_TOKEN'],
212
+ :region => ENV['AWS_DEFAULT_REGION'] || ENV['AWS_REGION'] || ENV['EC2_REGION']
213
+ }
214
+ elsif ENV['AWS_PROFILE']
215
+ Chef::Log.debug("Using AWS profile #{ENV['AWS_PROFILE']} from AWS_PROFILE environment variable")
216
+ aws_profile = aws_credentials[ENV['AWS_PROFILE']]
217
+ if !aws_profile
218
+ raise "Environment variable AWS_PROFILE is set to #{ENV['AWS_PROFILE'].inspect} but your AWS config file does not contain that profile!"
219
+ end
220
+ else
221
+ Chef::Log.debug("Using AWS default profile")
222
+ aws_profile = aws_credentials.default
223
+ end
224
+
225
+ default_ec2_endpoint = compute_options[:ec2_endpoint] || ENV['EC2_URL']
226
+ default_iam_endpoint = compute_options[:iam_endpoint] || ENV['AWS_IAM_URL']
227
+
228
+ # Merge in account info for profile
229
+ if aws_profile
230
+ aws_profile = aws_profile.merge(aws_account_info_for(aws_profile, default_iam_endpoint))
231
+ end
232
+
233
+ # If no profile is found (or the profile is not the right account), search
234
+ # for a profile that matches the given account ID
235
+ if aws_account_id && (!aws_profile || aws_profile[:aws_account_id] != aws_account_id)
236
+ aws_profile = find_aws_profile_for_account_id(aws_credentials, aws_account_id, default_iam_endpoint)
237
+ end
238
+
239
+ fail 'No AWS profile specified! Are you missing something in the Chef or AWS config?' unless aws_profile
240
+
241
+ aws_profile[:ec2_endpoint] ||= default_ec2_endpoint
242
+ aws_profile[:iam_endpoint] ||= default_iam_endpoint
243
+
244
+ aws_profile.delete_if { |key, value| value.nil? }
245
+ aws_profile
246
+ end
247
+
248
+ def self.find_aws_profile_for_account_id(aws_credentials, aws_account_id, default_iam_endpoint=nil)
249
+ aws_profile = nil
250
+ aws_credentials.each do |profile_name, profile|
251
+ begin
252
+ aws_account_info = aws_account_info_for(profile, default_iam_endpoint)
253
+ rescue
254
+ Chef::Log.warn("Could not connect to AWS profile #{aws_credentials[:name]}: #{$!}")
255
+ Chef::Log.debug($!.backtrace.join("\n"))
256
+ next
257
+ end
258
+ if aws_account_info[:aws_account_id] == aws_account_id
259
+ aws_profile = profile
260
+ aws_profile[:name] = profile_name
261
+ aws_profile = aws_profile.merge(aws_account_info)
262
+ break
263
+ end
264
+ end
265
+ if aws_profile
266
+ Chef::Log.info("Discovered AWS profile #{aws_profile[:name]} pointing at account #{aws_account_id}. Using ...")
267
+ else
268
+ raise "No AWS profile leads to account #{aws_account_id}. Do you need to add profiles to the AWS config?"
269
+ end
270
+ aws_profile
271
+ end
272
+
273
+ def self.aws_account_info_for(aws_profile, default_iam_endpoint = nil)
274
+ iam_endpoint = aws_profile[:iam_endpoint] || default_iam_endpoint
275
+
276
+ @@aws_account_info ||= {}
277
+ @@aws_account_info[aws_profile[:aws_access_key_id]] ||= begin
278
+ options = {
279
+ # Endpoint configuration
280
+ :aws_access_key_id => aws_profile[:aws_access_key_id],
281
+ :aws_secret_access_key => aws_profile[:aws_secret_access_key],
282
+ :aws_session_token => aws_profile[:aws_security_token]
283
+ }
284
+ if iam_endpoint
285
+ options[:host] = URI(iam_endpoint).host
286
+ options[:scheme] = URI(iam_endpoint).scheme
287
+ options[:port] = URI(iam_endpoint).port
288
+ options[:path] = URI(iam_endpoint).path
289
+ end
290
+ options.delete_if { |key, value| value.nil? }
291
+
292
+ iam = Fog::AWS::IAM.new(options)
293
+ arn = begin
294
+ # TODO it would be nice if Fog let you do this normally ...
295
+ iam.send(:request, {
296
+ 'Action' => 'GetUser',
297
+ :parser => Fog::Parsers::AWS::IAM::GetUser.new
298
+ }).body['User']['Arn']
299
+ rescue Fog::AWS::IAM::Error
300
+ # TODO Someone tell me there is a better way to find out your current
301
+ # user ID than this! This is what happens when you use an IAM user
302
+ # with default privileges.
303
+ if $!.message =~ /AccessDenied.+(arn:aws:iam::\d+:\S+)/
304
+ arn = $1
305
+ else
306
+ raise
307
+ end
308
+ end
309
+ arn_split = arn.split(':', 6)
310
+ {
311
+ :aws_account_id => arn_split[4],
312
+ :aws_username => arn_split[5],
313
+ :aws_user_arn => arn
314
+ }
315
+ end
316
+ end
317
+
318
+ def self.get_aws_credentials(driver_options)
319
+ # Grab the list of possible credentials
320
+ if driver_options[:aws_credentials]
321
+ aws_credentials = driver_options[:aws_credentials]
322
+ else
323
+ aws_credentials = Credentials.new
324
+ if driver_options[:aws_config_file]
325
+ aws_credentials.load_ini(driver_options[:aws_config_file])
326
+ elsif driver_options[:aws_csv_file]
327
+ aws_credentials.load_csv(driver_options[:aws_csv_file])
328
+ else
329
+ aws_credentials.load_default
330
+ end
331
+ end
332
+ aws_credentials
333
+ end
334
+
335
+ def self.compute_options_for(provider, id, config)
336
+ new_compute_options = {}
337
+ new_compute_options[:provider] = provider
338
+ new_config = { :driver_options => { :compute_options => new_compute_options }}
339
+ new_defaults = {
340
+ :driver_options => { :compute_options => {} },
341
+ :machine_options => { :bootstrap_options => {} }
342
+ }
343
+ result = Cheffish::MergedConfig.new(new_config, config, new_defaults)
344
+
345
+ if id && id != ''
346
+ # AWS canonical URLs are of the form fog:AWS:
347
+ if id =~ /^(\d{12}|IAM)(:(.+))?$/
348
+ if $2
349
+ id = $1
350
+ new_compute_options[:region] = $3
351
+ else
352
+ Chef::Log.warn("Old-style AWS URL #{id} from an early beta of chef-provisioning (before chef-metal 0.11-final) found. If you have servers in multiple regions on this account, you may see odd behavior like servers being recreated. To fix, edit any nodes with attribute chef_provisioning.location.driver_url to include the region like so: fog:AWS:#{id}:<region> (e.g. us-east-1)")
353
+ end
354
+ else
355
+ # Assume it is a profile name, and set that.
356
+ aws_profile, region = id.split(':', 2)
357
+ new_config[:driver_options][:aws_profile] = aws_profile
358
+ new_compute_options[:region] = region
359
+ id = nil
360
+ end
361
+ end
362
+ if id == 'IAM'
363
+ id = "IAM:#{result[:driver_options][:compute_options][:region]}"
364
+ new_config[:driver_options][:aws_account_info] = { aws_username: 'IAM' }
365
+ new_compute_options[:use_iam_profile] = true
366
+ else
367
+ aws_profile = get_aws_profile(result[:driver_options], id)
368
+ new_compute_options[:aws_access_key_id] = aws_profile[:aws_access_key_id]
369
+ new_compute_options[:aws_secret_access_key] = aws_profile[:aws_secret_access_key]
370
+ new_compute_options[:aws_session_token] = aws_profile[:aws_security_token]
371
+ new_defaults[:driver_options][:compute_options][:region] = aws_profile[:region]
372
+ new_defaults[:driver_options][:compute_options][:endpoint] = aws_profile[:ec2_endpoint]
373
+
374
+ account_info = aws_account_info_for(result[:driver_options][:compute_options])
375
+ new_config[:driver_options][:aws_account_info] = account_info
376
+ id = "#{account_info[:aws_account_id]}:#{result[:driver_options][:compute_options][:region]}"
377
+ end
378
+
379
+ # Make sure we're using a reasonable default AMI, for now this is Ubuntu 14.04 LTS
380
+ result[:machine_options][:bootstrap_options][:image_id] ||=
381
+ default_ami_for_region(result[:driver_options][:compute_options][:region])
382
+
383
+ [result, id]
384
+ end
385
+
386
+ def create_many_servers(num_servers, bootstrap_options, parallelizer)
387
+ # Create all the servers in one request if we have a version of Fog that can do that
388
+ if compute.servers.respond_to?(:create_many)
389
+ servers = compute.servers.create_many(num_servers, num_servers, bootstrap_options)
390
+ if block_given?
391
+ parallelizer.parallelize(servers) do |server|
392
+ yield server
393
+ end.to_a
394
+ end
395
+ servers
396
+ else
397
+ super
398
+ end
399
+ end
400
+
401
+ def servers_for(machine_specs)
402
+ # Grab all the servers in one request
403
+ instance_ids = machine_specs.map { |machine_spec| (machine_spec.location || {})['server_id'] }.select { |id| !id.nil? }
404
+ servers = compute.servers.all('instance-id' => instance_ids)
405
+ result = {}
406
+ machine_specs.each do |machine_spec|
407
+ if machine_spec.location
408
+ result[machine_spec] = servers.select { |s| s.id == machine_spec.location['server_id'] }.first
409
+ else
410
+ result[machine_spec] = nil
411
+ end
412
+ end
413
+ result
414
+ end
415
+
416
+ private
417
+ def user_data
418
+ # TODO: Make this use HTTPS at some point.
419
+ <<EOD
420
+ <powershell>
421
+ winrm quickconfig -q
422
+ winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="300"}'
423
+ winrm set winrm/config '@{MaxTimeoutms="1800000"}'
424
+ winrm set winrm/config/service '@{AllowUnencrypted="true"}'
425
+ winrm set winrm/config/service/auth '@{Basic="true"}'
426
+
427
+ netsh advfirewall firewall add rule name="WinRM 5985" protocol=TCP dir=in localport=5985 action=allow
428
+ netsh advfirewall firewall add rule name="WinRM 5986" protocol=TCP dir=in localport=5986 action=allow
429
+
430
+ net stop winrm
431
+ sc config winrm start=auto
432
+ net start winrm
433
+ </powershell>
434
+
435
+ EOD
436
+ end
437
+
438
+ def self.default_ami_for_region(region)
439
+ Chef::Log.debug("Choosing default AMI for region '#{region}'")
440
+
441
+ case region
442
+ when 'ap-northeast-1'
443
+ 'ami-c786dcc6'
444
+ when 'ap-southeast-1'
445
+ 'ami-eefca7bc'
446
+ when 'ap-southeast-2'
447
+ 'ami-996706a3'
448
+ when 'eu-west-1'
449
+ 'ami-4ab46b3d'
450
+ when 'sa-east-1'
451
+ 'ami-6770d87a'
452
+ when 'us-east-1'
453
+ 'ami-d2ff23ba'
454
+ when 'us-west-1'
455
+ 'ami-73717d36'
456
+ when 'us-west-2'
457
+ 'ami-f1ce8bc1'
458
+ end
459
+ end
460
+
461
+ # Wait for the Windows Admin password to become available
462
+ # @param [Hash] machine_spec Machine spec data
463
+ # @return [String] encrypted admin password
464
+ def wait_for_admin_password(machine_spec)
465
+ time_elapsed = 0
466
+ sleep_time = 10
467
+ max_wait_time = 900 # 15 minutes
468
+ encrypted_admin_password = nil
469
+ instance_id = machine_spec.location['server_id']
470
+
471
+
472
+ Chef::Log.info "waiting for #{machine_spec.name}'s admin password to be available..."
473
+ while time_elapsed < max_wait_time && encrypted_admin_password.nil?
474
+ response = compute.get_password_data(instance_id)
475
+ encrypted_admin_password = response.body['passwordData']
476
+ if encrypted_admin_password.nil?
477
+ Chef::Log.info "#{time_elapsed}/#{max_wait_time}s elapsed -- sleeping #{sleep_time} seconds for #{machine_spec.name}'s admin password."
478
+ sleep(sleep_time)
479
+ time_elapsed += sleep_time
480
+ end
481
+ end
482
+
483
+ Chef::Log.info "#{machine_spec.name}'s admin password is available!'"
484
+
485
+ encrypted_admin_password
486
+ end
487
+
488
+ end
489
+ end
490
+ end
491
+ end
492
+ end