chef-provisioning-fog 0.15.0 → 0.15.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +11 -0
  3. data/LICENSE +201 -201
  4. data/README.md +208 -3
  5. data/Rakefile +6 -6
  6. data/chef-provisioning-fog.gemspec +28 -0
  7. data/lib/chef/provider/fog_key_pair.rb +266 -266
  8. data/lib/chef/provisioning/driver_init/fog.rb +3 -3
  9. data/lib/chef/provisioning/fog_driver/driver.rb +736 -736
  10. data/lib/chef/provisioning/fog_driver/providers/aws.rb +492 -492
  11. data/lib/chef/provisioning/fog_driver/providers/aws/credentials.rb +115 -115
  12. data/lib/chef/provisioning/fog_driver/providers/cloudstack.rb +44 -44
  13. data/lib/chef/provisioning/fog_driver/providers/digitalocean.rb +136 -136
  14. data/lib/chef/provisioning/fog_driver/providers/google.rb +85 -85
  15. data/lib/chef/provisioning/fog_driver/providers/joyent.rb +63 -63
  16. data/lib/chef/provisioning/fog_driver/providers/openstack.rb +117 -117
  17. data/lib/chef/provisioning/fog_driver/providers/rackspace.rb +42 -42
  18. data/lib/chef/provisioning/fog_driver/providers/softlayer.rb +36 -36
  19. data/lib/chef/provisioning/fog_driver/providers/vcair.rb +409 -409
  20. data/lib/chef/provisioning/fog_driver/providers/xenserver.rb +210 -210
  21. data/lib/chef/provisioning/fog_driver/recipe_dsl.rb +32 -32
  22. data/lib/chef/provisioning/fog_driver/version.rb +7 -7
  23. data/lib/chef/resource/fog_key_pair.rb +34 -34
  24. data/spec/spec_helper.rb +18 -18
  25. data/spec/support/aws/config-file.csv +2 -2
  26. data/spec/support/aws/ini-file.ini +10 -10
  27. data/spec/support/chef_metal_fog/providers/testdriver.rb +16 -16
  28. data/spec/unit/chef/provisioning/fog_driver/driver_spec.rb +71 -71
  29. data/spec/unit/fog_driver_spec.rb +32 -32
  30. data/spec/unit/providers/aws/credentials_spec.rb +45 -45
  31. data/spec/unit/providers/rackspace_spec.rb +16 -16
  32. 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