Skip to content

Commit 342ab2d

Browse files
committed
Add IPI installation on AWS dedicated hosts
1 parent 1924260 commit 342ab2d

File tree

11 files changed

+507
-1
lines changed

11 files changed

+507
-1
lines changed

data/data/install.openshift.io_installconfigs.yaml

Lines changed: 216 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package aws
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/aws/aws-sdk-go/aws"
8+
"github.com/aws/aws-sdk-go/aws/session"
9+
"github.com/aws/aws-sdk-go/service/ec2"
10+
)
11+
12+
// Host holds metadata for a dedicated host.
13+
type Host struct {
14+
ID string
15+
Name string
16+
Zone string
17+
}
18+
19+
// dedicatedHosts retrieves a list of dedicated hosts for the given region and
20+
// returns them in a map keyed by the host ID.
21+
func dedicatedHosts(ctx context.Context, session *session.Session, region string) (map[string]Host, error) {
22+
hostsByID := map[string]Host{}
23+
24+
client := ec2.New(session, aws.NewConfig().WithRegion(region))
25+
input := &ec2.DescribeHostsInput{}
26+
27+
if err := client.DescribeHostsPagesWithContext(ctx, input, func(page *ec2.DescribeHostsOutput, lastPage bool) bool {
28+
for _, h := range page.Hosts {
29+
id := aws.StringValue(h.HostId)
30+
if id == "" {
31+
// Skip entries lacking an ID (should not happen)
32+
continue
33+
}
34+
var name string
35+
for _, tag := range h.Tags {
36+
if aws.StringValue(tag.Key) == "Name" {
37+
name = aws.StringValue(tag.Value)
38+
break
39+
}
40+
}
41+
42+
fmt.Printf("Found dedicated host %s\n", id)
43+
hostsByID[id] = Host{
44+
ID: id,
45+
Name: name,
46+
Zone: aws.StringValue(h.AvailabilityZone),
47+
}
48+
}
49+
return !lastPage
50+
}); err != nil {
51+
return nil, fmt.Errorf("fetching dedicated hosts: %w", err)
52+
}
53+
54+
return hostsByID, nil
55+
}

pkg/asset/installconfig/aws/metadata.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type Metadata struct {
2727
vpc VPC
2828
instanceTypes map[string]InstanceType
2929

30+
Hosts map[string]Host
3031
Region string `json:"region,omitempty"`
3132
ProvidedSubnets []typesaws.Subnet `json:"subnets,omitempty"`
3233
Services []typesaws.ServiceEndpoint `json:"services,omitempty"`
@@ -390,3 +391,21 @@ func (m *Metadata) InstanceTypes(ctx context.Context) (map[string]InstanceType,
390391

391392
return m.instanceTypes, nil
392393
}
394+
395+
// DedicatedHosts retrieves all hosts available for use to verify against this installation for configured region.
396+
func (m *Metadata) DedicatedHosts(ctx context.Context) (map[string]Host, error) {
397+
m.mutex.Lock()
398+
defer m.mutex.Unlock()
399+
400+
awsSession, err := m.unlockedSession(ctx)
401+
if err != nil {
402+
return nil, err
403+
}
404+
405+
m.Hosts, err = dedicatedHosts(ctx, awsSession, m.Region)
406+
if err != nil {
407+
return nil, fmt.Errorf("error listing dedicated hosts: %w", err)
408+
}
409+
410+
return m.Hosts, nil
411+
}

pkg/asset/installconfig/aws/validation.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,8 @@ func validateMachinePool(ctx context.Context, meta *Metadata, fldPath *field.Pat
466466
}
467467
}
468468

469+
allErrs = append(allErrs, validateHostPlacement(ctx, meta, fldPath, pool)...)
470+
469471
return allErrs
470472
}
471473

@@ -484,6 +486,32 @@ func translateEC2Arches(arches []string) sets.Set[string] {
484486
return res
485487
}
486488

