Module: Kitchen::Driver::Mixins::DedicatedHosts

Included in:
Ec2
Defined in:
lib/kitchen/driver/aws/dedicated_hosts.rb

Overview

Allocation and release of EC2 Dedicated Hosts.

A dedicated host is physical hardware reserved for one account, required for tenancy: host and for platforms such as macOS. Hosts are billed from allocation until release regardless of whether an instance is running on them, so both operations are gated behind explicit config and failures are fatal rather than warnings.

Only hosts tagged ManagedBy: Test Kitchen are ever considered, so a user's own dedicated hosts are never allocated to or released.

This module expects its includer to provide config, ec2 and the Test Kitchen logging methods; it is mixed into Ec2.

Instance Method Summary collapse

Instance Method Details

#allocate_hostString

Note:

Terminates the process with exit! when allocation is not enabled or no availability zone is configured, since an allocated host costs money whether or not it is used.

Allocate a new dedicated host for the configured instance type.

A bare-metal size occupies a whole host, so it is allocated for that exact type; every other size can share a host, so the whole instance family is allocated and EC2 places instances within it.

Returns:

  • (String)

    the new host's ID



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/kitchen/driver/aws/dedicated_hosts.rb', line 92

def allocate_host
  unless allow_allocate_host?
    warn "ERROR: Attempted to allocate a dedicated host, but the driver setting `allocate_dedicated_host` is not enabled. " \
         "Set `allocate_dedicated_host: true` to allow it, remembering that a dedicated host is billed from allocation " \
         "until it is released."
    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" },
        ],
      },
    ],
  }

  # Bare metal is a 1:1 association, everything else has multi-instance capability
  if metal_instance_type?(config[:instance_type])
    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

#allow_allocate_host?Boolean

Whether the user has opted in to allocating dedicated hosts.

Returns:

  • (Boolean)


183
184
185
# File 'lib/kitchen/driver/aws/dedicated_hosts.rb', line 183

def allow_allocate_host?
  config[:allocate_dedicated_host]
end

#allow_deallocate_host?Boolean

Whether the user has opted in to releasing dedicated hosts.

Returns:

  • (Boolean)


190
191
192
# File 'lib/kitchen/driver/aws/dedicated_hosts.rb', line 190

def allow_deallocate_host?
  config[:deallocate_dedicated_host]
end

#deallocate_host(host_id) ⇒ nil

Note:

Terminates the process with exit! when the release fails, as a host that stays allocated keeps accruing charges silently.

Release a dedicated host.

Parameters:

  • host_id (String)

    the host to release

Returns:

  • (nil)

    when the host was released successfully



140
141
142
143
144
145
146
147
148
# File 'lib/kitchen/driver/aws/dedicated_hosts.rb', line 140

def deallocate_host(host_id)
  info("Deallocating dedicated host #{host_id}")

  response = ec2.client.release_hosts({ host_ids: [host_id] })
  return if response.unsuccessful.empty?

  warn "ERROR: Could not release dedicated host #{host_id}. Host may remain allocated and incur cost"
  exit!
end

#host_available?Boolean

Whether any managed host has room for the configured instance type.

Returns:

  • (Boolean)


22
23
24
# File 'lib/kitchen/driver/aws/dedicated_hosts.rb', line 22

def host_available?
  !hosts_with_capacity.empty?
end

#host_for_id(host_id) ⇒ Aws::EC2::Types::Host?

Look a dedicated host up by ID.

Parameters:

  • host_id (String)

    the host ID, e.g. "h-0123456789abcdef0"

Returns:

  • (Aws::EC2::Types::Host, nil)

    the host, or nil when EC2 does not know it



61
62
63
# File 'lib/kitchen/driver/aws/dedicated_hosts.rb', line 61

def host_for_id(host_id)
  ec2.client.describe_hosts(host_ids: [host_id]).hosts.first
end

#host_unused?(host) ⇒ Boolean

Whether a host has no instances running on it.

Parameters:

  • host (Aws::EC2::Types::Host)

    the host to inspect

Returns:

  • (Boolean)

    true when the host can be released



52
53
54
# File 'lib/kitchen/driver/aws/dedicated_hosts.rb', line 52

def host_unused?(host)
  host.instances.empty?
end

#hosts_managedArray<Aws::EC2::Types::Host>

Available dedicated hosts that Test Kitchen allocated.

Filtered on the ManagedBy tag so that hosts belonging to the user are never touched, and on state so that hosts still being provisioned or already released are ignored.

Returns:

  • (Array<Aws::EC2::Types::Host>)


72
73
74
75
76
77
78
79
80
# File 'lib/kitchen/driver/aws/dedicated_hosts.rb', line 72

def hosts_managed
  response = ec2.client.describe_hosts(
    filter: [
      { name: "tag:ManagedBy", values: ["Test Kitchen"] },
    ]
  )

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

#hosts_with_capacityArray<Aws::EC2::Types::Host>

Managed hosts with room for the configured instance type.

T-family hosts report no capacity information and may be overprovisioned, so a host with no capacity block counts as available.

Returns:

  • (Array<Aws::EC2::Types::Host>)


32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/kitchen/driver/aws/dedicated_hosts.rb', line 32

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] }
      # A host that lists no capacity for this instance type cannot run
      # one, so treat a missing entry the same as zero rather than
      # raising on it.
      !capacity_for_type.nil? && capacity_for_type.available_capacity > 0
    end
  end
end

#instance_family_from_type(instance_type) ⇒ String

The family part of an instance type.

Parameters:

  • instance_type (String)

    a type in "family.size" form, e.g. "m5.large"

Returns:

  • (String)

    the family, e.g. "m5"



154
155
156
# File 'lib/kitchen/driver/aws/dedicated_hosts.rb', line 154

def instance_family_from_type(instance_type)
  instance_type.split(".").first
end

#instance_size_from_type(instance_type) ⇒ String

The size part of an instance type.

Parameters:

  • instance_type (String)

    a type in "family.size" form, e.g. "m5.large"

Returns:

  • (String)

    the size, e.g. "large"



162
163
164
# File 'lib/kitchen/driver/aws/dedicated_hosts.rb', line 162

def instance_size_from_type(instance_type)
  instance_type.split(".").last
end

#metal_instance_type?(instance_type) ⇒ Boolean

Whether an instance type runs on bare metal.

Older families expose a single bare-metal size named plainly ".metal". Newer ones expose several on the same family and name them ".metal-24xl", ".metal-48xl" and so on, so an equality check against "metal" would take them for virtualized sizes and allocate their host by family.

Parameters:

  • instance_type (String)

    a type in "family.size" form, e.g. "m7i.metal-24xl"

Returns:

  • (Boolean)

    true when an instance of this type occupies a whole host



176
177
178
# File 'lib/kitchen/driver/aws/dedicated_hosts.rb', line 176

def metal_instance_type?(instance_type)
  instance_size_from_type(instance_type).start_with?("metal")
end