chef-metal-fog 0.4 → 0.5.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,558 +0,0 @@
1
- require 'chef_metal/provisioner'
2
- require 'chef_metal/aws_credentials'
3
- require 'chef_metal/openstack_credentials'
4
- require 'chef_metal/machine/windows_machine'
5
- require 'chef_metal/machine/unix_machine'
6
- require 'chef_metal/convergence_strategy/install_msi'
7
- require 'chef_metal/convergence_strategy/install_cached'
8
- require 'chef_metal/transport/ssh'
9
- require 'chef_metal_fog/version'
10
- require 'fog'
11
- require 'fog/core'
12
- require 'fog/compute'
13
- require 'fog/aws'
14
-
15
- module ChefMetalFog
16
- # Provisions machines in vagrant.
17
- class FogProvisioner < ChefMetal::Provisioner
18
-
19
- include Chef::Mixin::ShellOut
20
-
21
- DEFAULT_OPTIONS = {
22
- :create_timeout => 600,
23
- :start_timeout => 600,
24
- :ssh_timeout => 20
25
- }
26
-
27
- def self.inflate(node)
28
- url = node['normal']['provisioner_output']['provisioner_url']
29
- scheme, provider, id = url.split(':', 3)
30
- FogProvisioner.new({ :provider => provider }, id)
31
- end
32
-
33
- # Create a new fog provisioner.
34
- #
35
- # ## Parameters
36
- # compute_options - hash of options to be passed to Fog::Compute.new
37
- # Special options:
38
- # - :base_bootstrap_options is merged with bootstrap_options in acquire_machine
39
- # to present the full set of bootstrap options. Write down any bootstrap_options
40
- # you intend to apply universally here.
41
- # - :aws_credentials is an AWS CSV file (created with Download Credentials)
42
- # containing your aws key information. If you do not specify aws_access_key_id
43
- # and aws_secret_access_key explicitly, the first line from this file
44
- # will be used. You may pass a Cheffish::AWSCredentials object.
45
- # - :create_timeout - the time to wait for the instance to boot to ssh (defaults to 600)
46
- # - :start_timeout - the time to wait for the instance to start (defaults to 600)
47
- # - :ssh_timeout - the time to wait for ssh to be available if the instance is detected as up (defaults to 20)
48
- # id - the ID in the provisioner_url (fog:PROVIDER:ID)
49
- def initialize(compute_options, id=nil)
50
- @compute_options = compute_options
51
- @base_bootstrap_options = compute_options.delete(:base_bootstrap_options) || {}
52
-
53
- case compute_options[:provider]
54
- when 'AWS'
55
- aws_credentials = compute_options.delete(:aws_credentials)
56
- if aws_credentials
57
- @aws_credentials = aws_credentials
58
- else
59
- @aws_credentials = ChefMetal::AWSCredentials.new
60
- @aws_credentials.load_default
61
- end
62
- compute_options[:aws_access_key_id] ||= @aws_credentials.default[:access_key_id]
63
- compute_options[:aws_secret_access_key] ||= @aws_credentials.default[:secret_access_key]
64
- # TODO actually find a key with the proper id
65
- # TODO let the user specify credentials and provider profiles that we can use
66
- if id && aws_login_info[0] != id
67
- raise "Default AWS credentials point at AWS account #{aws_login_info[0]}, but inflating from URL #{id}"
68
- end
69
- when 'OpenStack'
70
- openstack_credentials = compute_options.delete(:openstack_credentials)
71
- if openstack_credentials
72
- @openstack_credentials = openstack_credentials
73
- else
74
- @openstack_credentials = ChefMetal::OpenstackCredentials.new
75
- @openstack_credentials.load_default
76
- end
77
-
78
- compute_options[:openstack_username] ||= @openstack_credentials.default[:openstack_username]
79
- compute_options[:openstack_api_key] ||= @openstack_credentials.default[:openstack_api_key]
80
- compute_options[:openstack_auth_url] ||= @openstack_credentials.default[:openstack_auth_url]
81
- compute_options[:openstack_tenant] ||= @openstack_credentials.default[:openstack_tenant]
82
- end
83
- @key_pairs = {}
84
- @base_bootstrap_options_for = {}
85
- end
86
-
87
- attr_reader :compute_options
88
- attr_reader :aws_credentials
89
- attr_reader :openstack_credentials
90
- attr_reader :key_pairs
91
-
92
- def current_base_bootstrap_options
93
- result = @base_bootstrap_options.dup
94
- if key_pairs.size > 0
95
- last_pair_name = key_pairs.keys.last
96
- last_pair = key_pairs[last_pair_name]
97
- result[:key_name] ||= last_pair_name
98
- result[:private_key_path] ||= last_pair.private_key_path
99
- result[:public_key_path] ||= last_pair.public_key_path
100
- end
101
- result
102
- end
103
-
104
- # Inflate a provisioner from node information; we don't want to force the
105
- # driver to figure out what the provisioner really needs, since it varies
106
- # from provisioner to provisioner.
107
- #
108
- # ## Parameters
109
- # node - node to inflate the provisioner for
110
- #
111
- # returns a FogProvisioner
112
- # TODO: def self.inflate(node)
113
- # right now, not implemented, will raise error from base class until overridden
114
-
115
- # Acquire a machine, generally by provisioning it. Returns a Machine
116
- # object pointing at the machine, allowing useful actions like setup,
117
- # converge, execute, file and directory. The Machine object will have a
118
- # "node" property which must be saved to the server (if it is any
119
- # different from the original node object).
120
- #
121
- # ## Parameters
122
- # action_handler - the action_handler object that is calling this method; this
123
- # is generally a action_handler, but could be anything that can support the
124
- # ChefMetal::ActionHandler interface (i.e., in the case of the test
125
- # kitchen metal driver for acquiring and destroying VMs; see the base
126
- # class for what needs providing).
127
- # node - node object (deserialized json) representing this machine. If
128
- # the node has a provisioner_options hash in it, these will be used
129
- # instead of options provided by the provisioner. TODO compare and
130
- # fail if different?
131
- # node will have node['normal']['provisioner_options'] in it with any options.
132
- # It is a hash with this format:
133
- #
134
- # -- provisioner_url: fog:<relevant_fog_options>
135
- # -- bootstrap_options: hash of options to pass to compute.servers.create
136
- # -- is_windows: true if windows. TODO detect this from ami?
137
- # -- create_timeout - the time to wait for the instance to boot to ssh (defaults to 600)
138
- # -- start_timeout - the time to wait for the instance to start (defaults to 600)
139
- # -- ssh_timeout - the time to wait for ssh to be available if the instance is detected as up (defaults to 20)
140
- #
141
- # Example bootstrap_options for ec2:
142
- # 'bootstrap_options' => {
143
- # 'image_id' =>'ami-311f2b45',
144
- # 'flavor_id' =>'t1.micro',
145
- # 'key_name' => 'key-pair-name'
146
- # }
147
- #
148
- # node['normal']['provisioner_output'] will be populated with information
149
- # about the created machine. For vagrant, it is a hash with this
150
- # format:
151
- #
152
- # -- provisioner_url: fog:<relevant_fog_options>
153
- # -- server_id: the ID of the server so it can be found again
154
- #
155
- def acquire_machine(action_handler, node)
156
- # Set up the modified node data
157
- creator = case compute_options[:provider]
158
- when 'AWS'
159
- aws_login_info[1]
160
- when 'OpenStack'
161
- compute_options[:openstack_username]
162
- end
163
-
164
- provisioner_output = node['normal']['provisioner_output'] || {
165
- 'provisioner_url' => provisioner_url,
166
- 'provisioner_version' => ChefMetalFog::VERSION,
167
- 'creator' => creator
168
- }
169
-
170
- if provisioner_output['provisioner_url'] != provisioner_url
171
- raise "Switching a machine's provider from #{provisioner_output['provisioner_url']} to #{provisioner_url} for is not currently supported! Use machine :destroy and then re-create the machine on the new provisioner."
172
- end
173
-
174
- node['normal']['provisioner_output'] = provisioner_output
175
-
176
- if provisioner_output['server_id']
177
-
178
- # If the server already exists, make sure it is up
179
-
180
- # TODO verify that the server info matches the specification (ami, etc.)\
181
- server = server_for(node)
182
- if !server
183
- Chef::Log.warn "Machine #{node['name']} (#{provisioner_output['server_id']} on #{provisioner_url}) is not associated with the ec2 account. Recreating ..."
184
- need_to_create = true
185
- elsif %w(terminated archive).include?(server.state) # Can't come back from that
186
- Chef::Log.warn "Machine #{node['name']} (#{server.id} on #{provisioner_url}) is terminated. Recreating ..."
187
- need_to_create = true
188
- else
189
- need_to_create = false
190
- if !server.ready?
191
- action_handler.perform_action "start machine #{node['name']} (#{server.id} on #{provisioner_url})" do
192
- server.start
193
- end
194
- action_handler.perform_action "wait for machine #{node['name']} (#{server.id} on #{provisioner_url}) to be ready" do
195
- wait_until_ready(server, option_for(node, :start_timeout))
196
- end
197
- else
198
- wait_until_ready(server, option_for(node, :ssh_timeout))
199
- end
200
- end
201
- else
202
- need_to_create = true
203
- end
204
-
205
- if need_to_create
206
- # If the server does not exist, create it
207
- bootstrap_options = bootstrap_options_for(action_handler.new_resource, node)
208
- bootstrap_options.merge(:name => action_handler.new_resource.name)
209
-
210
- start_time = Time.now
211
- timeout = option_for(node, :create_timeout)
212
-
213
- description = [ "create machine #{node['name']} on #{provisioner_url}" ]
214
- bootstrap_options.each_pair { |key,value| description << " #{key}: #{value.inspect}" }
215
- server = nil
216
- action_handler.perform_action description do
217
- server = compute.servers.create(bootstrap_options)
218
- provisioner_output['server_id'] = server.id
219
- # Save quickly in case something goes wrong
220
- save_node(action_handler, node, action_handler.new_resource.chef_server)
221
- end
222
-
223
- if server
224
- @@ip_pool_lock = Mutex.new
225
- # Re-retrieve the server in a more malleable form and wait for it to be ready
226
- server = compute.servers.get(server.id)
227
- if bootstrap_options[:floating_ip_pool]
228
- Chef::Log.info 'Attaching IP from pool'
229
- server.wait_for { ready? }
230
- action_handler.perform_action "attach floating IP from #{bootstrap_options[:floating_ip_pool]} pool" do
231
- attach_ip_from_pool(server, bootstrap_options[:floating_ip_pool])
232
- end
233
- elsif bootstrap_options[:floating_ip]
234
- Chef::Log.info 'Attaching given IP'
235
- server.wait_for { ready? }
236
- action_handler.perform_action "attach floating IP #{bootstrap_options[:floating_ip]}" do
237
- attach_ip(server, bootstrap_options[:allocation_id], bootstrap_options[:floating_ip])
238
- end
239
- end
240
- action_handler.perform_action "machine #{node['name']} created as #{server.id} on #{provisioner_url}" do
241
- end
242
- # Wait for the machine to come up and for ssh to start listening
243
- transport = nil
244
- _self = self
245
- action_handler.perform_action "wait for machine #{node['name']} to boot" do
246
- server.wait_for(timeout - (Time.now - start_time)) do
247
- if ready?
248
- transport ||= _self.transport_for(server)
249
- begin
250
- transport.execute('pwd')
251
- true
252
- rescue ChefMetal::Transport::SSH::InitialConnectTimeout, Errno::ECONNREFUSED, Net::SSH::Disconnect, Errno::EHOSTUNREACH
253
- false
254
- rescue
255
- true
256
- end
257
- else
258
- false
259
- end
260
- end
261
- end
262
-
263
- # If there is some other error, we just wait patiently for SSH
264
- begin
265
- server.wait_for(option_for(node, :ssh_timeout)) { transport.available? }
266
- rescue Fog::Errors::TimeoutError
267
- # Sometimes (on EC2) the machine comes up but gets stuck or has
268
- # some other problem. If this is the case, we restart the server
269
- # to unstick it. Reboot covers a multitude of sins.
270
- Chef::Log.warn "Machine #{node['name']} (#{server.id} on #{provisioner_url}) was started but SSH did not come up. Rebooting machine in an attempt to unstick it ..."
271
- action_handler.perform_action "reboot machine #{node['name']} to try to unstick it" do
272
- server.reboot
273
- end
274
- action_handler.perform_action "wait for machine #{node['name']} to be ready after reboot" do
275
- wait_until_ready(server, option_for(node, :start_timeout))
276
- end
277
- end
278
- end
279
- end
280
-
281
- # Create machine object for callers to use
282
- machine_for(node, server)
283
- end
284
-
285
- # Attach IP to machine from IP pool
286
- # Code taken from kitchen-openstack driver
287
- # https://github.com/test-kitchen/kitchen-openstack/blob/master/lib/kitchen/driver/openstack.rb#L196-L207
288
- def attach_ip_from_pool(server, pool)
289
- @@ip_pool_lock.synchronize do
290
- Chef::Log.info "Attaching floating IP from <#{pool}> pool"
291
- free_addrs = compute.addresses.collect do |i|
292
- i.ip if i.fixed_ip.nil? and i.instance_id.nil? and i.pool == pool
293
- end.compact
294
- if free_addrs.empty?
295
- raise ActionFailed, "No available IPs in pool <#{pool}>"
296
- end
297
- attach_ip(server, free_addrs[0])
298
- end
299
- end
300
-
301
- # Attach given IP to machine
302
- # Code taken from kitchen-openstack driver
303
- # https://github.com/test-kitchen/kitchen-openstack/blob/master/lib/kitchen/driver/openstack.rb#L209-L213
304
- def attach_ip(server, allocation_id, ip)
305
- Chef::Log.info "Attaching floating IP <#{ip}>"
306
- compute.associate_address(:instance_id => server.id,
307
- :allocation_id => allocation_id,
308
- :public_ip => ip)
309
- end
310
-
311
- # Connect to machine without acquiring it
312
- def connect_to_machine(node)
313
- machine_for(node)
314
- end
315
-
316
- def delete_machine(action_handler, node)
317
- if node['normal']['provisioner_output'] && node['normal']['provisioner_output']['server_id']
318
- server = compute.servers.get(node['normal']['provisioner_output']['server_id'])
319
- if server
320
- action_handler.perform_action "destroy machine #{node['name']} (#{node['normal']['provisioner_output']['server_id']} at #{provisioner_url})" do
321
- server.destroy
322
- end
323
- end
324
- convergence_strategy_for(node).cleanup_convergence(action_handler, node)
325
- end
326
- end
327
-
328
- def stop_machine(action_handler, node)
329
- # If the machine doesn't exist, we silently do nothing
330
- if node['normal']['provisioner_output'] && node['normal']['provisioner_output']['server_id']
331
- server = compute.servers.get(node['normal']['provisioner_output']['server_id'])
332
- action_handler.perform_action "stop machine #{node['name']} (#{server.id} at #{provisioner_url})" do
333
- server.stop
334
- end
335
- end
336
- end
337
-
338
- def resource_created(machine)
339
- @base_bootstrap_options_for[machine] = current_base_bootstrap_options
340
- end
341
-
342
- def compute
343
- @compute ||= Fog::Compute.new(compute_options)
344
- end
345
-
346
- def provisioner_url
347
- provider_identifier = case compute_options[:provider]
348
- when 'AWS'
349
- aws_login_info[0]
350
- when 'DigitalOcean'
351
- compute_options[:digitalocean_client_id]
352
- when 'OpenStack'
353
- compute_options[:openstack_auth_url]
354
- else
355
- '???'
356
- end
357
- "fog:#{compute_options[:provider]}:#{provider_identifier}"
358
- end
359
-
360
- # Not meant to be part of public interface
361
- def transport_for(server)
362
- # TODO winrm
363
- create_ssh_transport(server)
364
- end
365
-
366
- protected
367
-
368
- def option_for(node, key)
369
- if node['normal']['provisioner_options'] && node['normal']['provisioner_options'][key.to_s]
370
- node['normal']['provisioner_options'][key.to_s]
371
- elsif compute_options[key]
372
- compute_options[key]
373
- else
374
- DEFAULT_OPTIONS[key]
375
- end
376
- end
377
-
378
- # Returns [ Account ID, User ]
379
- # Account ID is the 12 digit identifier on your Manage Account page in AWS Console. It is used as part of all ARNs identifying resources.
380
- # User is an identifier like "root" or "user/username" or "federated-user/username"
381
- def aws_login_info
382
- @aws_login_info ||= begin
383
- iam = Fog::AWS::IAM.new(:aws_access_key_id => compute_options[:aws_access_key_id], :aws_secret_access_key => compute_options[:aws_secret_access_key])
384
- arn = begin
385
- # TODO it would be nice if Fog let you do this normally ...
386
- iam.send(:request, {
387
- 'Action' => 'GetUser',
388
- :parser => Fog::Parsers::AWS::IAM::GetUser.new
389
- }).body['User']['Arn']
390
- rescue Fog::AWS::IAM::Error
391
- # TODO Someone tell me there is a better way to find out your current
392
- # user ID than this! This is what happens when you use an IAM user
393
- # with default privileges.
394
- if $!.message =~ /AccessDenied.+(arn:aws:iam::\d+:\S+)/
395
- arn = $1
396
- else
397
- raise
398
- end
399
- end
400
- arn.split(':')[4..5]
401
- end
402
- end
403
-
404
- def symbolize_keys(options)
405
- options.inject({}) { |result,(key,value)| result[key.to_sym] = value; result }
406
- end
407
-
408
- def server_for(node)
409
- if node['normal']['provisioner_output'] && node['normal']['provisioner_output']['server_id']
410
- compute.servers.get(node['normal']['provisioner_output']['server_id'])
411
- else
412
- nil
413
- end
414
- end
415
-
416
- def bootstrap_options_for(machine, node)
417
- provisioner_options = node['normal']['provisioner_options'] || {}
418
- bootstrap_options = @base_bootstrap_options_for[machine] || current_base_bootstrap_options
419
- bootstrap_options = bootstrap_options.merge(symbolize_keys(provisioner_options['bootstrap_options'] || {}))
420
- require 'socket'
421
- require 'etc'
422
- tags = {
423
- 'Name' => node['name'],
424
- 'BootstrapChefServer' => machine.chef_server[:chef_server_url],
425
- 'BootstrapHost' => Socket.gethostname,
426
- 'BootstrapUser' => Etc.getlogin,
427
- 'BootstrapNodeName' => node['name']
428
- }
429
- if machine.chef_server[:options] && machine.chef_server[:options][:data_store]
430
- tags['ChefLocalRepository'] = machine.chef_server[:options][:data_store].chef_fs.fs_description
431
- end
432
- # User-defined tags override the ones we set
433
- tags.merge!(bootstrap_options[:tags]) if bootstrap_options[:tags]
434
- bootstrap_options.merge!({ :tags => tags })
435
-
436
- # Provide reasonable defaults for DigitalOcean
437
- if compute_options[:provider] == 'DigitalOcean'
438
- if !bootstrap_options[:image_id]
439
- bootstrap_options[:image_name] ||= 'CentOS 6.4 x32'
440
- bootstrap_options[:image_id] = compute.images.select { |image| image.name == bootstrap_options[:image_name] }.first.id
441
- end
442
- if !bootstrap_options[:flavor_id]
443
- bootstrap_options[:flavor_name] ||= '512MB'
444
- bootstrap_options[:flavor_id] = compute.flavors.select { |flavor| flavor.name == bootstrap_options[:flavor_name] }.first.id
445
- end
446
- if !bootstrap_options[:region_id]
447
- bootstrap_options[:region_name] ||= 'San Francisco 1'
448
- bootstrap_options[:region_id] = compute.regions.select { |region| region.name == bootstrap_options[:region_name] }.first.id
449
- end
450
- bootstrap_options[:ssh_key_ids] ||= [ compute.ssh_keys.select { |k| k.name == bootstrap_options[:key_name] }.first.id ]
451
-
452
- # You don't get to specify name yourself
453
- bootstrap_options[:name] = node['name']
454
- end
455
-
456
- bootstrap_options
457
- end
458
-
459
- def machine_for(node, server = nil)
460
- server ||= server_for(node)
461
- if !server
462
- raise "Server for node #{node['name']} has not been created!"
463
- end
464
-
465
- if node['normal']['provisioner_options'] && node['normal']['provisioner_options']['is_windows']
466
- ChefMetal::Machine::WindowsMachine.new(node, transport_for(server), convergence_strategy_for(node))
467
- else
468
- ChefMetal::Machine::UnixMachine.new(node, transport_for(server), convergence_strategy_for(node))
469
- end
470
- end
471
-
472
- def convergence_strategy_for(node)
473
- if node['normal']['provisioner_options'] && node['normal']['provisioner_options']['is_windows']
474
- @windows_convergence_strategy ||= begin
475
- options = {}
476
- provisioner_options = node['normal']['provisioner_options'] || {}
477
- options[:chef_client_timeout] = provisioner_options['chef_client_timeout'] if provisioner_options.has_key?('chef_client_timeout')
478
- ChefMetal::ConvergenceStrategy::InstallMsi.new(options)
479
- end
480
- else
481
- @unix_convergence_strategy ||= begin
482
- options = {}
483
- provisioner_options = node['normal']['provisioner_options'] || {}
484
- options[:chef_client_timeout] = provisioner_options['chef_client_timeout'] if provisioner_options.has_key?('chef_client_timeout')
485
- ChefMetal::ConvergenceStrategy::InstallCached.new(options)
486
- end
487
- end
488
- end
489
-
490
- def ssh_options_for(server)
491
- result = {
492
- # TODO create a user known hosts file
493
- # :user_known_hosts_file => vagrant_ssh_config['UserKnownHostsFile'],
494
- # :paranoid => true,
495
- :auth_methods => [ 'publickey' ],
496
- :keys_only => true,
497
- :host_key_alias => "#{server.id}.#{compute_options[:provider]}"
498
- }
499
- if server.respond_to?(:private_key) && server.private_key
500
- result[:key_data] = [ server.private_key ]
501
- elsif server.respond_to?(:key_name) && key_pairs[server.key_name]
502
- # TODO generalize for others?
503
- result[:keys] ||= [ key_pairs[server.key_name].private_key_path ]
504
- else
505
- # TODO need a way to know which key if there were multiple
506
- result[:keys] = [ key_pairs.first[1].private_key_path ]
507
- end
508
- result
509
- end
510
-
511
- def create_ssh_transport(server)
512
- ssh_options = ssh_options_for(server)
513
- # If we're on AWS, the default is to use ubuntu, not root
514
- if compute_options[:provider] == 'AWS'
515
- username = compute_options[:ssh_username] || 'ubuntu'
516
- else
517
- username = compute_options[:ssh_username] || 'root'
518
- end
519
- options = {}
520
- if compute_options[:sudo] || (!compute_options.has_key?(:sudo) && username != 'root')
521
- options[:prefix] = 'sudo '
522
- end
523
-
524
- remote_host = nil
525
- if compute_options[:use_private_ip_for_ssh]
526
- remote_host = server.private_ip_address
527
- elsif !server.public_ip_address
528
- Chef::Log.warn("Server has no public ip address. Using private ip '#{server.private_ip_address}'. Set provisioner option 'use_private_ip_for_ssh' => true if this will always be the case ...")
529
- remote_host = server.private_ip_address
530
- elsif server.public_ip_address
531
- remote_host = server.public_ip_address
532
- else
533
- raise "Server #{server.id} has no private or public IP address!"
534
- end
535
-
536
- #Enable pty by default
537
- options[:ssh_pty_enable] = true
538
-
539
- ChefMetal::Transport::SSH.new(remote_host, username, ssh_options, options)
540
- end
541
-
542
- def wait_until_ready(server, timeout)
543
- transport = nil
544
- _self = self
545
- server.wait_for(timeout) do
546
- if transport
547
- transport.available?
548
- elsif ready?
549
- # Don't create the transport until the machine is ready (we won't have the host till then)
550
- transport = _self.transport_for(server)
551
- transport.available?
552
- else
553
- false
554
- end
555
- end
556
- end
557
- end
558
- end