chef-provisioning-fog 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +201 -201
- data/README.md +3 -3
- data/Rakefile +6 -6
- data/lib/chef/provider/fog_key_pair.rb +266 -266
- data/lib/chef/provisioning/driver_init/fog.rb +3 -3
- data/lib/chef/provisioning/fog_driver/driver.rb +736 -709
- data/lib/chef/provisioning/fog_driver/providers/aws.rb +492 -492
- data/lib/chef/provisioning/fog_driver/providers/aws/credentials.rb +115 -115
- data/lib/chef/provisioning/fog_driver/providers/cloudstack.rb +44 -44
- data/lib/chef/provisioning/fog_driver/providers/digitalocean.rb +136 -136
- data/lib/chef/provisioning/fog_driver/providers/google.rb +85 -84
- data/lib/chef/provisioning/fog_driver/providers/joyent.rb +63 -59
- data/lib/chef/provisioning/fog_driver/providers/openstack.rb +117 -41
- data/lib/chef/provisioning/fog_driver/providers/rackspace.rb +42 -42
- data/lib/chef/provisioning/fog_driver/providers/softlayer.rb +36 -36
- data/lib/chef/provisioning/fog_driver/providers/vcair.rb +409 -376
- data/lib/chef/provisioning/fog_driver/providers/xenserver.rb +210 -0
- data/lib/chef/provisioning/fog_driver/recipe_dsl.rb +32 -32
- data/lib/chef/provisioning/fog_driver/version.rb +7 -7
- data/lib/chef/resource/fog_key_pair.rb +34 -34
- data/spec/spec_helper.rb +18 -18
- data/spec/support/aws/config-file.csv +2 -2
- data/spec/support/aws/ini-file.ini +10 -10
- data/spec/support/chef_metal_fog/providers/testdriver.rb +16 -16
- data/spec/unit/chef/provisioning/fog_driver/driver_spec.rb +71 -0
- data/spec/unit/fog_driver_spec.rb +32 -32
- data/spec/unit/providers/aws/credentials_spec.rb +45 -45
- data/spec/unit/providers/rackspace_spec.rb +16 -16
- metadata +5 -3
@@ -1,492 +1,492 @@
|
|
1
|
-
require 'chef/log'
|
2
|
-
require 'fog/aws'
|
3
|
-
require 'uri'
|
4
|
-
require 'base64'
|
5
|
-
require 'openssl'
|
6
|
-
require 'pathname'
|
7
|
-
require 'chef/provisioning/transport/winrm'
|
8
|
-
|
9
|
-
# fog:AWS:<account_id>:<region>
|
10
|
-
# fog:AWS:<profile_name>
|
11
|
-
# fog:AWS:<profile_name>:<region>
|
12
|
-
class Chef
|
13
|
-
module Provisioning
|
14
|
-
module FogDriver
|
15
|
-
module Providers
|
16
|
-
class AWS < FogDriver::Driver
|
17
|
-
|
18
|
-
require_relative 'aws/credentials'
|
19
|
-
|
20
|
-
Driver.register_provider_class('AWS', FogDriver::Providers::AWS)
|
21
|
-
|
22
|
-
def creator
|
23
|
-
driver_options[:aws_account_info][:aws_username]
|
24
|
-
end
|
25
|
-
|
26
|
-
def default_ssh_username
|
27
|
-
'ubuntu'
|
28
|
-
end
|
29
|
-
|
30
|
-
# Create a WinRM transport for an AWS instance
|
31
|
-
# @param [Hash] machine_spec Machine-spec hash
|
32
|
-
# @param [Hash] machine_options Machine options (from the recipe)
|
33
|
-
# @param [Fog::Compute::Server] server A Fog mapping to the AWS instance
|
34
|
-
# @return [ChefMetal::Transport::WinRM] A WinRM Transport object to talk to the server
|
35
|
-
def create_winrm_transport(machine_spec, machine_options, server)
|
36
|
-
remote_host = if machine_spec.location['use_private_ip_for_ssh']
|
37
|
-
server.private_ip_address
|
38
|
-
elsif !server.public_ip_address
|
39
|
-
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 ...")
|
40
|
-
server.private_ip_address
|
41
|
-
elsif server.public_ip_address
|
42
|
-
server.public_ip_address
|
43
|
-
else
|
44
|
-
fail "Server #{server.id} has no private or public IP address!"
|
45
|
-
end
|
46
|
-
|
47
|
-
port = machine_spec.location['winrm_port'] || 5985
|
48
|
-
endpoint = "http://#{remote_host}:#{port}/wsman"
|
49
|
-
type = :plaintext
|
50
|
-
pem_bytes = private_key_for(machine_spec, machine_options, server)
|
51
|
-
encrypted_admin_password = wait_for_admin_password(machine_spec)
|
52
|
-
decoded = Base64.decode64(encrypted_admin_password)
|
53
|
-
private_key = OpenSSL::PKey::RSA.new(pem_bytes)
|
54
|
-
decrypted_password = private_key.private_decrypt decoded
|
55
|
-
|
56
|
-
# Use basic HTTP auth - this is required for the WinRM setup we
|
57
|
-
# are using
|
58
|
-
# TODO: Improve that.
|
59
|
-
options = {
|
60
|
-
:user => machine_spec.location['winrm.username'] || 'Administrator',
|
61
|
-
:pass => decrypted_password,
|
62
|
-
:disable_sspi => true,
|
63
|
-
:basic_auth_only => true
|
64
|
-
}
|
65
|
-
|
66
|
-
Chef::Provisioning::Transport::WinRM.new(endpoint, type, options, {})
|
67
|
-
end
|
68
|
-
|
69
|
-
def allocate_image(action_handler, image_spec, image_options, machine_spec)
|
70
|
-
if image_spec.location
|
71
|
-
image = compute.images.get(image_spec.location['image_id'])
|
72
|
-
if image
|
73
|
-
raise "The image already exists, why are you asking me to create it? I can't do that, Dave."
|
74
|
-
end
|
75
|
-
end
|
76
|
-
action_handler.perform_action "Create image #{image_spec.name} from machine #{machine_spec.name} with options #{image_options.inspect}" do
|
77
|
-
opt = image_options.dup
|
78
|
-
response = compute.create_image(machine_spec.location['server_id'],
|
79
|
-
image_spec.name,
|
80
|
-
opt.delete(:description) || "The image formerly and currently named '#{image_spec.name}'",
|
81
|
-
opt.delete(:no_reboot) || false,
|
82
|
-
opt)
|
83
|
-
image_spec.location = {
|
84
|
-
'driver_url' => driver_url,
|
85
|
-
'driver_version' => FogDriver::VERSION,
|
86
|
-
'image_id' => response.body['imageId'],
|
87
|
-
'creator' => creator,
|
88
|
-
'allocated_at' => Time.now.to_i
|
89
|
-
}
|
90
|
-
|
91
|
-
image_spec.machine_options ||= {}
|
92
|
-
image_spec.machine_options.merge!({
|
93
|
-
:bootstrap_options => {
|
94
|
-
:image_id => image_spec.location['image_id']
|
95
|
-
}
|
96
|
-
})
|
97
|
-
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
def ready_image(action_handler, image_spec, image_options)
|
102
|
-
if !image_spec.location
|
103
|
-
raise "Cannot ready an image that does not exist"
|
104
|
-
end
|
105
|
-
image = compute.images.get(image_spec.location['image_id'])
|
106
|
-
if !image.ready?
|
107
|
-
action_handler.report_progress "Waiting for image to be ready ..."
|
108
|
-
# TODO timeout
|
109
|
-
image.wait_for { ready? }
|
110
|
-
action_handler.report_progress "Image is ready!"
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
def destroy_image(action_handler, image_spec, image_options)
|
115
|
-
if !image_spec.location
|
116
|
-
return
|
117
|
-
end
|
118
|
-
image = compute.images.get(image_spec.location['image_id'])
|
119
|
-
if !image
|
120
|
-
return
|
121
|
-
end
|
122
|
-
delete_snapshots = image_options[:delete_snapshots]
|
123
|
-
delete_snapshots = true if delete_snapshots.nil?
|
124
|
-
image.deregister(delete_snapshots)
|
125
|
-
end
|
126
|
-
|
127
|
-
def bootstrap_options_for(action_handler, machine_spec, machine_options)
|
128
|
-
bootstrap_options = symbolize_keys(machine_options[:bootstrap_options] || {})
|
129
|
-
|
130
|
-
if !bootstrap_options[:key_name]
|
131
|
-
bootstrap_options[:key_name] = overwrite_default_key_willy_nilly(action_handler, machine_spec)
|
132
|
-
end
|
133
|
-
|
134
|
-
if machine_options[:is_windows]
|
135
|
-
Chef::Log.debug('Attaching WinRM data for user data.')
|
136
|
-
# Enable WinRM basic auth, HTTP and open the firewall
|
137
|
-
bootstrap_options[:user_data] = user_data
|
138
|
-
end
|
139
|
-
bootstrap_options.delete(:tags) # we handle these separately for performance reasons
|
140
|
-
bootstrap_options
|
141
|
-
end
|
142
|
-
|
143
|
-
def create_servers(action_handler, specs_and_options, parallelizer)
|
144
|
-
super(action_handler, specs_and_options, parallelizer) do |machine_spec, server|
|
145
|
-
yield machine_spec, server if block_given?
|
146
|
-
|
147
|
-
machine_options = specs_and_options[machine_spec]
|
148
|
-
bootstrap_options = symbolize_keys(machine_options[:bootstrap_options] || {})
|
149
|
-
tags = default_tags(machine_spec, bootstrap_options[:tags] || {})
|
150
|
-
|
151
|
-
# Right now, not doing that in case manual tagging is going on
|
152
|
-
server_tags = server.tags || {}
|
153
|
-
extra_tags = tags.keys.select { |tag_name| !server_tags.has_key?(tag_name) }.to_a
|
154
|
-
different_tags = server_tags.select { |tag_name, tag_value| tags.has_key?(tag_name) && tags[tag_name] != tag_value }.to_a
|
155
|
-
if extra_tags.size > 0 || different_tags.size > 0
|
156
|
-
tags_description = [ "Update tags for #{machine_spec.name} on #{driver_url}" ]
|
157
|
-
tags_description += extra_tags.map { |tag| " Add #{tag} = #{tags[tag].inspect}" }
|
158
|
-
tags_description += different_tags.map { |tag_name, tag_value| " Update #{tag_name} from #{tag_value.inspect} to #{tags[tag_name].inspect}"}
|
159
|
-
action_handler.perform_action tags_description do
|
160
|
-
# TODO should we narrow this down to just extra/different tags or
|
161
|
-
# is it OK to just pass 'em all? Certainly easier to do the
|
162
|
-
# latter, and I can't think of a consequence for doing so offhand.
|
163
|
-
compute.create_tags(server.identity, tags)
|
164
|
-
end
|
165
|
-
end
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
def convergence_strategy_for(machine_spec, machine_options)
|
170
|
-
machine_options = Cheffish::MergedConfig.new(machine_options, {
|
171
|
-
:convergence_options => {:ohai_hints => {'ec2' => ''}}
|
172
|
-
})
|
173
|
-
super(machine_spec, machine_options)
|
174
|
-
end
|
175
|
-
|
176
|
-
# Attach given IP to machine
|
177
|
-
def attach_ip(server, ip)
|
178
|
-
Chef::Log.info "Attaching floating IP <#{ip}>"
|
179
|
-
compute.associate_address(:instance_id => server.id,
|
180
|
-
:allocation_id => option_for(machine_options, :allocation_id),
|
181
|
-
:public_ip => ip)
|
182
|
-
end
|
183
|
-
|
184
|
-
def self.get_aws_profile(driver_options, aws_account_id)
|
185
|
-
aws_credentials = get_aws_credentials(driver_options)
|
186
|
-
compute_options = driver_options[:compute_options] || {}
|
187
|
-
|
188
|
-
# Order of operations:
|
189
|
-
# compute_options[:aws_access_key_id] / compute_options[:aws_secret_access_key] / compute_options[:aws_security_token] / compute_options[:region]
|
190
|
-
# compute_options[:aws_profile]
|
191
|
-
# ENV['AWS_ACCESS_KEY_ID'] / ENV['AWS_SECRET_ACCESS_KEY'] / ENV['AWS_SECURITY_TOKEN'] / ENV['AWS_DEFAULT_REGION']
|
192
|
-
# ENV['AWS_PROFILE']
|
193
|
-
# ENV['DEFAULT_PROFILE']
|
194
|
-
# 'default'
|
195
|
-
if compute_options[:aws_access_key_id]
|
196
|
-
Chef::Log.debug("Using AWS driver access key options")
|
197
|
-
aws_profile = {
|
198
|
-
:aws_access_key_id => compute_options[:aws_access_key_id],
|
199
|
-
:aws_secret_access_key => compute_options[:aws_secret_access_key],
|
200
|
-
:aws_security_token => compute_options[:aws_session_token],
|
201
|
-
:region => compute_options[:region]
|
202
|
-
}
|
203
|
-
elsif driver_options[:aws_profile]
|
204
|
-
Chef::Log.debug("Using AWS profile #{driver_options[:aws_profile]}")
|
205
|
-
aws_profile = aws_credentials[driver_options[:aws_profile]]
|
206
|
-
elsif ENV['AWS_ACCESS_KEY_ID'] || ENV['AWS_ACCESS_KEY']
|
207
|
-
Chef::Log.debug("Using AWS environment variable access keys")
|
208
|
-
aws_profile = {
|
209
|
-
:aws_access_key_id => ENV['AWS_ACCESS_KEY_ID'] || ENV['AWS_ACCESS_KEY'],
|
210
|
-
:aws_secret_access_key => ENV['AWS_SECRET_ACCESS_KEY'] || ENV['AWS_SECRET_KEY'],
|
211
|
-
:aws_security_token => ENV['AWS_SECURITY_TOKEN'],
|
212
|
-
:region => ENV['AWS_DEFAULT_REGION'] || ENV['AWS_REGION'] || ENV['EC2_REGION']
|
213
|
-
}
|
214
|
-
elsif ENV['AWS_PROFILE']
|
215
|
-
Chef::Log.debug("Using AWS profile #{ENV['AWS_PROFILE']} from AWS_PROFILE environment variable")
|
216
|
-
aws_profile = aws_credentials[ENV['AWS_PROFILE']]
|
217
|
-
if !aws_profile
|
218
|
-
raise "Environment variable AWS_PROFILE is set to #{ENV['AWS_PROFILE'].inspect} but your AWS config file does not contain that profile!"
|
219
|
-
end
|
220
|
-
else
|
221
|
-
Chef::Log.debug("Using AWS default profile")
|
222
|
-
aws_profile = aws_credentials.default
|
223
|
-
end
|
224
|
-
|
225
|
-
default_ec2_endpoint = compute_options[:ec2_endpoint] || ENV['EC2_URL']
|
226
|
-
default_iam_endpoint = compute_options[:iam_endpoint] || ENV['AWS_IAM_URL']
|
227
|
-
|
228
|
-
# Merge in account info for profile
|
229
|
-
if aws_profile
|
230
|
-
aws_profile = aws_profile.merge(aws_account_info_for(aws_profile, default_iam_endpoint))
|
231
|
-
end
|
232
|
-
|
233
|
-
# If no profile is found (or the profile is not the right account), search
|
234
|
-
# for a profile that matches the given account ID
|
235
|
-
if aws_account_id && (!aws_profile || aws_profile[:aws_account_id] != aws_account_id)
|
236
|
-
aws_profile = find_aws_profile_for_account_id(aws_credentials, aws_account_id, default_iam_endpoint)
|
237
|
-
end
|
238
|
-
|
239
|
-
fail 'No AWS profile specified! Are you missing something in the Chef or AWS config?' unless aws_profile
|
240
|
-
|
241
|
-
aws_profile[:ec2_endpoint] ||= default_ec2_endpoint
|
242
|
-
aws_profile[:iam_endpoint] ||= default_iam_endpoint
|
243
|
-
|
244
|
-
aws_profile.delete_if { |key, value| value.nil? }
|
245
|
-
aws_profile
|
246
|
-
end
|
247
|
-
|
248
|
-
def self.find_aws_profile_for_account_id(aws_credentials, aws_account_id, default_iam_endpoint=nil)
|
249
|
-
aws_profile = nil
|
250
|
-
aws_credentials.each do |profile_name, profile|
|
251
|
-
begin
|
252
|
-
aws_account_info = aws_account_info_for(profile, default_iam_endpoint)
|
253
|
-
rescue
|
254
|
-
Chef::Log.warn("Could not connect to AWS profile #{aws_credentials[:name]}: #{$!}")
|
255
|
-
Chef::Log.debug($!.backtrace.join("\n"))
|
256
|
-
next
|
257
|
-
end
|
258
|
-
if aws_account_info[:aws_account_id] == aws_account_id
|
259
|
-
aws_profile = profile
|
260
|
-
aws_profile[:name] = profile_name
|
261
|
-
aws_profile = aws_profile.merge(aws_account_info)
|
262
|
-
break
|
263
|
-
end
|
264
|
-
end
|
265
|
-
if aws_profile
|
266
|
-
Chef::Log.info("Discovered AWS profile #{aws_profile[:name]} pointing at account #{aws_account_id}. Using ...")
|
267
|
-
else
|
268
|
-
raise "No AWS profile leads to account #{aws_account_id}. Do you need to add profiles to the AWS config?"
|
269
|
-
end
|
270
|
-
aws_profile
|
271
|
-
end
|
272
|
-
|
273
|
-
def self.aws_account_info_for(aws_profile, default_iam_endpoint = nil)
|
274
|
-
iam_endpoint = aws_profile[:iam_endpoint] || default_iam_endpoint
|
275
|
-
|
276
|
-
@@aws_account_info ||= {}
|
277
|
-
@@aws_account_info[aws_profile[:aws_access_key_id]] ||= begin
|
278
|
-
options = {
|
279
|
-
# Endpoint configuration
|
280
|
-
:aws_access_key_id => aws_profile[:aws_access_key_id],
|
281
|
-
:aws_secret_access_key => aws_profile[:aws_secret_access_key],
|
282
|
-
:aws_session_token => aws_profile[:aws_security_token]
|
283
|
-
}
|
284
|
-
if iam_endpoint
|
285
|
-
options[:host] = URI(iam_endpoint).host
|
286
|
-
options[:scheme] = URI(iam_endpoint).scheme
|
287
|
-
options[:port] = URI(iam_endpoint).port
|
288
|
-
options[:path] = URI(iam_endpoint).path
|
289
|
-
end
|
290
|
-
options.delete_if { |key, value| value.nil? }
|
291
|
-
|
292
|
-
iam = Fog::AWS::IAM.new(options)
|
293
|
-
arn = begin
|
294
|
-
# TODO it would be nice if Fog let you do this normally ...
|
295
|
-
iam.send(:request, {
|
296
|
-
'Action' => 'GetUser',
|
297
|
-
:parser => Fog::Parsers::AWS::IAM::GetUser.new
|
298
|
-
}).body['User']['Arn']
|
299
|
-
rescue Fog::AWS::IAM::Error
|
300
|
-
# TODO Someone tell me there is a better way to find out your current
|
301
|
-
# user ID than this! This is what happens when you use an IAM user
|
302
|
-
# with default privileges.
|
303
|
-
if $!.message =~ /AccessDenied.+(arn:aws:iam::\d+:\S+)/
|
304
|
-
arn = $1
|
305
|
-
else
|
306
|
-
raise
|
307
|
-
end
|
308
|
-
end
|
309
|
-
arn_split = arn.split(':', 6)
|
310
|
-
{
|
311
|
-
:aws_account_id => arn_split[4],
|
312
|
-
:aws_username => arn_split[5],
|
313
|
-
:aws_user_arn => arn
|
314
|
-
}
|
315
|
-
end
|
316
|
-
end
|
317
|
-
|
318
|
-
def self.get_aws_credentials(driver_options)
|
319
|
-
# Grab the list of possible credentials
|
320
|
-
if driver_options[:aws_credentials]
|
321
|
-
aws_credentials = driver_options[:aws_credentials]
|
322
|
-
else
|
323
|
-
aws_credentials = Credentials.new
|
324
|
-
if driver_options[:aws_config_file]
|
325
|
-
aws_credentials.load_ini(driver_options[:aws_config_file])
|
326
|
-
elsif driver_options[:aws_csv_file]
|
327
|
-
aws_credentials.load_csv(driver_options[:aws_csv_file])
|
328
|
-
else
|
329
|
-
aws_credentials.load_default
|
330
|
-
end
|
331
|
-
end
|
332
|
-
aws_credentials
|
333
|
-
end
|
334
|
-
|
335
|
-
def self.compute_options_for(provider, id, config)
|
336
|
-
new_compute_options = {}
|
337
|
-
new_compute_options[:provider] = provider
|
338
|
-
new_config = { :driver_options => { :compute_options => new_compute_options }}
|
339
|
-
new_defaults = {
|
340
|
-
:driver_options => { :compute_options => {} },
|
341
|
-
:machine_options => { :bootstrap_options => {} }
|
342
|
-
}
|
343
|
-
result = Cheffish::MergedConfig.new(new_config, config, new_defaults)
|
344
|
-
|
345
|
-
if id && id != ''
|
346
|
-
# AWS canonical URLs are of the form fog:AWS:
|
347
|
-
if id =~ /^(\d{12}|IAM)(:(.+))?$/
|
348
|
-
if $2
|
349
|
-
id = $1
|
350
|
-
new_compute_options[:region] = $3
|
351
|
-
else
|
352
|
-
Chef::Log.warn("Old-style AWS URL #{id} from an early beta of chef-provisioning (before chef-metal 0.11-final) found. If you have servers in multiple regions on this account, you may see odd behavior like servers being recreated. To fix, edit any nodes with attribute chef_provisioning.location.driver_url to include the region like so: fog:AWS:#{id}:<region> (e.g. us-east-1)")
|
353
|
-
end
|
354
|
-
else
|
355
|
-
# Assume it is a profile name, and set that.
|
356
|
-
aws_profile, region = id.split(':', 2)
|
357
|
-
new_config[:driver_options][:aws_profile] = aws_profile
|
358
|
-
new_compute_options[:region] = region
|
359
|
-
id = nil
|
360
|
-
end
|
361
|
-
end
|
362
|
-
if id == 'IAM'
|
363
|
-
id = "IAM:#{result[:driver_options][:compute_options][:region]}"
|
364
|
-
new_config[:driver_options][:aws_account_info] = { aws_username: 'IAM' }
|
365
|
-
new_compute_options[:use_iam_profile] = true
|
366
|
-
else
|
367
|
-
aws_profile = get_aws_profile(result[:driver_options], id)
|
368
|
-
new_compute_options[:aws_access_key_id] = aws_profile[:aws_access_key_id]
|
369
|
-
new_compute_options[:aws_secret_access_key] = aws_profile[:aws_secret_access_key]
|
370
|
-
new_compute_options[:aws_session_token] = aws_profile[:aws_security_token]
|
371
|
-
new_defaults[:driver_options][:compute_options][:region] = aws_profile[:region]
|
372
|
-
new_defaults[:driver_options][:compute_options][:endpoint] = aws_profile[:ec2_endpoint]
|
373
|
-
|
374
|
-
account_info = aws_account_info_for(result[:driver_options][:compute_options])
|
375
|
-
new_config[:driver_options][:aws_account_info] = account_info
|
376
|
-
id = "#{account_info[:aws_account_id]}:#{result[:driver_options][:compute_options][:region]}"
|
377
|
-
end
|
378
|
-
|
379
|
-
# Make sure we're using a reasonable default AMI, for now this is Ubuntu 14.04 LTS
|
380
|
-
result[:machine_options][:bootstrap_options][:image_id] ||=
|
381
|
-
default_ami_for_region(result[:driver_options][:compute_options][:region])
|
382
|
-
|
383
|
-
[result, id]
|
384
|
-
end
|
385
|
-
|
386
|
-
def create_many_servers(num_servers, bootstrap_options, parallelizer)
|
387
|
-
# Create all the servers in one request if we have a version of Fog that can do that
|
388
|
-
if compute.servers.respond_to?(:create_many)
|
389
|
-
servers = compute.servers.create_many(num_servers, num_servers, bootstrap_options)
|
390
|
-
if block_given?
|
391
|
-
parallelizer.parallelize(servers) do |server|
|
392
|
-
yield server
|
393
|
-
end.to_a
|
394
|
-
end
|
395
|
-
servers
|
396
|
-
else
|
397
|
-
super
|
398
|
-
end
|
399
|
-
end
|
400
|
-
|
401
|
-
def servers_for(machine_specs)
|
402
|
-
# Grab all the servers in one request
|
403
|
-
instance_ids = machine_specs.map { |machine_spec| (machine_spec.location || {})['server_id'] }.select { |id| !id.nil? }
|
404
|
-
servers = compute.servers.all('instance-id' => instance_ids)
|
405
|
-
result = {}
|
406
|
-
machine_specs.each do |machine_spec|
|
407
|
-
if machine_spec.location
|
408
|
-
result[machine_spec] = servers.select { |s| s.id == machine_spec.location['server_id'] }.first
|
409
|
-
else
|
410
|
-
result[machine_spec] = nil
|
411
|
-
end
|
412
|
-
end
|
413
|
-
result
|
414
|
-
end
|
415
|
-
|
416
|
-
private
|
417
|
-
def user_data
|
418
|
-
# TODO: Make this use HTTPS at some point.
|
419
|
-
<<EOD
|
420
|
-
<powershell>
|
421
|
-
winrm quickconfig -q
|
422
|
-
winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="300"}'
|
423
|
-
winrm set winrm/config '@{MaxTimeoutms="1800000"}'
|
424
|
-
winrm set winrm/config/service '@{AllowUnencrypted="true"}'
|
425
|
-
winrm set winrm/config/service/auth '@{Basic="true"}'
|
426
|
-
|
427
|
-
netsh advfirewall firewall add rule name="WinRM 5985" protocol=TCP dir=in localport=5985 action=allow
|
428
|
-
netsh advfirewall firewall add rule name="WinRM 5986" protocol=TCP dir=in localport=5986 action=allow
|
429
|
-
|
430
|
-
net stop winrm
|
431
|
-
sc config winrm start=auto
|
432
|
-
net start winrm
|
433
|
-
</powershell>
|
434
|
-
|
435
|
-
EOD
|
436
|
-
end
|
437
|
-
|
438
|
-
def self.default_ami_for_region(region)
|
439
|
-
Chef::Log.debug("Choosing default AMI for region '#{region}'")
|
440
|
-
|
441
|
-
case region
|
442
|
-
when 'ap-northeast-1'
|
443
|
-
'ami-c786dcc6'
|
444
|
-
when 'ap-southeast-1'
|
445
|
-
'ami-eefca7bc'
|
446
|
-
when 'ap-southeast-2'
|
447
|
-
'ami-996706a3'
|
448
|
-
when 'eu-west-1'
|
449
|
-
'ami-4ab46b3d'
|
450
|
-
when 'sa-east-1'
|
451
|
-
'ami-6770d87a'
|
452
|
-
when 'us-east-1'
|
453
|
-
'ami-d2ff23ba'
|
454
|
-
when 'us-west-1'
|
455
|
-
'ami-73717d36'
|
456
|
-
when 'us-west-2'
|
457
|
-
'ami-f1ce8bc1'
|
458
|
-
end
|
459
|
-
end
|
460
|
-
|
461
|
-
# Wait for the Windows Admin password to become available
|
462
|
-
# @param [Hash] machine_spec Machine spec data
|
463
|
-
# @return [String] encrypted admin password
|
464
|
-
def wait_for_admin_password(machine_spec)
|
465
|
-
time_elapsed = 0
|
466
|
-
sleep_time = 10
|
467
|
-
max_wait_time = 900 # 15 minutes
|
468
|
-
encrypted_admin_password = nil
|
469
|
-
instance_id = machine_spec.location['server_id']
|
470
|
-
|
471
|
-
|
472
|
-
Chef::Log.info "waiting for #{machine_spec.name}'s admin password to be available..."
|
473
|
-
while time_elapsed < max_wait_time && encrypted_admin_password.nil?
|
474
|
-
response = compute.get_password_data(instance_id)
|
475
|
-
encrypted_admin_password = response.body['passwordData']
|
476
|
-
if encrypted_admin_password.nil?
|
477
|
-
Chef::Log.info "#{time_elapsed}/#{max_wait_time}s elapsed -- sleeping #{sleep_time} seconds for #{machine_spec.name}'s admin password."
|
478
|
-
sleep(sleep_time)
|
479
|
-
time_elapsed += sleep_time
|
480
|
-
end
|
481
|
-
end
|
482
|
-
|
483
|
-
Chef::Log.info "#{machine_spec.name}'s admin password is available!'"
|
484
|
-
|
485
|
-
encrypted_admin_password
|
486
|
-
end
|
487
|
-
|
488
|
-
end
|
489
|
-
end
|
490
|
-
end
|
491
|
-
end
|
492
|
-
end
|
1
|
+
require 'chef/log'
|
2
|
+
require 'fog/aws'
|
3
|
+
require 'uri'
|
4
|
+
require 'base64'
|
5
|
+
require 'openssl'
|
6
|
+
require 'pathname'
|
7
|
+
require 'chef/provisioning/transport/winrm'
|
8
|
+
|
9
|
+
# fog:AWS:<account_id>:<region>
|
10
|
+
# fog:AWS:<profile_name>
|
11
|
+
# fog:AWS:<profile_name>:<region>
|
12
|
+
class Chef
|
13
|
+
module Provisioning
|
14
|
+
module FogDriver
|
15
|
+
module Providers
|
16
|
+
class AWS < FogDriver::Driver
|
17
|
+
|
18
|
+
require_relative 'aws/credentials'
|
19
|
+
|
20
|
+
Driver.register_provider_class('AWS', FogDriver::Providers::AWS)
|
21
|
+
|
22
|
+
def creator
|
23
|
+
driver_options[:aws_account_info][:aws_username]
|
24
|
+
end
|
25
|
+
|
26
|
+
def default_ssh_username
|
27
|
+
'ubuntu'
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create a WinRM transport for an AWS instance
|
31
|
+
# @param [Hash] machine_spec Machine-spec hash
|
32
|
+
# @param [Hash] machine_options Machine options (from the recipe)
|
33
|
+
# @param [Fog::Compute::Server] server A Fog mapping to the AWS instance
|
34
|
+
# @return [ChefMetal::Transport::WinRM] A WinRM Transport object to talk to the server
|
35
|
+
def create_winrm_transport(machine_spec, machine_options, server)
|
36
|
+
remote_host = if machine_spec.location['use_private_ip_for_ssh']
|
37
|
+
server.private_ip_address
|
38
|
+
elsif !server.public_ip_address
|
39
|
+
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 ...")
|
40
|
+
server.private_ip_address
|
41
|
+
elsif server.public_ip_address
|
42
|
+
server.public_ip_address
|
43
|
+
else
|
44
|
+
fail "Server #{server.id} has no private or public IP address!"
|
45
|
+
end
|
46
|
+
|
47
|
+
port = machine_spec.location['winrm_port'] || 5985
|
48
|
+
endpoint = "http://#{remote_host}:#{port}/wsman"
|
49
|
+
type = :plaintext
|
50
|
+
pem_bytes = private_key_for(machine_spec, machine_options, server)
|
51
|
+
encrypted_admin_password = wait_for_admin_password(machine_spec)
|
52
|
+
decoded = Base64.decode64(encrypted_admin_password)
|
53
|
+
private_key = OpenSSL::PKey::RSA.new(pem_bytes)
|
54
|
+
decrypted_password = private_key.private_decrypt decoded
|
55
|
+
|
56
|
+
# Use basic HTTP auth - this is required for the WinRM setup we
|
57
|
+
# are using
|
58
|
+
# TODO: Improve that.
|
59
|
+
options = {
|
60
|
+
:user => machine_spec.location['winrm.username'] || 'Administrator',
|
61
|
+
:pass => decrypted_password,
|
62
|
+
:disable_sspi => true,
|
63
|
+
:basic_auth_only => true
|
64
|
+
}
|
65
|
+
|
66
|
+
Chef::Provisioning::Transport::WinRM.new(endpoint, type, options, {})
|
67
|
+
end
|
68
|
+
|
69
|
+
def allocate_image(action_handler, image_spec, image_options, machine_spec)
|
70
|
+
if image_spec.location
|
71
|
+
image = compute.images.get(image_spec.location['image_id'])
|
72
|
+
if image
|
73
|
+
raise "The image already exists, why are you asking me to create it? I can't do that, Dave."
|
74
|
+
end
|
75
|
+
end
|
76
|
+
action_handler.perform_action "Create image #{image_spec.name} from machine #{machine_spec.name} with options #{image_options.inspect}" do
|
77
|
+
opt = image_options.dup
|
78
|
+
response = compute.create_image(machine_spec.location['server_id'],
|
79
|
+
image_spec.name,
|
80
|
+
opt.delete(:description) || "The image formerly and currently named '#{image_spec.name}'",
|
81
|
+
opt.delete(:no_reboot) || false,
|
82
|
+
opt)
|
83
|
+
image_spec.location = {
|
84
|
+
'driver_url' => driver_url,
|
85
|
+
'driver_version' => FogDriver::VERSION,
|
86
|
+
'image_id' => response.body['imageId'],
|
87
|
+
'creator' => creator,
|
88
|
+
'allocated_at' => Time.now.to_i
|
89
|
+
}
|
90
|
+
|
91
|
+
image_spec.machine_options ||= {}
|
92
|
+
image_spec.machine_options.merge!({
|
93
|
+
:bootstrap_options => {
|
94
|
+
:image_id => image_spec.location['image_id']
|
95
|
+
}
|
96
|
+
})
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def ready_image(action_handler, image_spec, image_options)
|
102
|
+
if !image_spec.location
|
103
|
+
raise "Cannot ready an image that does not exist"
|
104
|
+
end
|
105
|
+
image = compute.images.get(image_spec.location['image_id'])
|
106
|
+
if !image.ready?
|
107
|
+
action_handler.report_progress "Waiting for image to be ready ..."
|
108
|
+
# TODO timeout
|
109
|
+
image.wait_for { ready? }
|
110
|
+
action_handler.report_progress "Image is ready!"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def destroy_image(action_handler, image_spec, image_options)
|
115
|
+
if !image_spec.location
|
116
|
+
return
|
117
|
+
end
|
118
|
+
image = compute.images.get(image_spec.location['image_id'])
|
119
|
+
if !image
|
120
|
+
return
|
121
|
+
end
|
122
|
+
delete_snapshots = image_options[:delete_snapshots]
|
123
|
+
delete_snapshots = true if delete_snapshots.nil?
|
124
|
+
image.deregister(delete_snapshots)
|
125
|
+
end
|
126
|
+
|
127
|
+
def bootstrap_options_for(action_handler, machine_spec, machine_options)
|
128
|
+
bootstrap_options = symbolize_keys(machine_options[:bootstrap_options] || {})
|
129
|
+
|
130
|
+
if !bootstrap_options[:key_name]
|
131
|
+
bootstrap_options[:key_name] = overwrite_default_key_willy_nilly(action_handler, machine_spec)
|
132
|
+
end
|
133
|
+
|
134
|
+
if machine_options[:is_windows]
|
135
|
+
Chef::Log.debug('Attaching WinRM data for user data.')
|
136
|
+
# Enable WinRM basic auth, HTTP and open the firewall
|
137
|
+
bootstrap_options[:user_data] = user_data
|
138
|
+
end
|
139
|
+
bootstrap_options.delete(:tags) # we handle these separately for performance reasons
|
140
|
+
bootstrap_options
|
141
|
+
end
|
142
|
+
|
143
|
+
def create_servers(action_handler, specs_and_options, parallelizer)
|
144
|
+
super(action_handler, specs_and_options, parallelizer) do |machine_spec, server|
|
145
|
+
yield machine_spec, server if block_given?
|
146
|
+
|
147
|
+
machine_options = specs_and_options[machine_spec]
|
148
|
+
bootstrap_options = symbolize_keys(machine_options[:bootstrap_options] || {})
|
149
|
+
tags = default_tags(machine_spec, bootstrap_options[:tags] || {})
|
150
|
+
|
151
|
+
# Right now, not doing that in case manual tagging is going on
|
152
|
+
server_tags = server.tags || {}
|
153
|
+
extra_tags = tags.keys.select { |tag_name| !server_tags.has_key?(tag_name) }.to_a
|
154
|
+
different_tags = server_tags.select { |tag_name, tag_value| tags.has_key?(tag_name) && tags[tag_name] != tag_value }.to_a
|
155
|
+
if extra_tags.size > 0 || different_tags.size > 0
|
156
|
+
tags_description = [ "Update tags for #{machine_spec.name} on #{driver_url}" ]
|
157
|
+
tags_description += extra_tags.map { |tag| " Add #{tag} = #{tags[tag].inspect}" }
|
158
|
+
tags_description += different_tags.map { |tag_name, tag_value| " Update #{tag_name} from #{tag_value.inspect} to #{tags[tag_name].inspect}"}
|
159
|
+
action_handler.perform_action tags_description do
|
160
|
+
# TODO should we narrow this down to just extra/different tags or
|
161
|
+
# is it OK to just pass 'em all? Certainly easier to do the
|
162
|
+
# latter, and I can't think of a consequence for doing so offhand.
|
163
|
+
compute.create_tags(server.identity, tags)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def convergence_strategy_for(machine_spec, machine_options)
|
170
|
+
machine_options = Cheffish::MergedConfig.new(machine_options, {
|
171
|
+
:convergence_options => {:ohai_hints => {'ec2' => ''}}
|
172
|
+
})
|
173
|
+
super(machine_spec, machine_options)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Attach given IP to machine
|
177
|
+
def attach_ip(server, ip)
|
178
|
+
Chef::Log.info "Attaching floating IP <#{ip}>"
|
179
|
+
compute.associate_address(:instance_id => server.id,
|
180
|
+
:allocation_id => option_for(machine_options, :allocation_id),
|
181
|
+
:public_ip => ip)
|
182
|
+
end
|
183
|
+
|
184
|
+
def self.get_aws_profile(driver_options, aws_account_id)
|
185
|
+
aws_credentials = get_aws_credentials(driver_options)
|
186
|
+
compute_options = driver_options[:compute_options] || {}
|
187
|
+
|
188
|
+
# Order of operations:
|
189
|
+
# compute_options[:aws_access_key_id] / compute_options[:aws_secret_access_key] / compute_options[:aws_security_token] / compute_options[:region]
|
190
|
+
# compute_options[:aws_profile]
|
191
|
+
# ENV['AWS_ACCESS_KEY_ID'] / ENV['AWS_SECRET_ACCESS_KEY'] / ENV['AWS_SECURITY_TOKEN'] / ENV['AWS_DEFAULT_REGION']
|
192
|
+
# ENV['AWS_PROFILE']
|
193
|
+
# ENV['DEFAULT_PROFILE']
|
194
|
+
# 'default'
|
195
|
+
if compute_options[:aws_access_key_id]
|
196
|
+
Chef::Log.debug("Using AWS driver access key options")
|
197
|
+
aws_profile = {
|
198
|
+
:aws_access_key_id => compute_options[:aws_access_key_id],
|
199
|
+
:aws_secret_access_key => compute_options[:aws_secret_access_key],
|
200
|
+
:aws_security_token => compute_options[:aws_session_token],
|
201
|
+
:region => compute_options[:region]
|
202
|
+
}
|
203
|
+
elsif driver_options[:aws_profile]
|
204
|
+
Chef::Log.debug("Using AWS profile #{driver_options[:aws_profile]}")
|
205
|
+
aws_profile = aws_credentials[driver_options[:aws_profile]]
|
206
|
+
elsif ENV['AWS_ACCESS_KEY_ID'] || ENV['AWS_ACCESS_KEY']
|
207
|
+
Chef::Log.debug("Using AWS environment variable access keys")
|
208
|
+
aws_profile = {
|
209
|
+
:aws_access_key_id => ENV['AWS_ACCESS_KEY_ID'] || ENV['AWS_ACCESS_KEY'],
|
210
|
+
:aws_secret_access_key => ENV['AWS_SECRET_ACCESS_KEY'] || ENV['AWS_SECRET_KEY'],
|
211
|
+
:aws_security_token => ENV['AWS_SECURITY_TOKEN'],
|
212
|
+
:region => ENV['AWS_DEFAULT_REGION'] || ENV['AWS_REGION'] || ENV['EC2_REGION']
|
213
|
+
}
|
214
|
+
elsif ENV['AWS_PROFILE']
|
215
|
+
Chef::Log.debug("Using AWS profile #{ENV['AWS_PROFILE']} from AWS_PROFILE environment variable")
|
216
|
+
aws_profile = aws_credentials[ENV['AWS_PROFILE']]
|
217
|
+
if !aws_profile
|
218
|
+
raise "Environment variable AWS_PROFILE is set to #{ENV['AWS_PROFILE'].inspect} but your AWS config file does not contain that profile!"
|
219
|
+
end
|
220
|
+
else
|
221
|
+
Chef::Log.debug("Using AWS default profile")
|
222
|
+
aws_profile = aws_credentials.default
|
223
|
+
end
|
224
|
+
|
225
|
+
default_ec2_endpoint = compute_options[:ec2_endpoint] || ENV['EC2_URL']
|
226
|
+
default_iam_endpoint = compute_options[:iam_endpoint] || ENV['AWS_IAM_URL']
|
227
|
+
|
228
|
+
# Merge in account info for profile
|
229
|
+
if aws_profile
|
230
|
+
aws_profile = aws_profile.merge(aws_account_info_for(aws_profile, default_iam_endpoint))
|
231
|
+
end
|
232
|
+
|
233
|
+
# If no profile is found (or the profile is not the right account), search
|
234
|
+
# for a profile that matches the given account ID
|
235
|
+
if aws_account_id && (!aws_profile || aws_profile[:aws_account_id] != aws_account_id)
|
236
|
+
aws_profile = find_aws_profile_for_account_id(aws_credentials, aws_account_id, default_iam_endpoint)
|
237
|
+
end
|
238
|
+
|
239
|
+
fail 'No AWS profile specified! Are you missing something in the Chef or AWS config?' unless aws_profile
|
240
|
+
|
241
|
+
aws_profile[:ec2_endpoint] ||= default_ec2_endpoint
|
242
|
+
aws_profile[:iam_endpoint] ||= default_iam_endpoint
|
243
|
+
|
244
|
+
aws_profile.delete_if { |key, value| value.nil? }
|
245
|
+
aws_profile
|
246
|
+
end
|
247
|
+
|
248
|
+
def self.find_aws_profile_for_account_id(aws_credentials, aws_account_id, default_iam_endpoint=nil)
|
249
|
+
aws_profile = nil
|
250
|
+
aws_credentials.each do |profile_name, profile|
|
251
|
+
begin
|
252
|
+
aws_account_info = aws_account_info_for(profile, default_iam_endpoint)
|
253
|
+
rescue
|
254
|
+
Chef::Log.warn("Could not connect to AWS profile #{aws_credentials[:name]}: #{$!}")
|
255
|
+
Chef::Log.debug($!.backtrace.join("\n"))
|
256
|
+
next
|
257
|
+
end
|
258
|
+
if aws_account_info[:aws_account_id] == aws_account_id
|
259
|
+
aws_profile = profile
|
260
|
+
aws_profile[:name] = profile_name
|
261
|
+
aws_profile = aws_profile.merge(aws_account_info)
|
262
|
+
break
|
263
|
+
end
|
264
|
+
end
|
265
|
+
if aws_profile
|
266
|
+
Chef::Log.info("Discovered AWS profile #{aws_profile[:name]} pointing at account #{aws_account_id}. Using ...")
|
267
|
+
else
|
268
|
+
raise "No AWS profile leads to account #{aws_account_id}. Do you need to add profiles to the AWS config?"
|
269
|
+
end
|
270
|
+
aws_profile
|
271
|
+
end
|
272
|
+
|
273
|
+
def self.aws_account_info_for(aws_profile, default_iam_endpoint = nil)
|
274
|
+
iam_endpoint = aws_profile[:iam_endpoint] || default_iam_endpoint
|
275
|
+
|
276
|
+
@@aws_account_info ||= {}
|
277
|
+
@@aws_account_info[aws_profile[:aws_access_key_id]] ||= begin
|
278
|
+
options = {
|
279
|
+
# Endpoint configuration
|
280
|
+
:aws_access_key_id => aws_profile[:aws_access_key_id],
|
281
|
+
:aws_secret_access_key => aws_profile[:aws_secret_access_key],
|
282
|
+
:aws_session_token => aws_profile[:aws_security_token]
|
283
|
+
}
|
284
|
+
if iam_endpoint
|
285
|
+
options[:host] = URI(iam_endpoint).host
|
286
|
+
options[:scheme] = URI(iam_endpoint).scheme
|
287
|
+
options[:port] = URI(iam_endpoint).port
|
288
|
+
options[:path] = URI(iam_endpoint).path
|
289
|
+
end
|
290
|
+
options.delete_if { |key, value| value.nil? }
|
291
|
+
|
292
|
+
iam = Fog::AWS::IAM.new(options)
|
293
|
+
arn = begin
|
294
|
+
# TODO it would be nice if Fog let you do this normally ...
|
295
|
+
iam.send(:request, {
|
296
|
+
'Action' => 'GetUser',
|
297
|
+
:parser => Fog::Parsers::AWS::IAM::GetUser.new
|
298
|
+
}).body['User']['Arn']
|
299
|
+
rescue Fog::AWS::IAM::Error
|
300
|
+
# TODO Someone tell me there is a better way to find out your current
|
301
|
+
# user ID than this! This is what happens when you use an IAM user
|
302
|
+
# with default privileges.
|
303
|
+
if $!.message =~ /AccessDenied.+(arn:aws:iam::\d+:\S+)/
|
304
|
+
arn = $1
|
305
|
+
else
|
306
|
+
raise
|
307
|
+
end
|
308
|
+
end
|
309
|
+
arn_split = arn.split(':', 6)
|
310
|
+
{
|
311
|
+
:aws_account_id => arn_split[4],
|
312
|
+
:aws_username => arn_split[5],
|
313
|
+
:aws_user_arn => arn
|
314
|
+
}
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def self.get_aws_credentials(driver_options)
|
319
|
+
# Grab the list of possible credentials
|
320
|
+
if driver_options[:aws_credentials]
|
321
|
+
aws_credentials = driver_options[:aws_credentials]
|
322
|
+
else
|
323
|
+
aws_credentials = Credentials.new
|
324
|
+
if driver_options[:aws_config_file]
|
325
|
+
aws_credentials.load_ini(driver_options[:aws_config_file])
|
326
|
+
elsif driver_options[:aws_csv_file]
|
327
|
+
aws_credentials.load_csv(driver_options[:aws_csv_file])
|
328
|
+
else
|
329
|
+
aws_credentials.load_default
|
330
|
+
end
|
331
|
+
end
|
332
|
+
aws_credentials
|
333
|
+
end
|
334
|
+
|
335
|
+
def self.compute_options_for(provider, id, config)
|
336
|
+
new_compute_options = {}
|
337
|
+
new_compute_options[:provider] = provider
|
338
|
+
new_config = { :driver_options => { :compute_options => new_compute_options }}
|
339
|
+
new_defaults = {
|
340
|
+
:driver_options => { :compute_options => {} },
|
341
|
+
:machine_options => { :bootstrap_options => {} }
|
342
|
+
}
|
343
|
+
result = Cheffish::MergedConfig.new(new_config, config, new_defaults)
|
344
|
+
|
345
|
+
if id && id != ''
|
346
|
+
# AWS canonical URLs are of the form fog:AWS:
|
347
|
+
if id =~ /^(\d{12}|IAM)(:(.+))?$/
|
348
|
+
if $2
|
349
|
+
id = $1
|
350
|
+
new_compute_options[:region] = $3
|
351
|
+
else
|
352
|
+
Chef::Log.warn("Old-style AWS URL #{id} from an early beta of chef-provisioning (before chef-metal 0.11-final) found. If you have servers in multiple regions on this account, you may see odd behavior like servers being recreated. To fix, edit any nodes with attribute chef_provisioning.location.driver_url to include the region like so: fog:AWS:#{id}:<region> (e.g. us-east-1)")
|
353
|
+
end
|
354
|
+
else
|
355
|
+
# Assume it is a profile name, and set that.
|
356
|
+
aws_profile, region = id.split(':', 2)
|
357
|
+
new_config[:driver_options][:aws_profile] = aws_profile
|
358
|
+
new_compute_options[:region] = region
|
359
|
+
id = nil
|
360
|
+
end
|
361
|
+
end
|
362
|
+
if id == 'IAM'
|
363
|
+
id = "IAM:#{result[:driver_options][:compute_options][:region]}"
|
364
|
+
new_config[:driver_options][:aws_account_info] = { aws_username: 'IAM' }
|
365
|
+
new_compute_options[:use_iam_profile] = true
|
366
|
+
else
|
367
|
+
aws_profile = get_aws_profile(result[:driver_options], id)
|
368
|
+
new_compute_options[:aws_access_key_id] = aws_profile[:aws_access_key_id]
|
369
|
+
new_compute_options[:aws_secret_access_key] = aws_profile[:aws_secret_access_key]
|
370
|
+
new_compute_options[:aws_session_token] = aws_profile[:aws_security_token]
|
371
|
+
new_defaults[:driver_options][:compute_options][:region] = aws_profile[:region]
|
372
|
+
new_defaults[:driver_options][:compute_options][:endpoint] = aws_profile[:ec2_endpoint]
|
373
|
+
|
374
|
+
account_info = aws_account_info_for(result[:driver_options][:compute_options])
|
375
|
+
new_config[:driver_options][:aws_account_info] = account_info
|
376
|
+
id = "#{account_info[:aws_account_id]}:#{result[:driver_options][:compute_options][:region]}"
|
377
|
+
end
|
378
|
+
|
379
|
+
# Make sure we're using a reasonable default AMI, for now this is Ubuntu 14.04 LTS
|
380
|
+
result[:machine_options][:bootstrap_options][:image_id] ||=
|
381
|
+
default_ami_for_region(result[:driver_options][:compute_options][:region])
|
382
|
+
|
383
|
+
[result, id]
|
384
|
+
end
|
385
|
+
|
386
|
+
def create_many_servers(num_servers, bootstrap_options, parallelizer)
|
387
|
+
# Create all the servers in one request if we have a version of Fog that can do that
|
388
|
+
if compute.servers.respond_to?(:create_many)
|
389
|
+
servers = compute.servers.create_many(num_servers, num_servers, bootstrap_options)
|
390
|
+
if block_given?
|
391
|
+
parallelizer.parallelize(servers) do |server|
|
392
|
+
yield server
|
393
|
+
end.to_a
|
394
|
+
end
|
395
|
+
servers
|
396
|
+
else
|
397
|
+
super
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
def servers_for(machine_specs)
|
402
|
+
# Grab all the servers in one request
|
403
|
+
instance_ids = machine_specs.map { |machine_spec| (machine_spec.location || {})['server_id'] }.select { |id| !id.nil? }
|
404
|
+
servers = compute.servers.all('instance-id' => instance_ids)
|
405
|
+
result = {}
|
406
|
+
machine_specs.each do |machine_spec|
|
407
|
+
if machine_spec.location
|
408
|
+
result[machine_spec] = servers.select { |s| s.id == machine_spec.location['server_id'] }.first
|
409
|
+
else
|
410
|
+
result[machine_spec] = nil
|
411
|
+
end
|
412
|
+
end
|
413
|
+
result
|
414
|
+
end
|
415
|
+
|
416
|
+
private
|
417
|
+
def user_data
|
418
|
+
# TODO: Make this use HTTPS at some point.
|
419
|
+
<<EOD
|
420
|
+
<powershell>
|
421
|
+
winrm quickconfig -q
|
422
|
+
winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="300"}'
|
423
|
+
winrm set winrm/config '@{MaxTimeoutms="1800000"}'
|
424
|
+
winrm set winrm/config/service '@{AllowUnencrypted="true"}'
|
425
|
+
winrm set winrm/config/service/auth '@{Basic="true"}'
|
426
|
+
|
427
|
+
netsh advfirewall firewall add rule name="WinRM 5985" protocol=TCP dir=in localport=5985 action=allow
|
428
|
+
netsh advfirewall firewall add rule name="WinRM 5986" protocol=TCP dir=in localport=5986 action=allow
|
429
|
+
|
430
|
+
net stop winrm
|
431
|
+
sc config winrm start=auto
|
432
|
+
net start winrm
|
433
|
+
</powershell>
|
434
|
+
|
435
|
+
EOD
|
436
|
+
end
|
437
|
+
|
438
|
+
def self.default_ami_for_region(region)
|
439
|
+
Chef::Log.debug("Choosing default AMI for region '#{region}'")
|
440
|
+
|
441
|
+
case region
|
442
|
+
when 'ap-northeast-1'
|
443
|
+
'ami-c786dcc6'
|
444
|
+
when 'ap-southeast-1'
|
445
|
+
'ami-eefca7bc'
|
446
|
+
when 'ap-southeast-2'
|
447
|
+
'ami-996706a3'
|
448
|
+
when 'eu-west-1'
|
449
|
+
'ami-4ab46b3d'
|
450
|
+
when 'sa-east-1'
|
451
|
+
'ami-6770d87a'
|
452
|
+
when 'us-east-1'
|
453
|
+
'ami-d2ff23ba'
|
454
|
+
when 'us-west-1'
|
455
|
+
'ami-73717d36'
|
456
|
+
when 'us-west-2'
|
457
|
+
'ami-f1ce8bc1'
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
# Wait for the Windows Admin password to become available
|
462
|
+
# @param [Hash] machine_spec Machine spec data
|
463
|
+
# @return [String] encrypted admin password
|
464
|
+
def wait_for_admin_password(machine_spec)
|
465
|
+
time_elapsed = 0
|
466
|
+
sleep_time = 10
|
467
|
+
max_wait_time = 900 # 15 minutes
|
468
|
+
encrypted_admin_password = nil
|
469
|
+
instance_id = machine_spec.location['server_id']
|
470
|
+
|
471
|
+
|
472
|
+
Chef::Log.info "waiting for #{machine_spec.name}'s admin password to be available..."
|
473
|
+
while time_elapsed < max_wait_time && encrypted_admin_password.nil?
|
474
|
+
response = compute.get_password_data(instance_id)
|
475
|
+
encrypted_admin_password = response.body['passwordData']
|
476
|
+
if encrypted_admin_password.nil?
|
477
|
+
Chef::Log.info "#{time_elapsed}/#{max_wait_time}s elapsed -- sleeping #{sleep_time} seconds for #{machine_spec.name}'s admin password."
|
478
|
+
sleep(sleep_time)
|
479
|
+
time_elapsed += sleep_time
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
Chef::Log.info "#{machine_spec.name}'s admin password is available!'"
|
484
|
+
|
485
|
+
encrypted_admin_password
|
486
|
+
end
|
487
|
+
|
488
|
+
end
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|