chef-provisioning-fog 0.10

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