kitchen-ec2 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.cane +2 -1
- data/.gitignore +2 -0
- data/.rspec +3 -0
- data/.rubocop.yml +9 -0
- data/.yardopts +3 -0
- data/CHANGELOG.md +60 -17
- data/Gemfile +3 -3
- data/README.md +241 -85
- data/Rakefile +30 -12
- data/data/amis.json +18 -0
- data/kitchen-ec2.gemspec +21 -10
- data/lib/kitchen/driver/aws/client.rb +110 -0
- data/lib/kitchen/driver/aws/instance_generator.rb +148 -0
- data/lib/kitchen/driver/ec2.rb +288 -88
- data/lib/kitchen/driver/ec2_version.rb +1 -1
- data/spec/kitchen/driver/ec2/client_spec.rb +119 -0
- data/spec/kitchen/driver/ec2/instance_generator_spec.rb +303 -0
- data/spec/kitchen/driver/ec2_spec.rb +82 -0
- data/spec/spec_helper.rb +104 -0
- metadata +131 -22
- data/.tailor +0 -106
- data/spec/create_spec.rb +0 -106
data/Rakefile
CHANGED
@@ -1,21 +1,39 @@
|
|
1
|
-
|
2
|
-
require 'cane/rake_task'
|
3
|
-
require 'tailor/rake_task'
|
1
|
+
# -*- encoding: utf-8 -*-
|
4
2
|
|
5
|
-
|
6
|
-
Cane::RakeTask.new do |cane|
|
7
|
-
cane.canefile = './.cane'
|
8
|
-
end
|
3
|
+
require "bundler/gem_tasks"
|
9
4
|
|
10
|
-
|
5
|
+
require "rspec/core/rake_task"
|
6
|
+
RSpec::Core::RakeTask.new(:test)
|
11
7
|
|
12
8
|
desc "Display LOC stats"
|
13
9
|
task :stats do
|
14
10
|
puts "\n## Production Code Stats"
|
15
|
-
sh "countloc -r lib/kitchen"
|
11
|
+
sh "countloc -r lib/kitchen lib/kitchen.rb"
|
12
|
+
puts "\n## Test Code Stats"
|
13
|
+
sh "countloc -r spec features"
|
14
|
+
end
|
15
|
+
|
16
|
+
require "finstyle"
|
17
|
+
require "rubocop/rake_task"
|
18
|
+
RuboCop::RakeTask.new(:style) do |task|
|
19
|
+
task.options << "--display-cop-names"
|
20
|
+
end
|
21
|
+
|
22
|
+
if RUBY_ENGINE != "jruby"
|
23
|
+
require "cane/rake_task"
|
24
|
+
desc "Run cane to check quality metrics"
|
25
|
+
Cane::RakeTask.new do |cane|
|
26
|
+
cane.canefile = "./.cane"
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "Run all quality tasks"
|
30
|
+
task :quality => [:cane, :style, :stats]
|
31
|
+
else
|
32
|
+
desc "Run all quality tasks"
|
33
|
+
task :quality => [:style, :stats]
|
16
34
|
end
|
17
35
|
|
18
|
-
|
19
|
-
|
36
|
+
require "yard"
|
37
|
+
YARD::Rake::YardocTask.new
|
20
38
|
|
21
|
-
task :default => [:quality]
|
39
|
+
task :default => [:test, :quality]
|
data/data/amis.json
CHANGED
@@ -5,6 +5,8 @@
|
|
5
5
|
"ubuntu-12.04": "ami-bb9a08ba",
|
6
6
|
"ubuntu-12.10": "ami-4556c544",
|
7
7
|
"ubuntu-13.04": "ami-97099a96",
|
8
|
+
"ubuntu-13.10": "ami-45f1a744",
|
9
|
+
"ubuntu-14.04": "ami-955c0c94",
|
8
10
|
"centos-6.4": "ami-9ffa709e",
|
9
11
|
"debian-7.1.0": "ami-f1f064f0"
|
10
12
|
},
|
@@ -13,6 +15,8 @@
|
|
13
15
|
"ubuntu-12.04": "ami-881c57da",
|
14
16
|
"ubuntu-12.10": "ami-3ce0a86e",
|
15
17
|
"ubuntu-13.04": "ami-4af5bd18",
|
18
|
+
"ubuntu-13.10": "ami-02e6b850",
|
19
|
+
"ubuntu-14.04": "ami-9a7c25c8",
|
16
20
|
"centos-6.4": "ami-46f5bb14",
|
17
21
|
"debian-7.1.0": "ami-fe8ac3ac"
|
18
22
|
},
|
@@ -21,6 +25,8 @@
|
|
21
25
|
"ubuntu-12.04": "ami-cb26b4f1",
|
22
26
|
"ubuntu-12.10": "ami-1703912d",
|
23
27
|
"ubuntu-13.04": "ami-8f32a0b5",
|
28
|
+
"ubuntu-13.10": "ami-e54423df",
|
29
|
+
"ubuntu-14.04": "ami-f3b1d6c9",
|
24
30
|
"centos-6.4": "ami-9352c1a9",
|
25
31
|
"debian-7.1.0": "ami-4e099a74"
|
26
32
|
},
|
@@ -29,6 +35,8 @@
|
|
29
35
|
"ubuntu-12.04": "ami-d3b5aea7",
|
30
36
|
"ubuntu-12.10": "ami-6b62791f",
|
31
37
|
"ubuntu-13.04": "ami-6fd4cf1b",
|
38
|
+
"ubuntu-13.10": "ami-39eb3f4e",
|
39
|
+
"ubuntu-14.04": "ami-c112c5b6",
|
32
40
|
"centos-6.4": "ami-75190b01",
|
33
41
|
"debian-7.1.0": "ami-954559e1"
|
34
42
|
},
|
@@ -37,6 +45,8 @@
|
|
37
45
|
"ubuntu-12.04": "ami-c905a1d4",
|
38
46
|
"ubuntu-12.10": "ami-e759fdfa",
|
39
47
|
"ubuntu-13.04": "ami-4b2b8f56",
|
48
|
+
"ubuntu-13.10": "ami-076cc21a",
|
49
|
+
"ubuntu-14.04": "ami-052b8518",
|
40
50
|
"centos-6.4": "ami-a665c0bb",
|
41
51
|
"debian-7.1.0": "ami-b03590ad"
|
42
52
|
},
|
@@ -45,6 +55,8 @@
|
|
45
55
|
"ubuntu-12.04": "ami-2f115c46",
|
46
56
|
"ubuntu-12.10": "ami-4d5b1824",
|
47
57
|
"ubuntu-13.04": "ami-a73371ce",
|
58
|
+
"ubuntu-13.10": "ami-a65393ce",
|
59
|
+
"ubuntu-14.04": "ami-4a915c22",
|
48
60
|
"centos-6.4": "ami-bf5021d6",
|
49
61
|
"debian-7.1.0": "ami-50d9a439"
|
50
62
|
},
|
@@ -53,6 +65,8 @@
|
|
53
65
|
"ubuntu-12.04": "ami-eaf0daaf",
|
54
66
|
"ubuntu-12.10": "ami-02220847",
|
55
67
|
"ubuntu-13.04": "ami-fe052fbb",
|
68
|
+
"ubuntu-13.10": "ami-bb2a2afe",
|
69
|
+
"ubuntu-14.04": "ami-d99a9a9c",
|
56
70
|
"centos-6.4": "ami-5d456c18",
|
57
71
|
"debian-7.1.0": "ami-1a9bb25f"
|
58
72
|
},
|
@@ -61,6 +75,8 @@
|
|
61
75
|
"ubuntu-12.04": "ami-e6f36fd6",
|
62
76
|
"ubuntu-12.10": "ami-0c069b3c",
|
63
77
|
"ubuntu-13.04": "ami-4ade427a",
|
78
|
+
"ubuntu-13.10": "ami-c18ff1f1",
|
79
|
+
"ubuntu-14.04": "ami-b7720b87",
|
64
80
|
"centos-6.4": "ami-b3bf2f83",
|
65
81
|
"debian-7.1.0": "ami-158a1925"
|
66
82
|
}
|
@@ -70,6 +86,8 @@
|
|
70
86
|
"ubuntu-12.04": "ubuntu",
|
71
87
|
"ubuntu-12.10": "ubuntu",
|
72
88
|
"ubuntu-13.04": "ubuntu",
|
89
|
+
"ubuntu-13.10": "ubuntu",
|
90
|
+
"ubuntu-14.04": "ubuntu",
|
73
91
|
"centos-6.4": "root",
|
74
92
|
"debian-7.1.0": "admin"
|
75
93
|
},
|
data/kitchen-ec2.gemspec
CHANGED
@@ -1,28 +1,39 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
lib = File.expand_path(
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require
|
4
|
+
require "kitchen/driver/ec2_version.rb"
|
5
5
|
|
6
6
|
Gem::Specification.new do |gem|
|
7
7
|
gem.name = "kitchen-ec2"
|
8
8
|
gem.version = Kitchen::Driver::EC2_VERSION
|
9
|
-
gem.license =
|
9
|
+
gem.license = "Apache 2.0"
|
10
10
|
gem.authors = ["Fletcher Nichol"]
|
11
11
|
gem.email = ["fnichol@nichol.ca"]
|
12
12
|
gem.description = "A Test Kitchen Driver for Amazon EC2"
|
13
13
|
gem.summary = gem.description
|
14
14
|
gem.homepage = "http://kitchen.ci/"
|
15
15
|
|
16
|
-
gem.files = `git ls-files`.split(
|
16
|
+
gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
17
17
|
gem.executables = []
|
18
18
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
19
|
gem.require_paths = ["lib"]
|
20
20
|
|
21
|
-
gem.add_dependency
|
22
|
-
gem.add_dependency
|
21
|
+
gem.add_dependency "test-kitchen", "~> 1.4"
|
22
|
+
gem.add_dependency "excon"
|
23
|
+
gem.add_dependency "multi_json"
|
24
|
+
gem.add_dependency "aws-sdk-v1", "~> 1.59.0"
|
25
|
+
gem.add_dependency "aws-sdk", "~> 2"
|
23
26
|
|
24
|
-
gem.add_development_dependency
|
25
|
-
gem.add_development_dependency
|
26
|
-
gem.add_development_dependency
|
27
|
-
gem.add_development_dependency
|
27
|
+
gem.add_development_dependency "rspec", "~> 3.2"
|
28
|
+
gem.add_development_dependency "countloc", "~> 0.4"
|
29
|
+
gem.add_development_dependency "maruku", "~> 0.6"
|
30
|
+
gem.add_development_dependency "simplecov", "~> 0.7"
|
31
|
+
gem.add_development_dependency "yard", "~> 0.8"
|
32
|
+
|
33
|
+
# style and complexity libraries are tightly version pinned as newer releases
|
34
|
+
# may introduce new and undesireable style choices which would be immediately
|
35
|
+
# enforced in CI
|
36
|
+
gem.add_development_dependency "finstyle", "1.4.0"
|
37
|
+
gem.add_development_dependency "cane", "2.6.2"
|
38
|
+
gem.add_development_dependency "climate_control"
|
28
39
|
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Author:: Tyler Ball (<tball@chef.io>)
|
4
|
+
#
|
5
|
+
# Copyright (C) 2015, Fletcher Nichol
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
|
19
|
+
require "aws-sdk"
|
20
|
+
require "aws-sdk-core/credentials"
|
21
|
+
require "aws-sdk-core/shared_credentials"
|
22
|
+
require "aws-sdk-core/instance_profile_credentials"
|
23
|
+
|
24
|
+
module Kitchen
|
25
|
+
|
26
|
+
module Driver
|
27
|
+
|
28
|
+
class Aws
|
29
|
+
|
30
|
+
# A class for creating and managing the EC2 client connection
|
31
|
+
#
|
32
|
+
# @author Tyler Ball <tball@chef.io>
|
33
|
+
class Client
|
34
|
+
|
35
|
+
def initialize(
|
36
|
+
region,
|
37
|
+
profile_name = nil,
|
38
|
+
access_key_id = nil,
|
39
|
+
secret_access_key = nil,
|
40
|
+
session_token = nil
|
41
|
+
)
|
42
|
+
creds = self.class.get_credentials(
|
43
|
+
profile_name, access_key_id, secret_access_key, session_token
|
44
|
+
)
|
45
|
+
::Aws.config.update(
|
46
|
+
:region => region,
|
47
|
+
:credentials => creds
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Try and get the credentials from an ordered list of locations
|
52
|
+
# http://docs.aws.amazon.com/sdkforruby/api/index.html#Configuration
|
53
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
54
|
+
def self.get_credentials(profile_name, access_key_id, secret_access_key, session_token)
|
55
|
+
shared_creds = ::Aws::SharedCredentials.new(:profile_name => profile_name)
|
56
|
+
if access_key_id && secret_access_key
|
57
|
+
::Aws::Credentials.new(access_key_id, secret_access_key, session_token)
|
58
|
+
# TODO: these are deprecated, remove them in the next major version
|
59
|
+
elsif ENV["AWS_ACCESS_KEY"] && ENV["AWS_SECRET_KEY"]
|
60
|
+
::Aws::Credentials.new(
|
61
|
+
ENV["AWS_ACCESS_KEY"],
|
62
|
+
ENV["AWS_SECRET_KEY"],
|
63
|
+
ENV["AWS_TOKEN"]
|
64
|
+
)
|
65
|
+
elsif ENV["AWS_ACCESS_KEY_ID"] && ENV["AWS_SECRET_ACCESS_KEY"]
|
66
|
+
::Aws::Credentials.new(
|
67
|
+
ENV["AWS_ACCESS_KEY_ID"],
|
68
|
+
ENV["AWS_SECRET_ACCESS_KEY"],
|
69
|
+
ENV["AWS_SESSION_TOKEN"]
|
70
|
+
)
|
71
|
+
elsif shared_creds.loadable?
|
72
|
+
shared_creds
|
73
|
+
else
|
74
|
+
::Aws::InstanceProfileCredentials.new(:retries => 1)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
78
|
+
|
79
|
+
def create_instance(options)
|
80
|
+
resource.create_instances(options)[0]
|
81
|
+
end
|
82
|
+
|
83
|
+
def get_instance(id)
|
84
|
+
resource.instance(id)
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_instance_from_spot_request(request_id)
|
88
|
+
resource.instances(
|
89
|
+
:filters => [{
|
90
|
+
:name => "spot-instance-request-id",
|
91
|
+
:values => [request_id]
|
92
|
+
}]
|
93
|
+
).to_a[0]
|
94
|
+
end
|
95
|
+
|
96
|
+
def client
|
97
|
+
@client ||= ::Aws::EC2::Client.new
|
98
|
+
end
|
99
|
+
|
100
|
+
def resource
|
101
|
+
@resource ||= ::Aws::EC2::Resource.new
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Author:: Tyler Ball (<tball@chef.io>)
|
4
|
+
#
|
5
|
+
# Copyright (C) 2015, Fletcher Nichol
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
|
19
|
+
require "kitchen/logging"
|
20
|
+
|
21
|
+
module Kitchen
|
22
|
+
|
23
|
+
module Driver
|
24
|
+
|
25
|
+
class Aws
|
26
|
+
|
27
|
+
# A class for encapsulating the instance payload logic
|
28
|
+
#
|
29
|
+
# @author Tyler Ball <tball@chef.io>
|
30
|
+
class InstanceGenerator
|
31
|
+
|
32
|
+
include Logging
|
33
|
+
|
34
|
+
attr_reader :config, :ec2
|
35
|
+
|
36
|
+
def initialize(config, ec2)
|
37
|
+
@config = config
|
38
|
+
@ec2 = ec2
|
39
|
+
end
|
40
|
+
|
41
|
+
# Transform the provided config into the hash to send to AWS. Some fields
|
42
|
+
# can be passed in null, others need to be ommitted if they are null
|
43
|
+
def ec2_instance_data # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
44
|
+
i = {
|
45
|
+
:placement => {
|
46
|
+
:availability_zone => config[:availability_zone]
|
47
|
+
},
|
48
|
+
:instance_type => config[:instance_type],
|
49
|
+
:ebs_optimized => config[:ebs_optimized],
|
50
|
+
:image_id => config[:image_id],
|
51
|
+
:key_name => config[:aws_ssh_key_id],
|
52
|
+
:subnet_id => config[:subnet_id],
|
53
|
+
:private_ip_address => config[:private_ip_address]
|
54
|
+
}
|
55
|
+
i[:block_device_mappings] = block_device_mappings unless block_device_mappings.empty?
|
56
|
+
i[:security_group_ids] = config[:security_group_ids] if config[:security_group_ids]
|
57
|
+
i[:user_data] = prepared_user_data if prepared_user_data
|
58
|
+
if config[:iam_instance_profile]
|
59
|
+
i[:iam_instance_profile] = { :name => config[:iam_profile_name] }
|
60
|
+
end
|
61
|
+
if !config.fetch(:associate_public_ip_address, nil).nil?
|
62
|
+
i[:network_interfaces] =
|
63
|
+
[{
|
64
|
+
:device_index => 0,
|
65
|
+
:associate_public_ip_address => config[:associate_public_ip_address]
|
66
|
+
}]
|
67
|
+
end
|
68
|
+
i
|
69
|
+
end
|
70
|
+
|
71
|
+
# Transforms the provided config into the appropriate hash for creating a BDM
|
72
|
+
# in AWS
|
73
|
+
def block_device_mappings # rubocop:disable all
|
74
|
+
return @bdms if @bdms
|
75
|
+
bdms = config[:block_device_mappings] || []
|
76
|
+
if bdms.empty?
|
77
|
+
if config[:ebs_volume_size] || config.fetch(:ebs_delete_on_termination, nil) ||
|
78
|
+
config[:ebs_device_name] || config[:ebs_volume_type]
|
79
|
+
# If the user didn't supply block_device_mappings but did supply
|
80
|
+
# the old configs, copy them into the block_device_mappings array correctly
|
81
|
+
# TODO: remove this logic when we remove the deprecated values
|
82
|
+
bdms << {
|
83
|
+
:ebs_volume_size => config[:ebs_volume_size] || 8,
|
84
|
+
:ebs_delete_on_termination => config.fetch(:ebs_delete_on_termination, true),
|
85
|
+
:ebs_device_name => config[:ebs_device_name] || "/dev/sda1",
|
86
|
+
:ebs_volume_type => config[:ebs_volume_type] || "standard"
|
87
|
+
}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Convert the provided keys to what AWS expects
|
92
|
+
bdms = bdms.map do |bdm|
|
93
|
+
b = {
|
94
|
+
:ebs => {
|
95
|
+
:volume_size => bdm[:ebs_volume_size],
|
96
|
+
:delete_on_termination => bdm[:ebs_delete_on_termination]
|
97
|
+
},
|
98
|
+
:device_name => bdm[:ebs_device_name]
|
99
|
+
}
|
100
|
+
b[:ebs][:volume_type] = bdm[:ebs_volume_type] if bdm[:ebs_volume_type]
|
101
|
+
b[:ebs][:snapshot_id] = bdm[:ebs_snapshot_id] if bdm[:ebs_snapshot_id]
|
102
|
+
b[:virtual_name] = bdm[:ebs_virtual_name] if bdm[:ebs_virtual_name]
|
103
|
+
b
|
104
|
+
end
|
105
|
+
|
106
|
+
debug_if_root_device(bdms)
|
107
|
+
|
108
|
+
@bdms = bdms
|
109
|
+
end
|
110
|
+
|
111
|
+
# If the provided bdms match the root device in the AMI, emit log that
|
112
|
+
# states this
|
113
|
+
def debug_if_root_device(bdms)
|
114
|
+
image_id = config[:image_id]
|
115
|
+
image = ec2.resource.image(image_id)
|
116
|
+
begin
|
117
|
+
root_device_name = image.root_device_name
|
118
|
+
rescue ::Aws::EC2::Errors::InvalidAMIIDNotFound
|
119
|
+
# Not raising here because AWS will give a more meaningful message
|
120
|
+
# when we try to create the instance
|
121
|
+
return
|
122
|
+
end
|
123
|
+
bdms.find { |bdm|
|
124
|
+
if bdm[:device_name] == root_device_name
|
125
|
+
info("Overriding root device [#{root_device_name}] from image [#{image_id}]")
|
126
|
+
end
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
def prepared_user_data
|
131
|
+
# If user_data is a file reference, lets read it as such
|
132
|
+
unless @user_data
|
133
|
+
if config[:user_data] && File.file?(config[:user_data])
|
134
|
+
@user_data = File.read(config[:user_data])
|
135
|
+
else
|
136
|
+
@user_data = config[:user_data]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
@user_data
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
data/lib/kitchen/driver/ec2.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
#
|
3
3
|
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
|
4
4
|
#
|
5
|
-
# Copyright (C)
|
5
|
+
# Copyright (C) 2015, Fletcher Nichol
|
6
6
|
#
|
7
7
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
8
|
# you may not use this file except in compliance with the License.
|
@@ -16,10 +16,13 @@
|
|
16
16
|
# See the License for the specific language governing permissions and
|
17
17
|
# limitations under the License.
|
18
18
|
|
19
|
-
require
|
20
|
-
require
|
21
|
-
require
|
22
|
-
require
|
19
|
+
require "benchmark"
|
20
|
+
require "json"
|
21
|
+
require "aws"
|
22
|
+
require "kitchen"
|
23
|
+
require "kitchen/driver/ec2_version"
|
24
|
+
require_relative "aws/client"
|
25
|
+
require_relative "aws/instance_generator"
|
23
26
|
|
24
27
|
module Kitchen
|
25
28
|
|
@@ -28,143 +31,340 @@ module Kitchen
|
|
28
31
|
# Amazon EC2 driver for Test Kitchen.
|
29
32
|
#
|
30
33
|
# @author Fletcher Nichol <fnichol@nichol.ca>
|
31
|
-
class Ec2 < Kitchen::Driver::
|
34
|
+
class Ec2 < Kitchen::Driver::Base # rubocop:disable Metrics/ClassLength
|
32
35
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
+
kitchen_driver_api_version 2
|
37
|
+
|
38
|
+
plugin_version Kitchen::Driver::EC2_VERSION
|
39
|
+
|
40
|
+
default_config :region, ENV["AWS_REGION"] || "us-east-1"
|
41
|
+
default_config :shared_credentials_profile, nil
|
42
|
+
default_config :availability_zone, nil
|
43
|
+
default_config :flavor_id, nil
|
44
|
+
default_config :instance_type, nil
|
36
45
|
default_config :ebs_optimized, false
|
37
|
-
default_config :security_group_ids,
|
38
|
-
default_config :tags,
|
39
|
-
default_config :
|
40
|
-
|
46
|
+
default_config :security_group_ids, nil
|
47
|
+
default_config :tags, "created-by" => "test-kitchen"
|
48
|
+
default_config :user_data, nil
|
49
|
+
default_config :private_ip_address, nil
|
50
|
+
default_config :iam_profile_name, nil
|
51
|
+
default_config :price, nil
|
52
|
+
default_config :retryable_tries, 60
|
53
|
+
default_config :retryable_sleep, 5
|
54
|
+
default_config :aws_access_key_id, nil
|
55
|
+
default_config :aws_secret_access_key, nil
|
56
|
+
default_config :aws_session_token, nil
|
57
|
+
default_config :aws_ssh_key_id, ENV["AWS_SSH_KEY_ID"]
|
58
|
+
default_config :image_id do |driver|
|
59
|
+
driver.default_ami
|
41
60
|
end
|
42
|
-
default_config :
|
43
|
-
|
61
|
+
default_config :username, nil
|
62
|
+
default_config :associate_public_ip, nil
|
63
|
+
|
64
|
+
required_config :aws_ssh_key_id
|
65
|
+
required_config :image_id
|
66
|
+
|
67
|
+
def self.validation_warn(driver, old_key, new_key)
|
68
|
+
driver.warn "WARN: The driver[#{driver.class.name}] config key `#{old_key}` " \
|
69
|
+
"is deprecated, please use `#{new_key}`"
|
44
70
|
end
|
45
|
-
|
46
|
-
|
71
|
+
|
72
|
+
# TODO: remove these in the next major version of TK
|
73
|
+
deprecated_configs = [:ebs_volume_size, :ebs_delete_on_termination, :ebs_device_name]
|
74
|
+
deprecated_configs.each do |d|
|
75
|
+
validations[d] = lambda do |attr, val, driver|
|
76
|
+
unless val.nil?
|
77
|
+
validation_warn(driver, attr, "block_device_mappings")
|
78
|
+
end
|
79
|
+
end
|
47
80
|
end
|
48
|
-
|
49
|
-
|
81
|
+
validations[:ssh_key] = lambda do |attr, val, driver|
|
82
|
+
unless val.nil?
|
83
|
+
validation_warn(driver, attr, "transport.ssh_key")
|
84
|
+
end
|
50
85
|
end
|
51
|
-
|
52
|
-
|
86
|
+
validations[:ssh_timeout] = lambda do |attr, val, driver|
|
87
|
+
unless val.nil?
|
88
|
+
validation_warn(driver, attr, "transport.connection_timeout")
|
89
|
+
end
|
53
90
|
end
|
54
|
-
|
55
|
-
|
91
|
+
validations[:ssh_retries] = lambda do |attr, val, driver|
|
92
|
+
unless val.nil?
|
93
|
+
validation_warn(driver, attr, "transport.connection_retries")
|
94
|
+
end
|
56
95
|
end
|
57
|
-
|
58
|
-
|
96
|
+
validations[:username] = lambda do |attr, val, driver|
|
97
|
+
unless val.nil?
|
98
|
+
validation_warn(driver, attr, "transport.username")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
validations[:flavor_id] = lambda do |attr, val, driver|
|
102
|
+
unless val.nil?
|
103
|
+
validation_warn(driver, attr, "instance_type")
|
104
|
+
end
|
59
105
|
end
|
60
106
|
|
61
|
-
default_config :
|
107
|
+
default_config :block_device_mappings, nil
|
108
|
+
validations[:block_device_mappings] = lambda do |_attr, val, _driver|
|
109
|
+
unless val.nil?
|
110
|
+
val.each do |bdm|
|
111
|
+
unless bdm.keys.include?(:ebs_volume_size) &&
|
112
|
+
bdm.keys.include?(:ebs_delete_on_termination) &&
|
113
|
+
bdm.keys.include?(:ebs_device_name)
|
114
|
+
raise "Every :block_device_mapping must include the keys :ebs_volume_size, " \
|
115
|
+
":ebs_delete_on_termination and :ebs_device_name"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
62
120
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
121
|
+
# The access key/secret are now using the priority list AWS uses
|
122
|
+
# Providing these inside the .kitchen.yml is no longer recommended
|
123
|
+
validations[:aws_access_key_id] = lambda do |attr, val, driver|
|
124
|
+
unless val.nil?
|
125
|
+
driver.warn "WARN: #{attr} has been deprecated, please use " \
|
126
|
+
"ENV['AWS_ACCESS_KEY_ID'] or ~/.aws/credentials. See " \
|
127
|
+
"the README for more details"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
validations[:aws_secret_access_key] = lambda do |attr, val, driver|
|
131
|
+
unless val.nil?
|
132
|
+
driver.warn "WARN: #{attr} has been deprecated, please use " \
|
133
|
+
"ENV['AWS_SECRET_ACCESS_KEY'] or ~/.aws/credentials. See " \
|
134
|
+
"the README for more details"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
validations[:aws_session_token] = lambda do |attr, val, driver|
|
138
|
+
unless val.nil?
|
139
|
+
driver.warn "WARN: #{attr} has been deprecated, please use " \
|
140
|
+
"ENV['AWS_SESSION_TOKEN'] or ~/.aws/credentials. See " \
|
141
|
+
"the README for more details"
|
142
|
+
end
|
143
|
+
end
|
67
144
|
|
68
|
-
|
69
|
-
|
70
|
-
|
145
|
+
# A lifecycle method that should be invoked when the object is about
|
146
|
+
# ready to be used. A reference to an Instance is required as
|
147
|
+
# configuration dependant data may be access through an Instance. This
|
148
|
+
# also acts as a hook point where the object may wish to perform other
|
149
|
+
# last minute checks, validations, or configuration expansions.
|
150
|
+
#
|
151
|
+
# @param instance [Instance] an associated instance
|
152
|
+
# @return [self] itself, for use in chaining
|
153
|
+
# @raise [ClientError] if instance parameter is nil
|
154
|
+
def finalize_config!(instance)
|
155
|
+
super
|
71
156
|
|
157
|
+
if config[:availability_zone].nil?
|
158
|
+
config[:availability_zone] = config[:region] + "b"
|
159
|
+
end
|
160
|
+
# TODO: when we get rid of flavor_id, move this to a default
|
161
|
+
if config[:instance_type].nil?
|
162
|
+
config[:instance_type] = config[:flavor_id] || "m1.small"
|
163
|
+
end
|
164
|
+
|
165
|
+
self
|
166
|
+
end
|
167
|
+
|
168
|
+
def create(state) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
169
|
+
copy_deprecated_configs(state)
|
170
|
+
return if state[:server_id]
|
171
|
+
|
172
|
+
info(Kitchen::Util.outdent!(<<-END))
|
173
|
+
Creating <#{state[:server_id]}>...
|
174
|
+
If you are not using an account that qualifies under the AWS
|
175
|
+
free-tier, you may be charged to run these suites. The charge
|
176
|
+
should be minimal, but neither Test Kitchen nor its maintainers
|
177
|
+
are responsible for your incurred costs.
|
178
|
+
END
|
179
|
+
|
180
|
+
if config[:price]
|
181
|
+
# Spot instance when a price is set
|
182
|
+
server = submit_spot(state)
|
183
|
+
else
|
184
|
+
# On-demand instance
|
185
|
+
server = submit_server
|
186
|
+
end
|
187
|
+
info("Instance <#{server.id}> requested.")
|
188
|
+
tag_server(server)
|
189
|
+
|
190
|
+
state[:server_id] = server.id
|
72
191
|
info("EC2 instance <#{state[:server_id]}> created.")
|
73
|
-
|
74
|
-
|
192
|
+
wait_log = proc do |attempts|
|
193
|
+
c = attempts * config[:retryable_sleep]
|
194
|
+
t = config[:retryable_tries] * config[:retryable_sleep]
|
195
|
+
info "Waited #{c}/#{t}s for instance <#{state[:server_id]}> to become ready."
|
196
|
+
end
|
197
|
+
server = server.wait_until(
|
198
|
+
:max_attempts => config[:retryable_tries],
|
199
|
+
:delay => config[:retryable_sleep],
|
200
|
+
:before_attempt => wait_log
|
201
|
+
) do |s|
|
202
|
+
hostname = hostname(s)
|
203
|
+
# Euca instances often report ready before they have an IP
|
204
|
+
s.state.name == "running" && !hostname.nil? && hostname != "0.0.0.0"
|
205
|
+
end
|
206
|
+
|
207
|
+
info("EC2 instance <#{state[:server_id]}> ready.")
|
75
208
|
state[:hostname] = hostname(server)
|
76
|
-
|
77
|
-
|
209
|
+
instance.transport.connection(state).wait_until_ready
|
210
|
+
create_ec2_json(state)
|
78
211
|
debug("ec2:create '#{state[:hostname]}'")
|
79
|
-
rescue Fog::Errors::Error, Excon::Errors::Error => ex
|
80
|
-
raise ActionFailed, ex.message
|
81
212
|
end
|
82
213
|
|
83
214
|
def destroy(state)
|
84
215
|
return if state[:server_id].nil?
|
85
216
|
|
86
|
-
server =
|
87
|
-
|
217
|
+
server = ec2.get_instance(state[:server_id])
|
218
|
+
unless server.nil?
|
219
|
+
instance.transport.connection(state).close
|
220
|
+
server.terminate unless server.nil?
|
221
|
+
end
|
222
|
+
if state[:spot_request_id]
|
223
|
+
debug("Deleting spot request <#{state[:server_id]}>")
|
224
|
+
ec2.client.cancel_spot_instance_requests(
|
225
|
+
:spot_instance_request_ids => [state[:spot_request_id]]
|
226
|
+
)
|
227
|
+
end
|
88
228
|
info("EC2 instance <#{state[:server_id]}> destroyed.")
|
89
229
|
state.delete(:server_id)
|
90
230
|
state.delete(:hostname)
|
91
231
|
end
|
92
232
|
|
93
233
|
def default_ami
|
94
|
-
region = amis[
|
234
|
+
region = amis["regions"][config[:region]]
|
95
235
|
region && region[instance.platform.name]
|
96
236
|
end
|
97
237
|
|
98
|
-
def default_username
|
99
|
-
amis['usernames'][instance.platform.name] || 'root'
|
100
|
-
end
|
101
|
-
|
102
238
|
private
|
103
239
|
|
104
|
-
def
|
105
|
-
|
106
|
-
:
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
:endpoint => config[:endpoint],
|
240
|
+
def ec2
|
241
|
+
@ec2 ||= Aws::Client.new(
|
242
|
+
config[:region],
|
243
|
+
config[:shared_credentials_profile],
|
244
|
+
config[:aws_access_key_id],
|
245
|
+
config[:aws_secret_access_key],
|
246
|
+
config[:aws_session_token]
|
112
247
|
)
|
113
248
|
end
|
114
249
|
|
115
|
-
def
|
116
|
-
|
250
|
+
def instance_generator
|
251
|
+
@instance_generator ||= Aws::InstanceGenerator.new(config, ec2)
|
252
|
+
end
|
117
253
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
:
|
126
|
-
|
127
|
-
|
254
|
+
# This copies transport config from the current config object into the
|
255
|
+
# state. This relies on logic in the transport that merges the transport
|
256
|
+
# config with the current state object, so its a bad coupling. But we
|
257
|
+
# can get rid of this when we get rid of these deprecated configs!
|
258
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
259
|
+
def copy_deprecated_configs(state)
|
260
|
+
if config[:ssh_timeout]
|
261
|
+
state[:connection_timeout] = config[:ssh_timeout]
|
262
|
+
end
|
263
|
+
if config[:ssh_retries]
|
264
|
+
state[:connection_retries] = config[:ssh_retries]
|
265
|
+
end
|
266
|
+
if config[:username]
|
267
|
+
state[:username] = config[:username]
|
268
|
+
elsif instance.transport[:username] == instance.transport.class.defaults[:username]
|
269
|
+
# If the transport has the default username, copy it from amis.json
|
270
|
+
# This duplicated old behavior but I hate amis.json
|
271
|
+
ami_username = amis["usernames"][instance.platform.name]
|
272
|
+
state[:username] = ami_username if ami_username
|
273
|
+
end
|
274
|
+
if config[:ssh_key]
|
275
|
+
state[:ssh_key] = config[:ssh_key]
|
276
|
+
end
|
128
277
|
end
|
278
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
129
279
|
|
130
|
-
|
131
|
-
|
132
|
-
debug("
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
280
|
+
# Fog AWS helper for creating the instance
|
281
|
+
def submit_server
|
282
|
+
debug("Creating EC2 Instance..")
|
283
|
+
instance_data = instance_generator.ec2_instance_data
|
284
|
+
instance_data[:min_count] = 1
|
285
|
+
instance_data[:max_count] = 1
|
286
|
+
ec2.create_instance(instance_data)
|
287
|
+
end
|
288
|
+
|
289
|
+
def submit_spot(state) # rubocop:disable Metrics/AbcSize
|
290
|
+
debug("Creating EC2 Spot Instance..")
|
291
|
+
request_data = {}
|
292
|
+
request_data[:spot_price] = config[:price].to_s
|
293
|
+
request_data[:launch_specification] = instance_generator.ec2_instance_data
|
294
|
+
|
295
|
+
response = ec2.client.request_spot_instances(request_data)
|
296
|
+
spot_request_id = response[:spot_instance_requests][0][:spot_instance_request_id]
|
297
|
+
# deleting the instance cancels the request, but deleting the request
|
298
|
+
# does not affect the instance
|
299
|
+
state[:spot_request_id] = spot_request_id
|
300
|
+
ec2.client.wait_until(
|
301
|
+
:spot_instance_request_fulfilled,
|
302
|
+
:spot_instance_request_ids => [spot_request_id]
|
303
|
+
) do |w|
|
304
|
+
w.max_attempts = config[:retryable_tries]
|
305
|
+
w.delay = config[:retryable_sleep]
|
306
|
+
w.before_attempt do |attempts|
|
307
|
+
c = attempts * config[:retryable_sleep]
|
308
|
+
t = config[:retryable_tries] * config[:retryable_sleep]
|
309
|
+
info "Waited #{c}/#{t}s for spot request <#{spot_request_id}> to become fulfilled."
|
310
|
+
end
|
311
|
+
end
|
312
|
+
ec2.get_instance_from_spot_request(spot_request_id)
|
313
|
+
end
|
314
|
+
|
315
|
+
def tag_server(server)
|
316
|
+
tags = []
|
317
|
+
config[:tags].each do |k, v|
|
318
|
+
tags << { :key => k, :value => v }
|
319
|
+
end
|
320
|
+
server.create_tags(:tags => tags)
|
140
321
|
end
|
141
322
|
|
142
323
|
def amis
|
143
324
|
@amis ||= begin
|
144
325
|
json_file = File.join(File.dirname(__FILE__),
|
145
|
-
%w
|
326
|
+
%w[.. .. .. data amis.json])
|
146
327
|
JSON.load(IO.read(json_file))
|
147
328
|
end
|
148
329
|
end
|
149
330
|
|
150
|
-
|
331
|
+
#
|
332
|
+
# Ordered mapping from config name to Fog name. Ordered by preference
|
333
|
+
# when looking up hostname.
|
334
|
+
#
|
335
|
+
INTERFACE_TYPES =
|
151
336
|
{
|
152
|
-
|
153
|
-
|
154
|
-
|
337
|
+
"dns" => "public_dns_name",
|
338
|
+
"public" => "public_ip_address",
|
339
|
+
"private" => "private_ip_address"
|
155
340
|
}
|
156
|
-
end
|
157
341
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
342
|
+
#
|
343
|
+
# Lookup hostname of provided server. If interface_type is provided use
|
344
|
+
# that interface to lookup hostname. Otherwise, try ordered list of
|
345
|
+
# options.
|
346
|
+
#
|
347
|
+
def hostname(server, interface_type = nil)
|
348
|
+
if interface_type
|
349
|
+
interface_type = INTERFACE_TYPES.fetch(interface_type) do
|
350
|
+
raise Kitchen::UserError, "Invalid interface [#{interface_type}]"
|
162
351
|
end
|
163
|
-
server.send(
|
352
|
+
server.send(interface_type)
|
164
353
|
else
|
165
|
-
|
354
|
+
potential_hostname = nil
|
355
|
+
INTERFACE_TYPES.values.each do |type|
|
356
|
+
potential_hostname ||= server.send(type)
|
357
|
+
end
|
358
|
+
potential_hostname
|
166
359
|
end
|
167
360
|
end
|
361
|
+
|
362
|
+
def create_ec2_json(state)
|
363
|
+
instance.transport.connection(state).execute(
|
364
|
+
"sudo mkdir -p /etc/chef/ohai/hints;sudo touch /etc/chef/ohai/hints/ec2.json"
|
365
|
+
)
|
366
|
+
end
|
367
|
+
|
168
368
|
end
|
169
369
|
end
|
170
370
|
end
|