chef-provisioning-fog 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +201 -201
  3. data/README.md +3 -3
  4. data/Rakefile +6 -6
  5. data/lib/chef/provider/fog_key_pair.rb +266 -266
  6. data/lib/chef/provisioning/driver_init/fog.rb +3 -3
  7. data/lib/chef/provisioning/fog_driver/driver.rb +736 -709
  8. data/lib/chef/provisioning/fog_driver/providers/aws.rb +492 -492
  9. data/lib/chef/provisioning/fog_driver/providers/aws/credentials.rb +115 -115
  10. data/lib/chef/provisioning/fog_driver/providers/cloudstack.rb +44 -44
  11. data/lib/chef/provisioning/fog_driver/providers/digitalocean.rb +136 -136
  12. data/lib/chef/provisioning/fog_driver/providers/google.rb +85 -84
  13. data/lib/chef/provisioning/fog_driver/providers/joyent.rb +63 -59
  14. data/lib/chef/provisioning/fog_driver/providers/openstack.rb +117 -41
  15. data/lib/chef/provisioning/fog_driver/providers/rackspace.rb +42 -42
  16. data/lib/chef/provisioning/fog_driver/providers/softlayer.rb +36 -36
  17. data/lib/chef/provisioning/fog_driver/providers/vcair.rb +409 -376
  18. data/lib/chef/provisioning/fog_driver/providers/xenserver.rb +210 -0
  19. data/lib/chef/provisioning/fog_driver/recipe_dsl.rb +32 -32
  20. data/lib/chef/provisioning/fog_driver/version.rb +7 -7
  21. data/lib/chef/resource/fog_key_pair.rb +34 -34
  22. data/spec/spec_helper.rb +18 -18
  23. data/spec/support/aws/config-file.csv +2 -2
  24. data/spec/support/aws/ini-file.ini +10 -10
  25. data/spec/support/chef_metal_fog/providers/testdriver.rb +16 -16
  26. data/spec/unit/chef/provisioning/fog_driver/driver_spec.rb +71 -0
  27. data/spec/unit/fog_driver_spec.rb +32 -32
  28. data/spec/unit/providers/aws/credentials_spec.rb +45 -45
  29. data/spec/unit/providers/rackspace_spec.rb +16 -16
  30. metadata +5 -3
@@ -1,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