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
@@ -0,0 +1,34 @@
|
|
1
|
+
require "kitchen/driver/aws/standard_platform"
|
2
|
+
|
3
|
+
module Kitchen
|
4
|
+
module Driver
|
5
|
+
class Aws
|
6
|
+
class StandardPlatform
|
7
|
+
# https://docs.fedoraproject.org/en-US/Fedora_Draft_Documentation/0.1/html/Cloud_Guide/ch02.html#id697643
|
8
|
+
class Fedora < StandardPlatform
|
9
|
+
StandardPlatform.platforms["fedora"] = self
|
10
|
+
|
11
|
+
def username
|
12
|
+
"fedora"
|
13
|
+
end
|
14
|
+
|
15
|
+
def image_search
|
16
|
+
search = {
|
17
|
+
"owner-id" => "125523088429",
|
18
|
+
"name" => version ? "Fedora-Cloud-Base-#{version}-*" : "Fedora-Cloud-Base-*"
|
19
|
+
}
|
20
|
+
search["architecture"] = architecture if architecture
|
21
|
+
search
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.from_image(driver, image)
|
25
|
+
if image.name =~ /fedora/i
|
26
|
+
image.name =~ /\b(\d+(\.\d+)?)\b/i
|
27
|
+
new(driver, "fedora", (Regexp.last_match || [])[1], image.architecture)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "kitchen/driver/aws/standard_platform"
|
2
|
+
|
3
|
+
module Kitchen
|
4
|
+
module Driver
|
5
|
+
class Aws
|
6
|
+
class StandardPlatform
|
7
|
+
# http://www.daemonology.net/freebsd-on-ec2/
|
8
|
+
class Freebsd < StandardPlatform
|
9
|
+
StandardPlatform.platforms["freebsd"] = self
|
10
|
+
|
11
|
+
def username
|
12
|
+
(version && version.to_f < 9.1) ? "root" : "ec2-user"
|
13
|
+
end
|
14
|
+
|
15
|
+
def sudo_command
|
16
|
+
end
|
17
|
+
|
18
|
+
def image_search
|
19
|
+
search = {
|
20
|
+
"owner-id" => "118940168514",
|
21
|
+
"name" => ["FreeBSD #{version}*-RELEASE*", "FreeBSD/EC2 #{version}*-RELEASE*"]
|
22
|
+
}
|
23
|
+
search["architecture"] = architecture if architecture
|
24
|
+
search
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.from_image(driver, image)
|
28
|
+
if image.name =~ /freebsd/i
|
29
|
+
image.name =~ /\b(\d+(\.\d+)?)\b/i
|
30
|
+
new(driver, "freebsd", (Regexp.last_match || [])[1], image.architecture)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "kitchen/driver/aws/standard_platform"
|
2
|
+
|
3
|
+
module Kitchen
|
4
|
+
module Driver
|
5
|
+
class Aws
|
6
|
+
class StandardPlatform
|
7
|
+
# https://aws.amazon.com/blogs/aws/now-available-red-hat-enterprise-linux-64-amis/
|
8
|
+
class El < StandardPlatform
|
9
|
+
StandardPlatform.platforms["rhel"] = self
|
10
|
+
StandardPlatform.platforms["el"] = self
|
11
|
+
|
12
|
+
def initialize(driver, _name, version, architecture)
|
13
|
+
# rhel = el
|
14
|
+
super(driver, "rhel", version, architecture)
|
15
|
+
end
|
16
|
+
|
17
|
+
def username
|
18
|
+
(version && version.to_f < 6.4) ? "root" : "ec2-user"
|
19
|
+
end
|
20
|
+
|
21
|
+
def image_search
|
22
|
+
search = {
|
23
|
+
"owner-id" => "309956199498",
|
24
|
+
"name" => "RHEL-#{version}*"
|
25
|
+
}
|
26
|
+
search["architecture"] = architecture if architecture
|
27
|
+
search
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.from_image(driver, image)
|
31
|
+
if image.name =~ /rhel/i
|
32
|
+
image.name =~ /\b(\d+(\.\d+)?)/i
|
33
|
+
new(driver, "rhel", (Regexp.last_match || [])[1], image.architecture)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "kitchen/driver/aws/standard_platform"
|
2
|
+
|
3
|
+
module Kitchen
|
4
|
+
module Driver
|
5
|
+
class Aws
|
6
|
+
class StandardPlatform
|
7
|
+
# https://help.ubuntu.com/community/EC2StartersGuide#Official_Ubuntu_Cloud_Guest_Amazon_Machine_Images_.28AMIs.29
|
8
|
+
class Ubuntu < StandardPlatform
|
9
|
+
StandardPlatform.platforms["ubuntu"] = self
|
10
|
+
|
11
|
+
def username
|
12
|
+
"ubuntu"
|
13
|
+
end
|
14
|
+
|
15
|
+
def image_search
|
16
|
+
search = {
|
17
|
+
"owner-id" => "099720109477",
|
18
|
+
"name" => "ubuntu/images/*/ubuntu-*-#{version}*"
|
19
|
+
}
|
20
|
+
search["architecture"] = architecture if architecture
|
21
|
+
search
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.from_image(driver, image)
|
25
|
+
if image.name =~ /ubuntu/i
|
26
|
+
image.name =~ /\b(\d+(\.\d+)?)\b/i
|
27
|
+
new(driver, "ubuntu", (Regexp.last_match || [])[1], image.architecture)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require "kitchen/driver/aws/standard_platform"
|
2
|
+
|
3
|
+
module Kitchen
|
4
|
+
module Driver
|
5
|
+
class Aws
|
6
|
+
class StandardPlatform
|
7
|
+
# http://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/finding-an-ami.html
|
8
|
+
class Windows < StandardPlatform
|
9
|
+
StandardPlatform.platforms["windows"] = self
|
10
|
+
|
11
|
+
def username
|
12
|
+
"administrator"
|
13
|
+
end
|
14
|
+
|
15
|
+
# Figure out the right set of names to search for:
|
16
|
+
#
|
17
|
+
# "windows" -> [nil, nil, nil]
|
18
|
+
# Windows_Server-*-R*_RTM-, Windows_Server-*-R*_SP*-,
|
19
|
+
# Windows_Server-*-RTM-, Windows_Server-*-SP*-
|
20
|
+
# "windows-2012" -> [2012, 0, nil]
|
21
|
+
# Windows_Server-2012-RTM-, Windows_Server-2012-SP*-
|
22
|
+
# "windows-2012r2" -> [2012, 2, nil]
|
23
|
+
# Windows_Server-2012-R2_RTM-, Windows_Server-2012-R2_SP*-
|
24
|
+
# "windows-2012sp1" -> [2012, 0, 1]
|
25
|
+
# Windows_Server-2012-SP1-
|
26
|
+
# "windows-2012rtm" -> [2012, 0, 0]
|
27
|
+
# Windows_Server-2012-RTM-
|
28
|
+
# "windows-2012r2sp1" -> [2012, 2, 1]
|
29
|
+
# Windows_Server-2012-R2_SP1-
|
30
|
+
# "windows-2012r2rtm" -> [2012, 2, 0]
|
31
|
+
# Windows_Server-2012-R2_RTM-
|
32
|
+
def image_search
|
33
|
+
search = {
|
34
|
+
"owner-alias" => "amazon",
|
35
|
+
"name" => windows_name_filter
|
36
|
+
}
|
37
|
+
search["architecture"] = architecture if architecture
|
38
|
+
search
|
39
|
+
end
|
40
|
+
|
41
|
+
def sort_by_version(images)
|
42
|
+
# 2008r2rtm -> [ img1, img2, img3 ]
|
43
|
+
# 2012r2sp1 -> [ img4, img5 ]
|
44
|
+
# ...
|
45
|
+
images.group_by { |image| self.class.from_image(driver, image).windows_version_parts }.
|
46
|
+
sort_by { |version, _platform_images| version }.
|
47
|
+
reverse.map { |_version, platform_images| platform_images }.flatten(1)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.from_image(driver, image)
|
51
|
+
if image.name =~ /Windows/i
|
52
|
+
# 2008 R2 SP2
|
53
|
+
if image.name =~ /(\b\d+)\W*(r\d+)?/i
|
54
|
+
major, revision = (Regexp.last_match || [])[1], (Regexp.last_match || [])[2]
|
55
|
+
if image.name =~ /(sp\d+|rtm)/i
|
56
|
+
service_pack = (Regexp.last_match || [])[1]
|
57
|
+
end
|
58
|
+
revision = revision.downcase if revision
|
59
|
+
service_pack ||= "rtm"
|
60
|
+
service_pack = service_pack.downcase
|
61
|
+
version = "#{major}#{revision}#{service_pack}"
|
62
|
+
end
|
63
|
+
|
64
|
+
new(driver, "windows", version, image.architecture)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
|
70
|
+
# Turn windows version into [ major, revision, service_pack ]
|
71
|
+
#
|
72
|
+
# nil -> [ nil, nil, nil ]
|
73
|
+
# 2012 -> [ 2012, 0, nil ]
|
74
|
+
# 2012r2 -> [ 2012, 2, nil ]
|
75
|
+
# 2012r2sp4 -> [ 2012, 2, 4 ]
|
76
|
+
# 2012sp4 -> [ 2012, 0, 4 ]
|
77
|
+
# 2012rtm -> [ 2012, 0, 0 ]
|
78
|
+
def windows_version_parts
|
79
|
+
version = self.version
|
80
|
+
if version
|
81
|
+
# windows-server-* -> windows-*
|
82
|
+
if version.split("-", 2)[0] == "server"
|
83
|
+
version = version.split("-", 2)[1]
|
84
|
+
end
|
85
|
+
|
86
|
+
if version =~ /^(\d+)(r\d+)?(sp\d+|rtm)?$/i
|
87
|
+
major, revision, service_pack = Regexp.last_match[1..3]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
if major
|
92
|
+
# Get major as an integer (2008 -> 2008, 7 -> 7)
|
93
|
+
major = major.to_i
|
94
|
+
|
95
|
+
# Get revision as an integer (no revision -> 0, R1 -> 1).
|
96
|
+
revision = revision ? revision[1..-1].to_i : 0
|
97
|
+
|
98
|
+
# Turn service_pack into an integer. rtm = 0, spN = N.
|
99
|
+
if service_pack
|
100
|
+
service_pack = (service_pack.downcase == "rtm") ? 0 : service_pack[2..-1].to_i
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
[major, revision, service_pack]
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def windows_name_filter
|
110
|
+
major, revision, service_pack = windows_version_parts
|
111
|
+
|
112
|
+
case revision
|
113
|
+
when nil
|
114
|
+
revision_strings = ["", "R*_"]
|
115
|
+
when 0
|
116
|
+
revision_strings = [""]
|
117
|
+
else
|
118
|
+
revision_strings = ["R#{revision}_"]
|
119
|
+
end
|
120
|
+
|
121
|
+
case service_pack
|
122
|
+
when nil
|
123
|
+
revision_strings = revision_strings.flat_map { |r| ["#{r}RTM", "#{r}SP*"] }
|
124
|
+
when 0
|
125
|
+
revision_strings = revision_strings.map { |r| "#{r}RTM" }
|
126
|
+
else
|
127
|
+
revision_strings = revision_strings.map { |r| "#{r}SP#{service_pack}" }
|
128
|
+
end
|
129
|
+
|
130
|
+
revision_strings.map do |r|
|
131
|
+
"Windows_Server-#{major || "*"}-#{r}-English-*-Base-*"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
data/lib/kitchen/driver/ec2.rb
CHANGED
@@ -22,7 +22,16 @@ require "kitchen"
|
|
22
22
|
require_relative "ec2_version"
|
23
23
|
require_relative "aws/client"
|
24
24
|
require_relative "aws/instance_generator"
|
25
|
+
require_relative "aws/standard_platform"
|
26
|
+
require_relative "aws/standard_platform/centos"
|
27
|
+
require_relative "aws/standard_platform/debian"
|
28
|
+
require_relative "aws/standard_platform/rhel"
|
29
|
+
require_relative "aws/standard_platform/fedora"
|
30
|
+
require_relative "aws/standard_platform/freebsd"
|
31
|
+
require_relative "aws/standard_platform/ubuntu"
|
32
|
+
require_relative "aws/standard_platform/windows"
|
25
33
|
require "aws-sdk-core/waiters/errors"
|
34
|
+
require "retryable"
|
26
35
|
|
27
36
|
module Kitchen
|
28
37
|
|
@@ -40,8 +49,9 @@ module Kitchen
|
|
40
49
|
default_config :region, ENV["AWS_REGION"] || "us-east-1"
|
41
50
|
default_config :shared_credentials_profile, nil
|
42
51
|
default_config :availability_zone, nil
|
43
|
-
default_config :
|
44
|
-
|
52
|
+
default_config :instance_type do |driver|
|
53
|
+
driver.default_instance_type
|
54
|
+
end
|
45
55
|
default_config :ebs_optimized, false
|
46
56
|
default_config :security_group_ids, nil
|
47
57
|
default_config :tags, "created-by" => "test-kitchen"
|
@@ -62,120 +72,87 @@ module Kitchen
|
|
62
72
|
default_config :image_id do |driver|
|
63
73
|
driver.default_ami
|
64
74
|
end
|
65
|
-
default_config :
|
75
|
+
default_config :image_search, nil
|
76
|
+
default_config :username, nil
|
66
77
|
default_config :associate_public_ip, nil
|
67
78
|
default_config :interface, nil
|
68
79
|
default_config :http_proxy, ENV["HTTPS_PROXY"] || ENV["HTTP_PROXY"]
|
80
|
+
default_config :retry_limit, 3
|
69
81
|
|
70
82
|
required_config :aws_ssh_key_id
|
71
|
-
required_config :image_id
|
72
83
|
|
73
84
|
def self.validation_warn(driver, old_key, new_key)
|
74
85
|
driver.warn "WARN: The driver[#{driver.class.name}] config key `#{old_key}` " \
|
75
86
|
"is deprecated, please use `#{new_key}`"
|
76
87
|
end
|
77
88
|
|
78
|
-
|
89
|
+
def self.validation_error(driver, old_key, new_key)
|
90
|
+
raise "ERROR: The driver[#{driver.class.name}] config key `#{old_key}` " \
|
91
|
+
"has been removed, please use `#{new_key}`"
|
92
|
+
end
|
93
|
+
|
94
|
+
# TODO: remove these in 1.1
|
79
95
|
deprecated_configs = [:ebs_volume_size, :ebs_delete_on_termination, :ebs_device_name]
|
80
96
|
deprecated_configs.each do |d|
|
81
97
|
validations[d] = lambda do |attr, val, driver|
|
82
98
|
unless val.nil?
|
83
|
-
|
99
|
+
validation_error(driver, attr, "block_device_mappings")
|
84
100
|
end
|
85
101
|
end
|
86
102
|
end
|
87
103
|
validations[:ssh_key] = lambda do |attr, val, driver|
|
88
104
|
unless val.nil?
|
89
|
-
|
105
|
+
validation_error(driver, attr, "transport.ssh_key")
|
90
106
|
end
|
91
107
|
end
|
92
108
|
validations[:ssh_timeout] = lambda do |attr, val, driver|
|
93
109
|
unless val.nil?
|
94
|
-
|
110
|
+
validation_error(driver, attr, "transport.connection_timeout")
|
95
111
|
end
|
96
112
|
end
|
97
113
|
validations[:ssh_retries] = lambda do |attr, val, driver|
|
98
114
|
unless val.nil?
|
99
|
-
|
115
|
+
validation_error(driver, attr, "transport.connection_retries")
|
100
116
|
end
|
101
117
|
end
|
102
118
|
validations[:username] = lambda do |attr, val, driver|
|
103
119
|
unless val.nil?
|
104
|
-
|
120
|
+
validation_error(driver, attr, "transport.username")
|
105
121
|
end
|
106
122
|
end
|
107
123
|
validations[:flavor_id] = lambda do |attr, val, driver|
|
108
124
|
unless val.nil?
|
109
|
-
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
default_config :block_device_mappings, nil
|
114
|
-
validations[:block_device_mappings] = lambda do |_attr, val, _driver|
|
115
|
-
unless val.nil?
|
116
|
-
val.each do |bdm|
|
117
|
-
unless bdm.keys.include?(:ebs_volume_size) &&
|
118
|
-
bdm.keys.include?(:ebs_delete_on_termination) &&
|
119
|
-
bdm.keys.include?(:ebs_device_name)
|
120
|
-
raise "Every :block_device_mapping must include the keys :ebs_volume_size, " \
|
121
|
-
":ebs_delete_on_termination and :ebs_device_name"
|
122
|
-
end
|
123
|
-
end
|
125
|
+
validation_error(driver, attr, "instance_type")
|
124
126
|
end
|
125
127
|
end
|
126
128
|
|
127
129
|
# The access key/secret are now using the priority list AWS uses
|
128
130
|
# Providing these inside the .kitchen.yml is no longer recommended
|
129
|
-
validations[:aws_access_key_id] = lambda do |attr, val,
|
131
|
+
validations[:aws_access_key_id] = lambda do |attr, val, _driver|
|
130
132
|
unless val.nil?
|
131
|
-
|
133
|
+
raise "#{attr} is no longer valid, please use " \
|
132
134
|
"ENV['AWS_ACCESS_KEY_ID'] or ~/.aws/credentials. See " \
|
133
135
|
"the README for more details"
|
134
136
|
end
|
135
137
|
end
|
136
|
-
validations[:aws_secret_access_key] = lambda do |attr, val,
|
138
|
+
validations[:aws_secret_access_key] = lambda do |attr, val, _driver|
|
137
139
|
unless val.nil?
|
138
|
-
|
140
|
+
raise "#{attr} is no longer valid, please use " \
|
139
141
|
"ENV['AWS_SECRET_ACCESS_KEY'] or ~/.aws/credentials. See " \
|
140
142
|
"the README for more details"
|
141
143
|
end
|
142
144
|
end
|
143
|
-
validations[:aws_session_token] = lambda do |attr, val,
|
145
|
+
validations[:aws_session_token] = lambda do |attr, val, _driver|
|
144
146
|
unless val.nil?
|
145
|
-
|
147
|
+
raise "#{attr} is no longer valid, please use " \
|
146
148
|
"ENV['AWS_SESSION_TOKEN'] or ~/.aws/credentials. See " \
|
147
149
|
"the README for more details"
|
148
150
|
end
|
149
151
|
end
|
150
152
|
|
151
|
-
# A lifecycle method that should be invoked when the object is about
|
152
|
-
# ready to be used. A reference to an Instance is required as
|
153
|
-
# configuration dependant data may be access through an Instance. This
|
154
|
-
# also acts as a hook point where the object may wish to perform other
|
155
|
-
# last minute checks, validations, or configuration expansions.
|
156
|
-
#
|
157
|
-
# @param instance [Instance] an associated instance
|
158
|
-
# @return [self] itself, for use in chaining
|
159
|
-
# @raise [ClientError] if instance parameter is nil
|
160
|
-
def finalize_config!(instance)
|
161
|
-
super
|
162
|
-
|
163
|
-
if config[:availability_zone].nil?
|
164
|
-
config[:availability_zone] = config[:region] + "b"
|
165
|
-
elsif config[:availability_zone] =~ /^[a-z]$/
|
166
|
-
config[:availability_zone] = config[:region] + config[:availability_zone]
|
167
|
-
end
|
168
|
-
# TODO: when we get rid of flavor_id, move this to a default
|
169
|
-
if config[:instance_type].nil?
|
170
|
-
config[:instance_type] = config[:flavor_id] || "m1.small"
|
171
|
-
end
|
172
|
-
|
173
|
-
self
|
174
|
-
end
|
175
|
-
|
176
153
|
def create(state) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
177
|
-
copy_deprecated_configs(state)
|
178
154
|
return if state[:server_id]
|
155
|
+
update_username(state)
|
179
156
|
|
180
157
|
info(Kitchen::Util.outdent!(<<-END))
|
181
158
|
If you are not using an account that qualifies under the AWS
|
@@ -192,15 +169,28 @@ module Kitchen
|
|
192
169
|
server = submit_server
|
193
170
|
end
|
194
171
|
info("Instance <#{server.id}> requested.")
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
172
|
+
server.wait_until_exists do |w|
|
173
|
+
w.before_attempt do |attempts|
|
174
|
+
info("Polling AWS for existence, attempt #{attempts}...")
|
175
|
+
end
|
176
|
+
end
|
200
177
|
|
201
|
-
|
202
|
-
|
203
|
-
|
178
|
+
# See https://github.com/aws/aws-sdk-ruby/issues/859
|
179
|
+
# Tagging can fail with a NotFound error even though we waited until the server exists
|
180
|
+
# Waiting can also fail, so we have to also retry on that. If it means we re-tag the
|
181
|
+
# instance, so be it.
|
182
|
+
Retryable.retryable(
|
183
|
+
:tries => 10,
|
184
|
+
:sleep => lambda { |n| [2**n, 30].min },
|
185
|
+
:on => ::Aws::EC2::Errors::InvalidInstanceIDNotFound
|
186
|
+
) do |r, _|
|
187
|
+
info("Attempting to tag the instance, #{r} retries")
|
188
|
+
tag_server(server)
|
189
|
+
|
190
|
+
state[:server_id] = server.id
|
191
|
+
info("EC2 instance <#{state[:server_id]}> created.")
|
192
|
+
wait_until_ready(server, state)
|
193
|
+
end
|
204
194
|
|
205
195
|
if windows_os? &&
|
206
196
|
instance.transport[:username] =~ /administrator/i &&
|
@@ -236,9 +226,68 @@ module Kitchen
|
|
236
226
|
state.delete(:hostname)
|
237
227
|
end
|
238
228
|
|
229
|
+
def image
|
230
|
+
return @image if defined?(@image)
|
231
|
+
|
232
|
+
if config[:image_id]
|
233
|
+
@image = ec2.resource.image(config[:image_id])
|
234
|
+
show_chosen_image
|
235
|
+
|
236
|
+
else
|
237
|
+
raise "Neither image_id nor an image_search specified for instance #{instance.name}!" \
|
238
|
+
" Please specify one or the other."
|
239
|
+
end
|
240
|
+
|
241
|
+
@image
|
242
|
+
end
|
243
|
+
|
244
|
+
def default_instance_type
|
245
|
+
@instance_type ||= begin
|
246
|
+
# We default to the free tier (t2.micro for hvm, t1.micro for paravirtual)
|
247
|
+
if image && image.virtualization_type == "hvm"
|
248
|
+
info("instance_type not specified. Using free tier t2.micro instance ...")
|
249
|
+
"t2.micro"
|
250
|
+
else
|
251
|
+
info("instance_type not specified. Using free tier t1.micro instance since" \
|
252
|
+
" image is paravirtual (pick an hvm image to use the superior t2.micro!) ...")
|
253
|
+
"t1.micro"
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# The actual platform is the platform detected from the image
|
259
|
+
def actual_platform
|
260
|
+
@actual_platform ||= Aws::StandardPlatform.from_image(self, image) if image
|
261
|
+
end
|
262
|
+
|
263
|
+
def desired_platform
|
264
|
+
@desired_platform ||= begin
|
265
|
+
platform = Aws::StandardPlatform.from_platform_string(self, instance.platform.name)
|
266
|
+
if platform
|
267
|
+
debug("platform name #{instance.platform.name} appears to be a standard platform." \
|
268
|
+
" Searching for #{platform} ...")
|
269
|
+
end
|
270
|
+
platform
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
239
274
|
def default_ami
|
240
|
-
|
241
|
-
|
275
|
+
@default_ami ||= begin
|
276
|
+
search_platform = desired_platform ||
|
277
|
+
Aws::StandardPlatform.from_platform_string(self, "ubuntu")
|
278
|
+
image_search = config[:image_search] || search_platform.image_search
|
279
|
+
search_platform.find_image(image_search)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def update_username(state)
|
284
|
+
# TODO: if the user explicitly specified the transport's default username,
|
285
|
+
# do NOT overwrite it!
|
286
|
+
if instance.transport[:username] == instance.transport.class.defaults[:username]
|
287
|
+
debug("No SSH username specified: using default username #{actual_platform.username} " \
|
288
|
+
" for image #{config[:image_id]}, which we detected as #{actual_platform}.")
|
289
|
+
state[:username] = actual_platform.username
|
290
|
+
end
|
242
291
|
end
|
243
292
|
|
244
293
|
def ec2
|
@@ -248,7 +297,8 @@ module Kitchen
|
|
248
297
|
config[:aws_access_key_id],
|
249
298
|
config[:aws_secret_access_key],
|
250
299
|
config[:aws_session_token],
|
251
|
-
config[:http_proxy]
|
300
|
+
config[:http_proxy],
|
301
|
+
config[:retry_limit]
|
252
302
|
)
|
253
303
|
end
|
254
304
|
|
@@ -256,36 +306,13 @@ module Kitchen
|
|
256
306
|
@instance_generator ||= Aws::InstanceGenerator.new(config, ec2, instance.logger)
|
257
307
|
end
|
258
308
|
|
259
|
-
# This copies transport config from the current config object into the
|
260
|
-
# state. This relies on logic in the transport that merges the transport
|
261
|
-
# config with the current state object, so its a bad coupling. But we
|
262
|
-
# can get rid of this when we get rid of these deprecated configs!
|
263
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
264
|
-
def copy_deprecated_configs(state)
|
265
|
-
if config[:ssh_timeout]
|
266
|
-
state[:connection_timeout] = config[:ssh_timeout]
|
267
|
-
end
|
268
|
-
if config[:ssh_retries]
|
269
|
-
state[:connection_retries] = config[:ssh_retries]
|
270
|
-
end
|
271
|
-
if config[:username]
|
272
|
-
state[:username] = config[:username]
|
273
|
-
elsif instance.transport[:username] == instance.transport.class.defaults[:username]
|
274
|
-
# If the transport has the default username, copy it from amis.json
|
275
|
-
# This duplicated old behavior but I hate amis.json
|
276
|
-
ami_username = amis["usernames"][instance.platform.name]
|
277
|
-
state[:username] = ami_username if ami_username
|
278
|
-
end
|
279
|
-
if config[:ssh_key]
|
280
|
-
state[:ssh_key] = config[:ssh_key]
|
281
|
-
end
|
282
|
-
end
|
283
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
284
|
-
|
285
309
|
# Fog AWS helper for creating the instance
|
286
310
|
def submit_server
|
287
|
-
debug("Creating EC2 Instance..")
|
288
311
|
instance_data = instance_generator.ec2_instance_data
|
312
|
+
debug("Creating EC2 instance in region #{config[:region]} with properties:")
|
313
|
+
instance_data.each do |key, value|
|
314
|
+
debug("- #{key} = #{value.inspect}")
|
315
|
+
end
|
289
316
|
instance_data[:min_count] = 1
|
290
317
|
instance_data[:max_count] = 1
|
291
318
|
ec2.create_instance(instance_data)
|
@@ -325,6 +352,8 @@ module Kitchen
|
|
325
352
|
server.create_tags(:tags => tags)
|
326
353
|
end
|
327
354
|
|
355
|
+
# Normally we could use `server.wait_until_running` but we actually need
|
356
|
+
# to check more than just the instance state
|
328
357
|
def wait_until_ready(server, state)
|
329
358
|
wait_with_destroy(server, state, "to become ready") do |aws_instance|
|
330
359
|
hostname = hostname(aws_instance, config[:interface])
|
@@ -347,21 +376,8 @@ module Kitchen
|
|
347
376
|
end
|
348
377
|
end
|
349
378
|
|
350
|
-
#
|
351
|
-
|
352
|
-
wait_with_destroy(server, state, "to fetch windows admin password") do |aws_instance|
|
353
|
-
enc = server.client.get_password_data(
|
354
|
-
:instance_id => state[:server_id]
|
355
|
-
).password_data
|
356
|
-
# Password data is blank until password is available
|
357
|
-
!enc.nil? && !enc.empty?
|
358
|
-
end
|
359
|
-
pass = server.decrypt_windows_password(instance.transport[:ssh_key])
|
360
|
-
state[:password] = pass
|
361
|
-
info("Retrieved Windows password for instance <#{state[:server_id]}>.")
|
362
|
-
end
|
363
|
-
# rubocop:enable Lint/UnusedBlockArgument
|
364
|
-
|
379
|
+
# Poll a block, waiting for it to return true. If it does not succeed
|
380
|
+
# within the configured time we destroy the instance to save people money
|
365
381
|
def wait_with_destroy(server, state, status_msg, &block)
|
366
382
|
wait_log = proc do |attempts|
|
367
383
|
c = attempts * config[:retryable_sleep]
|
@@ -383,13 +399,20 @@ module Kitchen
|
|
383
399
|
end
|
384
400
|
end
|
385
401
|
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
402
|
+
# rubocop:disable Lint/UnusedBlockArgument
|
403
|
+
def fetch_windows_admin_password(server, state)
|
404
|
+
wait_with_destroy(server, state, "to fetch windows admin password") do |aws_instance|
|
405
|
+
enc = server.client.get_password_data(
|
406
|
+
:instance_id => state[:server_id]
|
407
|
+
).password_data
|
408
|
+
# Password data is blank until password is available
|
409
|
+
!enc.nil? && !enc.empty?
|
391
410
|
end
|
411
|
+
pass = server.decrypt_windows_password(File.expand_path(instance.transport[:ssh_key]))
|
412
|
+
state[:password] = pass
|
413
|
+
info("Retrieved Windows password for instance <#{state[:server_id]}>.")
|
392
414
|
end
|
415
|
+
# rubocop:enable Lint/UnusedBlockArgument
|
393
416
|
|
394
417
|
#
|
395
418
|
# Ordered mapping from config name to Fog name. Ordered by preference
|
@@ -424,16 +447,24 @@ module Kitchen
|
|
424
447
|
end
|
425
448
|
end
|
426
449
|
|
450
|
+
#
|
451
|
+
# Returns the sudo command to use or empty string if sudo is not configured
|
452
|
+
#
|
453
|
+
def sudo_command
|
454
|
+
instance.provisioner[:sudo] ? instance.provisioner[:sudo_command].to_s : ""
|
455
|
+
end
|
456
|
+
|
457
|
+
# rubocop:disable Metrics/MethodLength, Metrics/LineLength
|
427
458
|
def create_ec2_json(state)
|
428
459
|
if windows_os?
|
429
460
|
cmd = "New-Item -Force C:\\chef\\ohai\\hints\\ec2.json -ItemType File"
|
430
461
|
else
|
431
|
-
|
462
|
+
debug "Using sudo_command='#{sudo_command}' for ohai hints"
|
463
|
+
cmd = "#{sudo_command} mkdir -p /etc/chef/ohai/hints; #{sudo_command} touch /etc/chef/ohai/hints/ec2.json"
|
432
464
|
end
|
433
465
|
instance.transport.connection(state).execute(cmd)
|
434
466
|
end
|
435
467
|
|
436
|
-
# rubocop:disable Metrics/MethodLength, Metrics/LineLength
|
437
468
|
def default_windows_user_data
|
438
469
|
# Preparing custom static admin user if we defined something other than Administrator
|
439
470
|
custom_admin_script = ""
|
@@ -476,6 +507,29 @@ module Kitchen
|
|
476
507
|
end
|
477
508
|
# rubocop:enable Metrics/MethodLength, Metrics/LineLength
|
478
509
|
|
510
|
+
def show_chosen_image
|
511
|
+
# Print some debug stuff
|
512
|
+
debug("Image for #{instance.name}: #{image.name}. #{image_info(image)}")
|
513
|
+
if actual_platform
|
514
|
+
info("Detected platform: #{actual_platform.name} version #{actual_platform.version}" \
|
515
|
+
" on #{actual_platform.architecture}. Instance Type: #{config[:instance_type]}." \
|
516
|
+
" Default username: #{actual_platform.username} (default).")
|
517
|
+
else
|
518
|
+
debug("No platform detected for #{image.name}.")
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
def image_info(image)
|
523
|
+
root_device = image.block_device_mappings.
|
524
|
+
find { |b| b.device_name == image.root_device_name }
|
525
|
+
volume_type = " #{root_device.ebs.volume_type}" if root_device && root_device.ebs
|
526
|
+
|
527
|
+
" Architecture: #{image.architecture}," \
|
528
|
+
" Virtualization: #{image.virtualization_type}," \
|
529
|
+
" Storage: #{image.root_device_type}#{volume_type}," \
|
530
|
+
" Created: #{image.creation_date}"
|
531
|
+
end
|
532
|
+
|
479
533
|
end
|
480
534
|
end
|
481
535
|
end
|