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