Skip to content

Commit

Permalink
Merge branch 'feature/common-tenant-support'
Browse files Browse the repository at this point in the history
Add support for assignment of ACI VRF and ACI Bridge Domain from ACI
Tenant "common".
  • Loading branch information
pheus committed Jul 2, 2024
2 parents c4cf2e8 + 5673271 commit 93350dd
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 67 deletions.
1 change: 1 addition & 0 deletions docs/features/tenants.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ The *ACIBridgeDomain* model has the following fields:
*Required fields*:

- **Name**: represent the Bridge Domain name in the ACI
- **ACI Tenant**: a reference to the ACITenant model.
- **ACI VRF**: a reference to the ACIVRF model.

*Optional fields*:
Expand Down
3 changes: 3 additions & 0 deletions netbox_aci_plugin/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ class ACIBridgeDomainSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:netbox_aci_plugin-api:acibridgedomain-detail"
)
aci_tenant = ACITenantSerializer(nested=True, required=True)
aci_vrf = ACIVRFSerializer(nested=True, required=True)
nb_tenant = TenantSerializer(nested=True, required=False, allow_null=True)

Expand All @@ -156,6 +157,7 @@ class Meta:
"name",
"name_alias",
"description",
"aci_tenant",
"aci_vrf",
"nb_tenant",
"advertise_host_routes_enabled",
Expand Down Expand Up @@ -191,6 +193,7 @@ class Meta:
"name",
"name_alias",
"description",
"aci_tenant",
"aci_vrf",
"nb_tenant",
)
Expand Down
1 change: 1 addition & 0 deletions netbox_aci_plugin/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class ACIBridgeDomainListViewSet(NetBoxModelViewSet):
"""API view for listing ACI Bridge Domain instances."""

queryset = ACIBridgeDomain.objects.prefetch_related(
"aci_tenant",
"aci_vrf",
"nb_tenant",
"tags",
Expand Down
37 changes: 33 additions & 4 deletions netbox_aci_plugin/filtersets/tenant_networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ class ACIVRFFilterSet(NetBoxModelFilterSet):
choices=VRFPCEnforcementPreferenceChoices,
null_value=None,
)
present_in_aci_tenant_or_common = django_filters.ModelChoiceFilter(
queryset=ACITenant.objects.all(),
method="filter_present_in_aci_tenant_or_common",
label=_("ACI Tenant (ID)"),
)

class Meta:
model = ACIVRF
Expand Down Expand Up @@ -107,18 +112,26 @@ def search(self, queryset, name, value):
)
return queryset.filter(queryset_filter)

def filter_present_in_aci_tenant_or_common(
self, queryset, name, aci_tenant
):
"""Return a QuerySet filtered by given ACI Tenant or 'common'."""
if aci_tenant is None:
return queryset.none
return queryset.filter(
Q(aci_tenant=aci_tenant) | Q(aci_tenant__name="common")
)


class ACIBridgeDomainFilterSet(NetBoxModelFilterSet):
"""Filter set for ACI Bridge Domain model."""

aci_tenant = django_filters.ModelMultipleChoiceFilter(
field_name="aci_vrf__aci_tenant",
queryset=ACITenant.objects.all(),
to_field_name="name",
label=_("ACI Tenant (name)"),
)
aci_tenant_id = django_filters.ModelMultipleChoiceFilter(
field_name="aci_vrf__aci_tenant",
queryset=ACITenant.objects.all(),
to_field_name="id",
label=_("ACI Tenant (ID)"),
Expand Down Expand Up @@ -151,6 +164,11 @@ class ACIBridgeDomainFilterSet(NetBoxModelFilterSet):
choices=BDMultiDestinationFloodingChoices,
null_value=None,
)
present_in_aci_tenant_or_common = django_filters.ModelChoiceFilter(
queryset=ACITenant.objects.all(),
method="filter_present_in_aci_tenant_or_common",
label=_("ACI Tenant (ID)"),
)
unknown_ipv4_multicast = django_filters.MultipleChoiceFilter(
choices=BDUnknownMulticastChoices,
null_value=None,
Expand All @@ -175,6 +193,7 @@ class Meta:
"name",
"name_alias",
"description",
"aci_tenant",
"aci_vrf",
"nb_tenant",
"advertise_host_routes_enabled",
Expand Down Expand Up @@ -218,18 +237,28 @@ def search(self, queryset, name, value):
)
return queryset.filter(queryset_filter)

