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