chef-provisioning-fog 0.14.0 → 0.15.0

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