def filter_present_in_aci_tenant_or_common(
self, queryset, name, aci_tenant
):
"""Return a QuerySet filtered by given ACI Tenant or 'common'."""
if aci_tenant is None:
return queryset.none
return queryset.filter(
Q(aci_tenant=aci_tenant) | Q(aci_tenant__name="common")
)


class ACIBridgeDomainSubnetFilterSet(NetBoxModelFilterSet):
"""Filter set for ACI Bridge Domain Subnet model."""

aci_tenant = django_filters.ModelMultipleChoiceFilter(
field_name="aci_bridge_domain__aci_vrf__aci_tenant",
field_name="aci_bridge_domain__aci_tenant",
queryset=ACITenant.objects.all(),
to_field_name="name",
label=_("ACI Tenant (name)"),
)
aci_tenant_id = django_filters.ModelMultipleChoiceFilter(
field_name="aci_bridge_domain__aci_vrf__aci_tenant",
field_name="aci_bridge_domain__aci_tenant",
queryset=ACITenant.objects.all(),
to_field_name="id",
label=_("ACI Tenant (ID)"),
Expand Down
58 changes: 40 additions & 18 deletions netbox_aci_plugin/forms/tenant_app_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,15 +237,15 @@ class ACIEndpointGroupForm(NetBoxModelForm):
)
aci_vrf = DynamicModelChoiceField(
queryset=ACIVRF.objects.all(),
query_params={"aci_tenant_id": "$aci_tenant"},
query_params={"present_in_aci_tenant_or_common": "$aci_tenant"},
initial_params={"aci_bridge_domains": "$aci_bridge_domain"},
required=False,
label=_("ACI VRF"),
)
aci_bridge_domain = DynamicModelChoiceField(
queryset=ACIBridgeDomain.objects.all(),
query_params={
"aci_tenant_id": "$aci_tenant",
"present_in_aci_tenant_or_common": "$aci_tenant",
"aci_vrf_id": "$aci_vrf",
},
label=_("ACI Bridge Domain"),
Expand Down Expand Up @@ -367,6 +367,29 @@ class Meta:
"tags",
)

def clean(self):
"""Cleaning and validation of ACI Endpoint Group Form."""

super().clean()

aci_app_profile = self.cleaned_data.get("aci_app_profile")
aci_bridge_domain = self.cleaned_data.get("aci_bridge_domain")

if (
not aci_app_profile.aci_tenant.id
== aci_bridge_domain.aci_tenant.id
and not aci_bridge_domain.aci_tenant.name == "common"
):
raise forms.ValidationError(
{
"aci_bridge_domain": _(
"A Bridge Domain can only be assigned from the same"
" ACI Tenant as the Endpoint Group or ACI Tenant"
" 'common'."
)
}
)


