Closes #13086: Virtual circuits (#17933)

* WIP

* Add API tests

* Add remaining tests

* Add model docs

* Show virtual circuit connections on interfaces

* Misc cleanup per PR feedback

* Renumber migration

* Support nested terminations for virtual circuit bulk import
This commit is contained in:
Jeremy Stretch
2024-11-19 10:58:39 -05:00
committed by GitHub
parent 7376314821
commit d2168b107f
36 changed files with 2164 additions and 15 deletions

View File

@@ -3,7 +3,9 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices
from circuits.choices import (
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices,
)
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
from circuits.models import *
from dcim.models import Site
@@ -28,6 +30,8 @@ __all__ = (
'ProviderBulkEditForm',
'ProviderAccountBulkEditForm',
'ProviderNetworkBulkEditForm',
'VirtualCircuitBulkEditForm',
'VirtualCircuitTerminationBulkEditForm',
)
@@ -291,3 +295,62 @@ class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
FieldSet('circuit', 'priority'),
)
nullable_fields = ('priority',)
class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm):
provider_network = DynamicModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
required=False
)
provider_account = DynamicModelChoiceField(
label=_('Provider account'),
queryset=ProviderAccount.objects.all(),
required=False
)
status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(CircuitStatusChoices),
required=False,
initial=''
)
tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=100,
required=False
)
comments = CommentField()
model = VirtualCircuit
fieldsets = (
FieldSet('provider_network', 'provider_account', 'status', 'description', name=_('Virtual circuit')),
FieldSet('tenant', name=_('Tenancy')),
)
nullable_fields = (
'provider_account', 'tenant', 'description', 'comments',
)
class VirtualCircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
role = forms.ChoiceField(
label=_('Role'),
choices=add_blank_choice(VirtualCircuitTerminationRoleChoices),
required=False,
initial=''
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
model = VirtualCircuitTermination
fieldsets = (
FieldSet('role', 'description'),
)
nullable_fields = ('description',)

View File

@@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _
from circuits.choices import *
from circuits.constants import *
from circuits.models import *
from dcim.models import Interface
from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
@@ -20,6 +21,9 @@ __all__ = (
'ProviderImportForm',
'ProviderAccountImportForm',
'ProviderNetworkImportForm',
'VirtualCircuitImportForm',
'VirtualCircuitTerminationImportForm',
'VirtualCircuitTerminationImportRelatedForm',
)
@@ -179,3 +183,73 @@ class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
class Meta:
model = CircuitGroupAssignment
fields = ('circuit', 'group', 'priority')
class VirtualCircuitImportForm(NetBoxModelImportForm):
provider_network = CSVModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
to_field_name='name',
help_text=_('The network to which this virtual circuit belongs')
)
provider_account = CSVModelChoiceField(
label=_('Provider account'),
queryset=ProviderAccount.objects.all(),
to_field_name='account',
help_text=_('Assigned provider account (if any)'),
required=False
)
status = CSVChoiceField(
label=_('Status'),
choices=CircuitStatusChoices,
help_text=_('Operational status')
)
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text=_('Assigned tenant')
)
class Meta:
model = VirtualCircuit
fields = [
'cid', 'provider_network', 'provider_account', 'status', 'tenant', 'description', 'comments', 'tags',
]
class BaseVirtualCircuitTerminationImportForm(forms.ModelForm):
virtual_circuit = CSVModelChoiceField(
label=_('Virtual circuit'),
queryset=VirtualCircuit.objects.all(),
to_field_name='cid',
)
role = CSVChoiceField(
label=_('Role'),
choices=VirtualCircuitTerminationRoleChoices,
help_text=_('Operational role')
)
interface = CSVModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.all(),
to_field_name='pk',
)
class VirtualCircuitTerminationImportRelatedForm(BaseVirtualCircuitTerminationImportForm):
class Meta:
model = VirtualCircuitTermination
fields = [
'virtual_circuit', 'role', 'interface', 'description',
]
class VirtualCircuitTerminationImportForm(NetBoxModelImportForm, BaseVirtualCircuitTerminationImportForm):
class Meta:
model = VirtualCircuitTermination
fields = [
'virtual_circuit', 'role', 'interface', 'description', 'tags',
]

View File

