Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for dedicated hosts #592

Merged
merged 2 commits into from
Nov 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions lib/kitchen/driver/aws/dedicated_hosts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
module Kitchen
module Driver
module Mixins
module DedicatedHosts
# check if a suitable dedicated host is available
# @return Boolean
def host_available?
!hosts_with_capacity.empty?
end

# get dedicated host with capacity for instance type
# @return Aws::EC2::Types::Host
def hosts_with_capacity
hosts_managed.select do |host|
# T-instance hosts do not report available capacity and can be overprovisioned
if host.available_capacity.nil?
true
else
instance_capacity = host.available_capacity.available_instance_capacity
capacity_for_type = instance_capacity.detect { |cap| cap.instance_type == config[:instance_type] }
capacity_for_type.available_capacity > 0
end
end
end

# check if host has no instances running
# @param host_id [Aws::EC2::Types::Host] dedicated host
# @return Boolean
def host_unused?(host)
host.instances.empty?
end

# get host data for host id
# @param host_id [Aws::EC2::Types::Host] dedicated host
# @return Array(Aws::EC2::Types::Host)
def host_for_id(host_id)
ec2.client.describe_hosts(host_ids: [host_id])&.first
end

# get dedicated hosts managed by Test Kitchen
# @return Array(Aws::EC2::Types::Host)
def hosts_managed
response = ec2.client.describe_hosts(
filter: [
{ name: "tag:ManagedBy", values: ["Test Kitchen"] },
]
)

response.hosts.select { |host| host.state == "available" }
end

# allocate new dedicated host for requested instance type
# @return String host id
def allocate_host
unless allow_allocate_host?
warn "ERROR: Attempted to allocate dedicated host but need environment variable TK_ALLOCATE_DEDICATED_HOST to be set"
exit!
end

unless config[:availability_zone]
warn "Attempted to allocate dedicated host but option 'availability_zone' is not set"
exit!
end

info("Allocating dedicated host for #{config[:instance_type]} instances. This will incur additional cost")

request = {
availability_zone: config[:availability_zone],
quantity: 1,

auto_placement: "on",

tag_specifications: [
{
resource_type: "dedicated-host",
tags: [
{ key: "ManagedBy", value: "Test Kitchen" },
],
},
],
}

# ".metal" is a 1:1 association, everything else has multi-instance capability
if instance_size_from_type(config[:instance_type]) == "metal"
request[:instance_type] = config[:instance_type]
else
request[:instance_family] = instance_family_from_type(config[:instance_type])
end

response = ec2.client.allocate_hosts(request)
response.host_ids.first
end

# deallocate a dedicated host
# @param host_id [String] dedicated host id
# @return Aws::EC2::Types::ReleaseHostsResult
def deallocate_host(host_id)
info("Deallocating dedicated host #{host_id}")

response = ec2.client.release_hosts({ host_ids: [host_id] })
unless response.unsuccessful.empty?
warn "ERROR: Could not release dedicated host #{host_id}. Host may remain allocated and incur cost"
exit!
end
end

# return instance family from type
# @param instance_type [String] type in format family.size
# @return String instance family
def instance_family_from_type(instance_type)
instance_type.split(".").first
end

# return instance size from type
# @param instance_type [String] type in format family.size
# @return String instance size
def instance_size_from_type(instance_type)
instance_type.split(".").last
end

# check config, if host allocation is enabled
# @return Boolean
def allow_allocate_host?
config[:allocate_dedicated_host]
end

# check config, if host deallocation is enabled
# @return Boolean
def allow_deallocate_host?
config[:deallocate_dedicated_host]
end
end
end
end
end
23 changes: 23 additions & 0 deletions lib/kitchen/driver/ec2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
require "kitchen"
require_relative "ec2_version"
require_relative "aws/client"
require_relative "aws/dedicated_hosts"
require_relative "aws/instance_generator"
require_relative "aws/standard_platform"
require_relative "aws/standard_platform/amazon"
Expand Down Expand Up @@ -89,6 +90,10 @@ class Ec2 < Kitchen::Driver::Base
default_config :instance_initiated_shutdown_behavior, nil
default_config :ssl_verify_peer, true
default_config :skip_cost_warning, false
default_config :allocate_dedicated_host, false
default_config :deallocate_dedicated_host, false

include Kitchen::Driver::Mixins::DedicatedHosts

def initialize(*args, &block)
super
Expand Down Expand Up @@ -225,6 +230,18 @@ def create(state)
config[:aws_ssh_key_id] = nil
end

# Allocate new dedicated hosts if needed and allowed
if config[:tenancy] == "host"
unless host_available? || allow_allocate_host?
warn "ERROR: tenancy `host` requested but no suitable host and allocation not allowed (set `allocate_dedicated_host` setting)"
exit!
end

allocate_host unless host_available?

info("Auto placement on one dedicated host out of: #{hosts_with_capacity.map(&:host_id).join(", ")}")
end

if config[:spot_price]
# Spot instance when a price is set
server = with_request_limit_backoff(state) { submit_spots }
Expand Down Expand Up @@ -296,6 +313,12 @@ def destroy(state)
# Clean up any auto-created security groups or keys.
delete_security_group(state)
delete_key(state)

# Clean up dedicated hosts matching instance_type and unused (if allowed)
if config[:tenancy] == "host" && allow_deallocate_host?
empty_hosts = hosts_with_capacity.select { |host| host_unused?(host) }
empty_hosts.each { |host| deallocate_host(host.host_id) }
end
end

def image
Expand Down