class ACIEndpointGroupBulkEditForm(NetBoxModelBulkEditForm):
"""NetBox bulk edit form for ACI Endpoint Group model."""
Expand Down Expand Up @@ -636,20 +659,18 @@ class ACIEndpointGroupImportForm(NetBoxModelImportForm):
label=_("ACI Application Profile"),
help_text=_("Assigned ACI Application Profile"),
)
aci_vrf = CSVModelChoiceField(
queryset=ACIVRF.objects.all(),
to_field_name="name",
required=True,
label=_("ACI VRF"),
help_text=_("Parent ACI VRF of ACI Bridge Domain"),
)
aci_bridge_domain = CSVModelChoiceField(
queryset=ACIBridgeDomain.objects.all(),
to_field_name="name",
required=True,
label=_("ACI Bridge Domain"),
help_text=_("Assigned ACI Bridge Domain"),
)
is_aci_bd_in_common = forms.BooleanField(
label=_("Is ACI Bridge Domain in 'common'"),
required=False,
help_text=_("Assigned ACI Bridge Domain is in ACI Tenant 'common'"),
)
nb_tenant = CSVModelChoiceField(
queryset=Tenant.objects.all(),
to_field_name="name",
Expand All @@ -674,10 +695,10 @@ class Meta:
"name_alias",
"aci_tenant",
"aci_app_profile",
"aci_vrf",
"aci_bridge_domain",
"description",
"nb_tenant",
"is_aci_bd_in_common",
"admin_shutdown",
"custom_qos_policy_name",
"flood_in_encap_enabled",
Expand Down Expand Up @@ -705,15 +726,16 @@ def __init__(self, data=None, *args, **kwargs) -> None:
)
self.fields["aci_app_profile"].queryset = aci_appprofile_queryset

