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