489+
func validateHostPlacement(ctx context.Context, meta *Metadata, fldPath *field.Path, pool *awstypes.MachinePool) field.ErrorList {
490+
allErrs := field.ErrorList{}
491+
492+
if pool.HostPlacement != nil {
493+
placementPath := fldPath.Child("hostPlacement")
494+
if pool.HostPlacement.DedicatedHost != nil {
495+
configuredHosts := pool.HostPlacement.DedicatedHost
496+
foundHosts, err := meta.DedicatedHosts(ctx)
497+
if err != nil {
498+
allErrs = append(allErrs, field.InternalError(placementPath.Child("dedicatedHost"), err))
499+
} else {
500+
// Check the returned configured hosts to see if the dedicated hosts defined in install-config exists.
501+
for _, host := range *configuredHosts {
502+
_, ok := foundHosts[host.ID]
503+
if !ok {
504+
errMsg := fmt.Sprintf("dedicated host %s not found", host.ID)
505+
allErrs = append(allErrs, field.Invalid(placementPath.Child("dedicatedHost"), pool.InstanceType, errMsg))
506+
}
507+
}
508+
}
509+
}
510+
}
511+
512+
return allErrs
513+
}
514+
487515
func validateSecurityGroupIDs(ctx context.Context, meta *Metadata, fldPath *field.Path, platform *awstypes.Platform, pool *awstypes.MachinePool) field.ErrorList {
488516
allErrs := field.ErrorList{}
489517

pkg/asset/machines/aws/awsmachines.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ type MachineInput struct {
3333
PublicIP bool
3434
PublicIpv4Pool string
3535
Ignition *capa.Ignition
36+
HostAffinity string
37+
HostIDs []string
3638
}
3739

3840
// GenerateMachines returns manifests and runtime objects to provision the control plane (including bootstrap, if applicable) nodes using CAPI.

pkg/asset/machines/aws/machines.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"k8s.io/apimachinery/pkg/runtime"
1212
"k8s.io/apimachinery/pkg/util/sets"
1313
"k8s.io/utils/pointer"
14+
"k8s.io/utils/ptr"
1415

1516
v1 "github.com/openshift/api/config/v1"
1617
machinev1 "github.com/openshift/api/machine/v1"
@@ -35,6 +36,7 @@ type machineProviderInput struct {
3536
userTags map[string]string
3637
publicSubnet bool
3738
securityGroupIDs []string
39+
dedicatedHost string
3840
}
3941

4042
// Machines returns a list of machines for a machinepool.
@@ -291,6 +293,15 @@ func provider(in *machineProviderInput) (*machineapi.AWSMachineProviderConfig, e
291293
config.MetadataServiceOptions.Authentication = machineapi.MetadataServiceAuthentication(in.imds.Authentication)
292294
}
293295

296+
if in.dedicatedHost != "" {
297+
config.HostPlacement = &machineapi.HostPlacement{
298+
Affinity: ptr.To(machineapi.HostAffinityDedicatedHost),
299+
DedicatedHost: &machineapi.DedicatedHost{
300+
ID: in.dedicatedHost,
301+
},
302+
}
303+
}
304+
294305
return config, nil
295306
}
296307

@@ -340,3 +351,18 @@ func ConfigMasters(machines []machineapi.Machine, controlPlane *machinev1.Contro
340351
providerSpec := controlPlane.Spec.Template.OpenShiftMachineV1Beta1Machine.Spec.ProviderSpec.Value.Object.(*machineapi.AWSMachineProviderConfig)
341352
providerSpec.LoadBalancers = lbrefs
342353
}
354+
355+
// DedicatedHost sets dedicated hosts for the specified zone.
356+
func DedicatedHost(hosts map[string]aws.Host, placement *awstypes.HostPlacement, zone string) string {
357+
// If install-config has HostPlacements configured, lets check the DedicatedHosts to see if one matches our region & zone.
358+
if placement != nil {
359+
// We only support one host ID currently for an instance. Need to also get host that matches the zone the machines will be put into.
360+
for _, host := range *placement.DedicatedHost {
361+
hostDetails, found := hosts[host.ID]
362+
if found && hostDetails.Zone == zone {
363+
return hostDetails.ID
364+
}
365+
}
366+
}
367+
return ""
368+
}

pkg/asset/machines/aws/machinesets.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type MachineSetInput struct {
2525
Pool *types.MachinePool
2626
Role string
2727
UserDataSecret string
28+
Hosts map[string]icaws.Host
2829
}
2930