# Limit ACIBridgeDomain queryset by parent ACIVRF and ACITenant
if data.get("aci_tenant") and data.get("aci_vrf"):
# Limit ACIVRF queryset by parent ACITenant
self.fields["aci_vrf"].queryset = ACIVRF.objects.filter(
aci_tenant__name=data["aci_tenant"]
# Limit ACIBridgeDomain queryset by "common" ACITenant
if data.get("is_aci_bd_in_common") == "true":
aci_bd_queryset = ACIBridgeDomain.objects.filter(
aci_tenant__name="common"
)
# Limit ACIBridgeDomain queryset by parent ACIVRF
self.fields["aci_bridge_domain"].queryset = aci_bd_queryset
# Limit ACIBridgeDomain queryset by ACITenant
elif data.get("aci_tenant") and data.get("aci_bridge_domain"):
# Limit ACIBridgeDomain queryset by parent ACITenant
aci_bd_queryset = ACIBridgeDomain.objects.filter(
aci_vrf__aci_tenant__name=data["aci_tenant"],
aci_vrf__name=data["aci_vrf"],
aci_tenant__name=data["aci_tenant"]
)
self.fields["aci_bridge_domain"].queryset = aci_bd_queryset
51 changes: 44 additions & 7 deletions netbox_aci_plugin/forms/tenant_networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,13 +521,11 @@ class ACIBridgeDomainForm(NetBoxModelForm):

aci_tenant = DynamicModelChoiceField(
queryset=ACITenant.objects.all(),
initial_params={"aci_vrfs": "$aci_vrf"},
required=False,
label=_("ACI Tenant"),
)
aci_vrf = DynamicModelChoiceField(
queryset=ACIVRF.objects.all(),
query_params={"aci_tenant_id": "$aci_tenant"},
query_params={"present_in_aci_tenant_or_common": "$aci_tenant"},
label=_("ACI VRF"),
)
nb_tenant_group = DynamicModelChoiceField(
Expand Down Expand Up @@ -710,6 +708,7 @@ class Meta:
"name",
"name_alias",
"description",
"aci_tenant",
"aci_vrf",
"nb_tenant",
"advertise_host_routes_enabled",
Expand All @@ -736,6 +735,27 @@ class Meta:
"tags",
)

def clean(self):
"""Cleaning and validation of ACI Bridge Domain Form."""

super().clean()

aci_tenant = self.cleaned_data.get("aci_tenant")
aci_vrf = self.cleaned_data.get("aci_vrf")

if (
not aci_tenant.id == aci_vrf.aci_tenant.id
and not aci_vrf.aci_tenant.name == "common"
):
raise forms.ValidationError(
{
"aci_vrf": _(
"A VRF can only be assigned from the same ACI Tenant"
" as the Bridge Domain or ACI Tenant 'common'."
)
}
)


class ACIBridgeDomainBulkEditForm(NetBoxModelBulkEditForm):
"""NetBox bulk edit form for ACI Bridge Domain model."""
Expand All @@ -750,6 +770,11 @@ class ACIBridgeDomainBulkEditForm(NetBoxModelBulkEditForm):
required=False,
label=_("Description"),
)
aci_tenant = DynamicModelChoiceField(
queryset=ACITenant.objects.all(),
required=False,
label=_("ACI Tenant"),
)
aci_vrf = DynamicModelChoiceField(
queryset=ACIVRF.objects.all(),
required=False,
Expand Down Expand Up @@ -878,6 +903,7 @@ class ACIBridgeDomainBulkEditForm(NetBoxModelBulkEditForm):
FieldSet(
"name",
"name_alias",
"aci_tenant",
"aci_vrf",
"description",
"tags",
Expand Down Expand Up @@ -1124,7 +1150,7 @@ class ACIBridgeDomainImportForm(NetBoxModelImportForm):
to_field_name="name",
required=True,
label=_("ACI Tenant"),
help_text=_("Parent ACI Tenant of ACI VRF"),
help_text=_("Assigned ACI Tenant"),
)
aci_vrf = CSVModelChoiceField(
queryset=ACIVRF.objects.all(),
Expand All @@ -1133,6 +1159,11 @@ class ACIBridgeDomainImportForm(NetBoxModelImportForm):
label=_("ACI VRF"),
help_text=_("Assigned ACI VRF"),
)
is_aci_vrf_in_common = forms.BooleanField(
label=_("Is ACI VRF in 'common'"),
required=False,
help_text=_("Assigned ACI VRF is in ACI Tenant 'common'"),
)
nb_tenant = CSVModelChoiceField(
queryset=Tenant.objects.all(),
to_field_name="name",
Expand Down Expand Up @@ -1177,6 +1208,7 @@ class Meta:
"aci_vrf",
"description",
"nb_tenant",
"is_aci_vrf_in_common",
"advertise_host_routes_enabled",
"arp_flooding_enabled",
"clear_remote_mac_enabled",
Expand Down Expand Up @@ -1209,8 +1241,13 @@ def __init__(self, data=None, *args, **kwargs) -> None:
if not data:
return

# Limit ACIVRF queryset by "common" ACITenant
if data.get("is_aci_vrf_in_common") == "true":
self.fields["aci_vrf"].queryset = ACIVRF.objects.filter(
aci_tenant__name="common"
)
# Limit ACIVRF queryset by parent ACITenant
if data.get("aci_tenant"):
elif data.get("aci_tenant"):
self.fields["aci_vrf"].queryset = ACIVRF.objects.filter(
aci_tenant__name=data["aci_tenant"]
)
Expand Down Expand Up @@ -1765,9 +1802,9 @@ def __init__(self, data=None, *args, **kwargs) -> None:
self.fields["aci_vrf"].queryset = ACIVRF.objects.filter(
aci_tenant__name=data["aci_tenant"]
)
# Limit ACIBridgeDomain queryset by parent ACIVRF
# Limit ACIBridgeDomain queryset by parent ACIVRF and ACITenant
aci_bd_queryset = ACIBridgeDomain.objects.filter(
aci_vrf__aci_tenant__name=data["aci_tenant"],
aci_tenant__name=data["aci_tenant"],
aci_vrf__name=data["aci_vrf"],
)
self.fields["aci_bridge_domain"].queryset = aci_bd_queryset
1 change: 1 addition & 0 deletions netbox_aci_plugin/graphql/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class ACIVRFType(NetBoxObjectType):
class ACIBridgeDomainType(NetBoxObjectType):
"""GraphQL type definition for ACIBridgeDomain model."""

aci_tenant: ACITenantType
aci_vrf: ACIVRFType
nb_tenant: Optional[TenantType]
dhcp_labels: Optional[List[str]]
Expand Down
Loading

0 comments on commit 93350dd

Please sign in to comment.