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.
@@ -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