chef-metal-fog 0.5.4 → 0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/chef/provider/fog_key_pair.rb +13 -2
- data/lib/chef_metal_fog/fog_driver.rb +64 -194
- data/lib/chef_metal_fog/providers/aws/credentials.rb +71 -0
- data/lib/chef_metal_fog/providers/aws.rb +214 -0
- data/lib/chef_metal_fog/providers/cloudstack.rb +36 -0
- data/lib/chef_metal_fog/providers/digitalocean.rb +106 -0
- data/lib/chef_metal_fog/providers/openstack.rb +37 -0
- data/lib/chef_metal_fog/providers/rackspace.rb +38 -0
- data/lib/chef_metal_fog/version.rb +1 -1
- data/spec/spec_helper.rb +18 -0
- data/spec/support/aws/config-file.csv +2 -0
- data/spec/support/aws/ini-file.ini +7 -0
- data/spec/support/chef_metal_fog/providers/testdriver.rb +14 -0
- data/spec/unit/fog_driver_spec.rb +32 -0
- data/spec/unit/providers/aws/credentials_spec.rb +42 -0
- data/spec/unit/providers/rackspace_spec.rb +16 -0
- metadata +29 -4
- data/lib/chef_metal_fog/aws_credentials.rb +0 -67
- data/lib/chef_metal_fog/fog_driver_aws.rb +0 -142
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b1fdb71aedeacd1389f9b204d2518a566cd129f8
|
4
|
+
data.tar.gz: 06a458753dad712619205ad5cccee51c8cbd00c3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a3b39d661695873096fd28868f735e8d73266cc19b08e5ab646a092e0a8cee2dceebe4ab09aaf9c4f4de04248dafd42fc9bc5eba34d172480a2d5aec37e53329
|
7
|
+
data.tar.gz: 1e0f3e6766d5aa9da0b38a2f8e236764bea3de9b68c54b4d70d13188e29ef4da2f5dcf47e11ac0b6e863aea7478b7d598a154341cf758f271f062c1368e5f563
|
@@ -70,8 +70,19 @@ class Chef::Provider::FogKeyPair < Chef::Provider::LWRPBase
|
|
70
70
|
# them matches.
|
71
71
|
new_fingerprints = [Cheffish::KeyFormatter.encode(desired_key, :format => :fingerprint)]
|
72
72
|
if RUBY_VERSION.to_f < 2.0
|
73
|
-
|
74
|
-
|
73
|
+
if @@use_pkcs8.nil?
|
74
|
+
begin
|
75
|
+
require 'openssl_pkcs8'
|
76
|
+
@@use_pkcs8 = true
|
77
|
+
rescue LoadError
|
78
|
+
Chef::Log.warn("The openssl_pkcs8 gem is not loaded: you may not be able to read key fingerprints created by some cloud providers. gem install openssl_pkcs8 to fix!")
|
79
|
+
@@use_pkcs8 = false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
if @@use_pkcs8
|
83
|
+
new_fingerprints << Cheffish::KeyFormatter.encode(desired_private_key,
|
84
|
+
:format => :pkcs8sha1fingerprint)
|
85
|
+
end
|
75
86
|
end
|
76
87
|
end
|
77
88
|
|
@@ -8,11 +8,9 @@ require 'chef_metal/convergence_strategy/install_cached'
|
|
8
8
|
require 'chef_metal/convergence_strategy/no_converge'
|
9
9
|
require 'chef_metal/transport/ssh'
|
10
10
|
require 'chef_metal_fog/version'
|
11
|
-
require 'chef_metal_fog/fog_driver_aws'
|
12
11
|
require 'fog'
|
13
12
|
require 'fog/core'
|
14
13
|
require 'fog/compute'
|
15
|
-
require 'fog/aws'
|
16
14
|
require 'socket'
|
17
15
|
require 'etc'
|
18
16
|
require 'time'
|
@@ -25,14 +23,8 @@ module ChefMetalFog
|
|
25
23
|
# ## Fog Driver URLs
|
26
24
|
#
|
27
25
|
# All Metal drivers use URLs to uniquely identify a driver's "bucket" of machines.
|
28
|
-
# Fog URLs are of the form fog:<provider>:<identifier
|
29
|
-
#
|
30
|
-
# fog:AWS:<account_id>:<region>
|
31
|
-
# fog:AWS:<profile_name>
|
32
|
-
# fog:AWS:<profile_name>:<region>
|
33
|
-
# fog:OpenStack:https://identityHost:portNumber/v2.0
|
34
|
-
# fog:DigitalOcean:<client id>
|
35
|
-
# fog:Rackspace:https://identity.api.rackspacecloud.com/v2.0
|
26
|
+
# Fog URLs are of the form fog:<provider>:<identifier:> - see individual providers
|
27
|
+
# for sample URLs.
|
36
28
|
#
|
37
29
|
# Identifier is generally something uniquely identifying the account. If multiple
|
38
30
|
# users can access the account, the identifier should be the same for all of
|
@@ -102,6 +94,31 @@ module ChefMetalFog
|
|
102
94
|
:ssh_timeout => 20
|
103
95
|
}
|
104
96
|
|
97
|
+
class << self
|
98
|
+
alias :__new__ :new
|
99
|
+
|
100
|
+
def inherited(klass)
|
101
|
+
class << klass
|
102
|
+
alias :new :__new__
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
@@registered_provider_classes = {}
|
108
|
+
def self.register_provider_class(name, driver)
|
109
|
+
@@registered_provider_classes[name] = driver
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.provider_class_for(provider)
|
113
|
+
require "chef_metal_fog/providers/#{provider.downcase}"
|
114
|
+
@@registered_provider_classes[provider]
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.new(driver_url, config)
|
118
|
+
provider = driver_url.split(':')[1]
|
119
|
+
provider_class_for(provider).new(driver_url, config)
|
120
|
+
end
|
121
|
+
|
105
122
|
# Passed in a driver_url, and a config in the format of Driver.config.
|
106
123
|
def self.from_url(driver_url, config)
|
107
124
|
FogDriver.new(driver_url, config)
|
@@ -109,7 +126,7 @@ module ChefMetalFog
|
|
109
126
|
|
110
127
|
def self.canonicalize_url(driver_url, config)
|
111
128
|
_, provider, id = driver_url.split(':', 3)
|
112
|
-
config, id = compute_options_for(provider, id, config)
|
129
|
+
config, id = provider_class_for(provider).compute_options_for(provider, id, config)
|
113
130
|
[ "fog:#{provider}:#{id}", config ]
|
114
131
|
end
|
115
132
|
|
@@ -117,7 +134,7 @@ module ChefMetalFog
|
|
117
134
|
# know what it is yet) but which has the same keys
|
118
135
|
def self.from_provider(provider, config)
|
119
136
|
# Figure out the options and merge them into the config
|
120
|
-
config, id = compute_options_for(provider, nil, config)
|
137
|
+
config, id = provider_class_for(provider).compute_options_for(provider, nil, config)
|
121
138
|
|
122
139
|
driver_url = "fog:#{provider}:#{id}"
|
123
140
|
|
@@ -230,6 +247,10 @@ module ChefMetalFog
|
|
230
247
|
machine_options[key] || DEFAULT_OPTIONS[key]
|
231
248
|
end
|
232
249
|
|
250
|
+
def creator
|
251
|
+
raise "unsupported fog provider #{provider} (please implement #creator)"
|
252
|
+
end
|
253
|
+
|
233
254
|
def create_server(action_handler, machine_spec, machine_options)
|
234
255
|
if machine_spec.location
|
235
256
|
if machine_spec.location['driver_url'] != driver_url
|
@@ -255,21 +276,13 @@ module ChefMetalFog
|
|
255
276
|
server = nil
|
256
277
|
action_handler.report_progress description
|
257
278
|
if action_handler.should_perform_actions
|
258
|
-
creator = case provider
|
259
|
-
when 'AWS'
|
260
|
-
driver_options[:aws_account_info][:aws_username]
|
261
|
-
when 'OpenStack'
|
262
|
-
compute_options[:openstack_username]
|
263
|
-
when 'Rackspace'
|
264
|
-
compute_options[:rackspace_username]
|
265
|
-
end
|
266
279
|
server = compute.servers.create(bootstrap_options)
|
267
280
|
machine_spec.location = {
|
268
281
|
'driver_url' => driver_url,
|
269
282
|
'driver_version' => ChefMetalFog::VERSION,
|
270
283
|
'server_id' => server.id,
|
271
284
|
'creator' => creator,
|
272
|
-
'allocated_at' => Time.now.
|
285
|
+
'allocated_at' => Time.now.to_i
|
273
286
|
}
|
274
287
|
machine_spec.location['key_name'] = bootstrap_options[:key_name] if bootstrap_options[:key_name]
|
275
288
|
%w(is_windows ssh_username sudo use_private_ip_for_ssh ssh_gateway).each do |key|
|
@@ -291,7 +304,7 @@ module ChefMetalFog
|
|
291
304
|
if server.state == 'stopped'
|
292
305
|
action_handler.perform_action "start machine #{machine_spec.name} (#{server.id} on #{driver_url})" do
|
293
306
|
server.start
|
294
|
-
machine_spec.location['started_at'] = Time.now.
|
307
|
+
machine_spec.location['started_at'] = Time.now.to_i
|
295
308
|
end
|
296
309
|
machine_spec.save(action_handler)
|
297
310
|
end
|
@@ -300,20 +313,28 @@ module ChefMetalFog
|
|
300
313
|
def restart_server(action_handler, machine_spec, server)
|
301
314
|
action_handler.perform_action "restart machine #{machine_spec.name} (#{server.id} on #{driver_url})" do
|
302
315
|
server.reboot
|
303
|
-
machine_spec.location['started_at'] = Time.now.
|
316
|
+
machine_spec.location['started_at'] = Time.now.to_i
|
304
317
|
end
|
305
318
|
machine_spec.save(action_handler)
|
306
319
|
end
|
307
320
|
|
308
321
|
def remaining_wait_time(machine_spec, machine_options)
|
309
322
|
if machine_spec.location['started_at']
|
310
|
-
timeout = option_for(machine_options, :start_timeout) - (Time.now.utc -
|
323
|
+
timeout = option_for(machine_options, :start_timeout) - (Time.now.utc - parse_time(machine_spec.location['started_at']))
|
311
324
|
else
|
312
|
-
timeout = option_for(machine_options, :create_timeout) - (Time.now.utc -
|
325
|
+
timeout = option_for(machine_options, :create_timeout) - (Time.now.utc - parse_time(machine_spec.location['allocated_at']))
|
313
326
|
end
|
314
327
|
timeout > 0 ? timeout : 0.01
|
315
328
|
end
|
316
329
|
|
330
|
+
def parse_time(value)
|
331
|
+
if value.is_a?(String)
|
332
|
+
Time.parse(value)
|
333
|
+
else
|
334
|
+
Time.at(value)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
317
338
|
def wait_until_ready(action_handler, machine_spec, machine_options, server)
|
318
339
|
if !server.ready?
|
319
340
|
if action_handler.should_perform_actions
|
@@ -418,9 +439,15 @@ module ChefMetalFog
|
|
418
439
|
|
419
440
|
def bootstrap_options_for(action_handler, machine_spec, machine_options)
|
420
441
|
bootstrap_options = symbolize_keys(machine_options[:bootstrap_options] || {})
|
421
|
-
|
422
|
-
|
423
|
-
|
442
|
+
|
443
|
+
bootstrap_options[:tags] = default_tags(machine_spec, bootstrap_options[:tags] || {})
|
444
|
+
|
445
|
+
bootstrap_options[:name] ||= machine_spec.name
|
446
|
+
|
447
|
+
bootstrap_options
|
448
|
+
end
|
449
|
+
|
450
|
+
def default_tags(machine_spec, bootstrap_tags = {})
|
424
451
|
tags = {
|
425
452
|
'Name' => machine_spec.name,
|
426
453
|
'BootstrapId' => machine_spec.id,
|
@@ -428,36 +455,7 @@ module ChefMetalFog
|
|
428
455
|
'BootstrapUser' => Etc.getlogin
|
429
456
|
}
|
430
457
|
# User-defined tags override the ones we set
|
431
|
-
tags.merge
|
432
|
-
bootstrap_options.merge!({ :tags => tags })
|
433
|
-
|
434
|
-
# Provide reasonable defaults for DigitalOcean
|
435
|
-
if provider == 'DigitalOcean'
|
436
|
-
if !bootstrap_options[:image_id]
|
437
|
-
bootstrap_options[:image_name] ||= 'CentOS 6.4 x32'
|
438
|
-
bootstrap_options[:image_id] = compute.images.select { |image| image.name == bootstrap_options[:image_name] }.first.id
|
439
|
-
end
|
440
|
-
if !bootstrap_options[:flavor_id]
|
441
|
-
bootstrap_options[:flavor_name] ||= '512MB'
|
442
|
-
bootstrap_options[:flavor_id] = compute.flavors.select { |flavor| flavor.name == bootstrap_options[:flavor_name] }.first.id
|
443
|
-
end
|
444
|
-
if !bootstrap_options[:region_id]
|
445
|
-
bootstrap_options[:region_name] ||= 'San Francisco 1'
|
446
|
-
bootstrap_options[:region_id] = compute.regions.select { |region| region.name == bootstrap_options[:region_name] }.first.id
|
447
|
-
end
|
448
|
-
found_key = compute.ssh_keys.select { |k| k.name == bootstrap_options[:key_name] }.first
|
449
|
-
if !found_key
|
450
|
-
raise "Could not find key named '#{bootstrap_options[:key_name]}' on #{driver_url}"
|
451
|
-
end
|
452
|
-
bootstrap_options[:ssh_key_ids] ||= [ found_key.id ]
|
453
|
-
|
454
|
-
# You don't get to specify name yourself
|
455
|
-
bootstrap_options[:name] = machine_spec.name
|
456
|
-
end
|
457
|
-
|
458
|
-
bootstrap_options[:name] ||= machine_spec.name
|
459
|
-
|
460
|
-
bootstrap_options
|
458
|
+
tags.merge(bootstrap_tags)
|
461
459
|
end
|
462
460
|
|
463
461
|
def machine_for(machine_spec, machine_options, server = nil)
|
@@ -503,6 +501,8 @@ module ChefMetalFog
|
|
503
501
|
result[:key_data] = [ get_private_key(server.key_name) ]
|
504
502
|
elsif machine_spec.location['key_name']
|
505
503
|
result[:key_data] = [ get_private_key(machine_spec.location['key_name']) ]
|
504
|
+
elsif machine_options[:bootstrap_options][:key_path]
|
505
|
+
result[:key_data] = [ IO.read(machine_options[:bootstrap_options][:key_path]) ]
|
506
506
|
elsif machine_options[:bootstrap_options][:key_name]
|
507
507
|
result[:key_data] = [ get_private_key(machine_options[:bootstrap_options][:key_name]) ]
|
508
508
|
else
|
@@ -512,14 +512,13 @@ module ChefMetalFog
|
|
512
512
|
result
|
513
513
|
end
|
514
514
|
|
515
|
+
def default_ssh_username
|
516
|
+
'root'
|
517
|
+
end
|
518
|
+
|
515
519
|
def create_ssh_transport(machine_spec, machine_options, server)
|
516
520
|
ssh_options = ssh_options_for(machine_spec, machine_options, server)
|
517
|
-
|
518
|
-
if provider == 'AWS'
|
519
|
-
username = machine_spec.location['ssh_username'] || 'ubuntu'
|
520
|
-
else
|
521
|
-
username = machine_spec.location['ssh_username'] || 'root'
|
522
|
-
end
|
521
|
+
username = machine_spec.location['ssh_username'] || default_ssh_username
|
523
522
|
if machine_options.has_key?(:ssh_username) && machine_options[:ssh_username] != machine_spec.location['ssh_username']
|
524
523
|
Chef::Log.warn("Server #{machine_spec.name} was created with SSH username #{machine_spec.location['ssh_username']} and machine_options specifies username #{machine_options[:ssh_username]}. Using #{machine_spec.location['ssh_username']}. Please edit the node and change the metal.location.ssh_username attribute if you want to change it.")
|
525
524
|
end
|
@@ -548,136 +547,7 @@ module ChefMetalFog
|
|
548
547
|
end
|
549
548
|
|
550
549
|
def self.compute_options_for(provider, id, config)
|
551
|
-
|
552
|
-
compute_options = driver_options[:compute_options] || {}
|
553
|
-
new_compute_options = {}
|
554
|
-
new_compute_options[:provider] = provider
|
555
|
-
new_config = { :driver_options => { :compute_options => new_compute_options }}
|
556
|
-
new_defaults = {
|
557
|
-
:driver_options => { :compute_options => {} },
|
558
|
-
:machine_options => { :bootstrap_options => {} }
|
559
|
-
}
|
560
|
-
result = Cheffish::MergedConfig.new(new_config, config, new_defaults)
|
561
|
-
|
562
|
-
# Get data from the identifier in the URL
|
563
|
-
if id && id != ''
|
564
|
-
case provider
|
565
|
-
when 'AWS'
|
566
|
-
# AWS canonical URLs are of the form fog:AWS:
|
567
|
-
if id =~ /^(\d{12})(:(.+))?$/
|
568
|
-
if $2
|
569
|
-
id = $1
|
570
|
-
new_compute_options[:region] = $3
|
571
|
-
else
|
572
|
-
Chef::Log.warn("Old-style AWS URL #{id} from an early beta of chef-metal (before 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 metal.location.driver_url to include the region like so: fog:AWS:#{id}:<region> (e.g. us-east-1)")
|
573
|
-
end
|
574
|
-
else
|
575
|
-
# Assume it is a profile name, and set that.
|
576
|
-
aws_profile, region = id.split(':', 2)
|
577
|
-
new_config[:driver_options][:aws_profile] = aws_profile
|
578
|
-
new_compute_options[:region] = region
|
579
|
-
id = nil
|
580
|
-
end
|
581
|
-
when 'DigitalOcean'
|
582
|
-
new_compute_options[:digitalocean_client_id] = id
|
583
|
-
when 'OpenStack'
|
584
|
-
new_compute_options[:openstack_auth_url] = id
|
585
|
-
when 'Rackspace'
|
586
|
-
new_compute_options[:rackspace_auth_url] = id
|
587
|
-
when 'CloudStack'
|
588
|
-
cloudstack_uri = URI.parse(id)
|
589
|
-
new_compute_options[:cloudstack_scheme] = cloudstack_uri.scheme
|
590
|
-
new_compute_options[:cloudstack_host] = cloudstack_uri.host
|
591
|
-
new_compute_options[:cloudstack_port] = cloudstack_uri.port
|
592
|
-
new_compute_options[:cloudstack_path] = cloudstack_uri.path
|
593
|
-
else
|
594
|
-
raise "unsupported fog provider #{provider}"
|
595
|
-
end
|
596
|
-
end
|
597
|
-
|
598
|
-
# Set auth info from environment
|
599
|
-
case provider
|
600
|
-
when 'AWS'
|
601
|
-
# Grab the profile
|
602
|
-
aws_profile = FogDriverAWS.get_aws_profile(result[:driver_options], id)
|
603
|
-
new_compute_options[:aws_access_key_id] = aws_profile[:aws_access_key_id]
|
604
|
-
new_compute_options[:aws_secret_access_key] = aws_profile[:aws_secret_access_key]
|
605
|
-
new_compute_options[:aws_session_token] = aws_profile[:aws_security_token]
|
606
|
-
new_defaults[:driver_options][:compute_options][:region] = aws_profile[:region]
|
607
|
-
when 'OpenStack'
|
608
|
-
# TODO it is supposed to be unnecessary to load credentials from fog this way;
|
609
|
-
# why are we doing it?
|
610
|
-
# TODO support http://docs.openstack.org/cli-reference/content/cli_openrc.html
|
611
|
-
credential = Fog.credentials
|
612
|
-
|
613
|
-
new_compute_options[:openstack_username] ||= credential[:openstack_username]
|
614
|
-
new_compute_options[:openstack_api_key] ||= credential[:openstack_api_key]
|
615
|
-
new_compute_options[:openstack_auth_url] ||= credential[:openstack_auth_url]
|
616
|
-
new_compute_options[:openstack_tenant] ||= credential[:openstack_tenant]
|
617
|
-
when 'Rackspace'
|
618
|
-
credential = Fog.credentials
|
619
|
-
|
620
|
-
new_compute_options[:rackspace_username] ||= credential[:rackspace_username]
|
621
|
-
new_compute_options[:rackspace_api_key] ||= credential[:rackspace_api_key]
|
622
|
-
new_compute_options[:rackspace_auth_url] ||= credential[:rackspace_auth_url]
|
623
|
-
new_compute_options[:rackspace_region] ||= credential[:rackspace_region]
|
624
|
-
new_compute_options[:rackspace_endpoint] ||= credential[:rackspace_endpoint]
|
625
|
-
new_compute_options[:rackspace_compute_url] ||= credential[:rackspace_compute_url]
|
626
|
-
when 'DigitalOcean'
|
627
|
-
# This uses ~/.tugboat, generated by "tugboat authorize" - see https://github.com/pearkes/tugboat
|
628
|
-
tugboat_file = File.expand_path('~/.tugboat')
|
629
|
-
if File.exist?(tugboat_file)
|
630
|
-
tugboat_data = YAML.load(IO.read(tugboat_file))
|
631
|
-
new_compute_options.merge!(
|
632
|
-
:digitalocean_client_id => tugboat_data['authentication']['client_key'],
|
633
|
-
:digitalocean_api_key => tugboat_data['authentication']['api_key']
|
634
|
-
)
|
635
|
-
new_defaults[:machine_options].merge!(
|
636
|
-
#:ssh_username => tugboat_data['ssh']['ssh_user'],
|
637
|
-
:ssh_options => {
|
638
|
-
:port => tugboat_data['ssh']['ssh_port'],
|
639
|
-
# TODO we ignore ssh_key_path in favor of ssh_key / key_name stuff
|
640
|
-
#:key_data => [ IO.read(tugboat_data['ssh']['ssh_key_path']) ] # TODO use paths, not data?
|
641
|
-
}
|
642
|
-
)
|
643
|
-
|
644
|
-
# TODO verify that the key_name exists and matches the ssh key path
|
645
|
-
|
646
|
-
new_defaults[:machine_options][:bootstrap_options].merge!(
|
647
|
-
:region_id => tugboat_data['defaults']['region'].to_i,
|
648
|
-
:image_id => tugboat_data['defaults']['image'].to_i,
|
649
|
-
:size_id => tugboat_data['defaults']['region'].to_i,
|
650
|
-
:private_networking => tugboat_data['defaults']['private_networking'] == 'true',
|
651
|
-
:backups_enabled => tugboat_data['defaults']['backups_enabled'] == 'true',
|
652
|
-
)
|
653
|
-
ssh_key = tugboat_data['defaults']['ssh_key']
|
654
|
-
if ssh_key && ssh_key.size > 0
|
655
|
-
new_defaults[:machine_options][:bootstrap_options][:key_name] = ssh_key
|
656
|
-
end
|
657
|
-
end
|
658
|
-
end
|
659
|
-
|
660
|
-
id = case provider
|
661
|
-
when 'AWS'
|
662
|
-
account_info = FogDriverAWS.aws_account_info_for(result[:driver_options][:compute_options])
|
663
|
-
new_config[:driver_options][:aws_account_info] = account_info
|
664
|
-
"#{account_info[:aws_account_id]}:#{result[:driver_options][:compute_options][:region]}"
|
665
|
-
when 'DigitalOcean'
|
666
|
-
result[:driver_options][:compute_options][:digitalocean_client_id]
|
667
|
-
when 'OpenStack'
|
668
|
-
result[:driver_options][:compute_options][:openstack_auth_url]
|
669
|
-
when 'Rackspace'
|
670
|
-
result[:driver_options][:compute_options][:rackspace_auth_url]
|
671
|
-
when 'CloudStack'
|
672
|
-
host = result[:driver_options][:compute_options][:cloudstack_host]
|
673
|
-
path = result[:driver_options][:compute_options][:cloudstack_path] || '/client/api'
|
674
|
-
port = result[:driver_options][:compute_options][:cloudstack_port] || 443
|
675
|
-
scheme = result[:driver_options][:compute_options][:cloudstack_scheme] || 'https'
|
676
|
-
|
677
|
-
URI.scheme_list[scheme.upcase].build(:host => host, :port => port, :path => path).to_s
|
678
|
-
end
|
679
|
-
|
680
|
-
[ result, id ]
|
550
|
+
raise "unsupported fog provider #{provider}"
|
681
551
|
end
|
682
552
|
end
|
683
553
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'inifile'
|
2
|
+
require 'csv'
|
3
|
+
|
4
|
+
module ChefMetalFog
|
5
|
+
module Providers
|
6
|
+
class AWS
|
7
|
+
# Reads in a credentials file in Amazon's download format and presents the credentials to you
|
8
|
+
class Credentials
|
9
|
+
def initialize
|
10
|
+
@credentials = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
include Enumerable
|
14
|
+
|
15
|
+
def default
|
16
|
+
@credentials[ENV['AWS_DEFAULT_PROFILE'] || 'default'] || @credentials.first[1]
|
17
|
+
end
|
18
|
+
|
19
|
+
def keys
|
20
|
+
@credentials.keys
|
21
|
+
end
|
22
|
+
|
23
|
+
def [](name)
|
24
|
+
@credentials[name]
|
25
|
+
end
|
26
|
+
|
27
|
+
def each(&block)
|
28
|
+
@credentials.each(&block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def load_ini(credentials_ini_file)
|
32
|
+
inifile = IniFile.load(File.expand_path(credentials_ini_file))
|
33
|
+
inifile.each_section do |section|
|
34
|
+
if section =~ /^\s*profile\s+(.+)$/ || section =~ /^\s*(default)\s*/
|
35
|
+
profile_name = $1.strip
|
36
|
+
profile = inifile[section].inject({}) do |result, pair|
|
37
|
+
result[pair[0].to_sym] = pair[1]
|
38
|
+
result
|
39
|
+
end
|
40
|
+
profile[:name] = profile_name
|
41
|
+
@credentials[profile_name] = profile
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def load_csv(credentials_csv_file)
|
47
|
+
CSV.new(File.open(credentials_csv_file), :headers => :first_row).each do |row|
|
48
|
+
@credentials[row['User Name']] = {
|
49
|
+
:name => row['User Name'],
|
50
|
+
:user_name => row['User Name'],
|
51
|
+
:aws_access_key_id => row['Access Key Id'],
|
52
|
+
:aws_secret_access_key => row['Secret Access Key']
|
53
|
+
}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def load_default
|
58
|
+
load_ini(ENV['AWS_CONFIG_FILE'] || File.expand_path('~/.aws/config'))
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.method_missing(name, *args, &block)
|
62
|
+
singleton.send(name, *args, &block)
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.singleton
|
66
|
+
@aws_credentials ||= Credentials.new
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
require 'chef/log'
|
2
|
+
require 'fog/aws'
|
3
|
+
|
4
|
+
# fog:AWS:<account_id>:<region>
|
5
|
+
# fog:AWS:<profile_name>
|
6
|
+
# fog:AWS:<profile_name>:<region>
|
7
|
+
module ChefMetalFog
|
8
|
+
module Providers
|
9
|
+
class AWS < ChefMetalFog::FogDriver
|
10
|
+
|
11
|
+
require_relative 'aws/credentials'
|
12
|
+
|
13
|
+
ChefMetalFog::FogDriver.register_provider_class('AWS', ChefMetalFog::Providers::AWS)
|
14
|
+
|
15
|
+
def creator
|
16
|
+
driver_options[:aws_account_info][:aws_username]
|
17
|
+
end
|
18
|
+
|
19
|
+
def default_ssh_username
|
20
|
+
'ubuntu'
|
21
|
+
end
|
22
|
+
|
23
|
+
def bootstrap_options_for(action_handler, machine_spec, machine_options)
|
24
|
+
bootstrap_options = symbolize_keys(machine_options[:bootstrap_options] || {})
|
25
|
+
|
26
|
+
unless !bootstrap_options[:key_name]
|
27
|
+
bootstrap_options[:key_name] = overwrite_default_key_willy_nilly(action_handler)
|
28
|
+
end
|
29
|
+
|
30
|
+
bootstrap_options[:tags] = default_tags(machine_spec, bootstrap_options[:tags] || {})
|
31
|
+
|
32
|
+
bootstrap_options[:name] ||= machine_spec.name
|
33
|
+
|
34
|
+
bootstrap_options
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.get_aws_profile(driver_options, aws_account_id)
|
38
|
+
aws_credentials = get_aws_credentials(driver_options)
|
39
|
+
compute_options = driver_options[:compute_options] || {}
|
40
|
+
|
41
|
+
# Order of operations:
|
42
|
+
# compute_options[:aws_access_key_id] / compute_options[:aws_secret_access_key] / compute_options[:aws_security_token] / compute_options[:region]
|
43
|
+
# compute_options[:aws_profile]
|
44
|
+
# ENV['AWS_ACCESS_KEY_ID'] / ENV['AWS_SECRET_ACCESS_KEY'] / ENV['AWS_SECURITY_TOKEN'] / ENV['AWS_REGION']
|
45
|
+
# ENV['AWS_PROFILE']
|
46
|
+
# ENV['DEFAULT_PROFILE']
|
47
|
+
# 'default'
|
48
|
+
aws_profile = if compute_options[:aws_access_key_id]
|
49
|
+
Chef::Log.debug("Using AWS driver access key options")
|
50
|
+
{
|
51
|
+
:aws_access_key_id => compute_options[:aws_access_key_id],
|
52
|
+
:aws_secret_access_key => compute_options[:aws_secret_access_key],
|
53
|
+
:aws_security_token => compute_options[:aws_session_token],
|
54
|
+
:region => compute_options[:region]
|
55
|
+
}
|
56
|
+
elsif driver_options[:aws_profile]
|
57
|
+
Chef::Log.debug("Using AWS profile #{driver_options[:aws_profile]}")
|
58
|
+
aws_credentials[driver_options[:aws_profile]]
|
59
|
+
elsif ENV['AWS_ACCESS_KEY_ID']
|
60
|
+
Chef::Log.debug("Using AWS environment variable access keys")
|
61
|
+
{
|
62
|
+
:aws_access_key_id => ENV['AWS_ACCESS_KEY_ID'],
|
63
|
+
:aws_secret_access_key => ENV['AWS_SECRET_ACCESS_KEY'],
|
64
|
+
:aws_security_token => ENV['AWS_SECURITY_TOKEN'],
|
65
|
+
:region => ENV['AWS_REGION']
|
66
|
+
}
|
67
|
+
elsif ENV['AWS_PROFILE']
|
68
|
+
Chef::Log.debug("Using AWS profile #{ENV['AWS_PROFILE']} from AWS_PROFILE environment variable")
|
69
|
+
aws_credentials[ENV['AWS_PROFILE']]
|
70
|
+
else
|
71
|
+
Chef::Log.debug("Using AWS default profile")
|
72
|
+
aws_credentials.default
|
73
|
+
end
|
74
|
+
|
75
|
+
# Merge in account info for profile
|
76
|
+
if aws_profile
|
77
|
+
aws_profile = aws_profile.merge(aws_account_info_for(aws_profile))
|
78
|
+
end
|
79
|
+
|
80
|
+
# If no profile is found (or the profile is not the right account), search
|
81
|
+
# for a profile that matches the given account ID
|
82
|
+
if aws_account_id && (!aws_profile || aws_profile[:aws_account_id] != aws_account_id)
|
83
|
+
aws_profile = find_aws_profile_for_account_id(aws_credentials, aws_account_id)
|
84
|
+
end
|
85
|
+
|
86
|
+
if !aws_profile
|
87
|
+
raise "No AWS profile specified! Are you missing something in the Chef config or ~/.aws/config?"
|
88
|
+
end
|
89
|
+
|
90
|
+
aws_profile.delete_if { |key, value| value.nil? }
|
91
|
+
aws_profile
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.find_aws_profile_for_account_id(aws_credentials, aws_account_id)
|
95
|
+
aws_profile = nil
|
96
|
+
aws_credentials.each do |profile_name, profile|
|
97
|
+
begin
|
98
|
+
aws_account_info = aws_account_info_for(profile)
|
99
|
+
rescue
|
100
|
+
Chef::Log.warn("Could not connect to AWS profile #{aws_credentials[:name]}: #{$!}")
|
101
|
+
Chef::Log.debug($!.backtrace.join("\n"))
|
102
|
+
next
|
103
|
+
end
|
104
|
+
if aws_account_info[:aws_account_id] == aws_account_id
|
105
|
+
aws_profile = profile
|
106
|
+
aws_profile[:name] = profile_name
|
107
|
+
aws_profile = aws_profile.merge(aws_account_info)
|
108
|
+
break
|
109
|
+
end
|
110
|
+
end
|
111
|
+
if aws_profile
|
112
|
+
Chef::Log.info("Discovered AWS profile #{aws_profile[:name]} pointing at account #{aws_account_id}. Using ...")
|
113
|
+
else
|
114
|
+
raise "No AWS profile leads to account ##{aws_account_id}. Do you need to add profiles to ~/.aws/config?"
|
115
|
+
end
|
116
|
+
aws_profile
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.aws_account_info_for(aws_profile)
|
120
|
+
@@aws_account_info ||= {}
|
121
|
+
@@aws_account_info[aws_profile[:aws_access_key_id]] ||= begin
|
122
|
+
options = {
|
123
|
+
:aws_access_key_id => aws_profile[:aws_access_key_id],
|
124
|
+
:aws_secret_access_key => aws_profile[:aws_secret_access_key],
|
125
|
+
:aws_session_token => aws_profile[:aws_security_token]
|
126
|
+
}
|
127
|
+
options.delete_if { |key, value| value.nil? }
|
128
|
+
|
129
|
+
iam = Fog::AWS::IAM.new(options)
|
130
|
+
arn = begin
|
131
|
+
# TODO it would be nice if Fog let you do this normally ...
|
132
|
+
iam.send(:request, {
|
133
|
+
'Action' => 'GetUser',
|
134
|
+
:parser => Fog::Parsers::AWS::IAM::GetUser.new
|
135
|
+
}).body['User']['Arn']
|
136
|
+
rescue Fog::AWS::IAM::Error
|
137
|
+
# TODO Someone tell me there is a better way to find out your current
|
138
|
+
# user ID than this! This is what happens when you use an IAM user
|
139
|
+
# with default privileges.
|
140
|
+
if $!.message =~ /AccessDenied.+(arn:aws:iam::\d+:\S+)/
|
141
|
+
arn = $1
|
142
|
+
else
|
143
|
+
raise
|
144
|
+
end
|
145
|
+
end
|
146
|
+
arn_split = arn.split(':', 6)
|
147
|
+
{
|
148
|
+
:aws_account_id => arn_split[4],
|
149
|
+
:aws_username => arn_split[5],
|
150
|
+
:aws_user_arn => arn
|
151
|
+
}
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.get_aws_credentials(driver_options)
|
156
|
+
# Grab the list of possible credentials
|
157
|
+
if driver_options[:aws_credentials]
|
158
|
+
aws_credentials = driver_options[:aws_credentials]
|
159
|
+
else
|
160
|
+
aws_credentials = Credentials.new
|
161
|
+
if driver_options[:aws_config_file]
|
162
|
+
aws_credentials.load_ini(driver_options.delete(:aws_config_file))
|
163
|
+
elsif driver_options[:aws_csv_file]
|
164
|
+
aws_credentials.load_csv(driver_options.delete(:aws_csv_file))
|
165
|
+
else
|
166
|
+
aws_credentials.load_default
|
167
|
+
end
|
168
|
+
end
|
169
|
+
aws_credentials
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.compute_options_for(provider, id, config)
|
173
|
+
new_compute_options = {}
|
174
|
+
new_compute_options[:provider] = provider
|
175
|
+
new_config = { :driver_options => { :compute_options => new_compute_options }}
|
176
|
+
new_defaults = {
|
177
|
+
:driver_options => { :compute_options => {} },
|
178
|
+
:machine_options => { :bootstrap_options => {} }
|
179
|
+
}
|
180
|
+
result = Cheffish::MergedConfig.new(new_config, config, new_defaults)
|
181
|
+
|
182
|
+
if id && id != ''
|
183
|
+
# AWS canonical URLs are of the form fog:AWS:
|
184
|
+
if id =~ /^(\d{12})(:(.+))?$/
|
185
|
+
if $2
|
186
|
+
id = $1
|
187
|
+
new_compute_options[:region] = $3
|
188
|
+
else
|
189
|
+
Chef::Log.warn("Old-style AWS URL #{id} from an early beta of chef-metal (before 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 metal.location.driver_url to include the region like so: fog:AWS:#{id}:<region> (e.g. us-east-1)")
|
190
|
+
end
|
191
|
+
else
|
192
|
+
# Assume it is a profile name, and set that.
|
193
|
+
aws_profile, region = id.split(':', 2)
|
194
|
+
new_config[:driver_options][:aws_profile] = aws_profile
|
195
|
+
new_compute_options[:region] = region
|
196
|
+
id = nil
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
aws_profile = get_aws_profile(result[:driver_options], id)
|
201
|
+
new_compute_options[:aws_access_key_id] = aws_profile[:aws_access_key_id]
|
202
|
+
new_compute_options[:aws_secret_access_key] = aws_profile[:aws_secret_access_key]
|
203
|
+
new_compute_options[:aws_session_token] = aws_profile[:aws_security_token]
|
204
|
+
new_defaults[:driver_options][:compute_options][:region] = aws_profile[:region]
|
205
|
+
|
206
|
+
account_info = aws_account_info_for(result[:driver_options][:compute_options])
|
207
|
+
new_config[:driver_options][:aws_account_info] = account_info
|
208
|
+
id = "#{account_info[:aws_account_id]}:#{result[:driver_options][:compute_options][:region]}"
|
209
|
+
|
210
|
+
[result, id]
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ChefMetalFog
|
2
|
+
module Providers
|
3
|
+
class CloudStack < ChefMetalFog::FogDriver
|
4
|
+
|
5
|
+
ChefMetalFog::FogDriver.register_provider_class('CloudStack', ChefMetalFog::Providers::CloudStack)
|
6
|
+
|
7
|
+
def self.compute_options_for(provider, id, config)
|
8
|
+
new_compute_options = {}
|
9
|
+
new_compute_options[:provider] = provider
|
10
|
+
new_config = { :driver_options => { :compute_options => new_compute_options }}
|
11
|
+
new_defaults = {
|
12
|
+
:driver_options => { :compute_options => {} },
|
13
|
+
:machine_options => { :bootstrap_options => {} }
|
14
|
+
}
|
15
|
+
result = Cheffish::MergedConfig.new(new_config, config, new_defaults)
|
16
|
+
|
17
|
+
if id && id != ''
|
18
|
+
cloudstack_uri = URI.parse(id)
|
19
|
+
new_compute_options[:cloudstack_scheme] = cloudstack_uri.scheme
|
20
|
+
new_compute_options[:cloudstack_host] = cloudstack_uri.host
|
21
|
+
new_compute_options[:cloudstack_port] = cloudstack_uri.port
|
22
|
+
new_compute_options[:cloudstack_path] = cloudstack_uri.path
|
23
|
+
end
|
24
|
+
|
25
|
+
host = result[:driver_options][:compute_options][:cloudstack_host]
|
26
|
+
path = result[:driver_options][:compute_options][:cloudstack_path] || '/client/api'
|
27
|
+
port = result[:driver_options][:compute_options][:cloudstack_port] || 443
|
28
|
+
scheme = result[:driver_options][:compute_options][:cloudstack_scheme] || 'https'
|
29
|
+
id = URI.scheme_list[scheme.upcase].build(:host => host, :port => port, :path => path).to_s
|
30
|
+
|
31
|
+
[result, id]
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# fog:DigitalOcean:<client id>
|
2
|
+
module ChefMetalFog
|
3
|
+
module Providers
|
4
|
+
class DigitalOcean < ChefMetalFog::FogDriver
|
5
|
+
ChefMetalFog::FogDriver.register_provider_class('DigitalOcean', ChefMetalFog::Providers::DigitalOcean)
|
6
|
+
|
7
|
+
def creator
|
8
|
+
''
|
9
|
+
end
|
10
|
+
|
11
|
+
def bootstrap_options_for(action_handler, machine_spec, machine_options)
|
12
|
+
bootstrap_options = symbolize_keys(machine_options[:bootstrap_options] || {})
|
13
|
+
if bootstrap_options[:key_path]
|
14
|
+
bootstrap_options[:key_name] ||= File.basename(bootstrap_options[:key_path])
|
15
|
+
# Verify that the provided key name and path are in line (or create the key pair if not!)
|
16
|
+
driver = self
|
17
|
+
ChefMetal.inline_resource(action_handler) do
|
18
|
+
fog_key_pair bootstrap_options[:key_name] do
|
19
|
+
private_key_path bootstrap_options[:key_path]
|
20
|
+
driver driver
|
21
|
+
end
|
22
|
+
end
|
23
|
+
else
|
24
|
+
bootstrap_options[:key_name] = overwrite_default_key_willy_nilly(action_handler)
|
25
|
+
end
|
26
|
+
|
27
|
+
bootstrap_options[:tags] = default_tags(machine_spec, bootstrap_options[:tags] || {})
|
28
|
+
|
29
|
+
if !bootstrap_options[:image_id]
|
30
|
+
bootstrap_options[:image_name] ||= 'CentOS 6.4 x32'
|
31
|
+
bootstrap_options[:image_id] = compute.images.select { |image| image.name == bootstrap_options[:image_name] }.first.id
|
32
|
+
end
|
33
|
+
if !bootstrap_options[:flavor_id]
|
34
|
+
bootstrap_options[:flavor_name] ||= '512MB'
|
35
|
+
bootstrap_options[:flavor_id] = compute.flavors.select { |flavor| flavor.name == bootstrap_options[:flavor_name] }.first.id
|
36
|
+
end
|
37
|
+
if !bootstrap_options[:region_id]
|
38
|
+
bootstrap_options[:region_name] ||= 'San Francisco 1'
|
39
|
+
bootstrap_options[:region_id] = compute.regions.select { |region| region.name == bootstrap_options[:region_name] }.first.id
|
40
|
+
end
|
41
|
+
found_key = compute.ssh_keys.select { |k| k.name == bootstrap_options[:key_name] }.first
|
42
|
+
if !found_key
|
43
|
+
raise "Could not find key named '#{bootstrap_options[:key_name]}' on #{driver_url}"
|
44
|
+
end
|
45
|
+
bootstrap_options[:ssh_key_ids] ||= [ found_key.id ]
|
46
|
+
|
47
|
+
# You don't get to specify name yourself
|
48
|
+
bootstrap_options[:name] = machine_spec.name
|
49
|
+
|
50
|
+
bootstrap_options
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.compute_options_for(provider, id, config)
|
54
|
+
new_compute_options = {}
|
55
|
+
new_compute_options[:provider] = provider
|
56
|
+
new_config = { :driver_options => { :compute_options => new_compute_options }}
|
57
|
+
new_defaults = {
|
58
|
+
:driver_options => { :compute_options => {} },
|
59
|
+
:machine_options => { :bootstrap_options => {} }
|
60
|
+
}
|
61
|
+
result = Cheffish::MergedConfig.new(new_config, config, new_defaults)
|
62
|
+
|
63
|
+
new_compute_options[:digitalocean_client_id] = id if (id && id != '')
|
64
|
+
|
65
|
+
# This uses ~/.tugboat, generated by "tugboat authorize" - see https://github.com/pearkes/tugboat
|
66
|
+
tugboat_file = File.expand_path('~/.tugboat')
|
67
|
+
if File.exist?(tugboat_file)
|
68
|
+
tugboat_data = YAML.load(IO.read(tugboat_file))
|
69
|
+
new_compute_options.merge!(
|
70
|
+
:digitalocean_client_id => tugboat_data['authentication']['client_key'],
|
71
|
+
:digitalocean_api_key => tugboat_data['authentication']['api_key']
|
72
|
+
)
|
73
|
+
new_defaults[:machine_options].merge!(
|
74
|
+
#:ssh_username => tugboat_data['ssh']['ssh_user'],
|
75
|
+
:ssh_options => {
|
76
|
+
:port => tugboat_data['ssh']['ssh_port'],
|
77
|
+
# TODO we ignore ssh_key_path in favor of ssh_key / key_name stuff
|
78
|
+
#:key_data => [ IO.read(tugboat_data['ssh']['ssh_key_path']) ] # TODO use paths, not data?
|
79
|
+
}
|
80
|
+
)
|
81
|
+
|
82
|
+
# TODO verify that the key_name exists and matches the ssh key path
|
83
|
+
|
84
|
+
new_defaults[:machine_options][:bootstrap_options].merge!(
|
85
|
+
:region_id => tugboat_data['defaults']['region'].to_i,
|
86
|
+
:image_id => tugboat_data['defaults']['image'].to_i,
|
87
|
+
:size_id => tugboat_data['defaults']['region'].to_i,
|
88
|
+
:private_networking => tugboat_data['defaults']['private_networking'] == 'true',
|
89
|
+
:backups_enabled => tugboat_data['defaults']['backups_enabled'] == 'true',
|
90
|
+
)
|
91
|
+
if tugboat_data['ssh']['ssh_key_path']
|
92
|
+
new_defaults[:machine_options][:bootstrap_options][:key_path] = tugboat_data['ssh']['ssh_key_path']
|
93
|
+
end
|
94
|
+
ssh_key = tugboat_data['defaults']['ssh_key']
|
95
|
+
if ssh_key && ssh_key.size > 0
|
96
|
+
new_defaults[:machine_options][:bootstrap_options][:key_name] = ssh_key
|
97
|
+
end
|
98
|
+
end
|
99
|
+
id = result[:driver_options][:compute_options][:digitalocean_client_id]
|
100
|
+
|
101
|
+
[result, id]
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# fog:OpenStack:https://identifyhost:portNumber/v2.0
|
2
|
+
module ChefMetalFog
|
3
|
+
module Providers
|
4
|
+
class OpenStack < ChefMetalFog::FogDriver
|
5
|
+
|
6
|
+
ChefMetalFog::FogDriver.register_provider_class('OpenStack', ChefMetalFog::Providers::OpenStack)
|
7
|
+
|
8
|
+
def creator
|
9
|
+
compute_options[:openstack_username]
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.compute_options_for(provider, id, config)
|
13
|
+
new_compute_options = {}
|
14
|
+
new_compute_options[:provider] = provider
|
15
|
+
new_config = { :driver_options => { :compute_options => new_compute_options }}
|
16
|
+
new_defaults = {
|
17
|
+
:driver_options => { :compute_options => {} },
|
18
|
+
:machine_options => { :bootstrap_options => {} }
|
19
|
+
}
|
20
|
+
result = Cheffish::MergedConfig.new(new_config, config, new_defaults)
|
21
|
+
|
22
|
+
new_compute_options[:openstack_auth_url] = id if (id && id != '')
|
23
|
+
credential = Fog.credentials
|
24
|
+
|
25
|
+
new_compute_options[:openstack_username] ||= credential[:openstack_username]
|
26
|
+
new_compute_options[:openstack_api_key] ||= credential[:openstack_api_key]
|
27
|
+
new_compute_options[:openstack_auth_url] ||= credential[:openstack_auth_url]
|
28
|
+
new_compute_options[:openstack_tenant] ||= credential[:openstack_tenant]
|
29
|
+
|
30
|
+
id = result[:driver_options][:compute_options][:openstack_auth_url]
|
31
|
+
|
32
|
+
[result, id]
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# fog:Rackspace:https://identity.api.rackspacecloud.com/v2.0
|
2
|
+
module ChefMetalFog
|
3
|
+
module Providers
|
4
|
+
class Rackspace < ChefMetalFog::FogDriver
|
5
|
+
|
6
|
+
ChefMetalFog::FogDriver.register_provider_class('Rackspace', ChefMetalFog::Providers::Rackspace)
|
7
|
+
|
8
|
+
def creator
|
9
|
+
compute_options[:rackspace_username]
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.compute_options_for(provider, id, config)
|
13
|
+
new_compute_options = {}
|
14
|
+
new_compute_options[:provider] = provider
|
15
|
+
new_config = { :driver_options => { :compute_options => new_compute_options }}
|
16
|
+
new_defaults = {
|
17
|
+
:driver_options => { :compute_options => {} },
|
18
|
+
:machine_options => { :bootstrap_options => {} }
|
19
|
+
}
|
20
|
+
result = Cheffish::MergedConfig.new(new_config, config, new_defaults)
|
21
|
+
|
22
|
+
new_compute_options[:rackspace_auth_url] = id if (id && id != '')
|
23
|
+
credential = Fog.credentials
|
24
|
+
|
25
|
+
new_compute_options[:rackspace_username] ||= credential[:rackspace_username]
|
26
|
+
new_compute_options[:rackspace_api_key] ||= credential[:rackspace_api_key]
|
27
|
+
new_compute_options[:rackspace_auth_url] ||= credential[:rackspace_auth_url]
|
28
|
+
new_compute_options[:rackspace_region] ||= credential[:rackspace_region]
|
29
|
+
new_compute_options[:rackspace_endpoint] ||= credential[:rackspace_endpoint]
|
30
|
+
|
31
|
+
id = result[:driver_options][:compute_options][:rackspace_auth_url]
|
32
|
+
|
33
|
+
[result, id]
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
$:.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
$:.unshift File.expand_path('../support', __FILE__)
|
3
|
+
require 'fog'
|
4
|
+
require 'chef_metal'
|
5
|
+
require 'chef_metal_fog'
|
6
|
+
|
7
|
+
RSpec.configure do |config|
|
8
|
+
config.run_all_when_everything_filtered = true
|
9
|
+
config.filter_run :focus
|
10
|
+
|
11
|
+
# Run specs in random order to surface order dependencies. If you find an
|
12
|
+
# order dependency and want to debug it, you can fix the order by providing
|
13
|
+
# the seed, which is printed after each run.
|
14
|
+
# --seed 1234
|
15
|
+
config.order = 'random'
|
16
|
+
end
|
17
|
+
|
18
|
+
Fog.mock!
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module ChefMetalFog::Providers
|
2
|
+
class TestDriver < ChefMetalFog::FogDriver
|
3
|
+
ChefMetalFog::FogDriver.register_provider_class('TestDriver', ChefMetalFog::Providers::TestDriver)
|
4
|
+
|
5
|
+
attr_reader :config
|
6
|
+
def initialize(driver_url, config)
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.compute_options_for(provider, id, config)
|
11
|
+
[config, 'test']
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'chef_metal_fog/fog_driver'
|
3
|
+
|
4
|
+
describe ChefMetalFog::FogDriver do
|
5
|
+
|
6
|
+
describe ".from_url" do
|
7
|
+
subject { ChefMetalFog::FogDriver.from_provider('TestDriver', {}) }
|
8
|
+
|
9
|
+
it "should return the correct class" do
|
10
|
+
expect(subject).to be_an_instance_of ChefMetalFog::Providers::TestDriver
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should call the target compute_options_for" do
|
14
|
+
expect(ChefMetalFog::Providers::TestDriver).to receive(:compute_options_for)
|
15
|
+
.with('TestDriver', anything, {}).and_return([{}, 'test']).twice
|
16
|
+
subject
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "when creating a new class" do
|
22
|
+
it "should return the correct class" do
|
23
|
+
test = ChefMetalFog::FogDriver.new('fog:TestDriver:foo', {})
|
24
|
+
expect(test).to be_an_instance_of ChefMetalFog::Providers::TestDriver
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should populate config" do
|
28
|
+
test = ChefMetalFog::FogDriver.new('fog:TestDriver:foo', {test: "metal"})
|
29
|
+
expect(test.config[:test]).to eq "metal"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'chef_metal_fog/providers/aws/credentials'
|
2
|
+
|
3
|
+
describe ChefMetalFog::Providers::AWS::Credentials do
|
4
|
+
describe "#load_ini" do
|
5
|
+
let(:aws_credentials_ini_file) { File.join(File.expand_path('../../../../support', __FILE__), 'aws/ini-file.ini') }
|
6
|
+
|
7
|
+
before do
|
8
|
+
described_class.load_ini(aws_credentials_ini_file)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should load a default profile" do
|
12
|
+
expect(described_class['default']).to include(:aws_access_key_id)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should load the correct values" do
|
16
|
+
expect(described_class['default'][:aws_access_key_id]).to eq "12345"
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should load several profiles" do
|
20
|
+
expect(described_class.keys.length).to eq 2
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#load_csv" do
|
25
|
+
let(:aws_credentials_csv_file) { File.join(File.expand_path('../../../../support', __FILE__), 'aws/config-file.csv') }
|
26
|
+
before do
|
27
|
+
described_class.load_csv(aws_credentials_csv_file)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should load a single profile" do
|
31
|
+
expect(described_class['default']).to include(:aws_access_key_id)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should load the correct values" do
|
35
|
+
expect(described_class['default'][:aws_access_key_id]).to eq "12345"
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should load several profiles" do
|
39
|
+
expect(described_class.keys.length).to eq 2
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'chef_metal_fog/fog_driver'
|
2
|
+
require 'chef_metal_fog/providers/rackspace'
|
3
|
+
|
4
|
+
describe ChefMetalFog::Providers::Rackspace do
|
5
|
+
subject { ChefMetalFog::FogDriver.from_provider('Rackspace',{}) }
|
6
|
+
|
7
|
+
it "returns the correct driver" do
|
8
|
+
expect(subject).to be_an_instance_of ChefMetalFog::Providers::Rackspace
|
9
|
+
end
|
10
|
+
|
11
|
+
it "has a fog backend" do
|
12
|
+
pending unless Fog.mock?
|
13
|
+
expect(subject.compute).to be_an_instance_of Fog::Compute::RackspaceV2::Mock
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: chef-metal-fog
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: '0.6'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Keiser
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-06-
|
11
|
+
date: 2014-06-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: chef
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0.4'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: chef-metal
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.12'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.12'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: fog
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -95,11 +109,22 @@ files:
|
|
95
109
|
- lib/chef/resource/fog_key_pair.rb
|
96
110
|
- lib/chef_metal/driver_init/fog.rb
|
97
111
|
- lib/chef_metal_fog.rb
|
98
|
-
- lib/chef_metal_fog/aws_credentials.rb
|
99
112
|
- lib/chef_metal_fog/fog_driver.rb
|
100
|
-
- lib/chef_metal_fog/
|
113
|
+
- lib/chef_metal_fog/providers/aws.rb
|
114
|
+
- lib/chef_metal_fog/providers/aws/credentials.rb
|
115
|
+
- lib/chef_metal_fog/providers/cloudstack.rb
|
116
|
+
- lib/chef_metal_fog/providers/digitalocean.rb
|
117
|
+
- lib/chef_metal_fog/providers/openstack.rb
|
118
|
+
- lib/chef_metal_fog/providers/rackspace.rb
|
101
119
|
- lib/chef_metal_fog/recipe_dsl.rb
|
102
120
|
- lib/chef_metal_fog/version.rb
|
121
|
+
- spec/spec_helper.rb
|
122
|
+
- spec/support/aws/config-file.csv
|
123
|
+
- spec/support/aws/ini-file.ini
|
124
|
+
- spec/support/chef_metal_fog/providers/testdriver.rb
|
125
|
+
- spec/unit/fog_driver_spec.rb
|
126
|
+
- spec/unit/providers/aws/credentials_spec.rb
|
127
|
+
- spec/unit/providers/rackspace_spec.rb
|
103
128
|
homepage: https://github.com/opscode/chef-metal-fog
|
104
129
|
licenses: []
|
105
130
|
metadata: {}
|
@@ -1,67 +0,0 @@
|
|
1
|
-
require 'inifile'
|
2
|
-
require 'csv'
|
3
|
-
|
4
|
-
module ChefMetalFog
|
5
|
-
# Reads in a credentials file in Amazon's download format and presents the credentials to you
|
6
|
-
class AWSCredentials
|
7
|
-
def initialize
|
8
|
-
@credentials = {}
|
9
|
-
end
|
10
|
-
|
11
|
-
include Enumerable
|
12
|
-
|
13
|
-
def default
|
14
|
-
@credentials[ENV['AWS_DEFAULT_PROFILE'] || 'default'] || @credentials.first[1]
|
15
|
-
end
|
16
|
-
|
17
|
-
def keys
|
18
|
-
@credentials.keys
|
19
|
-
end
|
20
|
-
|
21
|
-
def [](name)
|
22
|
-
@credentials[name]
|
23
|
-
end
|
24
|
-
|
25
|
-
def each(&block)
|
26
|
-
@credentials.each(&block)
|
27
|
-
end
|
28
|
-
|
29
|
-
def load_ini(credentials_ini_file)
|
30
|
-
inifile = IniFile.load(File.expand_path(credentials_ini_file))
|
31
|
-
inifile.each_section do |section|
|
32
|
-
if section =~ /^\s*profile\s+(.+)$/ || section =~ /^\s*(default)\s*/
|
33
|
-
profile_name = $1.strip
|
34
|
-
profile = inifile[section].inject({}) do |result, pair|
|
35
|
-
result[pair[0].to_sym] = pair[1]
|
36
|
-
result
|
37
|
-
end
|
38
|
-
profile[:name] = profile_name
|
39
|
-
@credentials[profile_name] = profile
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
def load_csv(credentials_csv_file)
|
45
|
-
CSV.new(File.open(credentials_csv_file), :headers => :first_row).each do |row|
|
46
|
-
@credentials[row['User Name']] = {
|
47
|
-
:name => row['User Name'],
|
48
|
-
:user_name => row['User Name'],
|
49
|
-
:aws_access_key_id => row['Access Key Id'],
|
50
|
-
:aws_secret_access_key => row['Secret Access Key']
|
51
|
-
}
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def load_default
|
56
|
-
load_ini(ENV['AWS_CONFIG_FILE'] || File.expand_path('~/.aws/config'))
|
57
|
-
end
|
58
|
-
|
59
|
-
def self.method_missing(name, *args, &block)
|
60
|
-
singleton.send(name, *args, &block)
|
61
|
-
end
|
62
|
-
|
63
|
-
def self.singleton
|
64
|
-
@aws_credentials ||= AWSCredentials.new
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
@@ -1,142 +0,0 @@
|
|
1
|
-
require 'chef_metal_fog/aws_credentials'
|
2
|
-
require 'chef/log'
|
3
|
-
require 'fog/aws'
|
4
|
-
|
5
|
-
module ChefMetalFog
|
6
|
-
module FogDriverAWS
|
7
|
-
def self.get_aws_profile(driver_options, aws_account_id)
|
8
|
-
aws_credentials = get_aws_credentials(driver_options)
|
9
|
-
compute_options = driver_options[:compute_options] || {}
|
10
|
-
|
11
|
-
# Order of operations:
|
12
|
-
# compute_options[:aws_access_key_id] / compute_options[:aws_secret_access_key] / compute_options[:aws_security_token] / compute_options[:region]
|
13
|
-
# compute_options[:aws_profile]
|
14
|
-
# ENV['AWS_ACCESS_KEY_ID'] / ENV['AWS_SECRET_ACCESS_KEY'] / ENV['AWS_SECURITY_TOKEN'] / ENV['AWS_REGION']
|
15
|
-
# ENV['AWS_PROFILE']
|
16
|
-
# ENV['DEFAULT_PROFILE']
|
17
|
-
# 'default'
|
18
|
-
aws_profile = if compute_options[:aws_access_key_id]
|
19
|
-
Chef::Log.debug("Using AWS driver access key options")
|
20
|
-
{
|
21
|
-
:aws_access_key_id => compute_options[:aws_access_key_id],
|
22
|
-
:aws_secret_access_key => compute_options[:aws_secret_access_key],
|
23
|
-
:aws_security_token => compute_options[:aws_session_token],
|
24
|
-
:region => compute_options[:region]
|
25
|
-
}
|
26
|
-
elsif driver_options[:aws_profile]
|
27
|
-
Chef::Log.debug("Using AWS profile #{driver_options[:aws_profile]}")
|
28
|
-
aws_credentials[driver_options[:aws_profile]]
|
29
|
-
elsif ENV['AWS_ACCESS_KEY_ID']
|
30
|
-
Chef::Log.debug("Using AWS environment variable access keys")
|
31
|
-
{
|
32
|
-
:aws_access_key_id => ENV['AWS_ACCESS_KEY_ID'],
|
33
|
-
:aws_secret_access_key => ENV['AWS_SECRET_ACCESS_KEY'],
|
34
|
-
:aws_security_token => ENV['AWS_SECURITY_TOKEN'],
|
35
|
-
:region => ENV['AWS_REGION']
|
36
|
-
}
|
37
|
-
elsif ENV['AWS_PROFILE']
|
38
|
-
Chef::Log.debug("Using AWS profile #{ENV['AWS_PROFILE']} from AWS_PROFILE environment variable")
|
39
|
-
aws_credentials[ENV['AWS_PROFILE']]
|
40
|
-
else
|
41
|
-
Chef::Log.debug("Using AWS default profile")
|
42
|
-
aws_credentials.default
|
43
|
-
end
|
44
|
-
|
45
|
-
# Merge in account info for profile
|
46
|
-
if aws_profile
|
47
|
-
aws_profile = aws_profile.merge(aws_account_info_for(aws_profile))
|
48
|
-
end
|
49
|
-
|
50
|
-
# If no profile is found (or the profile is not the right account), search
|
51
|
-
# for a profile that matches the given account ID
|
52
|
-
if aws_account_id && (!aws_profile || aws_profile[:aws_account_id] != aws_account_id)
|
53
|
-
aws_profile = find_aws_profile_for_account_id(aws_credentials, aws_account_id)
|
54
|
-
end
|
55
|
-
|
56
|
-
if !aws_profile
|
57
|
-
raise "No AWS profile specified! Are you missing something in the Chef config or ~/.aws/config?"
|
58
|
-
end
|
59
|
-
|
60
|
-
aws_profile.delete_if { |key, value| value.nil? }
|
61
|
-
aws_profile
|
62
|
-
end
|
63
|
-
|
64
|
-
def self.find_aws_profile_for_account_id(aws_credentials, aws_account_id)
|
65
|
-
aws_profile = nil
|
66
|
-
aws_credentials.each do |profile_name, profile|
|
67
|
-
begin
|
68
|
-
aws_account_info = aws_account_info_for(profile)
|
69
|
-
rescue
|
70
|
-
Chef::Log.warn("Could not connect to AWS profile #{aws_credentials[:name]}: #{$!}")
|
71
|
-
Chef::Log.debug($!.backtrace.join("\n"))
|
72
|
-
next
|
73
|
-
end
|
74
|
-
if aws_account_info[:aws_account_id] == aws_account_id
|
75
|
-
aws_profile = profile
|
76
|
-
aws_profile[:name] = profile_name
|
77
|
-
aws_profile = aws_profile.merge(aws_account_info)
|
78
|
-
break
|
79
|
-
end
|
80
|
-
end
|
81
|
-
if aws_profile
|
82
|
-
Chef::Log.info("Discovered AWS profile #{aws_profile[:name]} pointing at account #{aws_account_id}. Using ...")
|
83
|
-
else
|
84
|
-
raise "No AWS profile leads to account ##{aws_account_id}. Do you need to add profiles to ~/.aws/config?"
|
85
|
-
end
|
86
|
-
aws_profile
|
87
|
-
end
|
88
|
-
|
89
|
-
def self.aws_account_info_for(aws_profile)
|
90
|
-
@@aws_account_info ||= {}
|
91
|
-
@@aws_account_info[aws_profile[:aws_access_key_id]] ||= begin
|
92
|
-
options = {
|
93
|
-
:aws_access_key_id => aws_profile[:aws_access_key_id],
|
94
|
-
:aws_secret_access_key => aws_profile[:aws_secret_access_key],
|
95
|
-
:aws_session_token => aws_profile[:aws_security_token]
|
96
|
-
}
|
97
|
-
options.delete_if { |key, value| value.nil? }
|
98
|
-
|
99
|
-
iam = Fog::AWS::IAM.new(options)
|
100
|
-
arn = begin
|
101
|
-
# TODO it would be nice if Fog let you do this normally ...
|
102
|
-
iam.send(:request, {
|
103
|
-
'Action' => 'GetUser',
|
104
|
-
:parser => Fog::Parsers::AWS::IAM::GetUser.new
|
105
|
-
}).body['User']['Arn']
|
106
|
-
rescue Fog::AWS::IAM::Error
|
107
|
-
# TODO Someone tell me there is a better way to find out your current
|
108
|
-
# user ID than this! This is what happens when you use an IAM user
|
109
|
-
# with default privileges.
|
110
|
-
if $!.message =~ /AccessDenied.+(arn:aws:iam::\d+:\S+)/
|
111
|
-
arn = $1
|
112
|
-
else
|
113
|
-
raise
|
114
|
-
end
|
115
|
-
end
|
116
|
-
arn_split = arn.split(':', 6)
|
117
|
-
{
|
118
|
-
:aws_account_id => arn_split[4],
|
119
|
-
:aws_username => arn_split[5],
|
120
|
-
:aws_user_arn => arn
|
121
|
-
}
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
def self.get_aws_credentials(driver_options)
|
126
|
-
# Grab the list of possible credentials
|
127
|
-
if driver_options[:aws_credentials]
|
128
|
-
aws_credentials = driver_options[:aws_credentials]
|
129
|
-
else
|
130
|
-
aws_credentials = AWSCredentials.new
|
131
|
-
if driver_options[:aws_config_file]
|
132
|
-
aws_credentials.load_ini(driver_options.delete(:aws_config_file))
|
133
|
-
elsif driver_options[:aws_csv_file]
|
134
|
-
aws_credentials.load_csv(driver_options.delete(:aws_csv_file))
|
135
|
-
else
|
136
|
-
aws_credentials.load_default
|
137
|
-
end
|
138
|
-
end
|
139
|
-
aws_credentials
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|