kitchen-ec2 0.8.0 → 0.9.0

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.
data/Rakefile CHANGED
@@ -1,21 +1,39 @@
1
- require "bundler/gem_tasks"
2
- require 'cane/rake_task'
3
- require 'tailor/rake_task'
1
+ # -*- encoding: utf-8 -*-
4
2
 
5
- desc "Run cane to check quality metrics"
6
- Cane::RakeTask.new do |cane|
7
- cane.canefile = './.cane'
8
- end
3
+ require "bundler/gem_tasks"
9
4
 
10
- Tailor::RakeTask.new
5
+ require "rspec/core/rake_task"
6
+ RSpec::Core::RakeTask.new(:test)
11
7
 
12
8
  desc "Display LOC stats"
13
9
  task :stats do
14
10
  puts "\n## Production Code Stats"
15
- sh "countloc -r lib/kitchen"
11
+ sh "countloc -r lib/kitchen lib/kitchen.rb"
12
+ puts "\n## Test Code Stats"
13
+ sh "countloc -r spec features"
14
+ end
15
+
16
+ require "finstyle"
17
+ require "rubocop/rake_task"
18
+ RuboCop::RakeTask.new(:style) do |task|
19
+ task.options << "--display-cop-names"
20
+ end
21
+
22
+ if RUBY_ENGINE != "jruby"
23
+ require "cane/rake_task"
24
+ desc "Run cane to check quality metrics"
25
+ Cane::RakeTask.new do |cane|
26
+ cane.canefile = "./.cane"
27
+ end
28
+
29
+ desc "Run all quality tasks"
30
+ task :quality => [:cane, :style, :stats]
31
+ else
32
+ desc "Run all quality tasks"
33
+ task :quality => [:style, :stats]
16
34
  end
17
35
 
18
- desc "Run all quality tasks"
19
- task :quality => [:cane, :tailor, :stats]
36
+ require "yard"
37
+ YARD::Rake::YardocTask.new
20
38
 
21
- task :default => [:quality]
39
+ task :default => [:test, :quality]
data/data/amis.json CHANGED
@@ -5,6 +5,8 @@
5
5
  "ubuntu-12.04": "ami-bb9a08ba",
6
6
  "ubuntu-12.10": "ami-4556c544",
7
7
  "ubuntu-13.04": "ami-97099a96",
8
+ "ubuntu-13.10": "ami-45f1a744",
9
+ "ubuntu-14.04": "ami-955c0c94",
8
10
  "centos-6.4": "ami-9ffa709e",
9
11
  "debian-7.1.0": "ami-f1f064f0"
10
12
  },
@@ -13,6 +15,8 @@
13
15
  "ubuntu-12.04": "ami-881c57da",
14
16
  "ubuntu-12.10": "ami-3ce0a86e",
15
17
  "ubuntu-13.04": "ami-4af5bd18",
18
+ "ubuntu-13.10": "ami-02e6b850",
19
+ "ubuntu-14.04": "ami-9a7c25c8",
16
20
  "centos-6.4": "ami-46f5bb14",
17
21
  "debian-7.1.0": "ami-fe8ac3ac"
18
22
  },
@@ -21,6 +25,8 @@
21
25
  "ubuntu-12.04": "ami-cb26b4f1",
22
26
  "ubuntu-12.10": "ami-1703912d",
23
27
  "ubuntu-13.04": "ami-8f32a0b5",
28
+ "ubuntu-13.10": "ami-e54423df",
29
+ "ubuntu-14.04": "ami-f3b1d6c9",
24
30
  "centos-6.4": "ami-9352c1a9",
25
31
  "debian-7.1.0": "ami-4e099a74"
26
32
  },
@@ -29,6 +35,8 @@
29
35
  "ubuntu-12.04": "ami-d3b5aea7",
30
36
  "ubuntu-12.10": "ami-6b62791f",
31
37
  "ubuntu-13.04": "ami-6fd4cf1b",
38
+ "ubuntu-13.10": "ami-39eb3f4e",
39
+ "ubuntu-14.04": "ami-c112c5b6",
32
40
  "centos-6.4": "ami-75190b01",
33
41
  "debian-7.1.0": "ami-954559e1"
34
42
  },
@@ -37,6 +45,8 @@
37
45
  "ubuntu-12.04": "ami-c905a1d4",
38
46
  "ubuntu-12.10": "ami-e759fdfa",
39
47
  "ubuntu-13.04": "ami-4b2b8f56",
48
+ "ubuntu-13.10": "ami-076cc21a",
49
+ "ubuntu-14.04": "ami-052b8518",
40
50
  "centos-6.4": "ami-a665c0bb",
