chef-provisioning-aws 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/README.md +12 -0
- data/Rakefile +6 -0
- data/lib/chef/provider/aws_auto_scaling_group.rb +44 -0
- data/lib/chef/provider/aws_ebs_volume.rb +59 -0
- data/lib/chef/provider/aws_key_pair.rb +187 -0
- data/lib/chef/provider/aws_launch_config.rb +43 -0
- data/lib/chef/provider/aws_provider.rb +89 -0
- data/lib/chef/provider/aws_s3_bucket.rb +41 -0
- data/lib/chef/provider/aws_sns_topic.rb +39 -0
- data/lib/chef/provider/aws_sqs_queue.rb +47 -0
- data/lib/chef/provisioning/aws_driver.rb +3 -0
- data/lib/chef/provisioning/aws_driver/aws_profile.rb +73 -0
- data/lib/chef/provisioning/aws_driver/credentials.rb +84 -0
- data/lib/chef/provisioning/aws_driver/driver.rb +396 -0
- data/lib/chef/provisioning/aws_driver/resources.rb +7 -0
- data/lib/chef/provisioning/aws_driver/version.rb +7 -0
- data/lib/chef/provisioning/driver_init/aws.rb +3 -0
- data/lib/chef/resource/aws_auto_scaling_group.rb +16 -0
- data/lib/chef/resource/aws_ebs_volume.rb +31 -0
- data/lib/chef/resource/aws_key_pair.rb +29 -0
- data/lib/chef/resource/aws_launch_config.rb +14 -0
- data/lib/chef/resource/aws_resource.rb +10 -0
- data/lib/chef/resource/aws_s3_bucket.rb +21 -0
- data/lib/chef/resource/aws_sns_topic.rb +20 -0
- data/lib/chef/resource/aws_sqs_queue.rb +24 -0
- metadata +141 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'chef/provider/aws_provider'
|
2
|
+
|
3
|
+
class Chef::Provider::AwsLaunchConfig < Chef::Provider::AwsProvider
|
4
|
+
action :create do
|
5
|
+
if existing_launch_config.nil?
|
6
|
+
converge_by "Creating new Launch Config #{id} in #{new_resource.region_name}" do
|
7
|
+
@existing_launch_config = auto_scaling.launch_configurations.create(
|
8
|
+
new_resource.name,
|
9
|
+
new_resource.image,
|
10
|
+
new_resource.instance_type
|
11
|
+
)
|
12
|
+
|
13
|
+
new_resource.save
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
action :delete do
|
19
|
+
if existing_launch_config
|
20
|
+
converge_by "Deleting Launch Config #{id} in #{new_resource.region_name}" do
|
21
|
+
begin
|
22
|
+
existing_launch_config.delete
|
23
|
+
rescue AWS::AutoScaling::Errors::ResourceInUse
|
24
|
+
sleep 5
|
25
|
+
retry
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
new_resource.delete
|
31
|
+
end
|
32
|
+
|
33
|
+
def existing_launch_config
|
34
|
+
@existing_launch_config ||= begin
|
35
|
+
elc = auto_scaling.launch_configurations[new_resource.name]
|
36
|
+
elc.exists? ? elc : nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def id
|
41
|
+
new_resource.name
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'chef/provider/lwrp_base'
|
2
|
+
require 'chef/provisioning/aws_driver/credentials'
|
3
|
+
|
4
|
+
class Chef::Provider::AwsProvider < Chef::Provider::LWRPBase
|
5
|
+
use_inline_resources
|
6
|
+
|
7
|
+
attr_reader :credentials
|
8
|
+
|
9
|
+
# All these need to implement whyrun
|
10
|
+
def whyrun_supported?
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
14
|
+
def from_hash hash
|
15
|
+
hash.each do |k,v|
|
16
|
+
begin
|
17
|
+
self.instance_variable_set("@#{k}", v)
|
18
|
+
rescue NameError => ne
|
19
|
+
# nothing...
|
20
|
+
end
|
21
|
+
end
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def resource_from_databag(fqn, chef_server = Cheffish.default_chef_server)
|
26
|
+
chef_api = Cheffish.chef_server_api(chef_server)
|
27
|
+
begin
|
28
|
+
data = chef_api.get("/data/#{databag_name}/#{fqn}")
|
29
|
+
rescue Net::HTTPServerException => e
|
30
|
+
if e.response.code == '404'
|
31
|
+
nil
|
32
|
+
else
|
33
|
+
raise
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(*args)
|
39
|
+
super
|
40
|
+
# TODO - temporary, needs to be better
|
41
|
+
@credentials = Chef::Provisioning::AWSDriver::Credentials.new
|
42
|
+
@credentials.load_default
|
43
|
+
credentials = @credentials.default
|
44
|
+
AWS.config(:access_key_id => credentials[:aws_access_key_id],
|
45
|
+
:secret_access_key => credentials[:aws_secret_access_key])
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.databag_name
|
49
|
+
raise 'Class does not implement databag name'
|
50
|
+
end
|
51
|
+
|
52
|
+
def fqn
|
53
|
+
if id
|
54
|
+
id
|
55
|
+
else
|
56
|
+
"#{new_resource.name}_#{new_resource.region_name}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# AWS objects we might need
|
61
|
+
def ec2
|
62
|
+
@ec2 ||= aws_init { AWS::EC2.new }
|
63
|
+
end
|
64
|
+
|
65
|
+
def sns
|
66
|
+
@sns ||= aws_init { AWS::SNS.new }
|
67
|
+
end
|
68
|
+
|
69
|
+
def sqs
|
70
|
+
@sqs ||= aws_init { AWS::SQS.new }
|
71
|
+
end
|
72
|
+
|
73
|
+
def s3
|
74
|
+
@s3 ||= aws_init { AWS::S3.new }
|
75
|
+
end
|
76
|
+
|
77
|
+
def auto_scaling
|
78
|
+
@auto_scaling ||= aws_init { AWS::AutoScaling.new }
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
def aws_init
|
83
|
+
credentials = @credentials.default
|
84
|
+
region = new_resource.region_name || credentials[:region]
|
85
|
+
# Pivot region
|
86
|
+
AWS.config(:region => region)
|
87
|
+
yield
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'chef/provider/aws_provider'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
class Chef::Provider::AwsS3Bucket < Chef::Provider::AwsProvider
|
5
|
+
action :create do
|
6
|
+
if existing_bucket == nil
|
7
|
+
converge_by "Creating new S3 bucket #{fqn}" do
|
8
|
+
bucket = s3.buckets.create(fqn)
|
9
|
+
bucket.tags['Name'] = new_resource.name
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
new_resource.save
|
14
|
+
end
|
15
|
+
|
16
|
+
action :delete do
|
17
|
+
if existing_bucket
|
18
|
+
converge_by "Deleting S3 bucket #{fqn}" do
|
19
|
+
existing_bucket.delete
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
new_resource.delete
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def existing_bucket
|
28
|
+
Chef::Log.debug("Checking for S3 bucket #{fqn}")
|
29
|
+
@existing_bucket ||= s3.buckets[fqn] if s3.buckets[fqn].exists?
|
30
|
+
end
|
31
|
+
|
32
|
+
# Fully qualified bucket name (i.e resource_region unless otherwise specified)
|
33
|
+
def id
|
34
|
+
new_resource.bucket_name
|
35
|
+
end
|
36
|
+
|
37
|
+
def fqn
|
38
|
+
super.gsub('_', '-')
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'chef/provider/aws_provider'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
class Chef::Provider::AwsSnsTopic < Chef::Provider::AwsProvider
|
5
|
+
|
6
|
+
action :create do
|
7
|
+
if existing_topic == nil
|
8
|
+
converge_by "Creating new SNS topic #{fqn} in #{new_resource.region_name}" do
|
9
|
+
sns.topics.create(fqn)
|
10
|
+
|
11
|
+
new_resource.created_at DataTime.now.to_s
|
12
|
+
new_resource.save
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
action :delete do
|
18
|
+
if existing_topic
|
19
|
+
converge_by "Deleting SNS topic #{fqn} in #{new_resource.region_name}" do
|
20
|
+
existing_topic.delete
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
new_resource.delete
|
25
|
+
end
|
26
|
+
|
27
|
+
def existing_topic
|
28
|
+
@existing_topic ||= begin
|
29
|
+
sns.topics.named(fqn)
|
30
|
+
rescue
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def id
|
36
|
+
new_resource.topic_name
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'chef/provider/aws_provider'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
class Chef::Provider::AwsSqsQueue < Chef::Provider::AwsProvider
|
5
|
+
|
6
|
+
action :create do
|
7
|
+
if existing_queue == nil
|
8
|
+
converge_by "Creating new SQS queue #{fqn} in #{new_resource.region_name}" do
|
9
|
+
loop do
|
10
|
+
begin
|
11
|
+
sqs.queues.create(fqn)
|
12
|
+
break
|
13
|
+
rescue AWS::SQS::Errors::QueueDeletedRecently
|
14
|
+
sleep 5
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
new_resource.created_at DateTime.now.to_s
|
19
|
+
new_resource.save
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
action :delete do
|
25
|
+
if existing_queue
|
26
|
+
converge_by "Deleting SQS queue #{fqn} in #{new_resource.region_name}" do
|
27
|
+
existing_queue.delete
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
new_resource.delete
|
32
|
+
end
|
33
|
+
|
34
|
+
def existing_queue
|
35
|
+
@existing_queue ||= begin
|
36
|
+
sqs.queues.named(fqn)
|
37
|
+
rescue
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Fully qualified queue name (i.e luigi:us-east-1)
|
43
|
+
def id
|
44
|
+
new_resource.queue_name
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
class AwsProfile
|
2
|
+
|
3
|
+
# Order of operations:
|
4
|
+
# compute_options[:aws_access_key_id] / compute_options[:aws_secret_access_key] / compute_options[:aws_security_token] / compute_options[:region]
|
5
|
+
# compute_options[:aws_profile]
|
6
|
+
# ENV['AWS_ACCESS_KEY_ID'] / ENV['AWS_SECRET_ACCESS_KEY'] / ENV['AWS_SECURITY_TOKEN'] / ENV['AWS_REGION']
|
7
|
+
# ENV['AWS_PROFILE']
|
8
|
+
# ENV['DEFAULT_PROFILE']
|
9
|
+
# 'default'
|
10
|
+
def initialize(driver_options, aws_account_id)
|
11
|
+
aws_credentials = get_aws_credentials(driver_options)
|
12
|
+
compute_options = driver_options[:compute_options] || {}
|
13
|
+
|
14
|
+
aws_profile = if compute_options[:aws_access_key_id]
|
15
|
+
Chef::Log.debug('Using AWS driver access key options')
|
16
|
+
{
|
17
|
+
:aws_access_key_id => compute_options[:aws_access_key_id],
|
18
|
+
:aws_secret_access_key => compute_options[:aws_secret_access_key],
|
19
|
+
:aws_security_token => compute_options[:aws_session_token],
|
20
|
+
:region => compute_options[:region]
|
21
|
+
}
|
22
|
+
elsif driver_options[:aws_profile]
|
23
|
+
Chef::Log.debug("Using AWS profile #{driver_options[:aws_profile]}")
|
24
|
+
aws_credentials[driver_options[:aws_profile]]
|
25
|
+
elsif ENV['AWS_ACCESS_KEY_ID'] || ENV['AWS_ACCESS_KEY']
|
26
|
+
Chef::Log.debug('Using AWS environment variable access keys')
|
27
|
+
{
|
28
|
+
:aws_access_key_id => ENV['AWS_ACCESS_KEY_ID'] || ENV['AWS_ACCESS_KEY'],
|
29
|
+
:aws_secret_access_key => ENV['AWS_SECRET_ACCESS_KEY'] || ENV['AWS_SECRET_KEY'],
|
30
|
+
:aws_security_token => ENV['AWS_SECURITY_TOKEN'],
|
31
|
+
:region => ENV['AWS_REGION']
|
32
|
+
}
|
33
|
+
elsif ENV['AWS_PROFILE']
|
34
|
+
Chef::Log.debug("Using AWS profile #{ENV['AWS_PROFILE']} from AWS_PROFILE environment variable")
|
35
|
+
aws_credentials[ENV['AWS_PROFILE']]
|
36
|
+
else
|
37
|
+
Chef::Log.debug('Using AWS default profile')
|
38
|
+
aws_credentials.default
|
39
|
+
end
|
40
|
+
# Endpoint configuration
|
41
|
+
if compute_options[:ec2_endpoint]
|
42
|
+
aws_profile[:ec2_endpoint] = compute_options[:ec2_endpoint]
|
43
|
+
elsif ENV['EC2_URL']
|
44
|
+
aws_profile[:ec2_endpoint] = ENV['EC2_URL']
|
45
|
+
end
|
46
|
+
if compute_options[:iam_endpoint]
|
47
|
+
aws_profile[:iam_endpoint] = compute_options[:iam_endpoint]
|
48
|
+
elsif ENV['AWS_IAM_URL']
|
49
|
+
aws_profile[:iam_endpoint] = ENV['AWS_IAM_URL']
|
50
|
+
else
|
51
|
+
aws_profile[:iam_endpoint] = 'https://iam.amazonaws.com/'
|
52
|
+
end
|
53
|
+
|
54
|
+
# Merge in account info for profile
|
55
|
+
if aws_profile
|
56
|
+
aws_profile = aws_profile.merge(aws_account_info_for(aws_profile))
|
57
|
+
end
|
58
|
+
|
59
|
+
# If no profile is found (or the profile is not the right account), search
|
60
|
+
# for a profile that matches the given account ID
|
61
|
+
if aws_account_id && (!aws_profile || aws_profile[:aws_account_id] != aws_account_id)
|
62
|
+
aws_profile = find_aws_profile_for_account_id(aws_credentials, aws_account_id)
|
63
|
+
end
|
64
|
+
|
65
|
+
unless aws_profile
|
66
|
+
raise 'No AWS profile specified! Are you missing something in the Chef config or ~/.aws/config?'
|
67
|
+
end
|
68
|
+
|
69
|
+
aws_profile.delete_if { |_, value| value.nil? }
|
70
|
+
aws_profile
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'inifile'
|
2
|
+
require 'csv'
|
3
|
+
|
4
|
+
class Chef
|
5
|
+
module Provisioning
|
6
|
+
module AWSDriver
|
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
|
+
load_default
|
12
|
+
end
|
13
|
+
|
14
|
+
include Enumerable
|
15
|
+
|
16
|
+
def default
|
17
|
+
if @credentials.size == 0
|
18
|
+
raise 'No credentials loaded! Do you have a ~/.aws/config?'
|
19
|
+
end
|
20
|
+
@credentials[ENV['AWS_DEFAULT_PROFILE'] || 'default'] || @credentials.first[1]
|
21
|
+
end
|
22
|
+
|
23
|
+
def keys
|
24
|
+
@credentials.keys
|
25
|
+
end
|
26
|
+
|
27
|
+
def [](name)
|
28
|
+
@credentials[name]
|
29
|
+
end
|
30
|
+
|
31
|
+
def each(&block)
|
32
|
+
@credentials.each(&block)
|
33
|
+
end
|
34
|
+
|
35
|
+
def load_ini(credentials_ini_file)
|
36
|
+
inifile = IniFile.load(File.expand_path(credentials_ini_file))
|
37
|
+
if inifile
|
38
|
+
inifile.each_section do |section|
|
39
|
+
if section =~ /^\s*profile\s+(.+)$/ || section =~ /^\s*(default)\s*/
|
40
|
+
profile_name = $1.strip
|
41
|
+
profile = inifile[section].inject({}) do |result, pair|
|
42
|
+
result[pair[0].to_sym] = pair[1]
|
43
|
+
result
|
44
|
+
end
|
45
|
+
profile[:name] = profile_name
|
46
|
+
@credentials[profile_name] = profile
|
47
|
+
end
|
48
|
+
end
|
49
|
+
else
|
50
|
+
# Get it to throw an error
|
51
|
+
File.open(File.expand_path(credentials_ini_file)) do
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def load_csv(credentials_csv_file)
|
57
|
+
CSV.new(File.open(credentials_csv_file), :headers => :first_row).each do |row|
|
58
|
+
@credentials[row['User Name']] = {
|
59
|
+
:name => row['User Name'],
|
60
|
+
:user_name => row['User Name'],
|
61
|
+
:aws_access_key_id => row['Access Key Id'],
|
62
|
+
:aws_secret_access_key => row['Secret Access Key']
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def load_default
|
68
|
+
config_file = ENV['AWS_CONFIG_FILE'] || File.expand_path('~/.aws/config')
|
69
|
+
if File.file?(config_file)
|
70
|
+
load_ini(config_file)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.method_missing(name, *args, &block)
|
75
|
+
singleton.send(name, *args, &block)
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.singleton
|
79
|
+
@aws_credentials ||= Credentials.new
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,396 @@
|
|
1
|
+
require 'chef/mixin/shell_out'
|
2
|
+
require 'chef/provisioning/driver'
|
3
|
+
require 'chef/provisioning/convergence_strategy/install_cached'
|
4
|
+
require 'chef/provisioning/convergence_strategy/install_sh'
|
5
|
+
require 'chef/provisioning/convergence_strategy/no_converge'
|
6
|
+
require 'chef/provisioning/transport/ssh'
|
7
|
+
require 'chef/provisioning/machine/windows_machine'
|
8
|
+
require 'chef/provisioning/machine/unix_machine'
|
9
|
+
require 'chef/provisioning/machine_spec'
|
10
|
+
|
11
|
+
require 'chef/provisioning/aws_driver/version'
|
12
|
+
require 'chef/provisioning/aws_driver/credentials'
|
13
|
+
|
14
|
+
require 'yaml'
|
15
|
+
require 'aws-sdk-v1'
|
16
|
+
|
17
|
+
|
18
|
+
class Chef
|
19
|
+
module Provisioning
|
20
|
+
module AWSDriver
|
21
|
+
# Provisions machines using the AWS SDK
|
22
|
+
class Driver < Chef::Provisioning::Driver
|
23
|
+
|
24
|
+
include Chef::Mixin::ShellOut
|
25
|
+
|
26
|
+
attr_reader :region
|
27
|
+
|
28
|
+
# URL scheme:
|
29
|
+
# aws:account_id:region
|
30
|
+
# TODO: migration path from fog:AWS - parse that URL
|
31
|
+
# canonical URL calls realpath on <path>
|
32
|
+
def self.from_url(driver_url, config)
|
33
|
+
Driver.new(driver_url, config)
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(driver_url, config)
|
37
|
+
super
|
38
|
+
credentials = aws_credentials.default
|
39
|
+
@region = credentials[:region]
|
40
|
+
# TODO: fix credentials here
|
41
|
+
AWS.config(:access_key_id => credentials[:aws_access_key_id],
|
42
|
+
:secret_access_key => credentials[:aws_secret_access_key],
|
43
|
+
:region => credentials[:region])
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.canonicalize_url(driver_url, config)
|
47
|
+
url = driver_url.split(":")[0]
|
48
|
+
[ "aws:#{url}", config ]
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
# Load balancer methods
|
53
|
+
def allocate_load_balancer(action_handler, lb_spec, lb_options)
|
54
|
+
existing_elb = load_balancer_for(lb_spec)
|
55
|
+
if !existing_elb.exists?
|
56
|
+
lb_spec.location = {
|
57
|
+
'driver_url' => driver_url,
|
58
|
+
'driver_version' => Chef::Provisioning::AWSDriver::VERSION,
|
59
|
+
'allocated_at' => Time.now.utc.to_s,
|
60
|
+
'host_node' => action_handler.host_node,
|
61
|
+
}
|
62
|
+
|
63
|
+
availability_zones = lb_options[:availability_zones]
|
64
|
+
listeners = lb_options[:listeners]
|
65
|
+
elb.load_balancers.create(lb_spec.name,
|
66
|
+
:availability_zones => availability_zones,
|
67
|
+
:listeners => listeners)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def ready_load_balancer(action_handler, lb_spec, lb_options)
|
72
|
+
end
|
73
|
+
|
74
|
+
def destroy_load_balancer(action_handler, lb_spec, lb_options)
|
75
|
+
end
|
76
|
+
|
77
|
+
def update_load_balancer(action_handler, lb_spec, lb_options, opts = {})
|
78
|
+
existing_elb = load_balancer_for(lb_spec)
|
79
|
+
# TODO update listeners and zones?
|
80
|
+
if existing_elb.exists?
|
81
|
+
machines = opts[:machines]
|
82
|
+
existing_instance_ids = existing_elb.instances.collect { |i| i.instance_id }
|
83
|
+
new_instance_ids = machines.keys.collect do |machine_name|
|
84
|
+
machine_spec = machines[machine_name]
|
85
|
+
machine_spec.location['instance_id']
|
86
|
+
end
|
87
|
+
|
88
|
+
instance_ids_to_add = new_instance_ids - existing_instance_ids
|
89
|
+
instance_ids_to_remove = existing_instance_ids - new_instance_ids
|
90
|
+
|
91
|
+
if instance_ids_to_add && instance_ids_to_add.size > 0
|
92
|
+
existing_elb.instances.add(instance_ids_to_add)
|
93
|
+
end
|
94
|
+
|
95
|
+
if instance_ids_to_remove && instance_ids_to_remove.size > 0
|
96
|
+
existing_elb.instances.remove(instance_ids_to_remove)
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
# Image methods
|
104
|
+
def allocate_image(action_handler, image_spec, image_options, machine_spec)
|
105
|
+
end
|
106
|
+
|
107
|
+
def ready_image(action_handler, image_spec, image_options)
|
108
|
+
end
|
109
|
+
|
110
|
+
def destroy_image(action_handler, image_spec, image_options)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Machine methods
|
114
|
+
def allocate_machine(action_handler, machine_spec, machine_options)
|
115
|
+
existing_instance = instance_for(machine_spec)
|
116
|
+
if existing_instance == nil || !existing_instance.exists?
|
117
|
+
|
118
|
+
machine_spec.location = {
|
119
|
+
'driver_url' => driver_url,
|
120
|
+
'driver_version' => Chef::Provisioning::AWSDriver::VERSION,
|
121
|
+
'allocated_at' => Time.now.utc.to_s,
|
122
|
+
'host_node' => action_handler.host_node,
|
123
|
+
'image_id' => machine_options[:image_id]
|
124
|
+
}
|
125
|
+
|
126
|
+
image_id = machine_options[:image_id] || default_ami_for_region(@region)
|
127
|
+
action_handler.report_progress "Creating #{machine_spec.name} with AMI #{image_id} in #{@region}..."
|
128
|
+
bootstrap_options = machine_options[:bootstrap_options] || {}
|
129
|
+
bootstrap_options[:image_id] = image_id
|
130
|
+
Chef::Log.debug "AWS Bootstrap options: #{bootstrap_options.inspect}"
|
131
|
+
instance = ec2.instances.create(bootstrap_options)
|
132
|
+
instance.tags['Name'] = machine_spec.name
|
133
|
+
machine_spec.location['instance_id'] = instance.id
|
134
|
+
action_handler.report_progress "Created #{instance.id} in #{@region}..."
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def ready_machine(action_handler, machine_spec, machine_options)
|
139
|
+
instance = instance_for(machine_spec)
|
140
|
+
|
141
|
+
if instance.nil?
|
142
|
+
raise "Machine #{machine_spec.name} does not have an instance associated with it, or instance does not exist."
|
143
|
+
end
|
144
|
+
|
145
|
+
if instance.status != :running
|
146
|
+
action_handler.report_progress "Starting #{machine_spec.name} (#{machine_spec.location['instance_id']}) in #{@region}..."
|
147
|
+
wait_until_ready(action_handler, machine_spec)
|
148
|
+
wait_for_transport(action_handler, machine_spec, machine_options)
|
149
|
+
else
|
150
|
+
action_handler.report_progress "#{machine_spec.name} (#{machine_spec.location['instance_id']}) already running in #{@region}..."
|
151
|
+
end
|
152
|
+
|
153
|
+
machine_for(machine_spec, machine_options, instance)
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
def destroy_machine(action_handler, machine_spec, machine_options)
|
158
|
+
instance = instance_for(machine_spec)
|
159
|
+
if instance
|
160
|
+
instance.terminate
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
|
166
|
+
|
167
|
+
private
|
168
|
+
def machine_for(machine_spec, machine_options, instance = nil)
|
169
|
+
instance ||= instance_for(machine_spec)
|
170
|
+
|
171
|
+
if !instance
|
172
|
+
raise "Instance for node #{machine_spec.name} has not been created!"
|
173
|
+
end
|
174
|
+
|
175
|
+
if machine_spec.location['is_windows']
|
176
|
+
Chef::Provisioning::Machine::WindowsMachine.new(machine_spec, transport_for(machine_spec, machine_options, instance), convergence_strategy_for(machine_spec, machine_options))
|
177
|
+
else
|
178
|
+
Chef::Provisioning::Machine::UnixMachine.new(machine_spec, transport_for(machine_spec, machine_options, instance), convergence_strategy_for(machine_spec, machine_options))
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def start_machine(action_handler, machine_spec, machine_options, base_image_name)
|
183
|
+
end
|
184
|
+
|
185
|
+
def ec2
|
186
|
+
@ec2 ||= AWS.ec2
|
187
|
+
end
|
188
|
+
|
189
|
+
def elb
|
190
|
+
@elb ||= AWS::ELB.new
|
191
|
+
end
|
192
|
+
|
193
|
+
def default_ssh_username
|
194
|
+
'ubuntu'
|
195
|
+
end
|
196
|
+
|
197
|
+
def keypair_for(bootstrap_options)
|
198
|
+
if bootstrap_options[:key_name]
|
199
|
+
keypair_name = bootstrap_options[:key_name]
|
200
|
+
existing_key_pair = ec2.key_pairs[keypair_name]
|
201
|
+
if !existing_key_pair.exists?
|
202
|
+
ec2.key_pairs.create(keypair_name)
|
203
|
+
end
|
204
|
+
existing_key_pair
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def load_balancer_for(lb_spec)
|
209
|
+
if lb_spec.name
|
210
|
+
elb.load_balancers[lb_spec.name]
|
211
|
+
else
|
212
|
+
nil
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def instance_for(machine_spec)
|
217
|
+
if machine_spec.location && machine_spec.location['instance_id']
|
218
|
+
ec2.instances[machine_spec.location['instance_id']]
|
219
|
+
else
|
220
|
+
nil
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def transport_for(machine_spec, machine_options, instance)
|
225
|
+
# TODO winrm
|
226
|
+
create_ssh_transport(machine_spec, machine_options, instance)
|
227
|
+
end
|
228
|
+
|
229
|
+
def compute_options
|
230
|
+
|
231
|
+
end
|
232
|
+
|
233
|
+
def aws_credentials
|
234
|
+
# Grab the list of possible credentials
|
235
|
+
@aws_credentials ||= if driver_options[:aws_credentials]
|
236
|
+
driver_options[:aws_credentials]
|
237
|
+
else
|
238
|
+
credentials = Credentials.new
|
239
|
+
if driver_options[:aws_config_file]
|
240
|
+
credentials.load_ini(driver_options.delete(:aws_config_file))
|
241
|
+
elsif driver_options[:aws_csv_file]
|
242
|
+
credentials.load_csv(driver_options.delete(:aws_csv_file))
|
243
|
+
else
|
244
|
+
credentials.load_default
|
245
|
+
end
|
246
|
+
credentials
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def default_ami_for_region(region)
|
251
|
+
Chef::Log.debug("Choosing default AMI for region '#{region}'")
|
252
|
+
|
253
|
+
case region
|
254
|
+
when 'ap-northeast-1'
|
255
|
+
'ami-c786dcc6'
|
256
|
+
when 'ap-southeast-1'
|
257
|
+
'ami-eefca7bc'
|
258
|
+
when 'ap-southeast-2'
|
259
|
+
'ami-996706a3'
|
260
|
+
when 'eu-west-1'
|
261
|
+
'ami-4ab46b3d'
|
262
|
+
when 'sa-east-1'
|
263
|
+
'ami-6770d87a'
|
264
|
+
when 'us-east-1'
|
265
|
+
'ami-d2ff23ba'
|
266
|
+
when 'us-west-1'
|
267
|
+
'ami-73717d36'
|
268
|
+
when 'us-west-2'
|
269
|
+
'ami-f1ce8bc1'
|
270
|
+
else
|
271
|
+
raise 'Unsupported region!'
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def create_ssh_transport(machine_spec, machine_options, instance)
|
276
|
+
ssh_options = ssh_options_for(machine_spec, machine_options, instance)
|
277
|
+
username = machine_spec.location['ssh_username'] || default_ssh_username
|
278
|
+
if machine_options.has_key?(:ssh_username) && machine_options[:ssh_username] != machine_spec.location['ssh_username']
|
279
|
+
Chef::Log.warn("Server #{machine_spec.name} was created with SSH username #{machine_spec.location['ssh_username']} and machine_options specifies username #{machine_options[:ssh_username]}. Using #{machine_spec.location['ssh_username']}. Please edit the node and change the chef_provisioning.location.ssh_username attribute if you want to change it.")
|
280
|
+
end
|
281
|
+
options = {}
|
282
|
+
if machine_spec.location[:sudo] || (!machine_spec.location.has_key?(:sudo) && username != 'root')
|
283
|
+
options[:prefix] = 'sudo '
|
284
|
+
end
|
285
|
+
|
286
|
+
remote_host = nil
|
287
|
+
|
288
|
+
if machine_spec.location['use_private_ip_for_ssh']
|
289
|
+
remote_host = instance.private_ip_address
|
290
|
+
elsif !instance.public_ip_address
|
291
|
+
Chef::Log.warn("Server #{machine_spec.name} has no public ip address. Using private ip '#{instance.private_ip_address}'. Set driver option 'use_private_ip_for_ssh' => true if this will always be the case ...")
|
292
|
+
remote_host = instance.private_ip_address
|
293
|
+
elsif instance.public_ip_address
|
294
|
+
remote_host = instance.public_ip_address
|
295
|
+
else
|
296
|
+
raise "Server #{instance.id} has no private or public IP address!"
|
297
|
+
end
|
298
|
+
|
299
|
+
#Enable pty by default
|
300
|
+
options[:ssh_pty_enable] = true
|
301
|
+
options[:ssh_gateway] = machine_spec.location['ssh_gateway'] if machine_spec.location.has_key?('ssh_gateway')
|
302
|
+
|
303
|
+
Chef::Provisioning::Transport::SSH.new(remote_host, username, ssh_options, options, config)
|
304
|
+
end
|
305
|
+
|
306
|
+
def ssh_options_for(machine_spec, machine_options, instance)
|
307
|
+
result = {
|
308
|
+
# TODO create a user known hosts file
|
309
|
+
# :user_known_hosts_file => vagrant_ssh_config['UserKnownHostsFile'],
|
310
|
+
# :paranoid => true,
|
311
|
+
:auth_methods => [ 'publickey' ],
|
312
|
+
:keys_only => true,
|
313
|
+
:host_key_alias => "#{instance.id}.AWS"
|
314
|
+
}.merge(machine_options[:ssh_options] || {})
|
315
|
+
if instance.respond_to?(:private_key) && instance.private_key
|
316
|
+
result[:key_data] = [ instance.private_key ]
|
317
|
+
elsif instance.respond_to?(:key_name) && instance.key_name
|
318
|
+
key = get_private_key(instance.key_name)
|
319
|
+
unless key
|
320
|
+
raise "Server has key name '#{instance.key_name}', but the corresponding private key was not found locally. Check if the key is in Chef::Config.private_key_paths: #{Chef::Config.private_key_paths.join(', ')}"
|
321
|
+
end
|
322
|
+
result[:key_data] = [ key ]
|
323
|
+
elsif machine_spec.location['key_name']
|
324
|
+
key = get_private_key(machine_spec.location['key_name'])
|
325
|
+
unless key
|
326
|
+
raise "Server was created with key name '#{machine_spec.location['key_name']}', but the corresponding private key was not found locally. Check if the key is in Chef::Config.private_key_paths: #{Chef::Config.private_key_paths.join(', ')}"
|
327
|
+
end
|
328
|
+
result[:key_data] = [ key ]
|
329
|
+
elsif machine_options[:bootstrap_options] && machine_options[:bootstrap_options][:key_path]
|
330
|
+
result[:key_data] = [ IO.read(machine_options[:bootstrap_options][:key_path]) ]
|
331
|
+
elsif machine_options[:bootstrap_options] && machine_options[:bootstrap_options][:key_name]
|
332
|
+
result[:key_data] = [ get_private_key(machine_options[:bootstrap_options][:key_name]) ]
|
333
|
+
else
|
334
|
+
# TODO make a way to suggest other keys to try ...
|
335
|
+
raise "No key found to connect to #{machine_spec.name} (#{machine_spec.location.inspect})!"
|
336
|
+
end
|
337
|
+
result
|
338
|
+
end
|
339
|
+
|
340
|
+
def convergence_strategy_for(machine_spec, machine_options)
|
341
|
+
# Defaults
|
342
|
+
if !machine_spec.location
|
343
|
+
return Chef::Provisioning::ConvergenceStrategy::NoConverge.new(machine_options[:convergence_options], config)
|
344
|
+
end
|
345
|
+
|
346
|
+
if machine_spec.location['is_windows']
|
347
|
+
Chef::Provisioning::ConvergenceStrategy::InstallMsi.new(machine_options[:convergence_options], config)
|
348
|
+
elsif machine_options[:cached_installer] == true
|
349
|
+
Chef::Provisioning::ConvergenceStrategy::InstallCached.new(machine_options[:convergence_options], config)
|
350
|
+
else
|
351
|
+
Chef::Provisioning::ConvergenceStrategy::InstallSh.new(machine_options[:convergence_options], config)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
def wait_until_ready(action_handler, machine_spec)
|
356
|
+
instance = instance_for(machine_spec)
|
357
|
+
time_elapsed = 0
|
358
|
+
sleep_time = 10
|
359
|
+
max_wait_time = 120
|
360
|
+
unless instance.status == :running
|
361
|
+
if action_handler.should_perform_actions
|
362
|
+
action_handler.report_progress "waiting for #{machine_spec.name} (#{instance.id} on #{driver_url}) to be ready ..."
|
363
|
+
while time_elapsed < 120 && instance.status != :running
|
364
|
+
action_handler.report_progress "been waiting #{time_elapsed}/#{max_wait_time} -- sleeping #{sleep_time} seconds for #{machine_spec.name} (#{instance.id} on #{driver_url}) to be ready ..."
|
365
|
+
sleep(sleep_time)
|
366
|
+
time_elapsed += sleep_time
|
367
|
+
end
|
368
|
+
action_handler.report_progress "#{machine_spec.name} is now ready"
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
def wait_for_transport(action_handler, machine_spec, machine_options)
|
374
|
+
instance = instance_for(machine_spec)
|
375
|
+
time_elapsed = 0
|
376
|
+
sleep_time = 10
|
377
|
+
max_wait_time = 120
|
378
|
+
transport = transport_for(machine_spec, machine_options, instance)
|
379
|
+
unless transport.available?
|
380
|
+
if action_handler.should_perform_actions
|
381
|
+
action_handler.report_progress "waiting for #{machine_spec.name} (#{instance.id} on #{driver_url}) to be connectable (transport up and running) ..."
|
382
|
+
while time_elapsed < 120 && !transport.available?
|
383
|
+
action_handler.report_progress "been waiting #{time_elapsed}/#{max_wait_time} -- sleeping #{sleep_time} seconds for #{machine_spec.name} (#{instance.id} on #{driver_url}) to be connectable ..."
|
384
|
+
sleep(sleep_time)
|
385
|
+
time_elapsed += sleep_time
|
386
|
+
end
|
387
|
+
|
388
|
+
action_handler.report_progress "#{machine_spec.name} is now connectable"
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|