chef-metal-fog 0.4 → 0.5.beta

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.
@@ -0,0 +1,622 @@
1
+ require 'chef_metal/driver'
2
+ require 'chef_metal/machine/windows_machine'
3
+ require 'chef_metal/machine/unix_machine'
4
+ require 'chef_metal/machine_spec'
5
+ require 'chef_metal/convergence_strategy/install_msi'
6
+ require 'chef_metal/convergence_strategy/install_cached'
7
+ require 'chef_metal/convergence_strategy/no_converge'
8
+ require 'chef_metal/transport/ssh'
9
+ require 'chef_metal_fog/version'
10
+ require 'chef_metal_fog/fog_driver_aws'
11
+ require 'fog'
12
+ require 'fog/core'
13
+ require 'fog/compute'
14
+ require 'fog/aws'
15
+ require 'socket'
16
+ require 'etc'
17
+ require 'time'
18
+ require 'cheffish/merged_config'
19
+ require 'chef_metal_fog/recipe_dsl'
20
+
21
+ module ChefMetalFog
22
+ # Provisions cloud machines with the Fog driver.
23
+ #
24
+ # ## Fog Driver URLs
25
+ #
26
+ # All Metal drivers use URLs to uniquely identify a driver's "bucket" of machines.
27
+ # Fog URLs are of the form fog:<provider>:<identifier>:
28
+ #
29
+ # fog:AWS:<account_id>
30
+ # fog:OpenStack:https://identityHost:portNumber/v2.0
31
+ # fog:DigitalOcean:<client id>
32
+ #
33
+ # Identifier is generally something uniquely identifying the account. If multiple
34
+ # users can access the account, the identifier should be the same for all of
35
+ # them (do not use the username in these cases).
36
+ #
37
+ # ## Supporting a new Fog provider
38
+ #
39
+ # The Fog driver does not immediately support all Fog providers out of the box.
40
+ # Some minor work needs to be done to plug them into metal.
41
+ #
42
+ # To add a new supported Fog provider, pick an appropriate identifier, go to
43
+ # from_provider and compute_options_for, and add the new provider in the case
44
+ # statements so that URLs for your fog provider can be generated. If your
45
+ # cloud provider has environment variables or standard config files (like
46
+ # ~/.aws/config), you can read those and merge that information in the
47
+ # compute_options_for function.
48
+ #
49
+ # ## Location format
50
+ #
51
+ # All machines have a location hash to find them. These are the keys used by
52
+ # the fog provisioner:
53
+ #
54
+ # - driver_url: fog:<driver>:<unique_account_info>
55
+ # - server_id: the ID of the server so it can be found again
56
+ # - created_at: timestamp server was created
57
+ # - started_at: timestamp server was last started
58
+ # - is_windows, ssh_username, sudo, use_private_ip_for_ssh: copied from machine_options
59
+ #
60
+ # ## Machine options
61
+ #
62
+ # Machine options (for allocation and readying the machine) include:
63
+ #
64
+ # - bootstrap_options: hash of options to pass to compute.servers.create
65
+ # - is_windows: true if windows. TODO detect this from ami?
66
+ # - create_timeout: the time to wait for the instance to boot to ssh (defaults to 600)
67
+ # - start_timeout: the time to wait for the instance to start (defaults to 600)
68
+ # - ssh_timeout: the time to wait for ssh to be available if the instance is detected as up (defaults to 20)
69
+ # - ssh_username: username to use for ssh
70
+ # - sudo: true to prefix all commands with "sudo"
71
+ # - use_private_ip_for_ssh: hint to use private ip when available
72
+ # - convergence_options: hash of options for the convergence strategy
73
+ # - chef_client_timeout: the time to wait for chef-client to finish
74
+ # - chef_server - the chef server to point convergence at
75
+ #
76
+ # Example bootstrap_options for ec2:
77
+ #
78
+ # :bootstrap_options => {
79
+ # :image_id =>'ami-311f2b45',
80
+ # :flavor_id =>'t1.micro',
81
+ # :key_name => 'key-pair-name'
82
+ # }
83
+ #
84
+ class FogDriver < ChefMetal::Driver
85
+
86
+ include Chef::Mixin::ShellOut
87
+
88
+ DEFAULT_OPTIONS = {
89
+ :create_timeout => 180,
90
+ :start_timeout => 180,
91
+ :ssh_timeout => 20
92
+ }
93
+
94
+ # Passed in a driver_url, and a config in the format of Driver.config.
95
+ def self.from_url(driver_url, config)
96
+ scheme, provider, id = driver_url.split(':', 3)
97
+ config, id = compute_options_for(provider, id, config)
98
+ FogDriver.new("fog:#{provider}:#{id}", config)
99
+ end
100
+
101
+ # Passed in a config which is *not* merged with driver_url (because we don't
102
+ # know what it is yet) but which has the same keys
103
+ def self.from_provider(provider, config)
104
+ # Figure out the options and merge them into the config
105
+ config, id = compute_options_for(provider, nil, config)
106
+ driver_options = config[:driver_options] || {}
107
+ compute_options = driver_options[:compute_options] || {}
108
+
109
+ driver_url = "fog:#{provider}:#{id}"
110
+
111
+ config = ChefMetal.config_for_url(driver_url, config)
112
+ FogDriver.new(driver_url, config)
113
+ end
114
+
115
+ # Create a new fog driver.
116
+ #
117
+ # ## Parameters
118
+ # driver_url - URL of driver. "fog:<provider>:<provider_id>"
119
+ # config - configuration. :driver_options, :keys, :key_paths and :log_level are used.
120
+ # driver_options is a hash with these possible options:
121
+ # - compute_options: the hash of options to Fog::Compute.new.
122
+ # - aws_config_file: aws config file (default: ~/.aws/config)
123
+ # - aws_csv_file: aws csv credentials file downloaded from EC2 interface
124
+ # - aws_profile: profile name to use for credentials
125
+ # - aws_credentials: AWSCredentials object. (will be created for you by default)
126
+ # - log_level: :debug, :info, :warn, :error
127
+ def initialize(driver_url, config)
128
+ super(driver_url, config)
129
+ end
130
+
131
+ def compute_options
132
+ driver_options[:compute_options] || {}
133
+ end
134
+
135
+ def provider
136
+ compute_options[:provider]
137
+ end
138
+
139
+ # Acquire a machine, generally by provisioning it. Returns a Machine
140
+ # object pointing at the machine, allowing useful actions like setup,
141
+ # converge, execute, file and directory.
142
+ def allocate_machine(action_handler, machine_spec, machine_options)
143
+ # If the server does not exist, create it
144
+ create_server(action_handler, machine_spec, machine_options)
145
+ end
146
+
147
+ def ready_machine(action_handler, machine_spec, machine_options)
148
+ server = server_for(machine_spec)
149
+ if server.nil?
150
+ raise "Machine #{machine_spec.name} does not have a server associated with it, or server does not exist."
151
+ end
152
+
153
+ # Attach floating IPs if necessary
154
+ attach_floating_ips(action_handler, machine_spec, machine_options, server)
155
+
156
+ # Start the server if needed, and wait for it to start
157
+ start_server(action_handler, machine_spec, server)
158
+ wait_until_ready(action_handler, machine_spec, machine_options, server)
159
+ begin
160
+ wait_for_transport(action_handler, machine_spec, machine_options, server)
161
+ rescue Fog::Errors::TimeoutError
162
+ # Only ever reboot once, and only if it's been less than 10 minutes since we stopped waiting
163
+ if machine_spec.location['started_at'] || remaining_wait_time(machine_spec, machine_options) < -(10*60)
164
+ raise
165
+ else
166
+ # Sometimes (on EC2) the machine comes up but gets stuck or has
167
+ # some other problem. If this is the case, we restart the server
168
+ # to unstick it. Reboot covers a multitude of sins.
169
+ Chef::Log.warn "Machine #{machine_spec.name} (#{server.id} on #{driver_url}) was started but SSH did not come up. Rebooting machine in an attempt to unstick it ..."
170
+ restart_server(action_handler, machine_spec, server)
171
+ wait_until_ready(action_handler, machine_spec, machine_options, server)
172
+ wait_for_transport(action_handler, machine_spec, machine_options, server)
173
+ end
174
+ end
175
+
176
+ machine_for(machine_spec, machine_options, server)
177
+ end
178
+
179
+ # Connect to machine without acquiring it
180
+ def connect_to_machine(machine_spec, machine_options)
181
+ machine_for(machine_spec, machine_options)
182
+ end
183
+
184
+ def destroy_machine(action_handler, machine_spec, machine_options)
185
+ server = server_for(machine_spec)
186
+ if server
187
+ action_handler.perform_action "destroy machine #{machine_spec.name} (#{machine_spec.location['server_id']} at #{driver_url})" do
188
+ server.destroy
189
+ machine_spec.location = nil
190
+ end
191
+ end
192
+ strategy = convergence_strategy_for(machine_spec, machine_options)
193
+ strategy.cleanup_convergence(action_handler, machine_spec)
194
+ end
195
+
196
+ def stop_machine(action_handler, machine_spec, machine_options)
197
+ server = server_for(machine_spec)
198
+ if server
199
+ action_handler.perform_action "stop machine #{machine_spec.name} (#{server.id} at #{driver_url})" do
200
+ server.stop
201
+ end
202
+ end
203
+ end
204
+
205
+ def compute
206
+ @compute ||= Fog::Compute.new(compute_options)
207
+ end
208
+
209
+ # Not meant to be part of public interface
210
+ def transport_for(machine_spec, server)
211
+ # TODO winrm
212
+ create_ssh_transport(machine_spec, server)
213
+ end
214
+
215
+ protected
216
+
217
+ def option_for(machine_options, key)
218
+ machine_options[key] || DEFAULT_OPTIONS[key]
219
+ end
220
+
221
+ def create_server(action_handler, machine_spec, machine_options)
222
+ if machine_spec.location
223
+ if machine_spec.location['driver_url'] != driver_url
224
+ raise "Switching a machine's driver from #{machine_spec.location['driver_url']} to #{driver_url} for is not currently supported! Use machine :destroy and then re-create the machine on the new driver."
225
+ end
226
+
227
+ server = server_for(machine_spec)
228
+ if server
229
+ if %w(terminated archive).include?(server.state) # Can't come back from that
230
+ Chef::Log.warn "Machine #{machine_spec.name} (#{server.id} on #{driver_url}) is terminated. Recreating ..."
231
+ else
232
+ return server
233
+ end
234
+ else
235
+ Chef::Log.warn "Machine #{machine_spec.name} (#{machine_spec.location['server_id']} on #{driver_url}) no longer exists. Recreating ..."
236
+ end
237
+ end
238
+
239
+ bootstrap_options = bootstrap_options_for(action_handler, machine_spec, machine_options)
240
+
241
+ description = [ "creating machine #{machine_spec.name} on #{driver_url}" ]
242
+ bootstrap_options.each_pair { |key,value| description << " #{key}: #{value.inspect}" }
243
+ server = nil
244
+ action_handler.report_progress description
245
+ if action_handler.should_perform_actions
246
+ creator = case provider
247
+ when 'AWS'
248
+ driver_options[:aws_account_info][:aws_username]
249
+ when 'OpenStack'
250
+ compute_options[:openstack_username]
251
+ end
252
+ server = compute.servers.create(bootstrap_options)
253
+ machine_spec.location = {
254
+ 'driver_url' => driver_url,
255
+ 'driver_version' => ChefMetalFog::VERSION,
256
+ 'server_id' => server.id,
257
+ 'creator' => creator,
258
+ 'allocated_at' => Time.now.utc.to_s
259
+ }
260
+ machine_spec.location['key_name'] = bootstrap_options[:key_name] if bootstrap_options[:key_name]
261
+ %w(is_windows ssh_username sudo use_private_ip_for_ssh ssh_gateway).each do |key|
262
+ machine_spec.location[key] = machine_options[key.to_sym] if machine_options[key.to_sym]
263
+ end
264
+ end
265
+ action_handler.performed_action "machine #{machine_spec.name} created as #{server.id} on #{driver_url}"
266
+ server
267
+ end
268
+
269
+ def start_server(action_handler, machine_spec, server)
270
+ # If it is stopping, wait for it to get out of "stopping" transition state before starting
271
+ if server.state == 'stopping'
272
+ action_handler.report_progress "wait for #{machine_spec.name} (#{server.id} on #{driver_url}) to finish stopping ..."
273
+ server.wait_for { server.state != 'stopping' }
274
+ action_handler.report_progress "#{machine_spec.name} is now stopped"
275
+ end
276
+
277
+ if server.state == 'stopped'
278
+ action_handler.perform_action "start machine #{machine_spec.name} (#{server.id} on #{driver_url})" do
279
+ server.start
280
+ machine_spec.location['started_at'] = Time.now.utc.to_s
281
+ end
282
+ end
283
+ end
284
+
285
+ def restart_server(action_handler, machine_spec, server)
286
+ action_handler.perform_action "restart machine #{machine_spec.name} (#{server.id} on #{driver_url})" do
287
+ server.reboot
288
+ machine_spec.location['started_at'] = Time.now.utc.to_s
289
+ end
290
+ end
291
+
292
+ def remaining_wait_time(machine_spec, machine_options)
293
+ if machine_spec.location['started_at']
294
+ timeout = option_for(machine_options, :start_timeout) - (Time.now.utc - Time.parse(machine_spec.location['started_at']))
295
+ else
296
+ timeout = option_for(machine_options, :create_timeout) - (Time.now.utc - Time.parse(machine_spec.location['allocated_at']))
297
+ end
298
+ end
299
+
300
+ def wait_until_ready(action_handler, machine_spec, machine_options, server)
301
+ if !server.ready?
302
+ if action_handler.should_perform_actions
303
+ action_handler.report_progress "waiting for #{machine_spec.name} (#{server.id} on #{driver_url}) to be ready ..."
304
+ server.wait_for(remaining_wait_time(machine_spec, machine_options)) { ready? }
305
+ action_handler.report_progress "#{machine_spec.name} is now ready"
306
+ end
307
+ end
308
+ end
309
+
310
+ def wait_for_transport(action_handler, machine_spec, machine_options, server)
311
+ transport = transport_for(machine_spec, server)
312
+ if !transport.available?
313
+ if action_handler.should_perform_actions
314
+ action_handler.report_progress "waiting for #{machine_spec.name} (#{server.id} on #{driver_url}) to be connectable (transport up and running) ..."
315
+
316
+ _self = self
317
+
318
+ server.wait_for(remaining_wait_time(machine_spec, machine_options)) do
319
+ transport.available?
320
+ end
321
+ action_handler.report_progress "#{machine_spec.name} is now connectable"
322
+ end
323
+ end
324
+ end
325
+
326
+ def attach_floating_ips(action_handler, machine_spec, machine_options, server)
327
+ # TODO this is not particularly idempotent. OK, it is not idempotent AT ALL. Fix.
328
+ if option_for(machine_options, :floating_ip_pool)
329
+ Chef::Log.info 'Attaching IP from pool'
330
+ action_handler.perform_action "attach floating IP from #{option_for(machine_options, :floating_ip_pool)} pool" do
331
+ attach_ip_from_pool(server, option_for(machine_options, :floating_ip_pool))
332
+ end
333
+ elsif option_for(machine_options, :floating_ip)
334
+ Chef::Log.info 'Attaching given IP'
335
+ action_handler.perform_action "attach floating IP #{option_for(machine_options, :floating_ip)}" do
336
+ attach_ip(server, option_for(machine_options, :allocation_id), option_for(machine_options, :floating_ip))
337
+ end
338
+ end
339
+ end
340
+
341
+ # Attach IP to machine from IP pool
342
+ # Code taken from kitchen-openstack driver
343
+ # https://github.com/test-kitchen/kitchen-openstack/blob/master/lib/kitchen/driver/openstack.rb#L196-L207
344
+ def attach_ip_from_pool(server, pool)
345
+ @ip_pool_lock ||= Mutex.new
346
+ @ip_pool_lock.synchronize do
347
+ Chef::Log.info "Attaching floating IP from <#{pool}> pool"
348
+ free_addrs = compute.addresses.collect do |i|
349
+ i.ip if i.fixed_ip.nil? and i.instance_id.nil? and i.pool == pool
350
+ end.compact
351
+ if free_addrs.empty?
352
+ raise ActionFailed, "No available IPs in pool <#{pool}>"
353
+ end
354
+ attach_ip(server, free_addrs[0])
355
+ end
356
+ end
357
+
358
+ # Attach given IP to machine
359
+ # Code taken from kitchen-openstack driver
360
+ # https://github.com/test-kitchen/kitchen-openstack/blob/master/lib/kitchen/driver/openstack.rb#L209-L213
361
+ def attach_ip(server, allocation_id, ip)
362
+ Chef::Log.info "Attaching floating IP <#{ip}>"
363
+ compute.associate_address(:instance_id => server.id,
364
+ :allocation_id => allocation_id,
365
+ :public_ip => ip)
366
+ end
367
+
368
+ def symbolize_keys(options)
369
+ options.inject({}) { |result,(key,value)| result[key.to_sym] = value; result }
370
+ end
371
+
372
+ def server_for(machine_spec)
373
+ if machine_spec.location
374
+ compute.servers.get(machine_spec.location['server_id'])
375
+ else
376
+ nil
377
+ end
378
+ end
379
+
380
+ @@metal_default_lock = Mutex.new
381
+
382
+ def overwrite_default_key_willy_nilly(action_handler)
383
+ driver = self
384
+ updated = @@metal_default_lock.synchronize do
385
+ ChefMetal.inline_resource(action_handler) do
386
+ fog_key_pair 'metal_default' do
387
+ driver driver
388
+ allow_overwrite true
389
+ end
390
+ end
391
+ end
392
+ if updated
393
+ # Only warn the first time
394
+ Chef::Log.warn("Using metal_default key, which is not shared between machines! It is recommended to create an AWS key pair with the fog_key_pair resource, and set :bootstrap_options => { :key_name => <key name> }")
395
+ end
396
+ 'metal_default'
397
+ end
398
+
399
+ def bootstrap_options_for(action_handler, machine_spec, machine_options)
400
+ bootstrap_options = symbolize_keys(machine_options[:bootstrap_options] || {})
401
+ if !bootstrap_options[:key_name]
402
+ bootstrap_options[:key_name] = overwrite_default_key_willy_nilly(action_handler)
403
+ end
404
+ tags = {
405
+ 'Name' => machine_spec.name,
406
+ 'BootstrapId' => machine_spec.id,
407
+ 'BootstrapHost' => Socket.gethostname,
408
+ 'BootstrapUser' => Etc.getlogin
409
+ }
410
+ # User-defined tags override the ones we set
411
+ tags.merge!(bootstrap_options[:tags]) if bootstrap_options[:tags]
412
+ bootstrap_options.merge!({ :tags => tags })
413
+
414
+ # Provide reasonable defaults for DigitalOcean
415
+ if provider == 'DigitalOcean'
416
+ if !bootstrap_options[:image_id]
417
+ bootstrap_options[:image_name] ||= 'CentOS 6.4 x32'
418
+ bootstrap_options[:image_id] = compute.images.select { |image| image.name == bootstrap_options[:image_name] }.first.id
419
+ end
420
+ if !bootstrap_options[:flavor_id]
421
+ bootstrap_options[:flavor_name] ||= '512MB'
422
+ bootstrap_options[:flavor_id] = compute.flavors.select { |flavor| flavor.name == bootstrap_options[:flavor_name] }.first.id
423
+ end
424
+ if !bootstrap_options[:region_id]
425
+ bootstrap_options[:region_name] ||= 'San Francisco 1'
426
+ bootstrap_options[:region_id] = compute.regions.select { |region| region.name == bootstrap_options[:region_name] }.first.id
427
+ end
428
+ bootstrap_options[:ssh_key_ids] ||= [ compute.ssh_keys.select { |k| k.name == bootstrap_options[:key_name] }.first.id ]
429
+
430
+ # You don't get to specify name yourself
431
+ bootstrap_options[:name] = machine_spec.name
432
+ end
433
+
434
+ bootstrap_options[:name] ||= machine_spec.name
435
+
436
+ bootstrap_options
437
+ end
438
+
439
+ def machine_for(machine_spec, machine_options, server = nil)
440
+ server ||= server_for(machine_spec)
441
+ if !server
442
+ raise "Server for node #{machine_spec.name} has not been created!"
443
+ end
444
+
445
+ if machine_spec.location['is_windows']
446
+ ChefMetal::Machine::WindowsMachine.new(machine_spec, transport_for(machine_spec, server), convergence_strategy_for(machine_spec, machine_options))
447
+ else
448
+ ChefMetal::Machine::UnixMachine.new(machine_spec, transport_for(machine_spec, server), convergence_strategy_for(machine_spec, machine_options))
449
+ end
450
+ end
451
+
452
+ def convergence_strategy_for(machine_spec, machine_options)
453
+ # Defaults
454
+ if !machine_spec.location
455
+ return ChefMetal::ConvergenceStrategy::NoConverge.new(machine_options[:convergence_options], config)
456
+ end
457
+
458
+ if machine_spec.location['is_windows']
459
+ ChefMetal::ConvergenceStrategy::InstallMsi.new(machine_options[:convergence_options], config)
460
+ else
461
+ ChefMetal::ConvergenceStrategy::InstallCached.new(machine_options[:convergence_options], config)
462
+ end
463
+ end
464
+
465
+ def ssh_options_for(machine_spec, server)
466
+ result = {
467
+ # TODO create a user known hosts file
468
+ # :user_known_hosts_file => vagrant_ssh_config['UserKnownHostsFile'],
469
+ # :paranoid => true,
470
+ :auth_methods => [ 'publickey' ],
471
+ :keys_only => true,
472
+ :host_key_alias => "#{server.id}.#{provider}"
473
+ }
474
+ if server.respond_to?(:private_key) && server.private_key
475
+ result[:key_data] = [ server.private_key ]
476
+ elsif server.respond_to?(:key_name)
477
+ result[:key_data] = [ get_private_key(server.key_name) ]
478
+ elsif machine_spec.location['key_name']
479
+ result[:key_data] = [ get_private_key(machine_spec.location['key_name']) ]
480
+ else
481
+ # TODO make a way to suggest other keys to try ...
482
+ raise "No key found to connect to #{machine_spec.name}!"
483
+ end
484
+ result
485
+ end
486
+
487
+ def create_ssh_transport(machine_spec, server)
488
+ ssh_options = ssh_options_for(machine_spec, server)
489
+ # If we're on AWS, the default is to use ubuntu, not root
490
+ if provider == 'AWS'
491
+ username = machine_spec.location[:ssh_username] || 'ubuntu'
492
+ else
493
+ username = machine_spec.location[:ssh_username] || 'root'
494
+ end
495
+ options = {}
496
+ if machine_spec.location[:sudo] || (!machine_spec.location.has_key?(:sudo) && username != 'root')
497
+ options[:prefix] = 'sudo '
498
+ end
499
+
500
+ remote_host = nil
501
+ if machine_spec.location[:use_private_ip_for_ssh]
502
+ remote_host = server.private_ip_address
503
+ elsif !server.public_ip_address
504
+ Chef::Log.warn("Server 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 ...")
505
+ remote_host = server.private_ip_address
506
+ elsif server.public_ip_address
507
+ remote_host = server.public_ip_address
508
+ else
509
+ raise "Server #{server.id} has no private or public IP address!"
510
+ end
511
+
512
+ #Enable pty by default
513
+ options[:ssh_pty_enable] = true
514
+ options[:ssh_gateway] = machine_spec.location[:ssh_gateway] if machine_spec.location.has_key?(:ssh_gateway)
515
+
516
+ ChefMetal::Transport::SSH.new(remote_host, username, ssh_options, options, config)
517
+ end
518
+
519
+ def self.compute_options_for(provider, id, config)
520
+ driver_options = config[:driver_options] || {}
521
+ compute_options = driver_options[:compute_options] || {}
522
+ new_compute_options = {}
523
+ new_compute_options[:provider] = provider
524
+ new_config = { :driver_options => { :compute_options => new_compute_options }}
525
+
526
+ # Set the identifier from the URL
527
+ if id && id != ''
528
+ case provider
529
+ when 'AWS'
530
+ if id !~ /^\d{12}$/
531
+ # Assume it is a profile name, and set that.
532
+ driver_options[:aws_profile] = id
533
+ id = nil
534
+ end
535
+ when 'DigitalOcean'
536
+ new_compute_options[:digitalocean_client_id] = id
537
+ when 'OpenStack'
538
+ new_compute_options[:openstack_auth_url] = id
539
+ else
540
+ raise "unsupported fog provider #{provider}"
541
+ end
542
+ elsif provider == 'AWS'
543
+ id = 'default'
544
+ end
545
+
546
+ # Set auth info from environment
547
+ case provider
548
+ when 'AWS'
549
+ # Grab the profile
550
+ aws_profile = FogDriverAWS.get_aws_profile(driver_options, compute_options, id)
551
+ [ :aws_access_key_id, :aws_secret_access_key, :aws_session_token ].each do |key|
552
+ new_compute_options[key] = aws_profile[key] if aws_profile[key]
553
+ end
554
+ when 'OpenStack'
555
+ # TODO it is supposed to be unnecessary to load credentials from fog this way;
556
+ # why are we doing it?
557
+ # TODO support http://docs.openstack.org/cli-reference/content/cli_openrc.html
558
+ credential = Fog.credential
559
+
560
+ new_compute_options[:openstack_username] ||= credential[:openstack_username]
561
+ new_compute_options[:openstack_api_key] ||= credential[:openstack_api_key]
562
+ new_compute_options[:openstack_auth_url] ||= credential[:openstack_auth_url]
563
+ new_compute_options[:openstack_tenant] ||= credential[:openstack_tenant]
564
+ end
565
+
566
+ config = Cheffish::MergedConfig.new(new_config, config)
567
+
568
+ id = case provider
569
+ when 'AWS'
570
+ account_info = FogDriverAWS.aws_account_info_for(config[:driver_options][:compute_options])
571
+ new_config[:driver_options][:aws_account_info] = account_info
572
+ account_info[:aws_account_id]
573
+ when 'DigitalOcean'
574
+ compute_options[:digitalocean_client_id]
575
+ when 'OpenStack'
576
+ compute_options[:openstack_auth_url]
577
+ end
578
+
579
+ [ config, id ]
580
+ end
581
+
582
+ def get_default_private_key
583
+ if config[:private_keys] && config[:private_keys].size > 0
584
+ get_private_key(config[:private_keys].keys.first)
585
+ else
586
+ config[:private_key_paths].each do |private_key_path|
587
+ Dir.entries(private_key_path).each do |key|
588
+ ext = File.extname(key)
589
+ if ext == '' || ext == '.pem'
590
+ key_name = key[0..-(ext.length+1)]
591
+ if key_name == name
592
+ return IO.read("#{private_key_path}/#{key}")
593
+ end
594
+ end
595
+ end
596
+ end
597
+ end
598
+ end
599
+
600
+ def get_private_key(name)
601
+ if config[:private_keys] && config[:private_keys][name]
602
+ if config[:private_keys][name].is_a?(String)
603
+ IO.read(config[:private_keys][name])
604
+ else
605
+ config[:private_keys][name].to_pem
606
+ end
607
+ elsif config[:private_key_paths]
608
+ config[:private_key_paths].each do |private_key_path|
609
+ Dir.entries(private_key_path).each do |key|
610
+ ext = File.extname(key)
611
+ if ext == '' || ext == '.pem'
612
+ key_name = key[0..-(ext.length+1)]
613
+ if key_name == name
614
+ return IO.read("#{private_key_path}/#{key}")
615
+ end
616
+ end
617
+ end
618
+ end
619
+ end
620
+ end
621
+ end
622
+ end