41
51
  "debian-7.1.0": "ami-b03590ad"
42
52
  },
@@ -45,6 +55,8 @@
45
55
  "ubuntu-12.04": "ami-2f115c46",
46
56
  "ubuntu-12.10": "ami-4d5b1824",
47
57
  "ubuntu-13.04": "ami-a73371ce",
58
+ "ubuntu-13.10": "ami-a65393ce",
59
+ "ubuntu-14.04": "ami-4a915c22",
48
60
  "centos-6.4": "ami-bf5021d6",
49
61
  "debian-7.1.0": "ami-50d9a439"
50
62
  },
@@ -53,6 +65,8 @@
53
65
  "ubuntu-12.04": "ami-eaf0daaf",
54
66
  "ubuntu-12.10": "ami-02220847",
55
67
  "ubuntu-13.04": "ami-fe052fbb",
68
+ "ubuntu-13.10": "ami-bb2a2afe",
69
+ "ubuntu-14.04": "ami-d99a9a9c",
56
70
  "centos-6.4": "ami-5d456c18",
57
71
  "debian-7.1.0": "ami-1a9bb25f"
58
72
  },
@@ -61,6 +75,8 @@
61
75
  "ubuntu-12.04": "ami-e6f36fd6",
62
76
  "ubuntu-12.10": "ami-0c069b3c",
63
77
  "ubuntu-13.04": "ami-4ade427a",
78
+ "ubuntu-13.10": "ami-c18ff1f1",
79
+ "ubuntu-14.04": "ami-b7720b87",
64
80
  "centos-6.4": "ami-b3bf2f83",
65
81
  "debian-7.1.0": "ami-158a1925"
66
82
  }
@@ -70,6 +86,8 @@
70
86
  "ubuntu-12.04": "ubuntu",
71
87
  "ubuntu-12.10": "ubuntu",
72
88
  "ubuntu-13.04": "ubuntu",
89
+ "ubuntu-13.10": "ubuntu",
90
+ "ubuntu-14.04": "ubuntu",
73
91
  "centos-6.4": "root",
74
92
  "debian-7.1.0": "admin"
75
93
  },
data/kitchen-ec2.gemspec CHANGED
@@ -1,28 +1,39 @@
1
1
  # -*- encoding: utf-8 -*-
