kitchen-ec2 0.10.0 → 1.0.0.beta.1
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/.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
|