kitchen-ec2 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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