2
- lib = File.expand_path('../lib', __FILE__)
2
+ lib = File.expand_path("../lib", __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'kitchen/driver/ec2_version.rb'
4
+ require "kitchen/driver/ec2_version.rb"
5
5
 
6
6
  Gem::Specification.new do |gem|
7
7
  gem.name = "kitchen-ec2"
8
8
  gem.version = Kitchen::Driver::EC2_VERSION
9
- gem.license = 'Apache 2.0'
9
+ gem.license = "Apache 2.0"
10
10
  gem.authors = ["Fletcher Nichol"]
11
11
  gem.email = ["fnichol@nichol.ca"]
12
12
  gem.description = "A Test Kitchen Driver for Amazon EC2"
13
13
  gem.summary = gem.description
14
14
  gem.homepage = "http://kitchen.ci/"
15
15
 
16
- gem.files = `git ls-files`.split($/)
16
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
17
17
  gem.executables = []
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
- gem.add_dependency 'test-kitchen', '~> 1.0'
22
- gem.add_dependency 'fog'
21
+ gem.add_dependency "test-kitchen", "~> 1.4"
22
+ gem.add_dependency "excon"
23
+ gem.add_dependency "multi_json"
24
+ gem.add_dependency "aws-sdk-v1", "~> 1.59.0"
25
+ gem.add_dependency "aws-sdk", "~> 2"
23
26
 
24
- gem.add_development_dependency 'rspec'
25
- gem.add_development_dependency 'cane'
26
- gem.add_development_dependency 'tailor'
27
- gem.add_development_dependency 'countloc'
27
+ gem.add_development_dependency "rspec", "~> 3.2"
28
+ gem.add_development_dependency "countloc", "~> 0.4"
29
+ gem.add_development_dependency "maruku", "~> 0.6"
30
+ gem.add_development_dependency "simplecov", "~> 0.7"
31
+ gem.add_development_dependency "yard", "~> 0.8"
32
+
33
+ # style and complexity libraries are tightly version pinned as newer releases
34
+ # may introduce new and undesireable style choices which would be immediately
35
+ # enforced in CI
36
+ gem.add_development_dependency "finstyle", "1.4.0"
37
+ gem.add_development_dependency "cane", "2.6.2"
38
+ gem.add_development_dependency "climate_control"
28
39
  end
@@ -0,0 +1,110 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Tyler Ball (<tball@chef.io>)
4
+ #
5
+ # Copyright (C) 2015, Fletcher Nichol
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require "aws-sdk"
20
+ require "aws-sdk-core/credentials"
21
+ require "aws-sdk-core/shared_credentials"
22
+ require "aws-sdk-core/instance_profile_credentials"
23
+
24
+ module Kitchen
25
+
26
+ module Driver
27
+
28
+ class Aws
29
+
30
+ # A class for creating and managing the EC2 client connection
31
+ #
32
+ # @author Tyler Ball <tball@chef.io>
33
+ class Client
34
+
35
+ def initialize(
36
+ region,
37
+ profile_name = nil,
38
+ access_key_id = nil,
39
+ secret_access_key = nil,
40
+ session_token = nil
41
+ )
42
+ creds = self.class.get_credentials(
43
+ profile_name, access_key_id, secret_access_key, session_token
44
+ )
45
+ ::Aws.config.update(
46
+ :region => region,
47
+ :credentials => creds
48
+ )
49
+ end
50
+
51
+ # Try and get the credentials from an ordered list of locations
52
+ # http://docs.aws.amazon.com/sdkforruby/api/index.html#Configuration
53
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
54
+ def self.get_credentials(profile_name, access_key_id, secret_access_key, session_token)
55
+ shared_creds = ::Aws::SharedCredentials.new(:profile_name => profile_name)
56
+ if access_key_id && secret_access_key
57
+ ::Aws::Credentials.new(access_key_id, secret_access_key, session_token)
58
+ # TODO: these are deprecated, remove them in the next major version
59
+ elsif ENV["AWS_ACCESS_KEY"] && ENV["AWS_SECRET_KEY"]
60
+ ::Aws::Credentials.new(
61
+ ENV["AWS_ACCESS_KEY"],
62
+ ENV["AWS_SECRET_KEY"],
63
+ ENV["AWS_TOKEN"]
64
+ )
65
+ elsif ENV["AWS_ACCESS_KEY_ID"] && ENV["AWS_SECRET_ACCESS_KEY"]
66
+ ::Aws::Credentials.new(
67
+ ENV["AWS_ACCESS_KEY_ID"],
68
+ ENV["AWS_SECRET_ACCESS_KEY"],
69
+ ENV["AWS_SESSION_TOKEN"]
70
+ )
71
+ elsif shared_creds.loadable?
72
+ shared_creds
73
+ else
74
+ ::Aws::InstanceProfileCredentials.new(:retries => 1)
75
+ end
76
+ end
77
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
78
+
79
+ def create_instance(options)
80
+ resource.create_instances(options)[0]
81
+ end
82
+
83
+ def get_instance(id)
84
+ resource.instance(id)
85
+ end
86
+
87
+ def get_instance_from_spot_request(request_id)
88
+ resource.instances(
89
+ :filters => [{
90
+ :name => "spot-instance-request-id",
91
+ :values => [request_id]
92
+ }]
93
+ ).to_a[0]
94
+ end
95
+
96
+ def client
97
+ @client ||= ::Aws::EC2::Client.new
98
+ end
99
+
100
+ def resource
101
+ @resource ||= ::Aws::EC2::Resource.new
102
+ end
103
+
104
+ end
105
+
106
+ end
107
+
108
+ end
109
+
110
+ end
@@ -0,0 +1,148 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Tyler Ball (<tball@chef.io>)
4
+ #
5
+ # Copyright (C) 2015, Fletcher Nichol
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require "kitchen/logging"
20
+
21
+ module Kitchen
22
+
23
+ module Driver
24
+
25
+ class Aws
26
+
27
+ # A class for encapsulating the instance payload logic
28
+ #
29
+ # @author Tyler Ball <tball@chef.io>
30
+ class InstanceGenerator
31
+
32
+ include Logging
33
+
34
+ attr_reader :config, :ec2
35
+
36
+ def initialize(config, ec2)
37
+ @config = config
38
+ @ec2 = ec2
39
+ end
40
+
41
+ # Transform the provided config into the hash to send to AWS. Some fields
42
+ # can be passed in null, others need to be ommitted if they are null
43
+ def ec2_instance_data # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
44
+ i = {
45
+ :placement => {
46
+ :availability_zone => config[:availability_zone]
47
+ },
48
+ :instance_type => config[:instance_type],
49
+ :ebs_optimized => config[:ebs_optimized],
50
+ :image_id => config[:image_id],
51
+ :key_name => config[:aws_ssh_key_id],
52
+ :subnet_id => config[:subnet_id],
53
+ :private_ip_address => config[:private_ip_address]
54
+ }
55
+ i[:block_device_mappings] = block_device_mappings unless block_device_mappings.empty?
56
+ i[:security_group_ids] = config[:security_group_ids] if config[:security_group_ids]
57
+ i[:user_data] = prepared_user_data if prepared_user_data
58
+ if config[:iam_instance_profile]
59
+ i[:iam_instance_profile] = { :name => config[:iam_profile_name] }
60
+ end
61
+ if !config.fetch(:associate_public_ip_address, nil).nil?
62
+ i[:network_interfaces] =
63
+ [{
64
+ :device_index => 0,
65
+ :associate_public_ip_address => config[:associate_public_ip_address]
66
+ }]
67
+ end
68
+ i
69
+ end
70
+
71
+ # Transforms the provided config into the appropriate hash for creating a BDM
72
+ # in AWS
73
+ def block_device_mappings # rubocop:disable all
74
+ return @bdms if @bdms
75
+ bdms = config[:block_device_mappings] || []
76
+ if bdms.empty?
77
+ if config[:ebs_volume_size] || config.fetch(:ebs_delete_on_termination, nil) ||
78
+ config[:ebs_device_name] || config[:ebs_volume_type]
79
+ # If the user didn't supply block_device_mappings but did supply
80
+ # the old configs, copy them into the block_device_mappings array correctly
81
+ # TODO: remove this logic when we remove the deprecated values
82
+ bdms << {
83
+ :ebs_volume_size => config[:ebs_volume_size] || 8,
84
+ :ebs_delete_on_termination => config.fetch(:ebs_delete_on_termination, true),
85
+ :ebs_device_name => config[:ebs_device_name] || "/dev/sda1",
86
+ :ebs_volume_type => config[:ebs_volume_type] || "standard"
87
+ }
88
+ end
89
+ end
90
+
91
+ # Convert the provided keys to what AWS expects
92
+ bdms = bdms.map do |bdm|
93
+ b = {
94
+ :ebs => {
95
+ :volume_size => bdm[:ebs_volume_size],
96
+ :delete_on_termination => bdm[:ebs_delete_on_termination]
97
+ },
98
+ :device_name => bdm[:ebs_device_name]
99
+ }
100
+ b[:ebs][:volume_type] = bdm[:ebs_volume_type] if bdm[:ebs_volume_type]
101
+ b[:ebs][:snapshot_id] = bdm[:ebs_snapshot_id] if bdm[:ebs_snapshot_id]
102
+ b[:virtual_name] = bdm[:ebs_virtual_name] if bdm[:ebs_virtual_name]
103
+ b
104
+ end
105
+
106
+ debug_if_root_device(bdms)
107
+
108
+ @bdms = bdms
109
+ end
110
+
111
+ # If the provided bdms match the root device in the AMI, emit log that
112
+ # states this
113
+ def debug_if_root_device(bdms)
114
+ image_id = config[:image_id]
115
+ image = ec2.resource.image(image_id)
116
+ begin
117
+ root_device_name = image.root_device_name
118
+ rescue ::Aws::EC2::Errors::InvalidAMIIDNotFound
119
+ # Not raising here because AWS will give a more meaningful message
120
+ # when we try to create the instance
121
+ return
122
+ end
123
+ bdms.find { |bdm|
124
+ if bdm[:device_name] == root_device_name
125
+ info("Overriding root device [#{root_device_name}] from image [#{image_id}]")
126
+ end
127
+ }
128
+ end
129
+
130
+ def prepared_user_data
131
+ # If user_data is a file reference, lets read it as such
132
+ unless @user_data
133
+ if config[:user_data] && File.file?(config[:user_data])
134
+ @user_data = File.read(config[:user_data])
135
+ else
136
+ @user_data = config[:user_data]
137
+ end
138
+ end
139
+ @user_data
140
+ end
141
+
142
+ end
143
+
144
+ end
145
+
146
+ end
147
+
148
+ end
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
4
  #
5
- # Copyright (C) 2012, Fletcher Nichol
5
+ # Copyright (C) 2015, Fletcher Nichol
6
6
  #
7
7
  # Licensed under the Apache License, Version 2.0 (the "License");
8
8
  # you may not use this file except in compliance with the License.
@@ -16,10 +16,13 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
- require 'benchmark'
20
- require 'json'
21
- require 'fog'
22
- require 'kitchen'
19
+ require "benchmark"
20
+ require "json"
21
+ require "aws"
22
+ require "kitchen"
23
+ require "kitchen/driver/ec2_version"
24
+ require_relative "aws/client"
25
+ require_relative "aws/instance_generator"
23
26
 
24
27
  module Kitchen
25
28
 
@@ -28,143 +31,340 @@ module Kitchen
28
31
  # Amazon EC2 driver for Test Kitchen.
29
32
  #
30
33
  # @author Fletcher Nichol <fnichol@nichol.ca>
31
- class Ec2 < Kitchen::Driver::SSHBase
34
+ class Ec2 < Kitchen::Driver::Base # rubocop:disable Metrics/ClassLength
32
35
 
33
- default_config :region, 'us-east-1'
34
- default_config :availability_zone, 'us-east-1b'
35
- default_config :flavor_id, 'm1.small'
36
+ kitchen_driver_api_version 2
37
+
38
+ plugin_version Kitchen::Driver::EC2_VERSION
39
+
40
+ default_config :region, ENV["AWS_REGION"] || "us-east-1"
41
+ default_config :shared_credentials_profile, nil
42
+ default_config :availability_zone, nil
43
+ default_config :flavor_id, nil
44
+ default_config :instance_type, nil
36
45
  default_config :ebs_optimized, false
37
- default_config :security_group_ids, ['default']
38
- default_config :tags, { 'created-by' => 'test-kitchen' }
39
- default_config :aws_access_key_id do |driver|
40
- ENV['AWS_ACCESS_KEY'] || ENV['AWS_ACCESS_KEY_ID']
46
+ default_config :security_group_ids, nil
47
+ default_config :tags, "created-by" => "test-kitchen"
48
+ default_config :user_data, nil
49
+ default_config :private_ip_address, nil
50
+ default_config :iam_profile_name, nil
51
+ default_config :price, nil
52
+ default_config :retryable_tries, 60
53
+ default_config :retryable_sleep, 5
54
+ default_config :aws_access_key_id, nil
55
+ default_config :aws_secret_access_key, nil
56
+ default_config :aws_session_token, nil
57
+ default_config :aws_ssh_key_id, ENV["AWS_SSH_KEY_ID"]
58
+ default_config :image_id do |driver|
59
+ driver.default_ami
41
60
  end
42
- default_config :aws_secret_access_key do |driver|
43
- ENV['AWS_SECRET_KEY'] || ENV['AWS_SECRET_ACCESS_KEY']
61
+ default_config :username, nil
62
+ default_config :associate_public_ip, nil
63
+
64
+ required_config :aws_ssh_key_id
65
+ required_config :image_id
66
+
67
+ def self.validation_warn(driver, old_key, new_key)
68
+ driver.warn "WARN: The driver[#{driver.class.name}] config key `#{old_key}` " \
69
+ "is deprecated, please use `#{new_key}`"
44
70
  end
45
- default_config :aws_session_token do |driver|
46
- ENV['AWS_SESSION_TOKEN'] || ENV['AWS_TOKEN']
71
+
72
+ # TODO: remove these in the next major version of TK
73
+ deprecated_configs = [:ebs_volume_size, :ebs_delete_on_termination, :ebs_device_name]
74
+ deprecated_configs.each do |d|
75
+ validations[d] = lambda do |attr, val, driver|
76
+ unless val.nil?
77
+ validation_warn(driver, attr, "block_device_mappings")
78
+ end
79
+ end
47
80
  end
48
- default_config :aws_ssh_key_id do |driver|
49
- ENV['AWS_SSH_KEY_ID']
81
+ validations[:ssh_key] = lambda do |attr, val, driver|
82
+ unless val.nil?
83
+ validation_warn(driver, attr, "transport.ssh_key")
84
+ end
50
85
  end
51
- default_config :image_id do |driver|
52
- driver.default_ami
86
+ validations[:ssh_timeout] = lambda do |attr, val, driver|
87
+ unless val.nil?
88
+ validation_warn(driver, attr, "transport.connection_timeout")
89
+ end
53
90
  end
54
- default_config :username do |driver|
55
- driver.default_username
91
+ validations[:ssh_retries] = lambda do |attr, val, driver|
92
+ unless val.nil?
93
+ validation_warn(driver, attr, "transport.connection_retries")
94
+ end
56
95
  end
57
- default_config :endpoint do |driver|
58
- "https://ec2.#{driver[:region]}.amazonaws.com/"
96
+ validations[:username] = lambda do |attr, val, driver|
97
+ unless val.nil?
98
+ validation_warn(driver, attr, "transport.username")
99
+ end
100
+ end
101
+ validations[:flavor_id] = lambda do |attr, val, driver|
102
+ unless val.nil?
103
+ validation_warn(driver, attr, "instance_type")
104
+ end
59
105
  end
60
106
 
61
- default_config :interface, nil
107
+ default_config :block_device_mappings, nil
108
+ validations[:block_device_mappings] = lambda do |_attr, val, _driver|
109
+ unless val.nil?
110
+ val.each do |bdm|
111
+ unless bdm.keys.include?(:ebs_volume_size) &&
112
+ bdm.keys.include?(:ebs_delete_on_termination) &&
113
+ bdm.keys.include?(:ebs_device_name)
114
+ raise "Every :block_device_mapping must include the keys :ebs_volume_size, " \
115
+ ":ebs_delete_on_termination and :ebs_device_name"
116
+ end
117
+ end
118
+ end
119
+ end
62
120
 
63
- required_config :aws_access_key_id
64
- required_config :aws_secret_access_key
65
- required_config :aws_ssh_key_id
66
- required_config :image_id
121
+ # The access key/secret are now using the priority list AWS uses
122
+ # Providing these inside the .kitchen.yml is no longer recommended
123
+ validations[:aws_access_key_id] = lambda do |attr, val, driver|
124
+ unless val.nil?
125
+ driver.warn "WARN: #{attr} has been deprecated, please use " \
126
+ "ENV['AWS_ACCESS_KEY_ID'] or ~/.aws/credentials. See " \
127
+ "the README for more details"
128
+ end
129
+ end
130
+ validations[:aws_secret_access_key] = lambda do |attr, val, driver|
131
+ unless val.nil?
132
+ driver.warn "WARN: #{attr} has been deprecated, please use " \
133
+ "ENV['AWS_SECRET_ACCESS_KEY'] or ~/.aws/credentials. See " \
134
+ "the README for more details"
135
+ end
136
+ end
137
+ validations[:aws_session_token] = lambda do |attr, val, driver|
138
+ unless val.nil?
139
+ driver.warn "WARN: #{attr} has been deprecated, please use " \
140
+ "ENV['AWS_SESSION_TOKEN'] or ~/.aws/credentials. See " \
141
+ "the README for more details"
142
+ end
143
+ end
67
144
 
68
- def create(state)
69
- server = create_server
70
- state[:server_id] = server.id
145
+ # A lifecycle method that should be invoked when the object is about
146
+ # ready to be used. A reference to an Instance is required as
147
+ # configuration dependant data may be access through an Instance. This
148
+ # also acts as a hook point where the object may wish to perform other
149
+ # last minute checks, validations, or configuration expansions.
150
+ #
151
+ # @param instance [Instance] an associated instance
152
+ # @return [self] itself, for use in chaining
153
+ # @raise [ClientError] if instance parameter is nil
154
+ def finalize_config!(instance)
155
+ super
71
156
 
157
+ if config[:availability_zone].nil?
158
+ config[:availability_zone] = config[:region] + "b"
159
+ end
160
+ # TODO: when we get rid of flavor_id, move this to a default
161
+ if config[:instance_type].nil?
162
+ config[:instance_type] = config[:flavor_id] || "m1.small"
163
+ end
164
+
165
+ self
166
+ end
167
+
168
+ def create(state) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
169
+ copy_deprecated_configs(state)
170
+ return if state[:server_id]
171
+
172
+ info(Kitchen::Util.outdent!(<<-END))
173
+ Creating <#{state[:server_id]}>...
174
+ If you are not using an account that qualifies under the AWS
175
+ free-tier, you may be charged to run these suites. The charge
176
+ should be minimal, but neither Test Kitchen nor its maintainers
177
+ are responsible for your incurred costs.
178
+ END
179
+
180
+ if config[:price]
181
+ # Spot instance when a price is set
182
+ server = submit_spot(state)
183
+ else
184
+ # On-demand instance
185
+ server = submit_server
186
+ end
187
+ info("Instance <#{server.id}> requested.")
188
+ tag_server(server)
189
+
190
+ state[:server_id] = server.id
72
191
  info("EC2 instance <#{state[:server_id]}> created.")
73
- server.wait_for { print '.'; ready? }
74
- print '(server ready)'
192
+ wait_log = proc do |attempts|
193
+ c = attempts * config[:retryable_sleep]
194
+ t = config[:retryable_tries] * config[:retryable_sleep]
195
+ info "Waited #{c}/#{t}s for instance <#{state[:server_id]}> to become ready."
196
+ end
197
+ server = server.wait_until(
198
+ :max_attempts => config[:retryable_tries],
199
+ :delay => config[:retryable_sleep],
200
+ :before_attempt => wait_log
201
+ ) do |s|
202
+ hostname = hostname(s)
203
+ # Euca instances often report ready before they have an IP
204
+ s.state.name == "running" && !hostname.nil? && hostname != "0.0.0.0"
205
+ end
206
+
207
+ info("EC2 instance <#{state[:server_id]}> ready.")
75
208
  state[:hostname] = hostname(server)
76
- wait_for_sshd(state[:hostname], config[:username])
77
- print '(ssh ready)\n'
209
+ instance.transport.connection(state).wait_until_ready
210
+ create_ec2_json(state)
78
211
  debug("ec2:create '#{state[:hostname]}'")
79
- rescue Fog::Errors::Error, Excon::Errors::Error => ex
80
- raise ActionFailed, ex.message
81
212
  end
82
213
 
83
214
  def destroy(state)
84
215
  return if state[:server_id].nil?
85
216
 
86
- server = connection.servers.get(state[:server_id])
87
- server.destroy unless server.nil?
217
+ server = ec2.get_instance(state[:server_id])
218
+ unless server.nil?
219
+ instance.transport.connection(state).close
220
+ server.terminate unless server.nil?
221
+ end
222
+ if state[:spot_request_id]
223
+ debug("Deleting spot request <#{state[:server_id]}>")
224
+ ec2.client.cancel_spot_instance_requests(
225
+ :spot_instance_request_ids => [state[:spot_request_id]]
226
+ )
227
+ end
88
228
  info("EC2 instance <#{state[:server_id]}> destroyed.")
89
229
  state.delete(:server_id)
90
230
  state.delete(:hostname)
91
231
  end
92
232
 
93
233
  def default_ami
94
- region = amis['regions'][config[:region]]
234
+ region = amis["regions"][config[:region]]
95
235
  region && region[instance.platform.name]
96
236
  end
97
237
 
98
- def default_username
99
- amis['usernames'][instance.platform.name] || 'root'
100
- end
101
-
102
238
  private
103
239
 
104
- def connection
105
- Fog::Compute.new(
106
- :provider => :aws,
107
- :aws_access_key_id => config[:aws_access_key_id],
108
- :aws_secret_access_key => config[:aws_secret_access_key],
109
- :aws_session_token => config[:aws_session_token],
110
- :region => config[:region],
111
- :endpoint => config[:endpoint],
240
+ def ec2
241
+ @ec2 ||= Aws::Client.new(
242
+ config[:region],
243
+ config[:shared_credentials_profile],
244
+ config[:aws_access_key_id],
245
+ config[:aws_secret_access_key],
246
+ config[:aws_session_token]
112
247
  )
113
248
  end
114
249
 
115
- def create_server
116
- debug_server_config
250
+ def instance_generator
251
+ @instance_generator ||= Aws::InstanceGenerator.new(config, ec2)
252
+ end
117
253
 
118
- connection.servers.create(
119
- :availability_zone => config[:availability_zone],
120
- :security_group_ids => config[:security_group_ids],
121
- :tags => config[:tags],
122
- :flavor_id => config[:flavor_id],
123
- :ebs_optimized => config[:ebs_optimized],
124
- :image_id => config[:image_id],
125
- :key_name => config[:aws_ssh_key_id],
126
- :subnet_id => config[:subnet_id],
127
- )
254
+ # This copies transport config from the current config object into the
255
+ # state. This relies on logic in the transport that merges the transport
256
+ # config with the current state object, so its a bad coupling. But we
257
+ # can get rid of this when we get rid of these deprecated configs!
258
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
259
+ def copy_deprecated_configs(state)
260
+ if config[:ssh_timeout]
261
+ state[:connection_timeout] = config[:ssh_timeout]
262
+ end
263
+ if config[:ssh_retries]
264
+ state[:connection_retries] = config[:ssh_retries]
265
+ end
266
+ if config[:username]
267
+ state[:username] = config[:username]
268
+ elsif instance.transport[:username] == instance.transport.class.defaults[:username]
269
+ # If the transport has the default username, copy it from amis.json
270
+ # This duplicated old behavior but I hate amis.json
271
+ ami_username = amis["usernames"][instance.platform.name]
272
+ state[:username] = ami_username if ami_username
273
+ end
274
+ if config[:ssh_key]
275
+ state[:ssh_key] = config[:ssh_key]
276
+ end
128
277
  end
278
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
129
279
 
130
- def debug_server_config
131
- debug("ec2:region '#{config[:region]}'")
132
- debug("ec2:availability_zone '#{config[:availability_zone]}'")
133
- debug("ec2:flavor_id '#{config[:flavor_id]}'")
134
- debug("ec2:ebs_optimized '#{config[:ebs_optimized]}'")
135
- debug("ec2:image_id '#{config[:image_id]}'")
136
- debug("ec2:security_group_ids '#{config[:security_group_ids]}'")
137
- debug("ec2:tags '#{config[:tags]}'")
138
- debug("ec2:key_name '#{config[:aws_ssh_key_id]}'")
139
- debug("ec2:subnet_id '#{config[:subnet_id]}'")
280
+ # Fog AWS helper for creating the instance
281
+ def submit_server
282
+ debug("Creating EC2 Instance..")
283
+ instance_data = instance_generator.ec2_instance_data
284
+ instance_data[:min_count] = 1
285
+ instance_data[:max_count] = 1
286
+ ec2.create_instance(instance_data)
287
+ end
288
+
289
+ def submit_spot(state) # rubocop:disable Metrics/AbcSize
290
+ debug("Creating EC2 Spot Instance..")
291
+ request_data = {}
292
+ request_data[:spot_price] = config[:price].to_s
293
+ request_data[:launch_specification] = instance_generator.ec2_instance_data
294
+
295
+ response = ec2.client.request_spot_instances(request_data)
296
+ spot_request_id = response[:spot_instance_requests][0][:spot_instance_request_id]
297
+ # deleting the instance cancels the request, but deleting the request
298
+ # does not affect the instance
299
+ state[:spot_request_id] = spot_request_id
300
+ ec2.client.wait_until(
301
+ :spot_instance_request_fulfilled,
302
+ :spot_instance_request_ids => [spot_request_id]
303
+ ) do |w|
304
+ w.max_attempts = config[:retryable_tries]
305
+ w.delay = config[:retryable_sleep]
306
+ w.before_attempt do |attempts|
307
+ c = attempts * config[:retryable_sleep]
308
+ t = config[:retryable_tries] * config[:retryable_sleep]
309
+ info "Waited #{c}/#{t}s for spot request <#{spot_request_id}> to become fulfilled."
310
+ end
311
+ end
312
+ ec2.get_instance_from_spot_request(spot_request_id)
313
+ end
314
+
315
+ def tag_server(server)
316
+ tags = []
317
+ config[:tags].each do |k, v|
318
+ tags << { :key => k, :value => v }
319
+ end
320
+ server.create_tags(:tags => tags)
140
321
  end
141
322
 
142
323
  def amis
143
324
  @amis ||= begin
144
325
  json_file = File.join(File.dirname(__FILE__),
145
- %w{.. .. .. data amis.json})
326
+ %w[.. .. .. data amis.json])
146
327
  JSON.load(IO.read(json_file))
147
328
  end
148
329
  end
149
330
 
150
- def interface_types
331
+ #
332
+ # Ordered mapping from config name to Fog name. Ordered by preference
333
+ # when looking up hostname.
334
+ #
335
+ INTERFACE_TYPES =
151
336
  {
152
- 'dns' => 'dns_name',
153
- 'public' => 'public_ip_address',
154
- 'private' => 'private_ip_address'
337
+ "dns" => "public_dns_name",
338
+ "public" => "public_ip_address",
339
+ "private" => "private_ip_address"
155
340
  }
156
- end
157
341
 
158
- def hostname(server)
159
- if config[:interface]
160
- method = interface_types.fetch(config[:interface]) do
161
- raise Kitchen::UserError, 'Invalid interface'
342
+ #
343
+ # Lookup hostname of provided server. If interface_type is provided use
344
+ # that interface to lookup hostname. Otherwise, try ordered list of
345
+ # options.
346
+ #
347
+ def hostname(server, interface_type = nil)
348
+ if interface_type
349
+ interface_type = INTERFACE_TYPES.fetch(interface_type) do
350
+ raise Kitchen::UserError, "Invalid interface [#{interface_type}]"
162
351
  end
163
- server.send(method)
352
+ server.send(interface_type)
164
353
  else
165
- server.dns_name || server.public_ip_address || server.private_ip_address
354
+ potential_hostname = nil
355
+ INTERFACE_TYPES.values.each do |type|
356
+ potential_hostname ||= server.send(type)
357
+ end
358
+ potential_hostname
166
359
  end
167
360
  end
361
+
362
+ def create_ec2_json(state)
363
+ instance.transport.connection(state).execute(
364
+ "sudo mkdir -p /etc/chef/ohai/hints;sudo touch /etc/chef/ohai/hints/ec2.json"
365
+ )
366
+ end
367
+
168
368
  end
169
369
  end
170
370
  end