Skip to content
Open
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
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Note: This requires a running NetBox instance that you can use (e.g. <https://de
- Prepare NetBox (based on the demo NetBox instance):
- Open <https://demo.netbox.dev/plugins/demo/login/> and create any user
- Open <https://demo.netbox.dev/user/api-tokens/> and create a token "0123456789abcdef0123456789abcdef01234567" with default settings
- Open <https://demo.netbox.dev/extras/custom-fields/add/> and create a custom field called "netboxOperatorRestorationHash" for Object types "IPAM > IP Address" and "IPAM > Prefix"
- Open <https://demo.netbox.dev/extras/custom-fields/add/> and create a custom field called "netboxOperatorRestorationHash" for Object types "IPAM > IP Address", "IPAM > Prefix", "IPAM > IP Range" and "VPN > L2VPN"
- Open a new terminal window and export the following environment variables:
```bash
export NETBOX_HOST="demo.netbox.dev"
Expand All @@ -57,7 +57,7 @@ Note: This requires a running NetBox instance that you can use (e.g. <https://de

## Testing NetBox Operator using samples

In the folder `config/samples/` you can find example manifests to create IpAddress, IpAddressClaim, Prefix and PrefixClaim resources. Apply them to the cluster with `kubectl apply -f <file-name>` and use your favorite Kubernetes tools to display.
In the folder `config/samples/` you can find example manifests to create IpAddress, IpAddressClaim, Prefix, PrefixClaim, IpRange, IpRangeClaim, L2VPN and L2VPNClaim resources. Apply them to the cluster with `kubectl apply -f <file-name>` and use your favorite Kubernetes tools to display.

Example of assigning a Prefix using PrefixClaim:

Expand Down
16 changes: 16 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,20 @@ resources:
kind: IpRange
path: github.com/netbox-community/netbox-operator/api/v1
version: v1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: netbox.dev
kind: L2VPNClaim
path: github.com/netbox-community/netbox-operator/api/v1
version: v1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: netbox.dev
kind: L2VPN
path: github.com/netbox-community/netbox-operator/api/v1
version: v1
version: "3"
29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Disclaimer:** This project is currently under development and may change rapidly, including breaking changes. Use with caution in production environments.

NetBox Operator extends the Kubernetes API by allowing users to manage NetBox resources – such as IP addresses and prefixes – directly through Kubernetes. This integration brings Kubernetes-native features like reconciliation, ensuring that network configurations are maintained automatically, thereby improving both efficiency and reliability.
NetBox Operator extends the Kubernetes API by allowing users to manage NetBox resources – such as IP addresses, prefixes, and L2VPNs – directly through Kubernetes. This integration brings Kubernetes-native features like reconciliation, ensuring that network configurations are maintained automatically, thereby improving both efficiency and reliability.

## The Claim Model
The NetBox Operator implements a "Claim Model" which is also used in the Kubernetes PersistentVolumeClaims (PVCs).
Expand Down Expand Up @@ -53,7 +53,7 @@ To optionally access the NetBox UI:

## Testing NetBox Operator using samples

In the folder `config/samples/` you can find example manifests to create IpAddress, IpAddressClaim, Prefix, and PrefixClaim resources. Apply them to the cluster with `kubectl apply -f <file-name>` and use your favorite Kubernetes tools to display.
In the folder `config/samples/` you can find example manifests to create IpAddress, IpAddressClaim, Prefix, PrefixClaim, L2VPN, and L2VPNClaim resources. Apply them to the cluster with `kubectl apply -f <file-name>` and use your favorite Kubernetes tools to display.

Example of assigning a Prefix using PrefixClaim:

Expand All @@ -74,6 +74,25 @@ for i in {001..100}; do
done
```

# L2VPN Management

NetBox Operator supports managing [L2VPNs](https://github.com/netbox-community/netbox/blob/main/docs/models/vpn/l2vpn.md) (Layer 2 VPNs, e.g. to track VXLAN VNIs) through two custom resources:

- **L2VPN**: Represents a single L2VPN in NetBox. Similar to an IpAddress, it manages the lifecycle of a specific L2VPN (`name`, `type`, `identifier`).
- **L2VPNClaim**: Claims a VNI for an L2VPN, either an exact `identifier` or the next free one from an `identifierRangeStart`/`identifierRangeEnd` range. Similar to IpAddressClaim, it creates a child L2VPN CR with the assigned identifier.

Only VXLAN-based L2VPN types (`vxlan`, `vxlan-evpn`) are supported, since those are the ones that carry a VNI (4000-16777215) in their identifier.

## Example: Claiming an L2VPN

1. Apply an L2VPNClaim: `kubectl apply -f config/samples/netbox_v1_l2vpnclaim.yaml`
2. Wait for ready condition: `kubectl wait l2vpnclaim l2vpnclaim-sample --for=condition=Ready`
3. List L2VPNClaim and L2VPN resources: `kubectl get l2vc,l2v`

`identifier` and `identifierRangeStart`/`identifierRangeEnd` are mutually exclusive on `L2VPNClaim` — set exactly one form. When a range is used, the operator picks the next free VNI in NetBox from that range.

Restoration (via `preserveInNetbox: true`) works the same way as for IP Addresses and Prefixes — the L2VPN is preserved in NetBox upon CR deletion and can be reclaimed when the L2VPNClaim is re-created.

# Mixed usage of Prefixes

Note that NetBox does handle the Address management of Prefixes separately from IP Ranges and IP Addresses. This is important to know when you plan to use the same NetBox Prefix as a parentPrefix for your IpAddressClaims, IpRangeClaims and PrefixClaims.
Expand All @@ -97,13 +116,13 @@ The same applies if you use parentPrefixSelector with PrefixClaims. The above ex

In the case that the cluster containing the NetBox Custom Resources managed by this NetBox Operator is not backed up (e.g. using Velero), we need to be able to restore some information from NetBox. This includes two mechanisms implemented in this NetBox Operator:

- `IpAddressClaim` and `PrefixClaim` have the flag `preserveInNetbox` in their spec. If set to true, the NetBox Operator will not delete the assigned IP Address/Prefix in NetBox when the Kubernetes Custom Resource is deleted
- In NetBox, a custom field (by default `netboxOperatorRestorationHash`) is used to identify an IP Address/Prefix based on data from the IpAddressClaim/PrefixClaim resource
- `IpAddressClaim`, `PrefixClaim`, and `L2VPNClaim` have the flag `preserveInNetbox` in their spec. If set to true, the NetBox Operator will not delete the assigned IP Address/Prefix/L2VPN in NetBox when the Kubernetes Custom Resource is deleted
- In NetBox, a custom field (by default `netboxOperatorRestorationHash`) is used to identify an IP Address/Prefix/L2VPN based on data from the IpAddressClaim/PrefixClaim/L2VPNClaim resource

Use Cases for this Restoration:

- Disaster Recovery: In case the cluster is lost, IP Addresses can be restored with the IPAddressClaim only
- Sticky IPs: Some services do not handle changes to IPs well. This ensures the IP/Prefix assigned to a Custom Resource is always the same.
- Sticky IPs/VNIs: Some services do not handle changes to IPs or VNIs well. This ensures the IP/Prefix/VNI assigned to a Custom Resource is always the same.

# `ParentPrefixSelector` in `PrefixClaim`

Expand Down
167 changes: 167 additions & 0 deletions api/v1/l2vpn_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
Copyright 2026 Swisscom (Schweiz) AG.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// L2VPNSpec defines the desired state of L2VPN
type L2VPNSpec struct {
// The name of the L2VPN in NetBox
// Field is immutable, required
//+kubebuilder:validation:Required
//+kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'name' is immutable"
Name string `json:"name"`

// The NetBox L2VPN type. Only VXLAN-based types are supported, since those
// are the ones that carry a VNI in their identifier.
// Field is immutable, required
//+kubebuilder:validation:Required
//+kubebuilder:validation:Enum=vxlan;vxlan-evpn
//+kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'type' is immutable"
Type string `json:"type"`

// The VNI to be assigned to this L2VPN in NetBox
// Field is immutable, required, range from 4000-16777215
//+kubebuilder:validation:Required
//+kubebuilder:validation:Minimum=4000
//+kubebuilder:validation:Maximum=16777215
//+kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'identifier' is immutable"
Identifier int64 `json:"identifier"`

// The NetBox Tenant to be assigned to this resource in NetBox. Use the `name` value instead of the `slug` value
// Field is immutable, not required
// Example: "Initech" or "Cyberdyne Systems"
//+kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'tenant' is immutable"
Tenant string `json:"tenant,omitempty"`

// The NetBox Custom Fields that should be added to the resource in NetBox.
// Note that currently only Text Type is supported (GitHub #129)
// More info on NetBox Custom Fields:
// https://github.com/netbox-community/netbox/blob/main/docs/customization/custom-fields.md
// Field is mutable, not required
// Example:
// customfield1: "Production"
// customfield2: "This is a string"
CustomFields map[string]string `json:"customFields,omitempty"`

// Comment that should be added to the resource in NetBox
// Field is mutable, not required
Comments string `json:"comments,omitempty"`

// Description that should be added to the resource in NetBox
// Field is mutable, not required
Description string `json:"description,omitempty"`

// Defines whether the Resource should be preserved in NetBox when the
// Kubernetes Resource is deleted.
// - When set to true, the resource will not be deleted but preserved in
// NetBox upon CR deletion
// - When set to false, the resource will be cleaned up in NetBox
// upon CR deletion
// Setting preserveInNetbox to true is mandatory if the user wants to restore
// resources from NetBox (e.g. Sticky VNIs even if resources are deleted and
// recreated in Kubernetes)
// Field is mutable, not required
PreserveInNetbox bool `json:"preserveInNetbox,omitempty"`
}

// L2VPNStatus defines the observed state of L2VPN
type L2VPNStatus struct {
// The ID of the resource in NetBox
L2VPNId int64 `json:"id,omitempty"`

// The slug generated for the resource in NetBox
Slug string `json:"slug,omitempty"`

// Last updated, corresponds to the 'last_updated' returned by NetBox when NetBox Operator updates a resource in NetBox.
// Format: date-time
LastUpdated metav1.Time `json:"lastUpdated,omitempty"`

// The URL to the resource in the NetBox UI. Note that the base of this
// URL depends on the runtime config of NetBox Operator
L2VPNUrl string `json:"url,omitempty"`

// Conditions represent the latest available observations of an object's state
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:storageversion
//+kubebuilder:printcolumn:name="Name",type=string,JSONPath=`.spec.name`
//+kubebuilder:printcolumn:name="Type",type=string,JSONPath=`.spec.type`
//+kubebuilder:printcolumn:name="Identifier",type=integer,JSONPath=`.spec.identifier`
//+kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
//+kubebuilder:printcolumn:name="ID",type=string,JSONPath=`.status.id`
//+kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
//+kubebuilder:resource:shortName=l2v

// L2VPN allows to create a NetBox L2VPN, e.g. to track a VXLAN VNI. More info about NetBox L2VPNs: https://github.com/netbox-community/netbox/blob/main/docs/models/vpn/l2vpn.md
type L2VPN struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec L2VPNSpec `json:"spec,omitempty"`
Status L2VPNStatus `json:"status,omitempty"`
}

func (l *L2VPN) Conditions() *[]metav1.Condition {
return &l.Status.Conditions
}

//+kubebuilder:object:root=true

// L2VPNList contains a list of L2VPN
type L2VPNList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []L2VPN `json:"items"`
}

func init() {
register(&L2VPN{}, &L2VPNList{})
}

var ConditionL2VPNReadyTrue = metav1.Condition{
Type: "Ready",
Status: "True",
Reason: "L2VPNReservedInNetbox",
Message: "L2VPN was reserved/updated in NetBox",
}

var ConditionL2VPNReadyFalse = metav1.Condition{
Type: "Ready",
Status: "False",
Reason: "FailedToReserveL2VPNInNetbox",
Message: "Failed to reserve L2VPN in NetBox",
}

var ConditionL2VPNReadyFalseDeletionInProgress = metav1.Condition{
Type: "Ready",
Status: "False",
Reason: "DeletionInProgress",
Message: "L2VPN deletion in progress",
}

var ConditionL2VPNReadyFalseDeletionFailed = metav1.Condition{
Type: "Ready",
Status: "False",
Reason: "FailedToDeleteL2VPNInNetbox",
Message: "Failed to delete L2VPN in NetBox",
}
Loading
Loading