3031
// MachineSets returns a list of machinesets for a machinepool.
@@ -87,6 +88,8 @@ func MachineSets(in *MachineSetInput) ([]*machineapi.MachineSet, error) {
8788
instanceProfile = fmt.Sprintf("%s-worker-profile", in.ClusterID)
8889
}
8990

91+
dedicatedHost := DedicatedHost(in.Hosts, mpool.HostPlacement, az)
92+
9093
provider, err := provider(&machineProviderInput{
9194
clusterID: in.ClusterID,
9295
region: in.InstallConfigPlatformAWS.Region,
@@ -102,12 +105,21 @@ func MachineSets(in *MachineSetInput) ([]*machineapi.MachineSet, error) {
102105
userTags: in.InstallConfigPlatformAWS.UserTags,
103106
publicSubnet: publicSubnet,
104107
securityGroupIDs: in.Pool.Platform.AWS.AdditionalSecurityGroupIDs,
108+
dedicatedHost: dedicatedHost,
105109
})
106110
if err != nil {
107111
return nil, errors.Wrap(err, "failed to create provider")
108112
}
113+
114+
// If we are using any feature that is only available via CAPI, we must set the authoritativeAPI = ClusterAPI
115+
authoritativeAPI := machineapi.MachineAuthorityMachineAPI
116+
if isAuthoritativeClusterAPIRequired(provider) {
117+
authoritativeAPI = machineapi.MachineAuthorityClusterAPI
118+
}
119+
109120
name := fmt.Sprintf("%s-%s-%s", in.ClusterID, in.Pool.Name, az)
110121
spec := machineapi.MachineSpec{
122+
AuthoritativeAPI: authoritativeAPI,
111123
ProviderSpec: machineapi.ProviderSpec{
112124
Value: &runtime.RawExtension{Object: provider},
113125
},
@@ -130,7 +142,8 @@ func MachineSets(in *MachineSetInput) ([]*machineapi.MachineSet, error) {
130142
},
131143
},
132144
Spec: machineapi.MachineSetSpec{
133-
Replicas: &replicas,
145+
AuthoritativeAPI: authoritativeAPI,
146+
Replicas: &replicas,
134147
Selector: metav1.LabelSelector{
135148
MatchLabels: map[string]string{
136149
"machine.openshift.io/cluster-api-machineset": name,
@@ -151,8 +164,17 @@ func MachineSets(in *MachineSetInput) ([]*machineapi.MachineSet, error) {
151164
},
152165
},
153166
}
167+
154168
machinesets = append(machinesets, mset)
155169
}
156170

157171
return machinesets, nil
158172
}
173+
174+
// isAuthoritativeClusterAPIRequired is called to determine if the machine spec should have the AuthoritativeAPI set to ClusterAPI.
175+
func isAuthoritativeClusterAPIRequired(provider *machineapi.AWSMachineProviderConfig) bool {
176+
if provider.HostPlacement != nil && *provider.HostPlacement.Affinity != machineapi.HostAffinityAnyAvailable {
177+
return true
178+
}
179+
return false
180+
}

pkg/asset/machines/worker.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,12 @@ func (w *Worker) Generate(ctx context.Context, dependencies asset.Parents) error
533533
}
534534
}
535535