@@ -1,7 +1,10 @@
from django import forms
from django.utils.translation import gettext as _
from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices
from circuits.choices import (
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices,
VirtualCircuitTerminationRoleChoices,
)
from circuits.models import *
from dcim.models import Location, Region, Site, SiteGroup
from ipam.models import ASN
@@ -22,6 +25,8 @@ __all__ = (
'ProviderFilterForm',
'ProviderAccountFilterForm',
'ProviderNetworkFilterForm',
'VirtualCircuitFilterForm',
'VirtualCircuitTerminationFilterForm',
)
@@ -292,3 +297,74 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
required=False
)
tag = TagFilterField(model)
class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = VirtualCircuit
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
FieldSet('status', name=_('Attributes')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
)
selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id')
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider')
)
provider_account_id = DynamicModelMultipleChoiceField(
queryset=ProviderAccount.objects.all(),
required=False,
query_params={
'provider_id': '$provider_id'
},
label=_('Provider account')
)
provider_network_id = DynamicModelMultipleChoiceField(
queryset=ProviderNetwork.objects.all(),
required=False,
query_params={
'provider_id': '$provider_id'
},
label=_('Provider network')
)
status = forms.MultipleChoiceField(
label=_('Status'),
choices=CircuitStatusChoices,
required=False
)
tag = TagFilterField(model)
class VirtualCircuitTerminationFilterForm(NetBoxModelFilterSetForm):
model = VirtualCircuitTermination
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('virtual_circuit_id', 'role', name=_('Virtual circuit')),
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
)
virtual_circuit_id = DynamicModelMultipleChoiceField(
queryset=VirtualCircuit.objects.all(),
required=False,
label=_('Virtual circuit')
)
role = forms.MultipleChoiceField(
label=_('Role'),
choices=VirtualCircuitTerminationRoleChoices,
required=False
)
provider_network_id = DynamicModelMultipleChoiceField(
queryset=ProviderNetwork.objects.all(),
required=False,
query_params={
'provider_id': '$provider_id'
},
label=_('Provider network')
)
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider')
)
tag = TagFilterField(model)

View File

@@ -1,16 +1,21 @@
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 circuits.choices import CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices
from circuits.choices import (
CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices, VirtualCircuitTerminationRoleChoices,
)
from circuits.constants import *
from circuits.models import *
from dcim.models import Site
from dcim.models import Interface, Site
from ipam.models import ASN
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from utilities.forms import get_field_value
from utilities.forms.fields import CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
)
from utilities.forms.rendering import FieldSet, InlineFields
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
from utilities.templatetags.builtins.filters import bettertitle
@@ -24,6 +29,8 @@ __all__ = (
'ProviderForm',
'ProviderAccountForm',
'ProviderNetworkForm',
'VirtualCircuitForm',
'VirtualCircuitTerminationForm',
)
@@ -255,3 +262,66 @@ class CircuitGroupAssignmentForm(NetBoxModelForm):
fields = [
'group', 'circuit', 'priority', 'tags',
]
class VirtualCircuitForm(TenancyForm, NetBoxModelForm):
provider_network = DynamicModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
selector=True
)
provider_account = DynamicModelChoiceField(
label=_('Provider account'),
queryset=ProviderAccount.objects.all(),
required=False
)
comments = CommentField()
fieldsets = (
FieldSet(
'provider_network', 'provider_account', 'cid', 'status', 'description', 'tags', name=_('Virtual circuit'),
),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
class Meta:
model = VirtualCircuit
fields = [
'cid', 'provider_network', 'provider_account', 'status', 'description', 'tenant_group', 'tenant',
'comments', 'tags',
]
class VirtualCircuitTerminationForm(NetBoxModelForm):
virtual_circuit = DynamicModelChoiceField(
label=_('Virtual circuit'),
queryset=VirtualCircuit.objects.all(),
selector=True
)
role = forms.ChoiceField(
choices=VirtualCircuitTerminationRoleChoices,
widget=HTMXSelect(),
label=_('Role')
)
interface = DynamicModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.all(),
selector=True,
query_params={
'kind': 'virtual',
'virtual_circuit_termination_id': 'null',
},
context={
'parent': 'device',
}
)
fieldsets = (
FieldSet('virtual_circuit', 'role', 'interface', 'description', 'tags'),
)
class Meta:
model = VirtualCircuitTermination
fields = [
'virtual_circuit', 'role', 'interface', 'description', 'tags',
]