kitchen-ec2 0.10.0 → 1.0.0.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.travis.yml +5 -1
- data/CHANGELOG.md +1 -1
- data/Gemfile +4 -1
- data/README.md +218 -234
- data/kitchen-ec2.gemspec +1 -0
- data/lib/kitchen/driver/aws/client.rb +3 -8
- data/lib/kitchen/driver/aws/instance_generator.rb +11 -65
- data/lib/kitchen/driver/aws/standard_platform.rb +229 -0
- data/lib/kitchen/driver/aws/standard_platform/centos.rb +46 -0
- data/lib/kitchen/driver/aws/standard_platform/debian.rb +50 -0
- data/lib/kitchen/driver/aws/standard_platform/fedora.rb +34 -0
- data/lib/kitchen/driver/aws/standard_platform/freebsd.rb +37 -0
- data/lib/kitchen/driver/aws/standard_platform/rhel.rb +40 -0
- data/lib/kitchen/driver/aws/standard_platform/ubuntu.rb +34 -0
- data/lib/kitchen/driver/aws/standard_platform/windows.rb +138 -0
- data/lib/kitchen/driver/ec2.rb +171 -117
- data/lib/kitchen/driver/ec2_version.rb +1 -1
- data/spec/kitchen/driver/ec2/client_spec.rb +3 -17
- data/spec/kitchen/driver/ec2/image_selection_spec.rb +350 -0
- data/spec/kitchen/driver/ec2/instance_generator_spec.rb +94 -188
- data/spec/kitchen/driver/ec2_spec.rb +5 -29
- metadata +29 -6
- data/data/amis.json +0 -118
data/kitchen-ec2.gemspec
CHANGED
@@ -22,6 +22,7 @@ Gem::Specification.new do |gem|
|
|
22
22
|
gem.add_dependency "excon"
|
23
23
|
gem.add_dependency "multi_json"
|
24
24
|
gem.add_dependency "aws-sdk", "~> 2"
|
25
|
+
gem.add_dependency "retryable", "~> 2.0"
|
25
26
|
|
26
27
|
gem.add_development_dependency "rspec", "~> 3.2"
|
27
28
|
gem.add_development_dependency "countloc", "~> 0.4"
|
@@ -38,7 +38,8 @@ module Kitchen
|
|
38
38
|
access_key_id = nil,
|
39
39
|
secret_access_key = nil,
|
40
40
|
session_token = nil,
|
41
|
-
http_proxy = nil
|
41
|
+
http_proxy = nil,
|
42
|
+
retry_limit = nil
|
42
43
|
)
|
43
44
|
creds = self.class.get_credentials(
|
44
45
|
profile_name, access_key_id, secret_access_key, session_token
|
@@ -48,6 +49,7 @@ module Kitchen
|
|
48
49
|
:credentials => creds,
|
49
50
|
:http_proxy => http_proxy
|
50
51
|
)
|
52
|
+
::Aws.config.update(:retry_limit => retry_limit) unless retry_limit.nil?
|
51
53
|
end
|
52
54
|
|
53
55
|
# Try and get the credentials from an ordered list of locations
|
@@ -57,13 +59,6 @@ module Kitchen
|
|
57
59
|
shared_creds = ::Aws::SharedCredentials.new(:profile_name => profile_name)
|
58
60
|
if access_key_id && secret_access_key
|
59
61
|
::Aws::Credentials.new(access_key_id, secret_access_key, session_token)
|
60
|
-
# TODO: these are deprecated, remove them in the next major version
|
61
|
-
elsif ENV["AWS_ACCESS_KEY"] && ENV["AWS_SECRET_KEY"]
|
62
|
-
::Aws::Credentials.new(
|
63
|
-
ENV["AWS_ACCESS_KEY"],
|
64
|
-
ENV["AWS_SECRET_KEY"],
|
65
|
-
ENV["AWS_TOKEN"]
|
66
|
-
)
|
67
62
|
elsif ENV["AWS_ACCESS_KEY_ID"] && ENV["AWS_SECRET_ACCESS_KEY"]
|
68
63
|
::Aws::Credentials.new(
|
69
64
|
ENV["AWS_ACCESS_KEY_ID"],
|
@@ -41,9 +41,6 @@ module Kitchen
|
|
41
41
|
# can be passed in null, others need to be ommitted if they are null
|
42
42
|
def ec2_instance_data # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
43
43
|
i = {
|
44
|
-
:placement => {
|
45
|
-
:availability_zone => config[:availability_zone]
|
46
|
-
},
|
47
44
|
:instance_type => config[:instance_type],
|
48
45
|
:ebs_optimized => config[:ebs_optimized],
|
49
46
|
:image_id => config[:image_id],
|
@@ -51,7 +48,17 @@ module Kitchen
|
|
51
48
|
:subnet_id => config[:subnet_id],
|
52
49
|
:private_ip_address => config[:private_ip_address]
|
53
50
|
}
|
54
|
-
|
51
|
+
|
52
|
+
availability_zone = config[:availability_zone]
|
53
|
+
if availability_zone
|
54
|
+
if availability_zone =~ /^[a-z]$/i
|
55
|
+
availability_zone = "#{config[:region]}#{availability_zone}"
|
56
|
+
end
|
57
|
+
i[:placement] = { :availability_zone => availability_zone.downcase }
|
58
|
+
end
|
59
|
+
unless config[:block_device_mappings].nil? || config[:block_device_mappings].empty?
|
60
|
+
i[:block_device_mappings] = config[:block_device_mappings]
|
61
|
+
end
|
55
62
|
i[:security_group_ids] = Array(config[:security_group_ids]) if config[:security_group_ids]
|
56
63
|
i[:user_data] = prepared_user_data if prepared_user_data
|
57
64
|
if config[:iam_profile_name]
|
@@ -80,67 +87,6 @@ module Kitchen
|
|
80
87
|
i
|
81
88
|
end
|
82
89
|
|
83
|
-
# Transforms the provided config into the appropriate hash for creating a BDM
|
84
|
-
# in AWS
|
85
|
-
def block_device_mappings # rubocop:disable all
|
86
|
-
return @bdms if @bdms
|
87
|
-
bdms = config[:block_device_mappings] || []
|
88
|
-
if bdms.empty?
|
89
|
-
if config[:ebs_volume_size] || config.fetch(:ebs_delete_on_termination, nil) ||
|
90
|
-
config[:ebs_device_name] || config[:ebs_volume_type]
|
91
|
-
# If the user didn't supply block_device_mappings but did supply
|
92
|
-
# the old configs, copy them into the block_device_mappings array correctly
|
93
|
-
# TODO: remove this logic when we remove the deprecated values
|
94
|
-
bdms << {
|
95
|
-
:ebs_volume_size => config[:ebs_volume_size] || 8,
|
96
|
-
:ebs_delete_on_termination => config.fetch(:ebs_delete_on_termination, true),
|
97
|
-
:ebs_device_name => config[:ebs_device_name] || "/dev/sda1",
|
98
|
-
:ebs_volume_type => config[:ebs_volume_type] || "standard"
|
99
|
-
}
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
# Convert the provided keys to what AWS expects
|
104
|
-
bdms = bdms.map do |bdm|
|
105
|
-
b = {
|
106
|
-
:ebs => {
|
107
|
-
:volume_size => bdm[:ebs_volume_size],
|
108
|
-
:delete_on_termination => bdm[:ebs_delete_on_termination]
|
109
|
-
},
|
110
|
-
:device_name => bdm[:ebs_device_name]
|
111
|
-
}
|
112
|
-
b[:ebs][:volume_type] = bdm[:ebs_volume_type] if bdm[:ebs_volume_type]
|
113
|
-
b[:ebs][:iops] = bdm[:ebs_iops] if bdm[:ebs_iops]
|
114
|
-
b[:ebs][:snapshot_id] = bdm[:ebs_snapshot_id] if bdm[:ebs_snapshot_id]
|
115
|
-
b[:virtual_name] = bdm[:ebs_virtual_name] if bdm[:ebs_virtual_name]
|
116
|
-
b
|
117
|
-
end
|
118
|
-
|
119
|
-
debug_if_root_device(bdms)
|
120
|
-
|
121
|
-
@bdms = bdms
|
122
|
-
end
|
123
|
-
|
124
|
-
# If the provided bdms match the root device in the AMI, emit log that
|
125
|
-
# states this
|
126
|
-
def debug_if_root_device(bdms)
|
127
|
-
return if bdms.nil? || bdms.empty?
|
128
|
-
image_id = config[:image_id]
|
129
|
-
image = ec2.resource.image(image_id)
|
130
|
-
begin
|
131
|
-
root_device_name = image.root_device_name
|
132
|
-
rescue ::Aws::EC2::Errors::InvalidAMIIDNotFound
|
133
|
-
# Not raising here because AWS will give a more meaningful message
|
134
|
-
# when we try to create the instance
|
135
|
-
return
|
136
|
-
end
|
137
|
-
bdms.find { |bdm|
|
138
|
-
if bdm[:device_name] == root_device_name
|
139
|
-
logger.info("Overriding root device [#{root_device_name}] from image [#{image_id}]")
|
140
|
-
end
|
141
|
-
}
|
142
|
-
end
|
143
|
-
|
144
90
|
def prepared_user_data
|
145
91
|
# If user_data is a file reference, lets read it as such
|
146
92
|
return nil if config[:user_data].nil?
|
@@ -0,0 +1,229 @@
|
|
1
|
+
module Kitchen
|
2
|
+
module Driver
|
3
|
+
class Aws
|
4
|
+
#
|
5
|
+
# Lets you grab StandardPlatform objects that help search for official
|
6
|
+
# AMIs in your region and tell you useful tidbits like usernames.
|
7
|
+
#
|
8
|
+
# To use these, set your platform name to a supported platform name like:
|
9
|
+
#
|
10
|
+
# centos
|
11
|
+
# rhel
|
12
|
+
# fedora
|
13
|
+
# freebsd
|
14
|
+
# ubuntu
|
15
|
+
# windows
|
16
|
+
#
|
17
|
+
# The implementation will select the latest matching version and AMI.
|
18
|
+
#
|
19
|
+
# You can specify a version and optional architecture as well:
|
20
|
+
#
|
21
|
+
# windows-2012r2-i386
|
22
|
+
# centos-7
|
23
|
+
#
|
24
|
+
# Useful reference for platform AMIs:
|
25
|
+
# https://alestic.com/2014/01/ec2-ssh-username/
|
26
|
+
class StandardPlatform
|
27
|
+
#
|
28
|
+
# Create a new StandardPlatform object.
|
29
|
+
#
|
30
|
+
# @param driver [Kitchen::Driver::Ec2] The driver.
|
31
|
+
# @param name [String] The name of the platform (rhel, centos, etc.)
|
32
|
+
# @param version [String] The version of the platform (7.1, 2008sp1, etc.)
|
33
|
+
# @param architecture [String] The architecture (i386, x86_64)
|
34
|
+
#
|
35
|
+
def initialize(driver, name, version, architecture)
|
36
|
+
@driver = driver
|
37
|
+
@name = name
|
38
|
+
@version = version
|
39
|
+
@architecture = architecture
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# The driver.
|
44
|
+
#
|
45
|
+
# @return [Kitchen::Driver::Ec2]
|
46
|
+
#
|
47
|
+
attr_reader :driver
|
48
|
+
|
49
|
+
#
|
50
|
+
# The name of the platform (e.g. rhel, centos, etc.)
|
51
|
+
#
|
52
|
+
# @return [String]
|
53
|
+
#
|
54
|
+
attr_reader :name
|
55
|
+
|
56
|
+
#
|
57
|
+
# The version of the platform (e.g. 7.1, 2008sp1, etc.)
|
58
|
+
#
|
59
|
+
# @return [String]
|
60
|
+
#
|
61
|
+
attr_reader :version
|
62
|
+
|
63
|
+
#
|
64
|
+
# The architecture of the platform, e.g. i386, x86_64
|
65
|
+
#
|
66
|
+
# @return [String]
|
67
|
+
#
|
68
|
+
# @see ARCHITECTURES
|
69
|
+
#
|
70
|
+
attr_reader :architecture
|
71
|
+
|
72
|
+
#
|
73
|
+
# Find the best matching image for the given image search.
|
74
|
+
#
|
75
|
+
# @return [String] The image ID (e.g. ami-213984723)
|
76
|
+
def find_image(image_search)
|
77
|
+
driver.debug("Searching for images matching #{image_search} ...")
|
78
|
+
# Convert to ec2 search format (pairs of name+values)
|
79
|
+
filters = image_search.map do |key, value|
|
80
|
+
{ :name => key.to_s, :values => Array(value).map(&:to_s) }
|
81
|
+
end
|
82
|
+
|
83
|
+
# We prefer most recent first
|
84
|
+
images = driver.ec2.resource.images(:filters => filters)
|
85
|
+
images = sort_images(images)
|
86
|
+
show_returned_images(images)
|
87
|
+
|
88
|
+
# Grab the best match
|
89
|
+
images.first && images.first.id
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# The list of StandardPlatform objects. StandardPlatforms register
|
94
|
+
# themselves with this.
|
95
|
+
#
|
96
|
+
# @return Array[Kitchen::Driver::Aws::StandardPlatform]
|
97
|
+
#
|
98
|
+
def self.platforms
|
99
|
+
@platforms ||= {}
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_s
|
103
|
+
"#{name}#{version ? " #{version}" : ""}#{architecture ? " #{architecture}" : ""}"
|
104
|
+
end
|
105
|
+
|
106
|
+
#
|
107
|
+
# Instantiate a platform from a platform name.
|
108
|
+
#
|
109
|
+
# @param driver [Kitchen::Driver::Ec2] The driver.
|
110
|
+
# @param platform_string [String] The platform string, e.g. "windows",
|
111
|
+
# "ubuntu-7.1", "centos-7-i386"
|
112
|
+
#
|
113
|
+
# @return [Kitchen::Driver::Aws::StandardPlatform]
|
114
|
+
#
|
115
|
+
def self.from_platform_string(driver, platform_string)
|
116
|
+
platform, version, architecture = parse_platform_string(platform_string)
|
117
|
+
if platform && platforms[platform]
|
118
|
+
platforms[platform].new(driver, platform, version, architecture)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
#
|
123
|
+
# Detect platform from an image.
|
124
|
+
#
|
125
|
+
# @param driver [Kitchen::Driver::Ec2] The driver.
|
126
|
+
# @param image [Aws::Ec2::Image] The EC2 Image object.
|
127
|
+
#
|
128
|
+
# @return [Kitchen::Driver::Aws::StandardPlatform]
|
129
|
+
#
|
130
|
+
def self.from_image(driver, image)
|
131
|
+
platforms.each_value do |platform|
|
132
|
+
result = platform.from_image(driver, image)
|
133
|
+
return result if result
|
134
|
+
end
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
|
138
|
+
#
|
139
|
+
# The list of supported architectures
|
140
|
+
#
|
141
|
+
ARCHITECTURE = %w[x86_64 i386 i86pc sun4v powerpc]
|
142
|
+
|
143
|
+
protected
|
144
|
+
|
145
|
+
#
|
146
|
+
# Sort a list of images by their versions, from greatest to least.
|
147
|
+
#
|
148
|
+
# This MUST perform a stable sort. (Note that `sort` and `sort_by` are
|
149
|
+
# not, by default, stable sorts in Ruby.)
|
150
|
+
#
|
151
|
+
# Used by the default find_image. The default version calls platform_from_image()
|
152
|
+
# on each image, and interprets the versions as floats (7 < 7.1 < 8).
|
153
|
+
#
|
154
|
+
# @param images [Array[Aws::Ec2::Image]] The list of images to sort
|
155
|
+
#
|
156
|
+
# @return [Array[Aws::Ec2::Image]] A sorted list.
|
157
|
+
#
|
158
|
+
def sort_by_version(images)
|
159
|
+
# 7.1 -> [ img1, img2, img3 ]
|
160
|
+
# 6 -> [ img4, img5 ]
|
161
|
+
# ...
|
162
|
+
images.group_by do |image|
|
163
|
+
platform = self.class.from_image(driver, image)
|
164
|
+
platform ? platform.version : nil
|
165
|
+
end.sort_by { |k, _v| k ? k.to_f : nil }.reverse.map { |_k, v| v }.flatten(1)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Not supported yet: aix mac_os_x nexus solaris
|
169
|
+
|
170
|
+
def prefer(images, &block)
|
171
|
+
# Put the matching ones *before* the non-matching ones.
|
172
|
+
matching, non_matching = images.partition(&block)
|
173
|
+
matching + non_matching
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
def self.parse_platform_string(platform_string)
|
179
|
+
platform, version = platform_string.split("-", 2)
|
180
|
+
|
181
|
+
# If the right side is a valid architecture, use it as such
|
182
|
+
# i.e. debian-i386 or windows-server-2012r2-i386
|
183
|
+
if version && ARCHITECTURE.include?(version.split("-")[-1])
|
184
|
+
# server-2012r2-i386 -> server-2012r2, -, i386
|
185
|
+
version, _dash, architecture = version.rpartition("-")
|
186
|
+
version = nil if version == ""
|
187
|
+
end
|
188
|
+
|
189
|
+
[platform, version, architecture]
|
190
|
+
end
|
191
|
+
|
192
|
+
def sort_images(images)
|
193
|
+
# P6: We prefer more recent images over older ones
|
194
|
+
images = images.sort_by(&:creation_date).reverse
|
195
|
+
# P5: We prefer x86_64 over i386 (if available)
|
196
|
+
images = prefer(images) { |image| image.architecture == :x86_64 }
|
197
|
+
# P4: We prefer gp2 (SSD) (if available)
|
198
|
+
images = prefer(images) do |image|
|
199
|
+
image.block_device_mappings.any? do |b|
|
200
|
+
b.device_name == image.root_device_name && b.ebs && b.ebs.volume_type == "gp2"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
# P3: We prefer ebs over instance_store (if available)
|
204
|
+
images = prefer(images) { |image| image.root_device_type == "ebs" }
|
205
|
+
# P2: We prefer hvm (the modern standard)
|
206
|
+
images = prefer(images) { |image| image.virtualization_type == "hvm" }
|
207
|
+
# P1: We prefer the latest version over anything else
|
208
|
+
sort_by_version(images)
|
209
|
+
end
|
210
|
+
|
211
|
+
def show_returned_images(images)
|
212
|
+
if images.empty?
|
213
|
+
driver.error("Search returned 0 images.")
|
214
|
+
else
|
215
|
+
driver.debug("Search returned #{images.size} images:")
|
216
|
+
images.each do |image|
|
217
|
+
platform = self.class.from_image(driver, image)
|
218
|
+
if platform
|
219
|
+
driver.debug("- #{image.name}: Detected #{platform}. #{driver.image_info(image)}")
|
220
|
+
else
|
221
|
+
driver.debug("- #{image.name}: No platform detected. #{driver.image_info(image)}")
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "kitchen/driver/aws/standard_platform"
|
2
|
+
|
3
|
+
module Kitchen
|
4
|
+
module Driver
|
5
|
+
class Aws
|
6
|
+
class StandardPlatform
|
7
|
+
# https://wiki.centos.org/Cloud/AWS
|
8
|
+
class Centos < StandardPlatform
|
9
|
+
StandardPlatform.platforms["centos"] = self
|
10
|
+
|
11
|
+
def username
|
12
|
+
# Centos 6.x images use root as the username (but the "centos 6"
|
13
|
+
# updateable image uses "centos")
|
14
|
+
return "root" if version && version.start_with?("6.")
|
15
|
+
"centos"
|
16
|
+
end
|
17
|
+
|
18
|
+
def image_search
|
19
|
+
search = {
|
20
|
+
"owner-alias" => "aws-marketplace",
|
21
|
+
"name" => ["CentOS Linux #{version}*", "CentOS-#{version}*-GA-*"]
|
22
|
+
}
|
23
|
+
search["architecture"] = architecture if architecture
|
24
|
+
search
|
25
|
+
end
|
26
|
+
|
27
|
+
def sort_by_version(images)
|
28
|
+
# 7.1 -> [ img1, img2, img3 ]
|
29
|
+
# 6 -> [ img4, img5 ]
|
30
|
+
# ...
|
31
|
+
images.group_by { |image| self.class.from_image(driver, image).version }.
|
32
|
+
sort_by { |k, _v| (k && k.include?(".") ? k.to_f : "#{k}.999".to_f) }.
|
33
|
+
reverse.map { |_k, v| v }.flatten(1)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.from_image(driver, image)
|
37
|
+
if image.name =~ /centos/i
|
38
|
+
image.name =~ /\b(\d+(\.\d+)?)\b/i
|
39
|
+
new(driver, "centos", (Regexp.last_match || [])[1], image.architecture)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "kitchen/driver/aws/standard_platform"
|
2
|
+
|
3
|
+
module Kitchen
|
4
|
+
module Driver
|
5
|
+
class Aws
|
6
|
+
class StandardPlatform
|
7
|
+
# https://wiki.debian.org/Cloud/AmazonEC2Image
|
8
|
+
class Debian < StandardPlatform
|
9
|
+
StandardPlatform.platforms["debian"] = self
|
10
|
+
|
11
|
+
DEBIAN_CODENAMES = {
|
12
|
+
"8" => "jessie",
|
13
|
+
"7" => "wheezy",
|
14
|
+
"6" => "squeeze"
|
15
|
+
}
|
16
|
+
|
17
|
+
def username
|
18
|
+
"admin"
|
19
|
+
end
|
20
|
+
|
21
|
+
def codename
|
22
|
+
version ? DEBIAN_CODENAMES[version] : DEBIAN_CODENAMES.values.first
|
23
|
+
end
|
24
|
+
|
25
|
+
def image_search
|
26
|
+
search = {
|
27
|
+
"owner-id" => "379101102735",
|
28
|
+
"name" => "debian-#{codename}-*"
|
29
|
+
}
|
30
|
+
search["architecture"] = architecture if architecture
|
31
|
+
search
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.from_image(driver, image)
|
35
|
+
if image.name =~ /debian/i
|
36
|
+
image.name =~ /\b(\d+|#{DEBIAN_CODENAMES.values.join("|")})\b/i
|
37
|
+
version = (Regexp.last_match || [])[1]
|
38
|
+
if version && version.to_i == 0
|
39
|
+
version = DEBIAN_CODENAMES.find do |_v, codename|
|
40
|
+
codename == version.downcase
|
41
|
+
end.first
|
42
|
+
end
|
43
|
+
new(driver, "debian", version, image.architecture)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|