chef-provisioning-fog 0.15.0 → 0.15.1

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