diff --git a/nodeadm/doc/api.md b/nodeadm/doc/api.md
index 192866396..f2f770240 100644
--- a/nodeadm/doc/api.md
+++ b/nodeadm/doc/api.md
@@ -10,8 +10,7 @@
#### ClusterDetails
-ClusterDetails contains the coordinates of your EKS cluster.
-These details can be found using the [DescribeCluster API](https://docs.aws.amazon.com/eks/latest/APIReference/API_DescribeCluster.html).
+ClusterDetails contains the coordinates of your EKS cluster. These details can be found using the [DescribeCluster API](https://docs.aws.amazon.com/eks/latest/APIReference/API_DescribeCluster.html).
_Appears in:_
- [NodeConfigSpec](#nodeconfigspec)
@@ -20,7 +19,7 @@ _Appears in:_
| --- | --- |
| `name` _string_ | Name is the name of your EKS cluster |
| `apiServerEndpoint` _string_ | APIServerEndpoint is the URL of your EKS cluster's kube-apiserver. |
-| `certificateAuthority` _integer array_ | CertificateAuthority is a base64-encoded string of your cluster's certificate authority chain. |
+| `certificateAuthority` _[byte](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#byte-v1-meta) array_ | CertificateAuthority is a base64-encoded string of your cluster's certificate authority chain. |
| `cidr` _string_ | CIDR is your cluster's service CIDR block. This value is used to infer your cluster's DNS address. |
| `enableOutpost` _boolean_ | EnableOutpost determines how your node is configured when running on an AWS Outpost. |
| `id` _string_ | ID is an identifier for your cluster; this is only used when your node is running on an AWS Outpost. |
@@ -34,8 +33,8 @@ _Appears in:_
| Field | Description |
| --- | --- |
-| `config` _string_ | Config is an inline [`containerd` configuration TOML](https://github.com/containerd/containerd/blob/main/docs/man/containerd-config.toml.5.md)
that will be merged with the defaults. |
-| `baseRuntimeSpec` _object (keys:string, values:[RawExtension](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#rawextension-runtime-pkg))_ | BaseRuntimeSpec is the OCI runtime specification upon which all containers will be based.
The provided spec will be merged with the default spec; so that a partial spec may be provided.
For more information, see: https://github.com/opencontainers/runtime-spec |
+| `config` _string_ | Config is an inline [`containerd` configuration TOML](https://github.com/containerd/containerd/blob/main/docs/man/containerd-config.toml.5.md) that will be merged with the defaults. |
+| `baseRuntimeSpec` _object (keys:string, values:RawExtension)_ | BaseRuntimeSpec is the OCI runtime specification upon which all containers will be based. The provided spec will be merged with the default spec; so that a partial spec may be provided. For more information, see: https://github.com/opencontainers/runtime-spec |
#### Feature
@@ -69,13 +68,12 @@ _Appears in:_
| Field | Description |
| --- | --- |
-| `config` _object (keys:string, values:[RawExtension](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#rawextension-runtime-pkg))_ | Config is a [`KubeletConfiguration`](https://kubernetes.io/docs/reference/config-api/kubelet-config.v1beta1/)
that will be merged with the defaults. |
-| `flags` _string array_ | Flags are [command-line `kubelet` arguments](https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/).
that will be appended to the defaults. |
+| `config` _object (keys:string, values:RawExtension)_ | Config is a [`KubeletConfiguration`](https://kubernetes.io/docs/reference/config-api/kubelet-config.v1beta1/) that will be merged with the defaults. |
+| `flags` _string array_ | Flags are [command-line `kubelet` arguments](https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/). that will be appended to the defaults. |
#### LocalStorageOptions
-LocalStorageOptions control how [EC2 instance stores](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html)
-are used when available.
+LocalStorageOptions control how [EC2 instance stores](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html) are used when available.
_Appears in:_
- [InstanceOptions](#instanceoptions)
@@ -104,8 +102,8 @@ NodeConfig is the primary configuration object for `nodeadm`.
| --- | --- |
| `apiVersion` _string_ | `node.eks.aws/v1alpha1`
| `kind` _string_ | `NodeConfig`
-| `kind` _string_ | Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds |
-| `apiVersion` _string_ | APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources |
+| `kind` _string_ | Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds |
+| `apiVersion` _string_ | APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources |
| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. |
| `spec` _[NodeConfigSpec](#nodeconfigspec)_ | |
diff --git a/nodeadm/internal/api/status.go b/nodeadm/internal/api/status.go
index 4490f2e32..3396b459f 100644
--- a/nodeadm/internal/api/status.go
+++ b/nodeadm/internal/api/status.go
@@ -3,6 +3,8 @@ package api
import (
"context"
"fmt"
+ "strconv"
+ "strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
@@ -32,6 +34,10 @@ func GetInstanceDetails(ctx context.Context, featureGates map[Feature]bool, ec2C
return nil, err
}
}
+ networkCardDetails, err := getNetworkCardsDetails(ctx, imds.GetProperty)
+ if err != nil {
+ return nil, err
+ }
return &InstanceDetails{
ID: instanceIdenitityDocument.InstanceID,
@@ -40,6 +46,7 @@ func GetInstanceDetails(ctx context.Context, featureGates map[Feature]bool, ec2C
AvailabilityZone: instanceIdenitityDocument.AvailabilityZone,
MAC: string(mac),
PrivateDNSName: privateDNSName,
+ NetworkCards: networkCardDetails,
}, nil
}
@@ -64,3 +71,102 @@ func privateDNSNameAvailable(out *ec2.DescribeInstancesOutput) (bool, error) {
}
return aws.ToString(out.Reservations[0].Instances[0].PrivateDnsName) != "", nil
}
+
+func getNetworkCardsDetails(ctx context.Context, imdsFunc func(ctx context.Context, prop imds.IMDSProperty) (string, error)) ([]NetworkCardDetails, error) {
+
+ allMacs, err := imdsFunc(ctx, "network/interfaces/macs/")
+ if err != nil {
+ return nil, fmt.Errorf("failed to get network interfaces from imds: %w", err)
+ }
+
+ availableMacs := parseAvailableMacs(allMacs)
+ details := []NetworkCardDetails{}
+
+ for _, mac := range availableMacs {
+ cardDetails, err := getNetworkCardDetail(ctx, imdsFunc, mac)
+ if err != nil {
+ if isNotFoundError(err) {
+ continue
+ }
+ return nil, fmt.Errorf("failed to get network card details for MAC %s: %w", mac, err)
+ }
+ // ip address can be empty for efa-only cards
+ if cardDetails.IpV4Address == "" {
+ continue
+ }
+
+ details = append(details, cardDetails)
+ }
+
+ return details, nil
+}
+
+func parseAvailableMacs(allMacs string) []string {
+ allMacs = strings.ReplaceAll(allMacs, "\n", "")
+ allMacs = strings.TrimSuffix(allMacs, "/")
+ allMacs = strings.TrimSpace(allMacs)
+
+ return strings.Split(allMacs, "/")
+}
+
+func getNetworkCardDetail(ctx context.Context, imdsFunc func(ctx context.Context, prop imds.IMDSProperty) (string, error), mac string) (NetworkCardDetails, error) {
+ // imds will return 404 if we query network-card object for instance that doesn't support multiple cards
+ cardIndexPath := imds.IMDSProperty(fmt.Sprintf("network/interfaces/macs/%s/network-card", mac))
+ // imds will return 404 if we query local-ipv4s object if ip-address is not confirured on the interface from EC2 (efa-only)
+ ipV4AddressPath := imds.IMDSProperty(fmt.Sprintf("network/interfaces/macs/%s/local-ipv4s", mac))
+ ipV4SubnetPath := imds.IMDSProperty(fmt.Sprintf("network/interfaces/macs/%s/subnet-ipv4-cidr-block", mac))
+ ipV6SubnetPath := imds.IMDSProperty(fmt.Sprintf("network/interfaces/macs/%s/subnet-ipv6-cidr-blocks", mac))
+ ipV6AddressPath := imds.IMDSProperty(fmt.Sprintf("network/interfaces/macs/%s/ipv6s", mac))
+ interfaceIdPath := imds.IMDSProperty(fmt.Sprintf("network/interfaces/macs/%s/interface-id", mac))
+
+ cardIndex, err := imdsFunc(ctx, cardIndexPath)
+ if err != nil {
+ return NetworkCardDetails{}, err
+ }
+ cardIndexInt, err := strconv.Atoi(cardIndex)
+ if err != nil {
+ return NetworkCardDetails{}, fmt.Errorf("invalid card index: %w", err)
+ }
+
+ ipV4Address, err := imdsFunc(ctx, ipV4AddressPath)
+ if err != nil {
+ return NetworkCardDetails{}, err
+ }
+
+ ipV4Subnet, err := imdsFunc(ctx, ipV4SubnetPath)
+ if err != nil {
+ return NetworkCardDetails{}, err
+ }
+
+ ipV6Address, err := imdsFunc(ctx, ipV6AddressPath)
+ if err != nil {
+ return NetworkCardDetails{}, err
+ }
+
+ ipV6Subnet, err := imdsFunc(ctx, ipV6SubnetPath)
+ if err != nil {
+ return NetworkCardDetails{}, err
+ }
+
+ interfaceId, err := imdsFunc(ctx, interfaceIdPath)
+ if err != nil {
+ return NetworkCardDetails{}, err
+ }
+
+ return NetworkCardDetails{
+ MAC: mac,
+ CardIndex: cardIndexInt,
+ IpV4Address: ipV4Address,
+ IpV4Subnet: ipV4Subnet,
+ IpV6Address: ipV6Address,
+ IpV6Subnet: ipV6Subnet,
+ InterfaceId: interfaceId,
+ }, nil
+}
+
+func isNotFoundError(err error) bool {
+ if err == nil {
+ return false
+ }
+ return strings.Contains(err.Error(), "StatusCode: 404")
+}
diff --git a/nodeadm/internal/api/status_test.go b/nodeadm/internal/api/status_test.go
new file mode 100644
index 000000000..7a5d3ddc0
--- /dev/null
+++ b/nodeadm/internal/api/status_test.go
@@ -0,0 +1,143 @@
+package api
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/awslabs/amazon-eks-ami/nodeadm/internal/aws/imds"
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ nonMulticardInstanceMac = `06:83:e7:fd:fd:fd/
+`
+ validNetworkInterfacesMacs = `06:83:e7:fe:fe:fe/
+06:83:e7:ff:ff:ff/
+`
+ multicardNoIpOnOneCard = `06:83:e7:fb:fb:fb/
+06:83:e7:fc:fc:fc/
+06:83:e7:fc:fc:fd/
+`
+ validTwoNetworkCardDetails = []NetworkCardDetails{
+ {MAC: "06:83:e7:fe:fe:fe", IpV4Address: "1.2.3.4", CardIndex: 1},
+ {MAC: "06:83:e7:ff:ff:ff", IpV4Address: "5.6.7.8", CardIndex: 0},
+ }
+
+ validOneNetworkCardDetails = []NetworkCardDetails{
+ {MAC: "06:83:e7:fb:fb:fb", IpV4Address: "1.2.3.4", CardIndex: 0},
+ }
+ imds404 = errors.New("http response error StatusCode: 404, request to EC2 IMDS failed")
+ imdsGeneric = errors.New("IMDS error")
+)
+
+func mockGetPropertyImdsError(ctx context.Context, prop imds.IMDSProperty) (string, error) {
+ if prop == "network/interfaces/macs/" {
+ return "", imdsGeneric
+ }
+ return "", nil
+}
+
+func mockGetPropertyNonMulticardInstance(ctx context.Context, prop imds.IMDSProperty) (string, error) {
+ if prop == "network/interfaces/macs/" {
+ return nonMulticardInstanceMac, nil
+ }
+ if prop == "network/interfaces/macs/06:83:e7:fd:fd:fd/network-card" {
+ return "", imds404
+ }
+ return "", nil
+}
+
+func mockGetPropertyTwoValidCards(ctx context.Context, prop imds.IMDSProperty) (string, error) {
+ if prop == "network/interfaces/macs/" {
+ return validNetworkInterfacesMacs, nil
+ }
+ if prop == "network/interfaces/macs/06:83:e7:fe:fe:fe/network-card" {
+ return "1", nil
+ }
+ if prop == "network/interfaces/macs/06:83:e7:ff:ff:ff/network-card" {
+ return "0", nil
+ }
+ if prop == "network/interfaces/macs/06:83:e7:fe:fe:fe/local-ipv4s" {
+ return "1.2.3.4", nil
+ }
+ if prop == "network/interfaces/macs/06:83:e7:ff:ff:ff/local-ipv4s" {
+ return "5.6.7.8", nil
+ }
+
+ return "", nil
+}
+
+func mockGetPropertyNoIp(ctx context.Context, prop imds.IMDSProperty) (string, error) {
+ if prop == "network/interfaces/macs/" {
+ return multicardNoIpOnOneCard, nil
+ }
+ if prop == "network/interfaces/macs/06:83:e7:fb:fb:fb/network-card" {
+ return "0", nil
+ }
+ if prop == "network/interfaces/macs/06:83:e7:fc:fc:fc/network-card" {
+ return "1", nil
+ }
+ if prop == "network/interfaces/macs/06:83:e7:fc:fc:fd/network-card" {
+ return "2", nil
+ }
+ if prop == "network/interfaces/macs/06:83:e7:fb:fb:fb/local-ipv4s" {
+ return "1.2.3.4", nil
+ }
+ if prop == "network/interfaces/macs/06:83:e7:fc:fc:fc/local-ipv4s" {
+ return "", imds404
+ }
+ if prop == "network/interfaces/macs/06:83:e7:fc:fc:fd/local-ipv4s" {
+ return "", nil
+ }
+
+ return "", nil
+}
+
+func TestGetNetworkCardsDetails(t *testing.T) {
+
+ tests := []struct {
+ name string
+ mockGetProperty func(ctx context.Context, prop imds.IMDSProperty) (string, error)
+ expectedNetworkCardDetails []NetworkCardDetails
+ expectedError error
+ }{
+ {
+ name: "Success card 0 and card 1 available",
+ mockGetProperty: mockGetPropertyTwoValidCards,
+ expectedNetworkCardDetails: validTwoNetworkCardDetails,
+ expectedError: nil,
+ },
+ {
+ name: "Success non multicard instance",
+ mockGetProperty: mockGetPropertyNonMulticardInstance,
+ expectedNetworkCardDetails: []NetworkCardDetails{},
+ expectedError: nil,
+ },
+ {
+ name: "Success multicard instance no IP",
+ mockGetProperty: mockGetPropertyNoIp,
+ expectedNetworkCardDetails: validOneNetworkCardDetails,
+ expectedError: nil,
+ },
+ {
+ name: "Fail with IMDS error",
+ mockGetProperty: mockGetPropertyImdsError,
+ expectedNetworkCardDetails: nil,
+ expectedError: errors.New("failed to get network interfaces from imds: IMDS error"),
+ },
+ }
+
+ for _, test := range tests {
+ t.Logf("Running test: %s", test.name)
+ ctx := context.Background()
+ networkCardDetails, err := getNetworkCardsDetails(ctx, test.mockGetProperty)
+
+ assert.Equal(t, test.expectedNetworkCardDetails, networkCardDetails, t.Name())
+ if test.expectedError != nil {
+ assert.EqualError(t, err, test.expectedError.Error(), t.Name())
+ } else {
+ assert.NoError(t, err, t.Name())
+ }
+ }
+}
diff --git a/nodeadm/internal/api/types.go b/nodeadm/internal/api/types.go
index e177d9f3e..dab656c6b 100644
--- a/nodeadm/internal/api/types.go
+++ b/nodeadm/internal/api/types.go
@@ -40,13 +40,24 @@ type NodeConfigStatus struct {
Defaults DefaultOptions `json:"default,omitempty"`
}
+type NetworkCardDetails struct {
+ MAC string `json:"mac,omitempty"`
+ IpV4Address string `json:"ipV4Address,omitempty"`
+ IpV4Subnet string `json:"ipV4Subnet,omitempty"`
+ IpV6Address string `json:"ipV6Address,omitempty"`
+ IpV6Subnet string `json:"ipV6Subnet,omitempty"`
+ CardIndex int `json:"cardIndex,omitempty"`
+ InterfaceId string `json:"interfaceId,omitempty"`
+}
+
type InstanceDetails struct {
- ID string `json:"id,omitempty"`
- Region string `json:"region,omitempty"`
- Type string `json:"type,omitempty"`
- AvailabilityZone string `json:"availabilityZone,omitempty"`
- MAC string `json:"mac,omitempty"`
- PrivateDNSName string `json:"privateDnsName,omitempty"`
+ ID string `json:"id,omitempty"`
+ Region string `json:"region,omitempty"`
+ Type string `json:"type,omitempty"`
+ AvailabilityZone string `json:"availabilityZone,omitempty"`
+ MAC string `json:"mac,omitempty"`
+ PrivateDNSName string `json:"privateDnsName,omitempty"`
+ NetworkCards []NetworkCardDetails `json:"networkCards,omitempty"`
}
type DefaultOptions struct {
diff --git a/nodeadm/internal/api/zz_generated.deepcopy.go b/nodeadm/internal/api/zz_generated.deepcopy.go
index 4fc7659e8..5dff94065 100644
--- a/nodeadm/internal/api/zz_generated.deepcopy.go
+++ b/nodeadm/internal/api/zz_generated.deepcopy.go
@@ -94,6 +94,11 @@ func (in InlineDocument) DeepCopy() InlineDocument {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InstanceDetails) DeepCopyInto(out *InstanceDetails) {
*out = *in
+ if in.NetworkCards != nil {
+ in, out := &in.NetworkCards, &out.NetworkCards
+ *out = make([]NetworkCardDetails, len(*in))
+ copy(*out, *in)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceDetails.
@@ -183,13 +188,28 @@ func (in *LocalStorageOptions) DeepCopy() *LocalStorageOptions {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NetworkCardDetails) DeepCopyInto(out *NetworkCardDetails) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkCardDetails.
+func (in *NetworkCardDetails) DeepCopy() *NetworkCardDetails {
+ if in == nil {
+ return nil
+ }
+ out := new(NetworkCardDetails)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NodeConfig) DeepCopyInto(out *NodeConfig) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
- out.Status = in.Status
+ in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeConfig.
@@ -271,7 +291,7 @@ func (in *NodeConfigSpec) DeepCopy() *NodeConfigSpec {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NodeConfigStatus) DeepCopyInto(out *NodeConfigStatus) {
*out = *in
- out.Instance = in.Instance
+ in.Instance.DeepCopyInto(&out.Instance)
out.Defaults = in.Defaults
}
diff --git a/nodeadm/internal/system/_assets/10-eks_primary_eni_only.conf.template b/nodeadm/internal/system/_assets/10-eks_primary_eni_only.conf.template
index f1d77af29..344c859e0 100644
--- a/nodeadm/internal/system/_assets/10-eks_primary_eni_only.conf.template
+++ b/nodeadm/internal/system/_assets/10-eks_primary_eni_only.conf.template
@@ -1,2 +1,9 @@
[Match]
-PermanentMACAddress={{.PermanentMACAddress}}
\ No newline at end of file
+PermanentMACAddress={{.PermanentMACAddress}}
+
+[DHCPv4]
+RouteMetric=512
+
+[IPv6AcceptRA]
+RouteMetric=512
+UseGateway=true
\ No newline at end of file
diff --git a/nodeadm/internal/system/_assets/interface.network.template b/nodeadm/internal/system/_assets/interface.network.template
new file mode 100644
index 000000000..33df4fdcd
--- /dev/null
+++ b/nodeadm/internal/system/_assets/interface.network.template
@@ -0,0 +1,59 @@
+[Match]
+PermanentMACAddress={{.PermanentMACAddress}}
+
+[Link]
+MTUBytes=9001
+
+[Link]
+MTUBytes=9001
+
+[Network]
+DHCP=yes
+IPv6DuplicateAddressDetection=0
+LLMNR=no
+DNSDefaultRoute=yes
+
+[DHCPv4]
+UseHostname=no
+UseDNS=yes
+UseNTP=yes
+UseDomains=yes
+RouteMetric={{.RouteTableMetric}}
+UseRoutes=true
+UseGateway=true
+
+[DHCPv6]
+UseHostname=no
+UseDNS=yes
+UseNTP=yes
+WithoutRA=solicit
+
+[RoutingPolicyRule]
+From={{.IpV4Address}}
+Table={{.RouteTableId}}
+Priority={{.RouteTableId}}
+
+[RoutingPolicyRule]
+From={{.IpV6Address}}
+Table={{.RouteTableId}}
+Priority={{.RouteTableId}}
+
+[IPv6AcceptRA]
+RouteMetric={{.RouteTableMetric}}
+UseGateway=true
+
+[Route]
+Table={{.RouteTableId}}
+Gateway=_ipv6ra
+
+[Route]
+Table={{.RouteTableId}}
+Destination={{.IpV6Subnet}}
+
+[Route]
+Gateway=_dhcp4
+Table={{.RouteTableId}}
+
+[Route]
+Table={{.RouteTableId}}
+Destination={{.IpV4Subnet}}
\ No newline at end of file
diff --git a/nodeadm/internal/system/_assets/test_10-eks_primary_eni_only.conf b/nodeadm/internal/system/_assets/test_10-eks_primary_eni_only.conf
index 5c384f416..af573e60b 100644
--- a/nodeadm/internal/system/_assets/test_10-eks_primary_eni_only.conf
+++ b/nodeadm/internal/system/_assets/test_10-eks_primary_eni_only.conf
@@ -1,2 +1,9 @@
[Match]
-PermanentMACAddress=0e:f7:72:74:2d:43
\ No newline at end of file
+PermanentMACAddress=0e:f7:72:74:2d:43
+
+[DHCPv4]
+RouteMetric=512
+
+[IPv6AcceptRA]
+RouteMetric=512
+UseGateway=true
\ No newline at end of file
diff --git a/nodeadm/internal/system/networking.go b/nodeadm/internal/system/networking.go
index f955abd7b..90ff0344c 100644
--- a/nodeadm/internal/system/networking.go
+++ b/nodeadm/internal/system/networking.go
@@ -4,12 +4,13 @@ import (
"bytes"
_ "embed"
"fmt"
- "github.com/awslabs/amazon-eks-ami/nodeadm/internal/api"
- "github.com/awslabs/amazon-eks-ami/nodeadm/internal/util"
- "go.uber.org/zap"
"os"
"os/exec"
"text/template"
+
+ "github.com/awslabs/amazon-eks-ami/nodeadm/internal/api"
+ "github.com/awslabs/amazon-eks-ami/nodeadm/internal/util"
+ "go.uber.org/zap"
)
const (
@@ -28,6 +29,9 @@ var (
//go:embed _assets/10-eks_primary_eni_only.conf.template
eksPrimaryENIOnlyConfTemplateData string
eksPrimaryENIOnlyConfTemplate = template.Must(template.New(eksPrimaryENIOnlyConfName).Parse(eksPrimaryENIOnlyConfTemplateData))
+
+ //go:embed _assets/interface.network.template
+ eksAdditionalENINetworkFileTemplate string
)
// NewNetworkingAspect constructs new networkingAspect.
@@ -50,6 +54,12 @@ func (a *networkingAspect) Setup(cfg *api.NodeConfig) error {
if err := a.ensureEKSNetworkConfiguration(cfg); err != nil {
return fmt.Errorf("failed to ensure eks network configuration: %w", err)
}
+ if err := a.ensureMulticardNetworkConfiguration(cfg); err != nil {
+ return fmt.Errorf("failed to ensure multicard network configuration: %w", err)
+ }
+ if err := a.reloadNetworkConfigurations(); err != nil {
+ return fmt.Errorf("failed to reload network configurations: %w", err)
+ }
return nil
}
@@ -83,17 +93,82 @@ func (a *networkingAspect) ensureEKSNetworkConfiguration(cfg *api.NodeConfig) er
if err := os.WriteFile(eksPrimaryENIOnlyConfPathName, eksPrimaryENIOnlyConfContent, networkConfFilePerms); err != nil {
return fmt.Errorf("failed to write eks_primary_eni_only network configuration: %w", err)
}
- if err := a.reloadNetworkConfigurations(); err != nil {
- return fmt.Errorf("failed to reload network configurations: %w", err)
+ return nil
+}
+
+// ensureMulticardNetworkConfiguration configures the non-zero card interfaces in a way that mimics the
+// default AL2023 configuration. Non-zero card interfaces are not managed by vpc-cni and we're creating
+// systemd-networkd .network files for each interface.
+func (a *networkingAspect) ensureMulticardNetworkConfiguration(cfg *api.NodeConfig) error {
+ routeTableId := 10101
+ routeTableMetric := 613
+
+ for _, card := range cfg.Status.Instance.NetworkCards {
+ if card.CardIndex == 0 {
+ continue
+ }
+
+ networkInterfaceConfName := fmt.Sprintf("70-%s.network", card.InterfaceId)
+ networkInterfaceConfPathName := fmt.Sprintf("%s/%s", administrationNetworkDir, networkInterfaceConfName)
+
+ if exists, err := util.IsFilePathExists(networkInterfaceConfPathName); err != nil {
+ return fmt.Errorf("failed to check configuration existance for %s: %w", networkInterfaceConfName, err)
+ } else if exists {
+ zap.L().Sugar().Infof("%s already exists, skipping configuration", networkInterfaceConfName)
+ continue
+ }
+
+ templateVars := networkInterfaceTemplateVars{
+ PermanentMACAddress: card.MAC,
+ IpV4Address: card.IpV4Address,
+ IpV4Subnet: card.IpV4Subnet,
+ IpV6Address: card.IpV6Address,
+ IpV6Subnet: card.IpV6Subnet,
+ RouteTableId: int16(routeTableId),
+ RouteTableMetric: int16(routeTableMetric),
+ }
+ routeTableId += 100
+ routeTableMetric += 100
+
+ interfaceConfigContent, err := a.generateNetworkConfigFile(networkInterfaceConfName, templateVars)
+ if err != nil {
+ return fmt.Errorf("failed to generate %s configuration: %w", networkInterfaceConfName, err)
+ }
+
+ if err := os.WriteFile(networkInterfaceConfPathName, interfaceConfigContent, networkConfFilePerms); err != nil {
+ return fmt.Errorf("failed to write %s configuration: %w", networkInterfaceConfName, err)
+ }
+ zap.L().Sugar().Infof("Multicard instance found, configuring card with index: %d, network file: %s", card.CardIndex, networkInterfaceConfPathName)
}
return nil
}
+func (a *networkingAspect) generateNetworkConfigFile(interfaceConfName string, templateVars networkInterfaceTemplateVars) ([]byte, error) {
+ networkInterfaceConfTemplate := template.Must(template.New(interfaceConfName).Parse(eksAdditionalENINetworkFileTemplate))
+ var buf bytes.Buffer
+ if err := networkInterfaceConfTemplate.Execute(&buf, templateVars); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+
+}
+
// eksPrimaryENIOnlyTemplateVars holds the variables for eksPrimaryENIOnlyConfTemplate
type eksPrimaryENIOnlyTemplateVars struct {
PermanentMACAddress string
}
+// networkInterfaceTemplateVars holds the variables for networkInterfaceConfTemplate
+type networkInterfaceTemplateVars struct {
+ PermanentMACAddress string
+ IpV4Address string
+ IpV4Subnet string
+ IpV6Address string
+ IpV6Subnet string
+ RouteTableId int16
+ RouteTableMetric int16
+}
+
// generateEKSPrimaryENIOnlyConfiguration generates the eks primary eni only network configuration.
func (a *networkingAspect) generateEKSPrimaryENIOnlyConfiguration(cfg *api.NodeConfig) ([]byte, error) {
primaryENIMac := cfg.Status.Instance.MAC