Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ k8s_resource(new_name='po10', objects=['po-10:interface'], trigger_mode=TRIGGER_
k8s_resource(new_name='eth1-3', objects=['eth1-3:interface'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
k8s_resource(new_name='po20', objects=['po-20:interface'], resource_deps=['eth1-3'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
k8s_resource(new_name='svi-10', objects=['svi-10:interface'], resource_deps=['vlan-10'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
k8s_resource(new_name='lo2', objects=['lo2:interface'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
k8s_resource(new_name='eth1-4', objects=['eth1-4:interface'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])

k8s_yaml('./config/samples/v1alpha1_banner.yaml')
k8s_resource(new_name='banner', objects=['banner:banner'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
Expand Down
32 changes: 32 additions & 0 deletions api/core/v1alpha1/interface_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

// InterfaceSpec defines the desired state of Interface.
// +kubebuilder:validation:XValidation:rule="!has(self.switchport) || !has(self.ipv4)", message="switchport and ipv4 are mutually exclusive"
// +kubebuilder:validation:XValidation:rule="!has(self.switchport) || !has(self.ipv6)", message="switchport and ipv6 are mutually exclusive"
// +kubebuilder:validation:XValidation:rule="self.type != 'Loopback' || !has(self.switchport)", message="switchport must not be specified for interfaces of type Loopback"
// +kubebuilder:validation:XValidation:rule="self.type == 'Physical' || !has(self.ipv4) || !has(self.ipv4.unnumbered)", message="unnumbered ipv4 configuration can only be used for interfaces of type Physical"
// +kubebuilder:validation:XValidation:rule="self.type != 'Aggregate' || has(self.aggregation)", message="aggregation must be specified for interfaces of type Aggregate"
Expand Down Expand Up @@ -80,6 +81,10 @@ type InterfaceSpec struct {
// +optional
IPv4 *InterfaceIPv4 `json:"ipv4,omitempty"`

// IPv6 defines the IPv6 configuration for the interface.
// +optional
IPv6 *InterfaceIPv6 `json:"ipv6,omitempty"`

// Aggregation defines the aggregation (bundle) configuration for the interface.
// This is only applicable for interfaces of type Aggregate.
// +optional
Expand Down Expand Up @@ -289,6 +294,28 @@ type InterfaceIPv4Unnumbered struct {
InterfaceRef LocalObjectReference `json:"interfaceRef"`
}

// InterfaceIPv6 defines the IPv6 configuration for an interface.
// +kubebuilder:validation:XValidation:rule="has(self.addresses) || (has(self.useLinkLocalOnly) && self.useLinkLocalOnly)", message="either addresses or useLinkLocalOnly must be specified"
// +kubebuilder:validation:XValidation:rule="!has(self.addresses) || !has(self.useLinkLocalOnly) || !self.useLinkLocalOnly", message="addresses and useLinkLocalOnly are mutually exclusive"
type InterfaceIPv6 struct {
// Addresses defines the list of global unicast IPv6 addresses assigned to
// the interface. Unlike IPv4, all addresses are equal, there is no primary
// or secondary distinction.
// Link-local addresses cannot be assigned here, they are configured through
// UseLinkLocalOnly.
// +optional
// +listType=atomic
// +kubebuilder:validation:MinItems=1
Addresses []IPPrefix `json:"addresses,omitempty"`

// UseLinkLocalOnly configures the interface to operate with only its
// automatically generated IPv6 link-local address, without assigning a
// global address. This is what unnumbered, interface-based BGP peering
// requires in order to discover neighbours over their link-local address.
// +optional
UseLinkLocalOnly bool `json:"useLinkLocalOnly,omitempty"`
}

// BFD defines the Bidirectional Forwarding Detection configuration for an interface.
type BFD struct {
// Enabled indicates whether BFD is enabled on the interface.
Expand Down Expand Up @@ -600,6 +627,11 @@ func (in *Interface) HasIPv4() bool {
return in.Spec.Switchport == nil && in.Spec.IPv4 != nil
}

// HasIPv6 reports whether the Interface is configured as a routed IPv6 interface.
func (in *Interface) HasIPv6() bool {
return in.Spec.Switchport == nil && in.Spec.IPv6 != nil
}

// GetConditions implements conditions.Getter.
func (in *Interface) GetConditions() []metav1.Condition {
return in.Status.Conditions
Expand Down
27 changes: 27 additions & 0 deletions api/core/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions config/samples/v1alpha1_interface.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,44 @@ spec:
addresses:
- 192.168.10.254/24
anycastGateway: true
---
apiVersion: networking.metal.ironcore.dev/v1alpha1
kind: Interface
metadata:
labels:
app.kubernetes.io/name: network-operator
app.kubernetes.io/managed-by: kustomize
networking.metal.ironcore.dev/device-name: leaf1
name: lo2
spec:
deviceRef:
name: leaf1
name: lo2
description: IPv6 Router-ID Leaf1
adminState: Up
type: Loopback
mtu: 1500
ipv6:
addresses:
- 2001:db8::10/128
---
apiVersion: networking.metal.ironcore.dev/v1alpha1
kind: Interface
metadata:
labels:
app.kubernetes.io/name: network-operator
app.kubernetes.io/managed-by: kustomize
networking.metal.ironcore.dev/device-name: leaf1
name: eth1-4
spec:
deviceRef:
name: leaf1
name: eth1/4
description: Unnumbered uplink for interface-based BGP peering
adminState: Up
type: Physical
mtu: 9216
# Use only the automatically generated IPv6 link-local address, so the link
# needs no addressing of its own. Mutually exclusive with ipv6.addresses.
ipv6:
useLinkLocalOnly: true
19 changes: 19 additions & 0 deletions docs/api-reference/index.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions hack/provider/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,20 @@ func performCreate(ctx context.Context, prov provider.Provider, obj client.Objec
}
}

var ipv6 provider.IPv6
if res.Spec.IPv6 != nil {
switch {
case res.Spec.IPv6.UseLinkLocalOnly:
ipv6 = provider.IPv6LinkLocalOnly{}
case len(res.Spec.IPv6.Addresses) > 0:
addrs := make([]netip.Prefix, len(res.Spec.IPv6.Addresses))
for i, addr := range res.Spec.IPv6.Addresses {
addrs[i] = addr.Prefix
}
ipv6 = provider.IPv6AddressList(addrs)
}
}

var members []*v1alpha1.Interface
if res.Spec.Type == v1alpha1.InterfaceTypeAggregate && len(res.Spec.Aggregation.MemberInterfaceRefs) > 0 {
if len(refStore) == 0 {
Expand Down Expand Up @@ -697,6 +711,7 @@ func performCreate(ctx context.Context, prov provider.Provider, obj client.Objec
return ip.EnsureInterface(ctx, &provider.EnsureInterfaceRequest{
Interface: res,
IPv4: ipv4,
IPv6: ipv6,
Members: members,
MultiChassisID: multiChassisID,
VLAN: vlan,
Expand Down
22 changes: 22 additions & 0 deletions internal/controller/core/interface_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ func (r *InterfaceReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man
// Only trigger when fields that affect member Physical interface
// reconciliation change (e.g. layer, VRF membership, MTU).
return !equality.Semantic.DeepEqual(oldIntf.Spec.IPv4, newIntf.Spec.IPv4) ||
!equality.Semantic.DeepEqual(oldIntf.Spec.IPv6, newIntf.Spec.IPv6) ||
!equality.Semantic.DeepEqual(oldIntf.Spec.Switchport, newIntf.Spec.Switchport) ||
!equality.Semantic.DeepEqual(oldIntf.Spec.VrfRef, newIntf.Spec.VrfRef) ||
oldIntf.Spec.MTU != newIntf.Spec.MTU
Expand Down Expand Up @@ -569,6 +570,8 @@ func (r *InterfaceReconciler) reconcile(ctx context.Context, s *scope) (reterr e
}
}

ipv6 := interfaceIPv6(s.Interface.Spec.IPv6)

if err := s.Provider.Connect(ctx, s.Connection); err != nil {
return fmt.Errorf("failed to connect to provider: %w", err)
}
Expand All @@ -583,6 +586,7 @@ func (r *InterfaceReconciler) reconcile(ctx context.Context, s *scope) (reterr e
Interface: s.Interface,
ProviderConfig: s.ProviderConfig,
IPv4: ip,
IPv6: ipv6,
Members: members,
MultiChassisID: multiChassisID,
AggregateParent: aggregateParent,
Expand Down Expand Up @@ -777,6 +781,24 @@ func (r *InterfaceReconciler) validateLLDPAdjacencyThroughAnnotation(ctx context
return v1alpha1.NeighborVerified, nil
}

// interfaceIPv6 maps the IPv6 spec of an Interface to the provider representation.
// It returns nil if the Interface has no IPv6 configuration.
func interfaceIPv6(spec *v1alpha1.InterfaceIPv6) provider.IPv6 {
switch {
case spec == nil:
return nil
case spec.UseLinkLocalOnly:
return provider.IPv6LinkLocalOnly{}
case len(spec.Addresses) > 0:
addrs := make([]netip.Prefix, len(spec.Addresses))
for i, addr := range spec.Addresses {
addrs[i] = addr.Prefix
}
return provider.IPv6AddressList(addrs)
}
return nil
}

func (r *InterfaceReconciler) reconcileIPv4(ctx context.Context, s *scope) (provider.IPv4, error) {
switch {
case len(s.Interface.Spec.IPv4.Addresses) > 0:
Expand Down
9 changes: 8 additions & 1 deletion internal/provider/cisco/nxos/intf.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,14 @@ func (d *AddrDom) Key() string { return d.Name }
type AddrItem struct {
ID string `json:"id"`
Unnumbered string `json:"unnumbered,omitempty"`
AddrItems struct {

// UseLinkLocalAddr configures the interface to use only its automatically
// generated IPv6 link-local address ("ipv6 address use-link-local-only").
// Only set for IPv6; the IPv4 object has no such property, so it must stay
// empty there to keep the payload free of it.
UseLinkLocalAddr AdminSt `json:"useLinkLocalAddr,omitempty"`

AddrItems struct {
AddrList gnmiext.List[string, *IntfAddr] `json:"Addr-list,omitzero"`
} `json:"addr-items,omitzero"`

Expand Down
8 changes: 8 additions & 0 deletions internal/provider/cisco/nxos/intf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ func init() {
})
Register("intf_addr4", intfAddr4)

intfAddr6 := &AddrItem{ID: "lo0", Vrf: DefaultVRFName, Is6: true, UseLinkLocalAddr: AdminStDisabled}
intfAddr6.AddrItems.AddrList.Set(&IntfAddr{Addr: "2001:db8:1::1/64", Type: IntfAddrTypePrimary})
intfAddr6.AddrItems.AddrList.Set(&IntfAddr{Addr: "2001:db8:2::1/64", Type: IntfAddrTypePrimary})
Register("intf_addr6", intfAddr6)

// "ipv6 address use-link-local-only", as required for unnumbered peering.
Register("intf_lladdr6", &AddrItem{ID: "eth1/1", Vrf: DefaultVRFName, Is6: true, UseLinkLocalAddr: AdminStEnabled})

pc := &PortChannel{
AccessVlan: DefaultVLAN,
AdminSt: AdminStUp,
Expand Down
Loading
Loading