From 6ede4bf10111773b537e36967f8cf66759cf3616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20K=C3=A4stner?= Date: Sun, 15 Mar 2026 14:34:38 +0100 Subject: [PATCH] Add AS-path set actions to RoutingPolicy CRD Extend the RoutingPolicy API with a new SetASPath action under BgpActions, supporting prepend, prepend last-as, replace, and explicit AS-path operations. The API uses vendor-agnostic types with intstr.IntOrString for AS numbers (plain and dotted notation per RFC 5396) and CEL validation rules for mutual exclusivity. --- api/core/v1alpha1/routingpolicy_types.go | 64 ++++++++++- api/core/v1alpha1/zz_generated.deepcopy.go | 82 +++++++++++++ ...olicies.networking.metal.ironcore.dev.yaml | 77 +++++++++++++ ...ng.metal.ironcore.dev_routingpolicies.yaml | 77 +++++++++++++ config/samples/v1alpha1_routingpolicy.yaml | 28 +++++ docs/api-reference/index.md | 58 +++++++++- .../core/routingpolicy_controller_test.go | 108 ++++++++++++++++++ internal/provider/cisco/nxos/provider.go | 26 +++++ internal/provider/cisco/nxos/routemap.go | 15 +++ internal/provider/cisco/nxos/routemap_test.go | 51 +++++++++ .../testdata/route_map_aspath_lastas.json | 22 ++++ .../testdata/route_map_aspath_lastas.json.txt | 2 + .../testdata/route_map_aspath_prepend.json | 22 ++++ .../route_map_aspath_prepend.json.txt | 2 + .../testdata/route_map_aspath_replace.json | 34 ++++++ .../route_map_aspath_replace.json.txt | 4 + .../nxos/testdata/route_map_aspath_set.json | 22 ++++ .../testdata/route_map_aspath_set.json.txt | 2 + 18 files changed, 694 insertions(+), 2 deletions(-) create mode 100644 internal/provider/cisco/nxos/testdata/route_map_aspath_lastas.json create mode 100644 internal/provider/cisco/nxos/testdata/route_map_aspath_lastas.json.txt create mode 100644 internal/provider/cisco/nxos/testdata/route_map_aspath_prepend.json create mode 100644 internal/provider/cisco/nxos/testdata/route_map_aspath_prepend.json.txt create mode 100644 internal/provider/cisco/nxos/testdata/route_map_aspath_replace.json create mode 100644 internal/provider/cisco/nxos/testdata/route_map_aspath_replace.json.txt create mode 100644 internal/provider/cisco/nxos/testdata/route_map_aspath_set.json create mode 100644 internal/provider/cisco/nxos/testdata/route_map_aspath_set.json.txt diff --git a/api/core/v1alpha1/routingpolicy_types.go b/api/core/v1alpha1/routingpolicy_types.go index 4583854e..cf48825c 100644 --- a/api/core/v1alpha1/routingpolicy_types.go +++ b/api/core/v1alpha1/routingpolicy_types.go @@ -8,6 +8,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/intstr" ) // RoutingPolicySpec defines the desired state of RoutingPolicy @@ -96,7 +97,7 @@ const ( ) // BgpActions defines BGP-specific actions for a policy statement. -// +kubebuilder:validation:XValidation:rule="has(self.setCommunity) || has(self.setExtCommunity)",message="at least one BGP action must be specified" +// +kubebuilder:validation:XValidation:rule="has(self.setCommunity) || has(self.setExtCommunity) || has(self.setASPath)",message="at least one BGP action must be specified" type BgpActions struct { // SetCommunity configures BGP standard community attributes. // +optional @@ -105,6 +106,67 @@ type BgpActions struct { // SetExtCommunity configures BGP extended community attributes. // +optional SetExtCommunity *SetExtCommunityAction `json:"setExtCommunity,omitempty"` + + // SetASPath configures modifications to the BGP AS path attribute. + // Not all providers may support this action. + // +optional + SetASPath *SetASPathAction `json:"setASPath,omitempty"` +} + +// SetASPathAction defines actions to modify the BGP AS path attribute. +// +kubebuilder:validation:XValidation:rule="has(self.prepend) || has(self.replace) || has(self.asNumber)",message="at least one AS path action must be specified" +type SetASPathAction struct { + // Prepend configures prepending to the AS path. + // +optional + Prepend *SetASPathPrepend `json:"prepend,omitempty"` + + // Replace configures replacement of AS numbers in the AS path. + // +optional + Replace *SetASPathReplace `json:"replace,omitempty"` + + // ASNumber sets the AS path to the specified AS number. + // Supports both plain format (1-4294967295) and dotted notation (1-65535.0-65535) as per RFC 5396. + // +optional + ASNumber *intstr.IntOrString `json:"asNumber,omitempty"` +} + +// SetASPathPrepend configures prepending to the BGP AS path. +// Either asNumber or useLastAS must be specified, but not both. +// +kubebuilder:validation:XValidation:rule="has(self.asNumber) != has(self.useLastAS)",message="exactly one of asNumber or useLastAS must be specified" +type SetASPathPrepend struct { + // ASNumber is the autonomous system number to prepend to the AS path. + // Supports both plain format (1-4294967295) and dotted notation (1-65535.0-65535) as per RFC 5396. + // Mutually exclusive with useLastAS. + // +optional + ASNumber *intstr.IntOrString `json:"asNumber,omitempty"` + + // UseLastAS prepends the last AS number in the existing AS path the specified number of times. + // Mutually exclusive with asNumber. + // +optional + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=10 + UseLastAS *int32 `json:"useLastAS,omitempty"` +} + +// SetASPathReplace configures replacement of AS numbers in the BGP AS path. +// Either privateAS or asNumber must be specified, but not both. +// +kubebuilder:validation:XValidation:rule="has(self.privateAS) != has(self.asNumber)",message="exactly one of privateAS or asNumber must be specified" +type SetASPathReplace struct { + // PrivateAS, when set to true, targets all private AS numbers in the path for replacement. + // Mutually exclusive with asNumber. + // +optional + PrivateAS bool `json:"privateAS,omitempty"` + + // ASNumber targets a specific AS number in the path for replacement. + // Supports both plain format (1-4294967295) and dotted notation (1-65535.0-65535) as per RFC 5396. + // Mutually exclusive with privateAS. + // +optional + ASNumber *intstr.IntOrString `json:"asNumber,omitempty"` + + // Replacement is the AS number to substitute in place of matched AS numbers. + // Supports both plain format (1-4294967295) and dotted notation (1-65535.0-65535) as per RFC 5396. + // +required + Replacement intstr.IntOrString `json:"replacement"` } // SetCommunityAction defines the action to set BGP standard communities. diff --git a/api/core/v1alpha1/zz_generated.deepcopy.go b/api/core/v1alpha1/zz_generated.deepcopy.go index cde89b5c..bff93d77 100644 --- a/api/core/v1alpha1/zz_generated.deepcopy.go +++ b/api/core/v1alpha1/zz_generated.deepcopy.go @@ -10,6 +10,7 @@ package v1alpha1 import ( "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -772,6 +773,11 @@ func (in *BgpActions) DeepCopyInto(out *BgpActions) { *out = new(SetExtCommunityAction) (*in).DeepCopyInto(*out) } + if in.SetASPath != nil { + in, out := &in.SetASPath, &out.SetASPath + *out = new(SetASPathAction) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BgpActions. @@ -3209,6 +3215,82 @@ func (in *SecretReference) DeepCopy() *SecretReference { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SetASPathAction) DeepCopyInto(out *SetASPathAction) { + *out = *in + if in.Prepend != nil { + in, out := &in.Prepend, &out.Prepend + *out = new(SetASPathPrepend) + (*in).DeepCopyInto(*out) + } + if in.Replace != nil { + in, out := &in.Replace, &out.Replace + *out = new(SetASPathReplace) + (*in).DeepCopyInto(*out) + } + if in.ASNumber != nil { + in, out := &in.ASNumber, &out.ASNumber + *out = new(intstr.IntOrString) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SetASPathAction. +func (in *SetASPathAction) DeepCopy() *SetASPathAction { + if in == nil { + return nil + } + out := new(SetASPathAction) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SetASPathPrepend) DeepCopyInto(out *SetASPathPrepend) { + *out = *in + if in.ASNumber != nil { + in, out := &in.ASNumber, &out.ASNumber + *out = new(intstr.IntOrString) + **out = **in + } + if in.UseLastAS != nil { + in, out := &in.UseLastAS, &out.UseLastAS + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SetASPathPrepend. +func (in *SetASPathPrepend) DeepCopy() *SetASPathPrepend { + if in == nil { + return nil + } + out := new(SetASPathPrepend) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SetASPathReplace) DeepCopyInto(out *SetASPathReplace) { + *out = *in + if in.ASNumber != nil { + in, out := &in.ASNumber, &out.ASNumber + *out = new(intstr.IntOrString) + **out = **in + } + out.Replacement = in.Replacement +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SetASPathReplace. +func (in *SetASPathReplace) DeepCopy() *SetASPathReplace { + if in == nil { + return nil + } + out := new(SetASPathReplace) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SetCommunityAction) DeepCopyInto(out *SetCommunityAction) { *out = *in diff --git a/charts/network-operator/templates/crd/routingpolicies.networking.metal.ironcore.dev.yaml b/charts/network-operator/templates/crd/routingpolicies.networking.metal.ironcore.dev.yaml index c072d158..c68fa6c0 100644 --- a/charts/network-operator/templates/crd/routingpolicies.networking.metal.ironcore.dev.yaml +++ b/charts/network-operator/templates/crd/routingpolicies.networking.metal.ironcore.dev.yaml @@ -138,6 +138,82 @@ spec: BgpActions specifies BGP-specific actions to apply when the route is accepted. Only applicable when RouteDisposition is AcceptRoute. properties: + setASPath: + description: |- + SetASPath configures modifications to the BGP AS path attribute. + Not all providers may support this action. + properties: + asNumber: + anyOf: + - type: integer + - type: string + description: |- + ASNumber sets the AS path to the specified AS number. + Supports both plain format (1-4294967295) and dotted notation (1-65535.0-65535) as per RFC 5396. + x-kubernetes-int-or-string: true + prepend: + description: Prepend configures prepending to the + AS path. + properties: + asNumber: + anyOf: + - type: integer + - type: string + description: |- + ASNumber is the autonomous system number to prepend to the AS path. + Supports both plain format (1-4294967295) and dotted notation (1-65535.0-65535) as per RFC 5396. + Mutually exclusive with useLastAS. + x-kubernetes-int-or-string: true + useLastAS: + description: |- + UseLastAS prepends the last AS number in the existing AS path the specified number of times. + Mutually exclusive with asNumber. + format: int32 + maximum: 10 + minimum: 1 + type: integer + type: object + x-kubernetes-validations: + - message: exactly one of asNumber or useLastAS + must be specified + rule: has(self.asNumber) != has(self.useLastAS) + replace: + description: Replace configures replacement of AS + numbers in the AS path. + properties: + asNumber: + anyOf: + - type: integer + - type: string + description: |- + ASNumber targets a specific AS number in the path for replacement. + Supports both plain format (1-4294967295) and dotted notation (1-65535.0-65535) as per RFC 5396. + Mutually exclusive with privateAS. + x-kubernetes-int-or-string: true + privateAS: + description: |- + PrivateAS, when set to true, targets all private AS numbers in the path for replacement. + Mutually exclusive with asNumber. + type: boolean + replacement: + anyOf: + - type: integer + - type: string + description: |- + Replacement is the AS number to substitute in place of matched AS numbers. + Supports both plain format (1-4294967295) and dotted notation (1-65535.0-65535) as per RFC 5396. + x-kubernetes-int-or-string: true + required: + - replacement + type: object + x-kubernetes-validations: + - message: exactly one of privateAS or asNumber + must be specified + rule: has(self.privateAS) != has(self.asNumber) + type: object + x-kubernetes-validations: + - message: at least one AS path action must be specified + rule: has(self.prepend) || has(self.replace) || has(self.asNumber) setCommunity: description: SetCommunity configures BGP standard community attributes. @@ -174,6 +250,7 @@ spec: x-kubernetes-validations: - message: at least one BGP action must be specified rule: has(self.setCommunity) || has(self.setExtCommunity) + || has(self.setASPath) routeDisposition: description: RouteDisposition specifies whether to accept or reject the route. diff --git a/config/crd/bases/networking.metal.ironcore.dev_routingpolicies.yaml b/config/crd/bases/networking.metal.ironcore.dev_routingpolicies.yaml index 608bc64c..cf3212ad 100644 --- a/config/crd/bases/networking.metal.ironcore.dev_routingpolicies.yaml +++ b/config/crd/bases/networking.metal.ironcore.dev_routingpolicies.yaml @@ -135,6 +135,82 @@ spec: BgpActions specifies BGP-specific actions to apply when the route is accepted. Only applicable when RouteDisposition is AcceptRoute. properties: + setASPath: + description: |- + SetASPath configures modifications to the BGP AS path attribute. + Not all providers may support this action. + properties: + asNumber: + anyOf: + - type: integer + - type: string + description: |- + ASNumber sets the AS path to the specified AS number. + Supports both plain format (1-4294967295) and dotted notation (1-65535.0-65535) as per RFC 5396. + x-kubernetes-int-or-string: true + prepend: + description: Prepend configures prepending to the + AS path. + properties: + asNumber: + anyOf: + - type: integer + - type: string + description: |- + ASNumber is the autonomous system number to prepend to the AS path. + Supports both plain format (1-4294967295) and dotted notation (1-65535.0-65535) as per RFC 5396. + Mutually exclusive with useLastAS. + x-kubernetes-int-or-string: true + useLastAS: + description: |- + UseLastAS prepends the last AS number in the existing AS path the specified number of times. + Mutually exclusive with asNumber. + format: int32 + maximum: 10 + minimum: 1 + type: integer + type: object + x-kubernetes-validations: + - message: exactly one of asNumber or useLastAS + must be specified + rule: has(self.asNumber) != has(self.useLastAS) + replace: + description: Replace configures replacement of AS + numbers in the AS path. + properties: + asNumber: + anyOf: + - type: integer + - type: string + description: |- + ASNumber targets a specific AS number in the path for replacement. + Supports both plain format (1-4294967295) and dotted notation (1-65535.0-65535) as per RFC 5396. + Mutually exclusive with privateAS. + x-kubernetes-int-or-string: true + privateAS: + description: |- + PrivateAS, when set to true, targets all private AS numbers in the path for replacement. + Mutually exclusive with asNumber. + type: boolean + replacement: + anyOf: + - type: integer + - type: string + description: |- + Replacement is the AS number to substitute in place of matched AS numbers. + Supports both plain format (1-4294967295) and dotted notation (1-65535.0-65535) as per RFC 5396. + x-kubernetes-int-or-string: true + required: + - replacement + type: object + x-kubernetes-validations: + - message: exactly one of privateAS or asNumber + must be specified + rule: has(self.privateAS) != has(self.asNumber) + type: object + x-kubernetes-validations: + - message: at least one AS path action must be specified + rule: has(self.prepend) || has(self.replace) || has(self.asNumber) setCommunity: description: SetCommunity configures BGP standard community attributes. @@ -171,6 +247,7 @@ spec: x-kubernetes-validations: - message: at least one BGP action must be specified rule: has(self.setCommunity) || has(self.setExtCommunity) + || has(self.setASPath) routeDisposition: description: RouteDisposition specifies whether to accept or reject the route. diff --git a/config/samples/v1alpha1_routingpolicy.yaml b/config/samples/v1alpha1_routingpolicy.yaml index 567891c4..174c139d 100644 --- a/config/samples/v1alpha1_routingpolicy.yaml +++ b/config/samples/v1alpha1_routingpolicy.yaml @@ -44,6 +44,34 @@ spec: name: blocked-networks actions: routeDisposition: RejectRoute + - sequence: 40 + actions: + routeDisposition: AcceptRoute + bgpActions: + setASPath: + prepend: + asNumber: 65000 + - sequence: 50 + actions: + routeDisposition: AcceptRoute + bgpActions: + setASPath: + prepend: + useLastAS: 5 + - sequence: 60 + actions: + routeDisposition: AcceptRoute + bgpActions: + setASPath: + replace: + privateAS: true + replacement: 65100 + - sequence: 70 + actions: + routeDisposition: AcceptRoute + bgpActions: + setASPath: + asNumber: 65000 - sequence: 100 actions: routeDisposition: AcceptRoute diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md index 06e61d0d..ce43405f 100644 --- a/docs/api-reference/index.md +++ b/docs/api-reference/index.md @@ -649,6 +649,7 @@ _Appears in:_ | --- | --- | --- | --- | | `setCommunity` _[SetCommunityAction](#setcommunityaction)_ | SetCommunity configures BGP standard community attributes. | | Optional: \{\}
| | `setExtCommunity` _[SetExtCommunityAction](#setextcommunityaction)_ | SetExtCommunity configures BGP extended community attributes. | | Optional: \{\}
| +| `setASPath` _[SetASPathAction](#setaspathaction)_ | SetASPath configures modifications to the BGP AS path attribute.
Not all providers may support this action. | | Optional: \{\}
| #### Certificate @@ -920,7 +921,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `paused` _boolean_ | Paused can be used to prevent controllers from processing the Device and its associated objects. | | Optional: \{\}
| +| `paused` _boolean_ | Paused can be used to prevent controllers from processing the Device and its associated objects. | false | Optional: \{\}
| | `endpoint` _[Endpoint](#endpoint)_ | Endpoint contains the connection information for the device. | | Required: \{\}
| | `provisioning` _[Provisioning](#provisioning)_ | Provisioning is an optional configuration for the device provisioning process.
It can be used to provide initial configuration templates or scripts that are applied during the device provisioning. | | Optional: \{\}
| @@ -2602,6 +2603,61 @@ _Appears in:_ | `namespace` _string_ | Namespace defines the space within which the secret name must be unique.
If omitted, the namespace of the object being reconciled will be used. | | MaxLength: 63
MinLength: 1
Optional: \{\}
| +#### SetASPathAction + + + +SetASPathAction defines actions to modify the BGP AS path attribute. + + + +_Appears in:_ +- [BgpActions](#bgpactions) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `prepend` _[SetASPathPrepend](#setaspathprepend)_ | Prepend configures prepending to the AS path. | | Optional: \{\}
| +| `replace` _[SetASPathReplace](#setaspathreplace)_ | Replace configures replacement of AS numbers in the AS path. | | Optional: \{\}
| +| `asNumber` _[IntOrString](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#intorstring-intstr-util)_ | ASNumber sets the AS path to the specified AS number.
Supports both plain format (1-4294967295) and dotted notation (1-65535.0-65535) as per RFC 5396. | | Optional: \{\}
| + + +#### SetASPathPrepend + + + +SetASPathPrepend configures prepending to the BGP AS path. +Either asNumber or useLastAS must be specified, but not both. + + + +_Appears in:_ +- [SetASPathAction](#setaspathaction) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `asNumber` _[IntOrString](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#intorstring-intstr-util)_ | ASNumber is the autonomous system number to prepend to the AS path.
Supports both plain format (1-4294967295) and dotted notation (1-65535.0-65535) as per RFC 5396.
Mutually exclusive with useLastAS. | | Optional: \{\}
| +| `useLastAS` _integer_ | UseLastAS prepends the last AS number in the existing AS path the specified number of times.
Mutually exclusive with asNumber. | | Maximum: 10
Minimum: 1
Optional: \{\}
| + + +#### SetASPathReplace + + + +SetASPathReplace configures replacement of AS numbers in the BGP AS path. +Either privateAS or asNumber must be specified, but not both. + + + +_Appears in:_ +- [SetASPathAction](#setaspathaction) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `privateAS` _boolean_ | PrivateAS, when set to true, targets all private AS numbers in the path for replacement.
Mutually exclusive with asNumber. | | Optional: \{\}
| +| `asNumber` _[IntOrString](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#intorstring-intstr-util)_ | ASNumber targets a specific AS number in the path for replacement.
Supports both plain format (1-4294967295) and dotted notation (1-65535.0-65535) as per RFC 5396.
Mutually exclusive with privateAS. | | Optional: \{\}
| +| `replacement` _[IntOrString](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#intorstring-intstr-util)_ | Replacement is the AS number to substitute in place of matched AS numbers.
Supports both plain format (1-4294967295) and dotted notation (1-65535.0-65535) as per RFC 5396. | | Required: \{\}
| + + #### SetCommunityAction diff --git a/internal/controller/core/routingpolicy_controller_test.go b/internal/controller/core/routingpolicy_controller_test.go index e138bbb7..b7c99ac7 100644 --- a/internal/controller/core/routingpolicy_controller_test.go +++ b/internal/controller/core/routingpolicy_controller_test.go @@ -10,6 +10,7 @@ import ( . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -233,6 +234,113 @@ var _ = Describe("RoutingPolicy Controller", func() { }).Should(Succeed()) }) + It("Should successfully reconcile a RoutingPolicy with AS path actions", func() { + By("Creating a RoutingPolicy with various AS path actions") + asn65000 := intstr.FromInt32(65000) + asn65001 := intstr.FromInt32(65001) + asn65100 := intstr.FromInt32(65100) + asnDotted := intstr.FromString("1.100") + useLastAS := int32(5) + + rp := &v1alpha1.RoutingPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.RoutingPolicySpec{ + DeviceRef: v1alpha1.LocalObjectReference{Name: name}, + Name: name, + Statements: []v1alpha1.PolicyStatement{ + { + // Prepend a specific ASN + Sequence: 10, + Actions: v1alpha1.PolicyActions{ + RouteDisposition: v1alpha1.AcceptRoute, + BgpActions: &v1alpha1.BgpActions{ + SetASPath: &v1alpha1.SetASPathAction{ + Prepend: &v1alpha1.SetASPathPrepend{ + ASNumber: &asn65000, + }, + }, + }, + }, + }, + { + // Prepend using last-as + Sequence: 20, + Actions: v1alpha1.PolicyActions{ + RouteDisposition: v1alpha1.AcceptRoute, + BgpActions: &v1alpha1.BgpActions{ + SetASPath: &v1alpha1.SetASPathAction{ + Prepend: &v1alpha1.SetASPathPrepend{ + UseLastAS: &useLastAS, + }, + }, + }, + }, + }, + { + // Replace private AS with a specific ASN + Sequence: 30, + Actions: v1alpha1.PolicyActions{ + RouteDisposition: v1alpha1.AcceptRoute, + BgpActions: &v1alpha1.BgpActions{ + SetASPath: &v1alpha1.SetASPathAction{ + Replace: &v1alpha1.SetASPathReplace{ + PrivateAS: true, + Replacement: asn65100, + }, + }, + }, + }, + }, + { + // Replace a specific ASN with another (using dotted notation) + Sequence: 40, + Actions: v1alpha1.PolicyActions{ + RouteDisposition: v1alpha1.AcceptRoute, + BgpActions: &v1alpha1.BgpActions{ + SetASPath: &v1alpha1.SetASPathAction{ + Replace: &v1alpha1.SetASPathReplace{ + ASNumber: &asn65001, + Replacement: asnDotted, + }, + }, + }, + }, + }, + { + // Set explicit AS path + Sequence: 50, + Actions: v1alpha1.PolicyActions{ + RouteDisposition: v1alpha1.AcceptRoute, + BgpActions: &v1alpha1.BgpActions{ + SetASPath: &v1alpha1.SetASPathAction{ + ASNumber: &asn65000, + }, + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, rp)).To(Succeed()) + + By("Verifying the controller sets successful status conditions") + Eventually(func(g Gomega) { + resource := &v1alpha1.RoutingPolicy{} + g.Expect(k8sClient.Get(ctx, key, resource)).To(Succeed()) + g.Expect(resource.Status.Conditions).To(HaveLen(1)) + g.Expect(resource.Status.Conditions[0].Type).To(Equal(v1alpha1.ReadyCondition)) + g.Expect(resource.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue)) + }).Should(Succeed()) + + By("Verifying the RoutingPolicy is configured in the provider") + Eventually(func(g Gomega) { + g.Expect(testProvider.RoutingPolicies.Has(name)).To(BeTrue(), "Provider should have RoutingPolicy configured") + }).Should(Succeed()) + }) + It("Should handle PrefixSet on different device", func() { By("Creating a PrefixSet on a different device") ps := &v1alpha1.PrefixSet{ diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index c12fe772..1ef0ae93 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -1768,6 +1768,32 @@ func (p *Provider) EnsureRoutingPolicy(ctx context.Context, req *provider.Ensure return err } } + if stmt.Actions.BgpActions.SetASPath != nil { + asp := stmt.Actions.BgpActions.SetASPath + if asp.Prepend != nil { + if asp.Prepend.ASNumber != nil { + e.SetASPathPrependItems.AS = asp.Prepend.ASNumber.String() + } + if asp.Prepend.UseLastAS != nil { + e.SetASPathLastASItems.LastAS = *asp.Prepend.UseLastAS + } + } + if asp.Replace != nil { + if asp.Replace.PrivateAS { + e.SetASPathReplaceItems.MatchPrivateAS = true + e.SetASPathReplaceItems.ReplaceAsn = asp.Replace.Replacement.String() + e.SetASPathReplaceItems.ReplaceType = "asn" + } else if asp.Replace.ASNumber != nil { + e.SetASPathReplaceItems.MatchAsnList = asp.Replace.ASNumber.String() + e.SetASPathReplaceItems.MatchPrivateAS = false + e.SetASPathReplaceItems.ReplaceAsn = asp.Replace.Replacement.String() + e.SetASPathReplaceItems.ReplaceType = "asn" + } + } + if asp.ASNumber != nil { + e.SetASPathItems.AsnList = asp.ASNumber.String() + } + } } rm.EntItems.EntryList.Set(e) diff --git a/internal/provider/cisco/nxos/routemap.go b/internal/provider/cisco/nxos/routemap.go index 04f1e4d0..70b14080 100644 --- a/internal/provider/cisco/nxos/routemap.go +++ b/internal/provider/cisco/nxos/routemap.go @@ -42,6 +42,21 @@ type RouteMapEntry struct { RsRtDstAttList gnmiext.List[string, *RsRtDstAtt] `json:"RsRtDstAtt-list,omitzero"` } `json:"rsrtDstAtt-items,omitzero"` } `json:"mrtdst-items,omitzero"` + SetASPathPrependItems struct { + AS string `json:"as"` + } `json:"setaspathprepend-items,omitzero"` + SetASPathLastASItems struct { + LastAS int32 `json:"lastas"` + } `json:"setaspathlastas-items,omitzero"` + SetASPathReplaceItems struct { + MatchAsnList string `json:"matchAsnList,omitempty"` + MatchPrivateAS bool `json:"matchPrivateAs"` + ReplaceAsn string `json:"replaceAsn"` + ReplaceType string `json:"replaceType"` + } `json:"setaspathreplace-items,omitzero"` + SetASPathItems struct { + AsnList string `json:"asnList"` + } `json:"setaspath-items,omitzero"` } func (e *RouteMapEntry) Key() int32 { return e.Order } diff --git a/internal/provider/cisco/nxos/routemap_test.go b/internal/provider/cisco/nxos/routemap_test.go index fdfc7610..f743fa27 100644 --- a/internal/provider/cisco/nxos/routemap_test.go +++ b/internal/provider/cisco/nxos/routemap_test.go @@ -16,4 +16,55 @@ func init() { rm.Name = "RM-REDIST" rm.EntItems.EntryList.Set(e) Register("route_map", rm) + + prependEntry := &RouteMapEntry{} + prependEntry.Order = 10 + prependEntry.Action = ActionPermit + prependEntry.SetASPathPrependItems.AS = "65000" + + prependRM := &RouteMap{} + prependRM.Name = "RM-ASPATH-PREPEND" + prependRM.EntItems.EntryList.Set(prependEntry) + Register("route_map_aspath_prepend", prependRM) + + lastASEntry := &RouteMapEntry{} + lastASEntry.Order = 10 + lastASEntry.Action = ActionPermit + lastASEntry.SetASPathLastASItems.LastAS = 10 + + lastASRM := &RouteMap{} + lastASRM.Name = "RM-ASPATH-LASTAS" + lastASRM.EntItems.EntryList.Set(lastASEntry) + Register("route_map_aspath_lastas", lastASRM) + + replaceEntry1 := &RouteMapEntry{} + replaceEntry1.Order = 10 + replaceEntry1.Action = ActionPermit + replaceEntry1.SetASPathReplaceItems.MatchPrivateAS = true + replaceEntry1.SetASPathReplaceItems.ReplaceAsn = "65000" + replaceEntry1.SetASPathReplaceItems.ReplaceType = "asn" + + replaceEntry2 := &RouteMapEntry{} + replaceEntry2.Order = 20 + replaceEntry2.Action = ActionPermit + replaceEntry2.SetASPathReplaceItems.MatchAsnList = "65001" + replaceEntry2.SetASPathReplaceItems.MatchPrivateAS = false + replaceEntry2.SetASPathReplaceItems.ReplaceAsn = "65100" + replaceEntry2.SetASPathReplaceItems.ReplaceType = "asn" + + replaceRM := &RouteMap{} + replaceRM.Name = "RM-ASPATH-REPLACE" + replaceRM.EntItems.EntryList.Set(replaceEntry1) + replaceRM.EntItems.EntryList.Set(replaceEntry2) + Register("route_map_aspath_replace", replaceRM) + + setEntry := &RouteMapEntry{} + setEntry.Order = 10 + setEntry.Action = ActionPermit + setEntry.SetASPathItems.AsnList = "65000" + + setRM := &RouteMap{} + setRM.Name = "RM-ASPATH-SET" + setRM.EntItems.EntryList.Set(setEntry) + Register("route_map_aspath_set", setRM) } diff --git a/internal/provider/cisco/nxos/testdata/route_map_aspath_lastas.json b/internal/provider/cisco/nxos/testdata/route_map_aspath_lastas.json new file mode 100644 index 00000000..4da78be9 --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/route_map_aspath_lastas.json @@ -0,0 +1,22 @@ +{ + "rpm-items": { + "rtmap-items": { + "Rule-list": [ + { + "name": "RM-ASPATH-LASTAS", + "ent-items": { + "Entry-list": [ + { + "action": "permit", + "order": 10, + "setaspathlastas-items": { + "lastas": 10 + } + } + ] + } + } + ] + } + } +} diff --git a/internal/provider/cisco/nxos/testdata/route_map_aspath_lastas.json.txt b/internal/provider/cisco/nxos/testdata/route_map_aspath_lastas.json.txt new file mode 100644 index 00000000..1c044434 --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/route_map_aspath_lastas.json.txt @@ -0,0 +1,2 @@ +route-map RM-ASPATH-LASTAS permit 10 + set as-path prepend last-as 10 diff --git a/internal/provider/cisco/nxos/testdata/route_map_aspath_prepend.json b/internal/provider/cisco/nxos/testdata/route_map_aspath_prepend.json new file mode 100644 index 00000000..3b4ed7ab --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/route_map_aspath_prepend.json @@ -0,0 +1,22 @@ +{ + "rpm-items": { + "rtmap-items": { + "Rule-list": [ + { + "name": "RM-ASPATH-PREPEND", + "ent-items": { + "Entry-list": [ + { + "action": "permit", + "order": 10, + "setaspathprepend-items": { + "as": "65000" + } + } + ] + } + } + ] + } + } +} diff --git a/internal/provider/cisco/nxos/testdata/route_map_aspath_prepend.json.txt b/internal/provider/cisco/nxos/testdata/route_map_aspath_prepend.json.txt new file mode 100644 index 00000000..1b6a6151 --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/route_map_aspath_prepend.json.txt @@ -0,0 +1,2 @@ +route-map RM-ASPATH-PREPEND permit 10 + set as-path prepend 65000 diff --git a/internal/provider/cisco/nxos/testdata/route_map_aspath_replace.json b/internal/provider/cisco/nxos/testdata/route_map_aspath_replace.json new file mode 100644 index 00000000..68379e9e --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/route_map_aspath_replace.json @@ -0,0 +1,34 @@ +{ + "rpm-items": { + "rtmap-items": { + "Rule-list": [ + { + "name": "RM-ASPATH-REPLACE", + "ent-items": { + "Entry-list": [ + { + "action": "permit", + "order": 10, + "setaspathreplace-items": { + "matchPrivateAs": true, + "replaceAsn": "65000", + "replaceType": "asn" + } + }, + { + "action": "permit", + "order": 20, + "setaspathreplace-items": { + "matchAsnList": "65001", + "matchPrivateAs": false, + "replaceAsn": "65100", + "replaceType": "asn" + } + } + ] + } + } + ] + } + } +} diff --git a/internal/provider/cisco/nxos/testdata/route_map_aspath_replace.json.txt b/internal/provider/cisco/nxos/testdata/route_map_aspath_replace.json.txt new file mode 100644 index 00000000..9615a351 --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/route_map_aspath_replace.json.txt @@ -0,0 +1,4 @@ +route-map RM-ASPATH-REPLACE permit 10 + set as-path replace private-as with 65000 +route-map RM-ASPATH-REPLACE permit 20 + set as-path replace 65001 with 65100 diff --git a/internal/provider/cisco/nxos/testdata/route_map_aspath_set.json b/internal/provider/cisco/nxos/testdata/route_map_aspath_set.json new file mode 100644 index 00000000..2920076c --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/route_map_aspath_set.json @@ -0,0 +1,22 @@ +{ + "rpm-items": { + "rtmap-items": { + "Rule-list": [ + { + "name": "RM-ASPATH-SET", + "ent-items": { + "Entry-list": [ + { + "action": "permit", + "order": 10, + "setaspath-items": { + "asnList": "65000" + } + } + ] + } + } + ] + } + } +} diff --git a/internal/provider/cisco/nxos/testdata/route_map_aspath_set.json.txt b/internal/provider/cisco/nxos/testdata/route_map_aspath_set.json.txt new file mode 100644 index 00000000..7d11f01e --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/route_map_aspath_set.json.txt @@ -0,0 +1,2 @@ +route-map RM-ASPATH-SET permit 10 + set as-path 65000