Skip to content

Commit f624e2f

Browse files
committed
Add IPI installation on AWS dedicated hosts
1 parent ccad892 commit f624e2f

File tree

10 files changed

+555
-1
lines changed

10 files changed

+555
-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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
"github.com/sirupsen/logrus"
11+
)
12+
13+
// Host holds metadata for a dedicated host.
14+
type Host struct {
15+
ID string
16+
Name string
17+
Zone string
18+
}
19+
20+
// dedicatedHosts retrieves a list of dedicated hosts for the given region and
21+
// returns them in a map keyed by the host ID.
22+
func dedicatedHosts(ctx context.Context, session *session.Session, region string) (map[string]Host, error) {
23+
hostsByID := map[string]Host{}
24+
25+
client := ec2.New(session, aws.NewConfig().WithRegion(region))
26+
input := &ec2.DescribeHostsInput{}
27+
28+
if err := client.DescribeHostsPagesWithContext(ctx, input, func(page *ec2.DescribeHostsOutput, lastPage bool) bool {
29+
for _, h := range page.Hosts {
30+
id := aws.StringValue(h.HostId)
31+
if id == "" {
32+
// Skip entries lacking an ID (should not happen)
33+
continue
34+
}
35+
36+
logrus.Debugf("Found dedicatd host: %s", id)
37+
hostsByID[id] = Host{
38+
ID: id,
39+
Zone: aws.StringValue(h.AvailabilityZone),
40+
}
41+
}
42+
return !lastPage
43+
}); err != nil {
44+
return nil, fmt.Errorf("fetching dedicated hosts: %w", err)
45+
}
46+
47+
return hostsByID, nil
48+
}

pkg/asset/installconfig/aws/metadata.go

Lines changed: 21 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,23 @@ 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+
if len(m.Hosts) == 0 {
401+
awsSession, err := m.unlockedSession(ctx)
402+
if err != nil {
403+
return nil, err
404+
}
405+
406+
m.Hosts, err = dedicatedHosts(ctx, awsSession, m.Region)
407+
if err != nil {
408+
return nil, fmt.Errorf("error listing dedicated hosts: %w", err)
409+
}
410+
}
411+
412+
return m.Hosts, nil
413+
}

pkg/asset/installconfig/aws/validation.go

Lines changed: 32 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,36 @@ 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+
return allErrs
494+
}
495+
496+
if pool.HostPlacement.Affinity != nil && *pool.HostPlacement.Affinity == awstypes.HostAffinityDedicatedHost {
497+
placementPath := fldPath.Child("hostPlacement")
498+
if pool.HostPlacement.DedicatedHost != nil {
499+
configuredHosts := pool.HostPlacement.DedicatedHost
500+
foundHosts, err := meta.DedicatedHosts(ctx)
501+
if err != nil {
502+
allErrs = append(allErrs, field.InternalError(placementPath.Child("dedicatedHost"), err))
503+
} else {
504+
// Check the returned configured hosts to see if the dedicated hosts defined in install-config exists.
505+
for _, host := range configuredHosts {
506+
_, ok := foundHosts[host.ID]
507+
if !ok {
508+
errMsg := fmt.Sprintf("dedicated host %s not found", host.ID)
509+
allErrs = append(allErrs, field.Invalid(placementPath.Child("dedicatedHost"), pool.InstanceType, errMsg))
510+
}
511+
}
512+
}
513+
}
514+
}
515+
516+
return allErrs
517+
}
518+
487519
func validateSecurityGroupIDs(ctx context.Context, meta *Metadata, fldPath *field.Path, platform *awstypes.Platform, pool *awstypes.MachinePool) field.ErrorList {
488520
allErrs := field.ErrorList{}
489521

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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,13 @@ func (w *Worker) Generate(ctx context.Context, dependencies asset.Parents) error
533533
}
534534
}
535535

536+
// TODO: See if we can make changes mentioned in the review.
537+
// Depending on how this func is called, sometimes AWS instance is not set. So in this case, assume no dedicated hosts
538+
dHosts := map[string]icaws.Host{}
539+
if installConfig.AWS != nil {
540+
dHosts = installConfig.AWS.Hosts
541+
}
542+
536543
pool.Platform.AWS = &mpool
537544
sets, err := aws.MachineSets(&aws.MachineSetInput{
538545
ClusterID: clusterID.InfraID,
@@ -543,6 +550,7 @@ func (w *Worker) Generate(ctx context.Context, dependencies asset.Parents) error
543550
Pool: &pool,
544551
Role: pool.Name,
545552
UserDataSecret: workerUserDataSecretName,
553+
Hosts: dHosts,
546554
})
547555
if err != nil {
548556
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.affinity) && 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)