536+
// Depending on how this func is called, sometimes AWS instance is not set. So in this case, assume no dedicated hosts
537+
dHosts := map[string]icaws.Host{}
538+
if installConfig.AWS != nil {
539+
dHosts = installConfig.AWS.Hosts
540+
}
541+
536542
pool.Platform.AWS = &mpool
537543
sets, err := aws.MachineSets(&aws.MachineSetInput{
538544
ClusterID: clusterID.InfraID,
@@ -543,6 +549,7 @@ func (w *Worker) Generate(ctx context.Context, dependencies asset.Parents) error
543549
Pool: &pool,
544550
Role: pool.Name,
545551
UserDataSecret: workerUserDataSecretName,
552+
Hosts: dHosts,
546553
})
547554
if err != nil {
548555
return errors.Wrap(err, "failed to create worker machine objects")

pkg/types/aws/machinepool.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ type MachinePool struct {
4848
// +kubebuilder:validation:MaxItems=10
4949
// +optional
5050
AdditionalSecurityGroupIDs []string `json:"additionalSecurityGroupIDs,omitempty"`
51+
52+
// hostPlacement configures placement on AWS Dedicated Hosts. This allows admins to assign instances to specific host
53+
// for a variety of needs including for regulatory compliance, to leverage existing per-socket or per-core software licenses (BYOL),
54+
// and to gain visibility and control over instance placement on a physical server.
55+
// When omitted, the instance is not constrained to a dedicated host.
56+
// +openshift:enable:FeatureGate=AWSDedicatedHosts
57+
// +optional
58+
HostPlacement *HostPlacement `json:"hostPlacement,omitempty"`
5159
}
5260

5361
// Set sets the values from `required` to `a`.
@@ -96,6 +104,10 @@ func (a *MachinePool) Set(required *MachinePool) {
96104
if len(required.AdditionalSecurityGroupIDs) > 0 {
97105
a.AdditionalSecurityGroupIDs = required.AdditionalSecurityGroupIDs
98106
}
107+
108+
if required.HostPlacement != nil {
109+
a.HostPlacement = required.HostPlacement
110+
}
99111
}
100112

101113
// EC2RootVolume defines the storage for an ec2 instance.
@@ -135,3 +147,50 @@ type EC2Metadata struct {
135147
// +optional
136148
Authentication string `json:"authentication,omitempty"`
137149
}
150+
151+
// HostPlacement is the type that will be used to configure the placement of AWS instances.
152+
// This can be configured for default placement (AnyAvailable) and dedicated hosts (DedicatedHost).
153+
// +kubebuilder:validation:XValidation:rule="has(self.type) && self.affinity == 'DedicatedHost' ? has(self.dedicatedHost) : !has(self.dedicatedHost)",message="dedicatedHost is required when affinity is DedicatedHost, and forbidden otherwise"
154+
type HostPlacement struct {
155+
// affinity specifies the affinity setting for the instance.
156+
// Allowed values are AnyAvailable and DedicatedHost.
157+
// When Affinity is set to DedicatedHost, an instance started onto a specific host always restarts on the same host if stopped. In this scenario, the `dedicatedHost` field must be set.
158+
// When Affinity is set to AnyAvailable, and you stop and restart the instance, it can be restarted on any available host.
159+
// +required
160+
// +unionDiscriminator
161+
Affinity *HostAffinity `json:"affinity,omitempty"`
162+
163+
// dedicatedHost specifies the exact host that an instance should be restarted on if stopped.
164+
// dedicatedHost is required when 'affinity' is set to DedicatedHost, and forbidden otherwise.
165+
// +optional
166+
// +unionMember
167+
DedicatedHost *[]DedicatedHost `json:"dedicatedHost,omitempty"`
168+
}
169+
170+
// HostAffinity selects how an instance should be placed on AWS Dedicated Hosts.
171+
// +kubebuilder:validation:Enum:=DedicatedHost;AnyAvailable
172+
type HostAffinity string
173+
174+
const (
175+
// HostAffinityAnyAvailable lets the platform select any available dedicated host.
176+
HostAffinityAnyAvailable HostAffinity = "AnyAvailable"
177+
178+
// HostAffinityDedicatedHost requires specifying a particular host via dedicatedHost.host.hostID.
179+
HostAffinityDedicatedHost HostAffinity = "DedicatedHost"
180+
)
181+
182+
// DedicatedHost represents the configuration for the usage of dedicated host.
183+
type DedicatedHost struct {
184+
// id identifies the AWS Dedicated Host on which the instance must run.
185+
// The value must start with "h-" followed by 17 lowercase hexadecimal characters (0-9 and a-f).
186+
// Must be exactly 19 characters in length.
187+
// +kubebuilder:validation:XValidation:rule="self.matches('^h-[0-9a-f]{17}$')",message="hostID must start with 'h-' followed by 17 lowercase hexadecimal characters (0-9 and a-f)"
188+
// +kubebuilder:validation:MinLength=19
189+
// +kubebuilder:validation:MaxLength=19
190+
// +required
191+
ID string `json:"id,omitempty"`
192+
193+
// zone is the availability zone that the dedicated host belongs to
194+
// +optional
195+
Zone string `json:"zone,omitempty"`
196+
}

0 commit comments

Comments
 (0)