Skip to content

Commit

Permalink
Implement dynamic parent selector
Browse files Browse the repository at this point in the history
  • Loading branch information
henrybear327 committed Oct 8, 2024
1 parent 5a04ec4 commit 97c512d
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 30 deletions.
6 changes: 2 additions & 4 deletions api/v1/prefixclaim_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type PrefixClaimSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file

/* parentPrefixSelector will take precedence over parentPrefix */
/* parentPrefix will take precedence over parentPrefixSelector */
//+kubebuilder:validation:Format=cidr
//+kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'parentPrefix' is immutable"
ParentPrefix string `json:"parentPrefix,omitempty"`
Expand Down Expand Up @@ -61,9 +61,7 @@ type PrefixClaimStatus struct {
// Important: Run "make" to regenerate code after modifying this file
// Prefix status: container, active, reserved, deprecated

// due to the fact that we can use ParentPrefixSelector to assign parent prefixes
// we use this field to label exactly which parent prefix we are using for prefix allocation
ParentPrefix string `json:"parentPrefix,omitempty"`
ParentPrefix string `json:"parentPrefix,omitempty"` // Due to the fact that we can use ParentPrefixSelector to assign parent prefixes, we use this field to store exactly which parent prefix we are using for prefix allocation
Prefix string `json:"prefix,omitempty"`
PrefixName string `json:"prefixName,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
Expand Down
5 changes: 1 addition & 4 deletions config/crd/bases/netbox.dev_prefixclaims.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ spec:
description:
type: string
parentPrefix:
description: parentPrefixSelector will take precedence over parentPrefix
description: parentPrefix will take precedence over parentPrefixSelector
format: cidr
type: string
x-kubernetes-validations:
Expand Down Expand Up @@ -170,9 +170,6 @@ spec:
type: object
type: array
parentPrefix:
description: |-
due to the fact that we can use ParentPrefixSelector to assign parent prefixes
we use this field to label exactly which parent prefix we are using for prefix allocation
type: string
prefix:
type: string
Expand Down
23 changes: 23 additions & 0 deletions config/samples/netbox_v1_prefixclaim_customfields.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
apiVersion: netbox.dev/v1
kind: PrefixClaim
metadata:
labels:
app.kubernetes.io/name: netbox-operator
app.kubernetes.io/managed-by: kustomize
name: prefixclaim-customfields-sample
spec:
tenant: "Dunder-Mifflin, Inc."
site: "DataCenter"
description: "some description"
comments: "your comments"
preserveInNetbox: true
prefixLength: "/31"
# parentPrefix will take precedence over parentPrefixSelector
# parentPrefix: "2.0.0.0/16"
parentPrefixSelector:
environment: "Production"
ipVersion: "4"
poolName: "Pool 3"
securityZone: "Entry Zone / EZ / YEL"
site: "lss"
subnetSize: "29"
17 changes: 12 additions & 5 deletions internal/controller/prefix_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,17 @@ func (r *PrefixReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
return ctrl.Result{}, err
}

if prefixClaim.Status.ParentPrefix == "" {
// the parent prefix is not computed
logger.Info("the parent prefix is not computed")
return ctrl.Result{
Requeue: true,
}, nil
}

// get the name of the parent prefix
leaseLockerNSN := types.NamespacedName{
Name: convertCIDRToLeaseLockName(prefixClaim.Spec.ParentPrefix),
Name: convertCIDRToLeaseLockName(prefixClaim.Status.ParentPrefix),
Namespace: r.OperatorNamespace,
}
ll, err = leaselocker.NewLeaseLocker(r.RestConfig, leaseLockerNSN, req.NamespacedName.String())
Expand All @@ -147,14 +155,13 @@ func (r *PrefixReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr

// create lock
if locked := ll.TryLock(lockCtx); !locked {
logger.Info(fmt.Sprintf("failed to lock parent prefix %s", prefixClaim.Spec.ParentPrefix))
r.Recorder.Eventf(prefix, corev1.EventTypeWarning, "FailedToLockParentPrefix", "failed to lock parent prefix %s",
prefixClaim.Spec.ParentPrefix)
logger.Info(fmt.Sprintf("failed to lock parent prefix %s", prefixClaim.Status.ParentPrefix))
r.Recorder.Eventf(prefix, corev1.EventTypeWarning, "FailedToLockParentPrefix", "failed to lock parent prefix %s", prefixClaim.Status.ParentPrefix)
return ctrl.Result{
RequeueAfter: 2 * time.Second,
}, nil
}
debugLogger.Info("successfully locked parent prefix %s", prefixClaim.Spec.ParentPrefix)
debugLogger.Info("successfully locked parent prefix %s", prefixClaim.Status.ParentPrefix)
}

/* 2. reserve or update Prefix in netbox */
Expand Down
69 changes: 55 additions & 14 deletions internal/controller/prefixclaim_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,49 @@ func (r *PrefixClaimReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{}, nil
}

/* 1. check if the matching Prefix object exists */
/* 1. compute and assign the parent prefix if required */
// The current design will use prefixClaim.Status.ParentPrefix for storing the computed parent prefix, and as the source of truth for future parent prefix references
if prefixClaim.Status.ParentPrefix == "" /* parent prefix not yet computed/assigned */ {
if prefixClaim.Spec.ParentPrefix != "" {
// ParentPrefix takes precedence over ParentPrefixSelector
prefixClaim.Status.ParentPrefix = prefixClaim.Spec.ParentPrefix
} else if len(prefixClaim.Spec.ParentPrefixSelector) > 0 {
// The main idea is that we select one of the available parent prefixes as the ParentPrefix for all subsequent computation
//
// The existing algorithm for prefix allocation within a ParentPrefix remains unchanged

// fetch available prefixes from netbox
parentPrefixCandidates, err := r.NetboxClient.GetAvailablePrefixByParentPrefixSelector(prefixClaim.Spec.ParentPrefixSelector, prefixClaim.Spec.Tenant, prefixClaim.Spec.PrefixLength)
if err != nil || len(parentPrefixCandidates) == 0 {
logger.Info(fmt.Sprintf("no parent prefix can be obtained with the query conditions set in ParentPrefixSelector, err = %v, number of candidates = %v", err, len(parentPrefixCandidates)))
if err := r.SetConditionAndCreateEvent(ctx, prefixClaim, netboxv1.ConditionParentPrefixComputedFalse, corev1.EventTypeWarning, ""); err != nil {
return ctrl.Result{}, err
}

// we requeue as this might be a temporary exhausation
return ctrl.Result{Requeue: true}, nil
}

// set prefixClaim.Spec.ParentPrefix
// TODO(henrybear327): use best-fit algorithm to pick a parent prefix
parentPrefixCandidate := parentPrefixCandidates[0]
prefixClaim.Status.ParentPrefix = parentPrefixCandidate.Prefix
} else {
logger.Info("either ParentPrefixSelector or ParentPrefix needs to be set")
if err := r.SetConditionAndCreateEvent(ctx, prefixClaim, netboxv1.ConditionParentPrefixComputedFalse, corev1.EventTypeWarning, ""); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{Requeue: false}, nil
}

// set status, and condition field
logger.Info(fmt.Sprintf("parentPrefix is computed: %v", prefixClaim.Status.ParentPrefix))
if err := r.SetConditionAndCreateEvent(ctx, prefixClaim, netboxv1.ConditionParentPrefixComputedTrue, corev1.EventTypeWarning, ""); err != nil {
return ctrl.Result{}, err
}
}

/* 2. check if the matching Prefix object exists */
prefix := &netboxv1.Prefix{}
prefixName := prefixClaim.ObjectMeta.Name
prefixLookupKey := types.NamespacedName{
Expand All @@ -89,9 +131,9 @@ func (r *PrefixClaimReconciler) Reconcile(ctx context.Context, req ctrl.Request)
}
debugLogger.Info("the prefix was not found, will create a new prefix object now")

/* 2. check if the lease for parent prefix is available */
/* 3. check if the lease for parent prefix is available */
leaseLockerNSN := types.NamespacedName{
Name: convertCIDRToLeaseLockName(prefixClaim.Spec.ParentPrefix),
Name: convertCIDRToLeaseLockName(prefixClaim.Status.ParentPrefix),
Namespace: r.OperatorNamespace,
}
ll, err := leaselocker.NewLeaseLocker(r.RestConfig, leaseLockerNSN, req.Namespace+"/"+prefixName)
Expand All @@ -102,20 +144,19 @@ func (r *PrefixClaimReconciler) Reconcile(ctx context.Context, req ctrl.Request)
lockCtx, cancel := context.WithCancel(ctx)
defer cancel()

/* 3. try to lock the lease for the parent prefix */
/* 4. try to lock the lease for the parent prefix */
locked := ll.TryLock(lockCtx)
if !locked {
// lock for parent prefix was not available, rescheduling
logger.Info(fmt.Sprintf("failed to lock parent prefix %s", prefixClaim.Spec.ParentPrefix))
r.Recorder.Eventf(prefixClaim, corev1.EventTypeWarning, "FailedToLockParentPrefix", "failed to lock parent prefix %s",
prefixClaim.Spec.ParentPrefix)
logger.Info(fmt.Sprintf("failed to lock parent prefix %s", prefixClaim.Status.ParentPrefix))
r.Recorder.Eventf(prefixClaim, corev1.EventTypeWarning, "FailedToLockParentPrefix", "failed to lock parent prefix %s", prefixClaim.Status.ParentPrefix)
return ctrl.Result{
RequeueAfter: 2 * time.Second,
}, nil
}
debugLogger.Info(fmt.Sprintf("successfully locked parent prefix %s", prefixClaim.Spec.ParentPrefix))
debugLogger.Info(fmt.Sprintf("successfully locked parent prefix %s", prefixClaim.Status.ParentPrefix))

// 4. try to reclaim Prefix using restorationHash
// 5. try to reclaim Prefix using restorationHash
h := generatePrefixRestorationHash(prefixClaim)
prefixModel, err := r.NetboxClient.RestoreExistingPrefixByHash(h)
if err != nil {
Expand All @@ -127,12 +168,12 @@ func (r *PrefixClaimReconciler) Reconcile(ctx context.Context, req ctrl.Request)

if prefixModel == nil {
// Prefix cannot be restored from netbox
// 5.a assign new available Prefix
// 6.a assign new available Prefix

// get available Prefix under parent prefix in netbox with equal mask length
prefixModel, err = r.NetboxClient.GetAvailablePrefixByClaim(
&models.PrefixClaim{
ParentPrefix: prefixClaim.Spec.ParentPrefix,
ParentPrefix: prefixClaim.Status.ParentPrefix,
PrefixLength: prefixClaim.Spec.PrefixLength,
Metadata: &models.NetboxMetadata{
Tenant: prefixClaim.Spec.Tenant,
Expand All @@ -146,13 +187,13 @@ func (r *PrefixClaimReconciler) Reconcile(ctx context.Context, req ctrl.Request)
}
debugLogger.Info(fmt.Sprintf("prefix is not reserved in netbox, assignined new prefix: %s", prefixModel.Prefix))
} else {
// 5.b reassign reserved Prefix from netbox
// 6.b reassign reserved Prefix from netbox

// do nothing, Prefix restored
debugLogger.Info(fmt.Sprintf("reassign reserved prefix from netbox, prefix: %s", prefixModel.Prefix))
}

/* 6-1, create the Prefix object */
/* 7.a create the Prefix object */
prefixResource := generatePrefixFromPrefixClaim(prefixClaim, prefixModel.Prefix, logger)
err = controllerutil.SetControllerReference(prefixClaim, prefixResource, r.Scheme)
if err != nil {
Expand All @@ -170,7 +211,7 @@ func (r *PrefixClaimReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{}, err
}
} else { // Prefix object exists
/* 6-2. update fields of the Prefix object */
/* 7.b update fields of the Prefix object */
debugLogger.Info("update prefix resource")
if err := r.Client.Get(ctx, prefixLookupKey, prefix); err != nil {
return ctrl.Result{}, err
Expand Down
2 changes: 1 addition & 1 deletion internal/controller/prefixclaim_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func generatePrefixRestorationHash(claim *netboxv1.PrefixClaim) string {
rd := PrefixClaimRestorationData{
Namespace: claim.Namespace,
Name: claim.Name,
ParentPrefix: claim.Spec.ParentPrefix,
ParentPrefix: claim.Status.ParentPrefix,
PrefixLength: claim.Spec.PrefixLength,
Tenant: claim.Spec.Tenant,
}
Expand Down
50 changes: 48 additions & 2 deletions pkg/netbox/api/prefix_claim.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ import (
"strconv"
"strings"

"github.com/go-openapi/runtime"
"github.com/netbox-community/go-netbox/v3/netbox/client/ipam"
"github.com/netbox-community/netbox-operator/pkg/config"
"github.com/netbox-community/netbox-operator/pkg/netbox/models"
)

var (
// TODO(henrybear327): centralize errors
ErrNoPrefixMatchsSizeCriteria = errors.New("no available prefix matches size criterias")
)

func (r *NetboxClient) RestoreExistingPrefixByHash(hash string) (*models.Prefix, error) {
customPrefixSearch := newCustomFieldStringFilterOperation(config.GetOperatorConfig().NetboxRestorationHashFieldName, hash)
list, err := r.Ipam.IpamPrefixesList(ipam.NewIpamPrefixesListParams(), nil, customPrefixSearch)
Expand Down Expand Up @@ -80,6 +86,48 @@ func validatePrefixLengthOrError(prefixClaim *models.PrefixClaim, prefixFamily i
return nil
}

func (r *NetboxClient) GetAvailablePrefixByParentPrefixSelector(customFields map[string]string, tenant, prefixLength string) ([]*models.Prefix, error) {
// TODO(henrybear327): extend to support multiple custom fields
var conditions func(co *runtime.ClientOperation)
for k, v := range customFields {
conditions = newCustomFieldStringFilterOperation(k, v)
}

list, err := r.Ipam.IpamPrefixesList(ipam.NewIpamPrefixesListParams(), nil, conditions)
if err != nil {
return nil, err
}

// TODO: find a better way?
if list.Payload.Count != nil && *list.Payload.Count == 0 {
return nil, nil
}

prefixes := make([]*models.Prefix, 0)
for _, prefix := range list.Payload.Results {
if prefix.Prefix != nil {
// if we can allocate a prefix from it, we can take it as a parent prefix
if _, err := r.getAvailablePrefixByPrefix(tenant, *prefix.Prefix, prefixLength); err == nil {
prefixes = append(prefixes, &models.Prefix{
Prefix: *prefix.Prefix,
})
}
}
}

return prefixes, nil
}

func (r *NetboxClient) getAvailablePrefixByPrefix(tenant, prefix, prefixLength string) (*models.Prefix, error) {
return r.GetAvailablePrefixByClaim(&models.PrefixClaim{
ParentPrefix: prefix,
PrefixLength: prefixLength,
Metadata: &models.NetboxMetadata{
Tenant: tenant,
},
})
}

// GetAvailablePrefixByClaim searches an available Prefix in Netbox matching PrefixClaim requirements
func (r *NetboxClient) GetAvailablePrefixByClaim(prefixClaim *models.PrefixClaim) (*models.Prefix, error) {
_, err := r.GetTenantDetails(prefixClaim.Metadata.Tenant)
Expand Down Expand Up @@ -159,8 +207,6 @@ func (r *NetboxClient) GetAvailablePrefixesByParentPrefix(parentPrefixId int64)
return responseAvailablePrefixes, nil
}

var ErrNoPrefixMatchsSizeCriteria = errors.New("no available prefix matches size criterias")

func getSmallestMatchingPrefix(prefixList *ipam.IpamPrefixesAvailablePrefixesListOK, prefixClaimLengthString string) (string, bool, error) {
// input valiation
if len(prefixClaimLengthString) == 0 {
Expand Down

0 comments on commit 97c512d

Please sign in to comment.