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