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.
@@ -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
- i[:block_device_mappings] = block_device_mappings unless block_device_mappings.empty?
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