mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-12 03:19:36 -06:00
* VLANTranslationPolicy and VLANTranslationRule models and all associated UI classes
* Change VLANTranslationPolicy to a PrimaryModel and make name unique
* Add serializer classes to InterfaceSerializer
* Remake migrations
* Add GraphQL typing
* Skip tagged models in test
* Missing migration
* Remove get_absolute_url methods
* Remove package-lock.json
* Rebuild migration and add constraints and field options
* Rebuild migrations
* Use DynamicModelChoiceField for policy field
* Make vlan_translation_policy fields on filtersets more consistent with existing __name convention
* Add vlan_translation_table to VMInterface detail page
* Add vlan_translation_policy to VMInterfaceSerializer
* Move vlan_translation_policy fields to model and filterset mixins
* Protect in-use policies against deletion
* Add vlan_translation_policy to fields in VMInterfaceSerializer
* Cleanup indentation
* Remove unnecessary ordering column
* Rebuild migrations
* Search methods and registration
* Ensure 'id' column is present by default
* Add graphql types/filters/schema for VLANTranslationRule
* Filterset tests
* View tests
* API and viewset tests (incomplete)
* Add tags to VLANTranslationRuleForm
* Complete viewset tests for VLANTranslationRule
* Make VLANTranslationRule.policy nullable (but still required)
* Revert "Make VLANTranslationRule.policy nullable (but still required)"
This reverts commit 4c1bb437ef.
* Revert nullability
* Explicitly prefetch policy in graphql
* Documentation of new and affected models
* Add note about select_related in graphql
* Rework policy/rule documentation
* Move vlan_translation_policy into 802.1Q Switching fieldset
* Remove redundant InterfaceVLANTranslationTable
* Conditionally include vlan_translation_table in interface.html and vminterface.html
* Add description field to VLANTranslationRule
* Define vlan_translation_table conditionally
* Add policy (name) filter to VLANTranslationRuleFilterSet
* Revert changes to adding-models.md (moved to another PR)
* Dynamic table for linked rules in vlantranslationpolicy.html
* Misc cleanup
---------
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
602 lines
16 KiB
Python
602 lines
16 KiB
Python
from django import forms
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.core.exceptions import ObjectDoesNotExist
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from dcim.models import Region, Site, SiteGroup
|
|
from ipam.choices import *
|
|
from ipam.constants import *
|
|
from ipam.models import *
|
|
from ipam.models import ASN
|
|
from netbox.forms import NetBoxModelBulkEditForm
|
|
from tenancy.models import Tenant
|
|
from utilities.forms import add_blank_choice, get_field_value
|
|
from utilities.forms.fields import (
|
|
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
|
|
NumericRangeArrayField,
|
|
)
|
|
from utilities.forms.rendering import FieldSet
|
|
from utilities.forms.widgets import BulkEditNullBooleanSelect, HTMXSelect
|
|
from utilities.templatetags.builtins.filters import bettertitle
|
|
|
|
__all__ = (
|
|
'AggregateBulkEditForm',
|
|
'ASNBulkEditForm',
|
|
'ASNRangeBulkEditForm',
|
|
'FHRPGroupBulkEditForm',
|
|
'IPAddressBulkEditForm',
|
|
'IPRangeBulkEditForm',
|
|
'PrefixBulkEditForm',
|
|
'RIRBulkEditForm',
|
|
'RoleBulkEditForm',
|
|
'RouteTargetBulkEditForm',
|
|
'ServiceBulkEditForm',
|
|
'ServiceTemplateBulkEditForm',
|
|
'VLANBulkEditForm',
|
|
'VLANGroupBulkEditForm',
|
|
'VLANTranslationPolicyBulkEditForm',
|
|
'VLANTranslationRuleBulkEditForm',
|
|
'VRFBulkEditForm',
|
|
)
|
|
|
|
|
|
class VRFBulkEditForm(NetBoxModelBulkEditForm):
|
|
tenant = DynamicModelChoiceField(
|
|
label=_('Tenant'),
|
|
queryset=Tenant.objects.all(),
|
|
required=False
|
|
)
|
|
enforce_unique = forms.NullBooleanField(
|
|
required=False,
|
|
widget=BulkEditNullBooleanSelect(),
|
|
label=_('Enforce unique space')
|
|
)
|
|
description = forms.CharField(
|
|
label=_('Description'),
|
|
max_length=200,
|
|
required=False
|
|
)
|
|
comments = CommentField()
|
|
|
|
model = VRF
|
|
fieldsets = (
|
|
FieldSet('tenant', 'enforce_unique', 'description'),
|
|
)
|
|
nullable_fields = ('tenant', 'description', 'comments')
|
|
|
|
|
|
class RouteTargetBulkEditForm(NetBoxModelBulkEditForm):
|
|
tenant = DynamicModelChoiceField(
|
|
label=_('Tenant'),
|
|
queryset=Tenant.objects.all(),
|
|
required=False
|
|
)
|
|
description = forms.CharField(
|
|
label=_('Description'),
|
|
max_length=200,
|
|
required=False
|
|
)
|
|
comments = CommentField()
|
|
|
|
model = RouteTarget
|
|
fieldsets = (
|
|
FieldSet('tenant', 'description'),
|
|
)
|
|
nullable_fields = ('tenant', 'description', 'comments')
|
|
|
|
|
|
class RIRBulkEditForm(NetBoxModelBulkEditForm):
|
|
is_private = forms.NullBooleanField(
|
|
label=_('Is private'),
|
|
required=False,
|
|
widget=BulkEditNullBooleanSelect
|
|
)
|
|
description = forms.CharField(
|
|
label=_('Description'),
|
|
max_length=200,
|
|
required=False
|
|
)
|
|
|
|
model = RIR
|
|
fieldsets = (
|
|
FieldSet('is_private', 'description'),
|
|
)
|
|
nullable_fields = ('is_private', 'description')
|
|
|
|
|
|
class ASNRangeBulkEditForm(NetBoxModelBulkEditForm):
|
|
rir = DynamicModelChoiceField(
|
|
queryset=RIR.objects.all(),
|
|
required=False,
|
|
label=_('RIR')
|
|
)
|
|
tenant = DynamicModelChoiceField(
|
|
label=_('Tenant'),
|
|
queryset=Tenant.objects.all(),
|
|
required=False
|
|
)
|
|
description = forms.CharField(
|
|
label=_('Description'),
|
|
max_length=200,
|
|
required=False
|
|
)
|
|
|
|
model = ASNRange
|
|
fieldsets = (
|
|
FieldSet('rir', 'tenant', 'description'),
|
|
)
|
|
nullable_fields = ('description',)
|
|
|
|
|
|
class ASNBulkEditForm(NetBoxModelBulkEditForm):
|
|
sites = DynamicModelMultipleChoiceField(
|
|
label=_('Sites'),
|
|
queryset=Site.objects.all(),
|
|
required=False
|
|
)
|
|
rir = DynamicModelChoiceField(
|
|
queryset=RIR.objects.all(),
|
|
required=False,
|
|
label=_('RIR')
|
|
)
|
|
tenant = DynamicModelChoiceField(
|
|
label=_('Tenant'),
|
|
queryset=Tenant.objects.all(),
|
|
required=False
|
|
)
|
|
description = forms.CharField(
|
|
label=_('Description'),
|
|
max_length=200,
|
|
required=False
|
|
)
|
|
comments = CommentField()
|
|
|
|
model = ASN
|
|
fieldsets = (
|
|
FieldSet('sites', 'rir', 'tenant', 'description'),
|
|
)
|
|
nullable_fields = ('tenant', 'description', 'comments')
|
|
|
|
|
|
class AggregateBulkEditForm(NetBoxModelBulkEditForm):
|
|
rir = DynamicModelChoiceField(
|
|
queryset=RIR.objects.all(),
|
|
required=False,
|
|
label=_('RIR')
|
|
)
|
|
tenant = DynamicModelChoiceField(
|
|
label=_('Tenant'),
|
|
queryset=Tenant.objects.all(),
|
|
required=False
|
|
)
|
|
date_added = forms.DateField(
|
|
label=_('Date added'),
|
|
required=False
|
|
)
|
|
description = forms.CharField(
|
|
label=_('Description'),
|
|
max_length=200,
|
|
required=False
|
|
)
|
|
comments = CommentField()
|
|
|
|
model = Aggregate
|
|
fieldsets = (
|
|
FieldSet('rir', 'tenant', 'date_added', 'description'),
|
|
)
|
|
nullable_fields = ('date_added', 'description', 'comments')
|
|
|
|
|
|
class RoleBulkEditForm(NetBoxModelBulkEditForm):
|
|
weight = forms.IntegerField(
|
|
label=_('Weight'),
|
|
required=False
|
|
)
|
|
description = forms.CharField(
|
|
label=_('Description'),
|
|
max_length=200,
|
|
required=False
|
|
)
|
|
|
|
model = Role
|
|
fieldsets = (
|
|
FieldSet('weight', 'description'),
|
|
)
|
|
nullable_fields = ('description',)
|
|
|
|
|
|
class PrefixBulkEditForm(NetBoxModelBulkEditForm):
|
|
scope_type = ContentTypeChoiceField(
|
|
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
|
|
widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}),
|
|
required=False,
|
|
label=_('Scope type')
|
|
)
|
|
scope = DynamicModelChoiceField(
|
|
label=_('Scope'),
|
|
queryset=Site.objects.none(), # Initial queryset
|
|
required=False,
|
|
disabled=True,
|
|
selector=True
|
|
)
|
|
vlan_group = DynamicModelChoiceField(
|
|
queryset=VLANGroup.objects.all(),
|
|
required=False,
|
|
label=_('VLAN Group')
|
|
)
|
|
vlan = DynamicModelChoiceField(
|
|
queryset=VLAN.objects.all(),
|
|
required=False,
|
|
label=_('VLAN'),
|
|
query_params={
|
|
'group_id': '$vlan_group',
|
|
}
|
|
)
|
|
vrf = DynamicModelChoiceField(
|
|
queryset=VRF.objects.all(),
|
|
required=False,
|
|
label=_('VRF')
|
|
)
|
|
prefix_length = forms.IntegerField(
|
|
label=_('Prefix length'),
|
|
min_value=PREFIX_LENGTH_MIN,
|
|
max_value=PREFIX_LENGTH_MAX,
|
|
required=False
|
|
)
|
|
tenant = DynamicModelChoiceField(
|
|
label=_('Tenant'),
|
|
queryset=Tenant.objects.all(),
|
|
required=False
|
|
)
|
|
status = forms.ChoiceField(
|
|
label=_('Status'),
|
|
choices=add_blank_choice(PrefixStatusChoices),
|
|
required=False
|
|
)
|
|
role = DynamicModelChoiceField(
|
|
label=_('Role'),
|
|
queryset=Role.objects.all(),
|
|
required=False
|
|
)
|
|
is_pool = forms.NullBooleanField(
|
|
required=False,
|
|
widget=BulkEditNullBooleanSelect(),
|
|
label=_('Is a pool')
|
|
)
|
|
mark_utilized = forms.NullBooleanField(
|
|
required=False,
|
|
widget=BulkEditNullBooleanSelect(),
|
|
label=_('Treat as fully utilized')
|
|
)
|
|
description = forms.CharField(
|
|
label=_('Description'),
|
|
max_length=200,
|
|
required=False
|
|
)
|
|
comments = CommentField()
|
|
|
|
model = Prefix
|
|
fieldsets = (
|
|
FieldSet('tenant', 'status', 'role', 'description'),
|
|
FieldSet('vrf', 'prefix_length', 'is_pool', 'mark_utilized', name=_('Addressing')),
|
|
FieldSet('scope_type', 'scope', name=_('Scope')),
|
|
FieldSet('vlan_group', 'vlan', name=_('VLAN Assignment')),
|
|
)
|
|
nullable_fields = (
|
|
'vlan', 'vrf', 'tenant', 'role', 'scope', 'description', 'comments',
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if scope_type_id := get_field_value(self, 'scope_type'):
|
|
try:
|
|
scope_type = ContentType.objects.get(pk=scope_type_id)
|
|
model = scope_type.model_class()
|
|
self.fields['scope'].queryset = model.objects.all()
|
|
self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower
|
|
self.fields['scope'].disabled = False
|
|
self.fields['scope'].label = _(bettertitle(model._meta.verbose_name))
|
|
except ObjectDoesNotExist:
|
|
pass
|
|
|
|
|
|
class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
|
|
vrf = DynamicModelChoiceField(
|
|
queryset=VRF.objects.all(),
|
|
required=False,
|
|
label=_('VRF')
|
|
)
|
|
tenant = DynamicModelChoiceField(
|
|
label=_('Tenant'),
|
|
queryset=Tenant.objects.all(),
|
|
required=False
|
|
)
|
|
status = forms.ChoiceField(
|
|
label=_('Status'),
|
|
choices=add_blank_choice(IPRangeStatusChoices),
|
|
required=False
|
|
)
|
|
role = DynamicModelChoiceField(
|
|
label=_('Role'),
|
|
queryset=Role.objects.all(),
|
|
required=False
|
|
)
|
|
mark_utilized = forms.NullBooleanField(
|
|
required=False,
|
|
widget=BulkEditNullBooleanSelect(),
|
|
label=_('Treat as fully utilized')
|
|
)
|
|
description = forms.CharField(
|
|
label=_('Description'),
|
|
max_length=200,
|
|
required=False
|
|
)
|
|
comments = CommentField()
|
|
|
|
model = IPRange
|
|
fieldsets = (
|
|
FieldSet('status', 'role', 'vrf', 'tenant', 'mark_utilized', 'description'),
|
|
)
|
|
nullable_fields = (
|
|
'vrf', 'tenant', 'role', 'description', 'comments',
|
|
)
|
|
|
|
|
|
class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
|
|
vrf = DynamicModelChoiceField(
|
|
queryset=VRF.objects.all(),
|
|
required=False,
|
|
label=_('VRF')
|
|
)
|
|
mask_length = forms.IntegerField(
|
|
label=_('Mask length'),
|
|
min_value=IPADDRESS_MASK_LENGTH_MIN,
|
|
max_value=IPADDRESS_MASK_LENGTH_MAX,
|
|
required=False
|
|
)
|
|
tenant = DynamicModelChoiceField(
|
|
label=_('Tenant'),
|
|
queryset=Tenant.objects.all(),
|
|
required=False
|
|
)
|
|
status = forms.ChoiceField(
|
|
label=_('Status'),
|
|
choices=add_blank_choice(IPAddressStatusChoices),
|
|
required=False
|
|
)
|
|
role = forms.ChoiceField(
|
|
label=_('Role'),
|
|
choices=add_blank_choice(IPAddressRoleChoices),
|
|
required=False
|
|
)
|
|
dns_name = forms.CharField(
|
|
max_length=255,
|
|
required=False,
|
|
label=_('DNS name')
|
|
)
|
|
description = forms.CharField(
|
|
label=_('Description'),
|
|
max_length=200,
|
|
required=False
|
|
)
|
|
comments = CommentField()
|
|
|
|
model = IPAddress
|
|
fieldsets = (
|
|
FieldSet('status', 'role', 'tenant', 'description'),
|
|
FieldSet('vrf', 'mask_length', 'dns_name', name=_('Addressing')),
|
|
)
|
|
nullable_fields = (
|
|
'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments',
|
|
)
|
|
|
|
|
|
class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
|
|
protocol = forms.ChoiceField(
|
|
label=_('Protocol'),
|
|
choices=add_blank_choice(FHRPGroupProtocolChoices),
|
|
required=False
|
|
)
|
|
group_id = forms.IntegerField(
|
|
min_value=0,
|
|
required=False,
|
|
label=_('Group ID')
|
|
)
|
|
auth_type = forms.ChoiceField(
|
|
choices=add_blank_choice(FHRPGroupAuthTypeChoices),
|
|
required=False,
|
|
label=_('Authentication type')
|
|
)
|
|
auth_key = forms.CharField(
|
|
max_length=255,
|
|
required=False,
|
|
label=_('Authentication key')
|
|
)
|
|
name = forms.CharField(
|
|
label=_('Name'),
|
|
max_length=100,
|
|
required=False
|
|
)
|
|
description = forms.CharField(
|
|
label=_('Description'),
|
|
max_length=200,
|
|
required=False
|
|
)
|
|
comments = CommentField()
|
|
|
|
model = FHRPGroup
|
|
fieldsets = (
|
|
FieldSet('protocol', 'group_id', 'name', 'description'),
|
|
FieldSet('auth_type', 'auth_key', name=_('Authentication')),
|
|
)
|
|
nullable_fields = ('auth_type', 'auth_key', 'name', 'description', 'comments')
|
|
|
|
|
|
class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
|
|
description = forms.CharField(
|
|
label=_('Description'),
|
|
max_length=200,
|
|
required=False
|
|
)
|
|
scope_type = ContentTypeChoiceField(
|
|
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
|
|
widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}),
|
|
required=False,
|
|
label=_('Scope type')
|
|
)
|
|
scope = DynamicModelChoiceField(
|
|
label=_('Scope'),
|
|
queryset=Site.objects.none(), # Initial queryset
|
|
required=False,
|
|
disabled=True,
|
|
selector=True
|
|
)
|
|
vid_ranges = NumericRangeArrayField(
|
|
label=_('VLAN ID ranges'),
|
|
required=False
|
|
)
|
|
|
|
model = VLANGroup
|
|
fieldsets = (
|
|
FieldSet('site', 'vid_ranges', 'description'),
|
|
FieldSet('scope_type', 'scope', name=_('Scope')),
|
|
)
|
|
nullable_fields = ('description', 'scope')
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if scope_type_id := get_field_value(self, 'scope_type'):
|
|
try:
|
|
scope_type = ContentType.objects.get(pk=scope_type_id)
|
|
model = scope_type.model_class()
|
|
self.fields['scope'].queryset = model.objects.all()
|
|
self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower
|
|
self.fields['scope'].disabled = False
|
|
self.fields['scope'].label = _(bettertitle(model._meta.verbose_name))
|
|
except ObjectDoesNotExist:
|
|
pass
|
|
|
|
|
|
class VLANBulkEditForm(NetBoxModelBulkEditForm):
|
|
region = DynamicModelChoiceField(
|
|
label=_('Region'),
|
|
queryset=Region.objects.all(),
|
|
required=False
|
|
)
|
|
site_group = DynamicModelChoiceField(
|
|
label=_('Site group'),
|
|
queryset=SiteGroup.objects.all(),
|
|
required=False
|
|
)
|
|
site = DynamicModelChoiceField(
|
|
label=_('Site'),
|
|
queryset=Site.objects.all(),
|
|
required=False,
|
|
query_params={
|
|
'region_id': '$region',
|
|
'group_id': '$site_group',
|
|
}
|
|
)
|
|
group = DynamicModelChoiceField(
|
|
label=_('Group'),
|
|
queryset=VLANGroup.objects.all(),
|
|
required=False,
|
|
query_params={
|
|
'site_id': '$site'
|
|
}
|
|
)
|
|
tenant = DynamicModelChoiceField(
|
|
label=_('Tenant'),
|
|
queryset=Tenant.objects.all(),
|
|
required=False
|
|
)
|
|
status = forms.ChoiceField(
|
|
label=_('Status'),
|
|
choices=add_blank_choice(VLANStatusChoices),
|
|
required=False
|
|
)
|
|
role = DynamicModelChoiceField(
|
|
label=_('Role'),
|
|
queryset=Role.objects.all(),
|
|
required=False
|
|
)
|
|
description = forms.CharField(
|
|
label=_('Description'),
|
|
max_length=200,
|
|
required=False
|
|
)
|
|
comments = CommentField()
|
|
|
|
model = VLAN
|
|
fieldsets = (
|
|
FieldSet('status', 'role', 'tenant', 'description'),
|
|
FieldSet('region', 'site_group', 'site', 'group', name=_('Site & Group')),
|
|
)
|
|
nullable_fields = (
|
|
'site', 'group', 'tenant', 'role', 'description', 'comments',
|
|
)
|
|
|
|
|
|
class VLANTranslationPolicyBulkEditForm(NetBoxModelBulkEditForm):
|
|
description = forms.CharField(
|
|
label=_('Description'),
|
|
max_length=200,
|
|
required=False
|
|
)
|
|
|
|
model = VLANTranslationPolicy
|
|
fieldsets = (
|
|
FieldSet('description'),
|
|
)
|
|
nullable_fields = ('description',)
|
|
|
|
|
|
class VLANTranslationRuleBulkEditForm(NetBoxModelBulkEditForm):
|
|
policy = DynamicModelChoiceField(
|
|
label=_('Policy'),
|
|
queryset=VLANTranslationPolicy.objects.all(),
|
|
selector=True
|
|
)
|
|
local_vid = forms.IntegerField(required=False)
|
|
remote_vid = forms.IntegerField(required=False)
|
|
|
|
model = VLANTranslationRule
|
|
fieldsets = (
|
|
FieldSet('policy', 'local_vid', 'remote_vid'),
|
|
)
|
|
fields = ('policy', 'local_vid', 'remote_vid')
|
|
|
|
|
|
class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
|
|
protocol = forms.ChoiceField(
|
|
label=_('Protocol'),
|
|
choices=add_blank_choice(ServiceProtocolChoices),
|
|
required=False
|
|
)
|
|
ports = NumericArrayField(
|
|
label=_('Ports'),
|
|
base_field=forms.IntegerField(
|
|
min_value=SERVICE_PORT_MIN,
|
|
max_value=SERVICE_PORT_MAX
|
|
),
|
|
required=False
|
|
)
|
|
description = forms.CharField(
|
|
label=_('Description'),
|
|
max_length=200,
|
|
required=False
|
|
)
|
|
comments = CommentField()
|
|
|
|
model = ServiceTemplate
|
|
fieldsets = (
|
|
FieldSet('protocol', 'ports', 'description'),
|
|
)
|
|
nullable_fields = ('description', 'comments')
|
|
|
|
|
|
class ServiceBulkEditForm(ServiceTemplateBulkEditForm):
|
|
model = Service
|