mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-13 16:47:34 -06:00
* 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:
parent
7376314821
commit
d2168b107f
33
docs/models/circuits/virtualcircuit.md
Normal file
33
docs/models/circuits/virtualcircuit.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Virtual Circuits
|
||||
|
||||
A virtual circuit can connect two or more interfaces atop a set of decoupled physical connections. For example, it's very common to form a virtual connection between two virtual interfaces, each of which is bound to a physical interface on its respective device and physically connected to a [provider network](./providernetwork.md) via an independent [physical circuit](./circuit.md).
|
||||
|
||||
## Fields
|
||||
|
||||
### Provider Network
|
||||
|
||||
The [provider network](./providernetwork.md) across which the virtual circuit is formed.
|
||||
|
||||
### Provider Account
|
||||
|
||||
The [provider account](./provideraccount.md) with which the virtual circuit is associated (if any).
|
||||
|
||||
### Circuit ID
|
||||
|
||||
The unique identifier assigned to the virtual circuit by its [provider](./provider.md).
|
||||
|
||||
### Status
|
||||
|
||||
The operational status of the virtual circuit. By default, the following statuses are available:
|
||||
|
||||
| Name |
|
||||
|----------------|
|
||||
| Planned |
|
||||
| Provisioning |
|
||||
| Active |
|
||||
| Offline |
|
||||
| Deprovisioning |
|
||||
| Decommissioned |
|
||||
|
||||
!!! tip "Custom circuit statuses"
|
||||
Additional circuit statuses may be defined by setting `Circuit.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
21
docs/models/circuits/virtualcircuittermination.md
Normal file
21
docs/models/circuits/virtualcircuittermination.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Virtual Circuit Terminations
|
||||
|
||||
This model represents the connection of a virtual [interface](../dcim/interface.md) to a [virtual circuit](./virtualcircuit.md).
|
||||
|
||||
## Fields
|
||||
|
||||
### Virtual Circuit
|
||||
|
||||
The [virtual circuit](./virtualcircuit.md) to which the interface is connected.
|
||||
|
||||
### Interface
|
||||
|
||||
The [interface](../dcim/interface.md) connected to the virtual circuit.
|
||||
|
||||
### Role
|
||||
|
||||
The functional role of the termination. This depends on the virtual circuit's topology, which is typically either peer-to-peer or hub-and-spoke (multipoint). Valid choices include:
|
||||
|
||||
* Peer
|
||||
* Hub
|
||||
* Spoke
|
@ -174,6 +174,8 @@ nav:
|
||||
- Provider: 'models/circuits/provider.md'
|
||||
- Provider Account: 'models/circuits/provideraccount.md'
|
||||
- Provider Network: 'models/circuits/providernetwork.md'
|
||||
- Virtual Circuit: 'models/circuits/virtualcircuit.md'
|
||||
- Virtual Circuit Termination: 'models/circuits/virtualcircuittermination.md'
|
||||
- Core:
|
||||
- DataFile: 'models/core/datafile.md'
|
||||
- DataSource: 'models/core/datasource.md'
|
||||
|
@ -2,9 +2,13 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices
|
||||
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
|
||||
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
from circuits.models import Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType
|
||||
from circuits.models import (
|
||||
Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType, VirtualCircuit,
|
||||
VirtualCircuitTermination,
|
||||
)
|
||||
from dcim.api.serializers_.device_components import InterfaceSerializer
|
||||
from dcim.api.serializers_.cables import CabledObjectSerializer
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
|
||||
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
||||
@ -20,6 +24,8 @@ __all__ = (
|
||||
'CircuitGroupSerializer',
|
||||
'CircuitTerminationSerializer',
|
||||
'CircuitTypeSerializer',
|
||||
'VirtualCircuitSerializer',
|
||||
'VirtualCircuitTerminationSerializer',
|
||||
)
|
||||
|
||||
|
||||
@ -156,3 +162,32 @@ class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
|
||||
'id', 'url', 'display_url', 'display', 'group', 'circuit', 'priority', 'tags', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'group', 'circuit', 'priority')
|
||||
|
||||
|
||||
class VirtualCircuitSerializer(NetBoxModelSerializer):
|
||||
provider_network = ProviderNetworkSerializer(nested=True)
|
||||
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuit
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'cid', 'provider_network', 'provider_account', 'status', 'tenant',
|
||||
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'provider_network', 'cid', 'description')
|
||||
|
||||
|
||||
class VirtualCircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
virtual_circuit = VirtualCircuitSerializer(nested=True)
|
||||
role = ChoiceField(choices=VirtualCircuitTerminationRoleChoices, required=False)
|
||||
interface = InterfaceSerializer(nested=True)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'virtual_circuit', 'role', 'interface', 'description', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'virtual_circuit', 'role', 'interface', 'description')
|
||||
|
@ -17,5 +17,9 @@ router.register('circuit-terminations', views.CircuitTerminationViewSet)
|
||||
router.register('circuit-groups', views.CircuitGroupViewSet)
|
||||
router.register('circuit-group-assignments', views.CircuitGroupAssignmentViewSet)
|
||||
|
||||
# Virtual circuits
|
||||
router.register('virtual-circuits', views.VirtualCircuitViewSet)
|
||||
router.register('virtual-circuit-terminations', views.VirtualCircuitTerminationViewSet)
|
||||
|
||||
app_name = 'circuits-api'
|
||||
urlpatterns = router.urls
|
||||
|
@ -93,3 +93,23 @@ class ProviderNetworkViewSet(NetBoxModelViewSet):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
serializer_class = serializers.ProviderNetworkSerializer
|
||||
filterset_class = filtersets.ProviderNetworkFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuits
|
||||
#
|
||||
|
||||
class VirtualCircuitViewSet(NetBoxModelViewSet):
|
||||
queryset = VirtualCircuit.objects.all()
|
||||
serializer_class = serializers.VirtualCircuitSerializer
|
||||
filterset_class = filtersets.VirtualCircuitFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuit terminations
|
||||
#
|
||||
|
||||
class VirtualCircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
serializer_class = serializers.VirtualCircuitTerminationSerializer
|
||||
filterset_class = filtersets.VirtualCircuitTerminationFilterSet
|
||||
|
@ -92,3 +92,19 @@ class CircuitPriorityChoices(ChoiceSet):
|
||||
(PRIORITY_TERTIARY, _('Tertiary')),
|
||||
(PRIORITY_INACTIVE, _('Inactive')),
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuits
|
||||
#
|
||||
|
||||
class VirtualCircuitTerminationRoleChoices(ChoiceSet):
|
||||
ROLE_PEER = 'peer'
|
||||
ROLE_HUB = 'hub'
|
||||
ROLE_SPOKE = 'spoke'
|
||||
|
||||
CHOICES = [
|
||||
(ROLE_PEER, _('Peer'), 'green'),
|
||||
(ROLE_HUB, _('Hub'), 'blue'),
|
||||
(ROLE_SPOKE, _('Spoke'), 'orange'),
|
||||
]
|
||||
|
@ -3,7 +3,7 @@ from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.filtersets import CabledObjectFilterSet
|
||||
from dcim.models import Location, Region, Site, SiteGroup
|
||||
from dcim.models import Interface, Location, Region, Site, SiteGroup
|
||||
from ipam.models import ASN
|
||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
|
||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||
@ -20,6 +20,8 @@ __all__ = (
|
||||
'ProviderNetworkFilterSet',
|
||||
'ProviderAccountFilterSet',
|
||||
'ProviderFilterSet',
|
||||
'VirtualCircuitFilterSet',
|
||||
'VirtualCircuitTerminationFilterSet',
|
||||
)
|
||||
|
||||
|
||||
@ -404,3 +406,108 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
Q(circuit__cid__icontains=value) |
|
||||
Q(group__name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_network__provider',
|
||||
queryset=Provider.objects.all(),
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_network__provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
label=_('Provider account (ID)'),
|
||||
)
|
||||
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account__account',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='account',
|
||||
label=_('Provider account (account)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
label=_('Provider network (ID)'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=CircuitStatusChoices,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuit
|
||||
fields = ('id', 'cid', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(cid__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
).distinct()
|
||||
|
||||
|
||||
class VirtualCircuitTerminationFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
virtual_circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VirtualCircuit.objects.all(),
|
||||
label=_('Virtual circuit'),
|
||||
)
|
||||
role = django_filters.MultipleChoiceFilter(
|
||||
choices=VirtualCircuitTerminationRoleChoices,
|
||||
null_value=None
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_network__provider',
|
||||
queryset=Provider.objects.all(),
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_network__provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
label=_('Provider account (ID)'),
|
||||
)
|
||||
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_account__account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
to_field_name='account',
|
||||
label=_('Provider account (account)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
field_name='virtual_circuit__provider_network',
|
||||
label=_('Provider network (ID)'),
|
||||
)
|
||||
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Interface.objects.all(),
|
||||
field_name='interface',
|
||||
label=_('Interface (ID)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitTermination
|
||||
fields = ('id', 'interface_id', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(virtual_circuit__cid__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
).distinct()
|
||||
|
@ -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',)
|
||||
|
@ -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',
|
||||
]
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
]
|
||||
|
@ -4,14 +4,16 @@ from circuits import filtersets, models
|
||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
|
||||
__all__ = (
|
||||
'CircuitTerminationFilter',
|
||||
'CircuitFilter',
|
||||
'CircuitGroupAssignmentFilter',
|
||||
'CircuitGroupFilter',
|
||||
'CircuitTerminationFilter',
|
||||
'CircuitTypeFilter',
|
||||
'ProviderFilter',
|
||||
'ProviderAccountFilter',
|
||||
'ProviderNetworkFilter',
|
||||
'VirtualCircuitFilter',
|
||||
'VirtualCircuitTerminationFilter',
|
||||
)
|
||||
|
||||
|
||||
@ -61,3 +63,15 @@ class ProviderAccountFilter(BaseFilterMixin):
|
||||
@autotype_decorator(filtersets.ProviderNetworkFilterSet)
|
||||
class ProviderNetworkFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualCircuit, lookups=True)
|
||||
@autotype_decorator(filtersets.VirtualCircuitFilterSet)
|
||||
class VirtualCircuitFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualCircuitTermination, lookups=True)
|
||||
@autotype_decorator(filtersets.VirtualCircuitTerminationFilterSet)
|
||||
class VirtualCircuitTerminationFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
@ -31,3 +31,9 @@ class CircuitsQuery:
|
||||
|
||||
provider_network: ProviderNetworkType = strawberry_django.field()
|
||||
provider_network_list: List[ProviderNetworkType] = strawberry_django.field()
|
||||
|
||||
virtual_circuit: VirtualCircuitType = strawberry_django.field()
|
||||
virtual_circuit_list: List[VirtualCircuitType] = strawberry_django.field()
|
||||
|
||||
virtual_circuit_termination: VirtualCircuitTerminationType = strawberry_django.field()
|
||||
virtual_circuit_termination_list: List[VirtualCircuitTerminationType] = strawberry_django.field()
|
||||
|
@ -19,6 +19,8 @@ __all__ = (
|
||||
'ProviderType',
|
||||
'ProviderAccountType',
|
||||
'ProviderNetworkType',
|
||||
'VirtualCircuitTerminationType',
|
||||
'VirtualCircuitType',
|
||||
)
|
||||
|
||||
|
||||
@ -120,3 +122,32 @@ class CircuitGroupType(OrganizationalObjectType):
|
||||
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
|
||||
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
|
||||
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VirtualCircuitTermination,
|
||||
fields='__all__',
|
||||
filters=VirtualCircuitTerminationFilter
|
||||
)
|
||||
class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
|
||||
virtual_circuit: Annotated[
|
||||
"VirtualCircuitType",
|
||||
strawberry.lazy('circuits.graphql.types')
|
||||
] = strawberry_django.field(select_related=["virtual_circuit"])
|
||||
interface: Annotated[
|
||||
"InterfaceType",
|
||||
strawberry.lazy('dcim.graphql.types')
|
||||
] = strawberry_django.field(select_related=["interface"])
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VirtualCircuit,
|
||||
fields='__all__',
|
||||
filters=VirtualCircuitFilter
|
||||
)
|
||||
class VirtualCircuitType(NetBoxObjectType):
|
||||
provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"])
|
||||
provider_account: ProviderAccountType | None
|
||||
tenant: TenantType | None
|
||||
|
||||
terminations: List[VirtualCircuitTerminationType]
|
||||
|
67
netbox/circuits/migrations/0050_virtual_circuits.py
Normal file
67
netbox/circuits/migrations/0050_virtual_circuits.py
Normal file
@ -0,0 +1,67 @@
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
from django.db import migrations, models
|
||||
|
||||
import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0049_natural_ordering'),
|
||||
('dcim', '0196_qinq_svlan'),
|
||||
('extras', '0122_charfield_null_choices'),
|
||||
('tenancy', '0016_charfield_null_choices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VirtualCircuit',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('cid', models.CharField(max_length=100)),
|
||||
('status', models.CharField(default='active', max_length=50)),
|
||||
('provider_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='virtual_circuits', to='circuits.provideraccount')),
|
||||
('provider_network', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='virtual_circuits', to='circuits.providernetwork')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='virtual_circuits', to='tenancy.tenant')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'circuit',
|
||||
'verbose_name_plural': 'circuits',
|
||||
'ordering': ['provider_network', 'provider_account', 'cid'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VirtualCircuitTermination',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||
('role', models.CharField(default='peer', max_length=50)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('interface', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='virtual_circuit_termination', to='dcim.interface')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
('virtual_circuit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='circuits.virtualcircuit')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'virtual circuit termination',
|
||||
'verbose_name_plural': 'virtual circuit terminations',
|
||||
'ordering': ['virtual_circuit', 'role', 'pk'],
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='virtualcircuit',
|
||||
constraint=models.UniqueConstraint(fields=('provider_network', 'cid'), name='circuits_virtualcircuit_unique_provider_network_cid'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='virtualcircuit',
|
||||
constraint=models.UniqueConstraint(fields=('provider_account', 'cid'), name='circuits_virtualcircuit_unique_provideraccount_cid'),
|
||||
),
|
||||
]
|
@ -1,2 +1,3 @@
|
||||
from .circuits import *
|
||||
from .providers import *
|
||||
from .virtual_circuits import *
|
||||
|
164
netbox/circuits/models/virtual_circuits.py
Normal file
164
netbox/circuits/models/virtual_circuits.py
Normal file
@ -0,0 +1,164 @@
|
||||
from functools import cached_property
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import *
|
||||
from netbox.models import ChangeLoggedModel, PrimaryModel
|
||||
from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, TagsMixin
|
||||
|
||||
__all__ = (
|
||||
'VirtualCircuit',
|
||||
'VirtualCircuitTermination',
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuit(PrimaryModel):
|
||||
"""
|
||||
A virtual connection between two or more endpoints, delivered across one or more physical circuits.
|
||||
"""
|
||||
cid = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name=_('circuit ID'),
|
||||
help_text=_('Unique circuit ID')
|
||||
)
|
||||
provider_network = models.ForeignKey(
|
||||
to='circuits.ProviderNetwork',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='virtual_circuits'
|
||||
)
|
||||
provider_account = models.ForeignKey(
|
||||
to='circuits.ProviderAccount',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='virtual_circuits',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
status = models.CharField(
|
||||
verbose_name=_('status'),
|
||||
max_length=50,
|
||||
choices=CircuitStatusChoices,
|
||||
default=CircuitStatusChoices.STATUS_ACTIVE
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='virtual_circuits',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'provider_network', 'provider_account', 'status', 'tenant', 'description',
|
||||
)
|
||||
prerequisite_models = (
|
||||
'circuits.ProviderNetwork',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['provider_network', 'provider_account', 'cid']
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('provider_network', 'cid'),
|
||||
name='%(app_label)s_%(class)s_unique_provider_network_cid'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('provider_account', 'cid'),
|
||||
name='%(app_label)s_%(class)s_unique_provideraccount_cid'
|
||||
),
|
||||
)
|
||||
verbose_name = _('virtual circuit')
|
||||
verbose_name_plural = _('virtual circuits')
|
||||
|
||||
def __str__(self):
|
||||
return self.cid
|
||||
|
||||
def get_status_color(self):
|
||||
return CircuitStatusChoices.colors.get(self.status)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.provider_account and self.provider_network.provider != self.provider_account.provider:
|
||||
raise ValidationError({
|
||||
'provider_account': "The assigned account must belong to the provider of the assigned network."
|
||||
})
|
||||
|
||||
@property
|
||||
def provider(self):
|
||||
return self.provider_network.provider
|
||||
|
||||
|
||||
class VirtualCircuitTermination(
|
||||
CustomFieldsMixin,
|
||||
CustomLinksMixin,
|
||||
TagsMixin,
|
||||
ChangeLoggedModel
|
||||
):
|
||||
virtual_circuit = models.ForeignKey(
|
||||
to='circuits.VirtualCircuit',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='terminations'
|
||||
)
|
||||
role = models.CharField(
|
||||
verbose_name=_('role'),
|
||||
max_length=50,
|
||||
choices=VirtualCircuitTerminationRoleChoices,
|
||||
default=VirtualCircuitTerminationRoleChoices.ROLE_PEER
|
||||
)
|
||||
interface = models.OneToOneField(
|
||||
to='dcim.Interface',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='virtual_circuit_termination'
|
||||
)
|
||||
description = models.CharField(
|
||||
verbose_name=_('description'),
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['virtual_circuit', 'role', 'pk']
|
||||
verbose_name = _('virtual circuit termination')
|
||||
verbose_name_plural = _('virtual circuit terminations')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.virtual_circuit}: {self.get_role_display()} termination'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:virtualcircuittermination', args=[self.pk])
|
||||
|
||||
def get_role_color(self):
|
||||
return VirtualCircuitTerminationRoleChoices.colors.get(self.role)
|
||||
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
objectchange.related_object = self.virtual_circuit
|
||||
return objectchange
|
||||
|
||||
@property
|
||||
def parent_object(self):
|
||||
return self.virtual_circuit
|
||||
|
||||
@cached_property
|
||||
def peer_terminations(self):
|
||||
if self.role == VirtualCircuitTerminationRoleChoices.ROLE_PEER:
|
||||
return self.virtual_circuit.terminations.exclude(pk=self.pk).filter(
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER
|
||||
)
|
||||
if self.role == VirtualCircuitTerminationRoleChoices.ROLE_HUB:
|
||||
return self.virtual_circuit.terminations.filter(
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_SPOKE
|
||||
)
|
||||
if self.role == VirtualCircuitTerminationRoleChoices.ROLE_SPOKE:
|
||||
return self.virtual_circuit.terminations.filter(
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_HUB
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.interface and not self.interface.is_virtual:
|
||||
raise ValidationError("Virtual circuits may be terminated only to virtual interfaces.")
|
@ -80,3 +80,23 @@ class ProviderNetworkIndex(SearchIndex):
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('provider', 'service_id', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
class VirtualCircuitIndex(SearchIndex):
|
||||
model = models.VirtualCircuit
|
||||
fields = (
|
||||
('cid', 100),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('provider', 'provider_network', 'provider_account', 'status', 'tenant', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
class VirtualCircuitTerminationIndex(SearchIndex):
|
||||
model = models.VirtualCircuitTermination
|
||||
fields = (
|
||||
('description', 500),
|
||||
)
|
||||
display_attrs = ('virtual_circuit', 'role', 'description')
|
||||
|
@ -1,3 +1,4 @@
|
||||
from .circuits import *
|
||||
from .columns import *
|
||||
from .providers import *
|
||||
from .virtual_circuits import *
|
||||
|
95
netbox/circuits/tables/virtual_circuits.py
Normal file
95
netbox/circuits/tables/virtual_circuits.py
Normal file
@ -0,0 +1,95 @@
|
||||
import django_tables2 as tables
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.models import *
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
|
||||
__all__ = (
|
||||
'VirtualCircuitTable',
|
||||
'VirtualCircuitTerminationTable',
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
cid = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Circuit ID')
|
||||
)
|
||||
provider = tables.Column(
|
||||
accessor=tables.A('provider_network__provider'),
|
||||
verbose_name=_('Provider'),
|
||||
linkify=True
|
||||
)
|
||||
provider_network = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Provider network')
|
||||
)
|
||||
provider_account = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Account')
|
||||
)
|
||||
status = columns.ChoiceFieldColumn()
|
||||
termination_count = columns.LinkedCountColumn(
|
||||
viewname='circuits:virtualcircuittermination_list',
|
||||
url_params={'virtual_circuit_id': 'pk'},
|
||||
verbose_name=_('Terminations')
|
||||
)
|
||||
comments = columns.MarkdownColumn(
|
||||
verbose_name=_('Comments')
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:virtualcircuit_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = VirtualCircuit
|
||||
fields = (
|
||||
'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'status', 'tenant', 'tenant_group',
|
||||
'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'cid', 'provider', 'provider_account', 'provider_network', 'status', 'tenant', 'termination_count',
|
||||
'description',
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitTerminationTable(NetBoxTable):
|
||||
virtual_circuit = tables.Column(
|
||||
verbose_name=_('Virtual circuit'),
|
||||
linkify=True
|
||||
)
|
||||
provider = tables.Column(
|
||||
accessor=tables.A('virtual_circuit__provider_network__provider'),
|
||||
verbose_name=_('Provider'),
|
||||
linkify=True
|
||||
)
|
||||
provider_network = tables.Column(
|
||||
accessor=tables.A('virtual_circuit__provider_network'),
|
||||
linkify=True,
|
||||
verbose_name=_('Provider network')
|
||||
)
|
||||
provider_account = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Account')
|
||||
)
|
||||
role = columns.ChoiceFieldColumn()
|
||||
device = tables.Column(
|
||||
accessor=tables.A('interface__device'),
|
||||
linkify=True,
|
||||
verbose_name=_('Device')
|
||||
)
|
||||
interface = tables.Column(
|
||||
verbose_name=_('Interface'),
|
||||
linkify=True
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = VirtualCircuitTermination
|
||||
fields = (
|
||||
'pk', 'id', 'virtual_circuit', 'provider', 'provider_network', 'provider_account', 'role', 'interfaces',
|
||||
'description', 'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'id', 'virtual_circuit', 'role', 'device', 'interface', 'description',
|
||||
)
|
@ -2,7 +2,8 @@ from django.urls import reverse
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from dcim.choices import InterfaceTypeChoices
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
|
||||
from ipam.models import ASN, RIR
|
||||
from utilities.testing import APITestCase, APIViewTestCases
|
||||
|
||||
@ -397,3 +398,240 @@ class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
|
||||
'provider': providers[1].pk,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class VirtualCircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VirtualCircuit
|
||||
brief_fields = ['cid', 'description', 'display', 'id', 'provider_network', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'planned',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
|
||||
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
|
||||
|
||||
virtual_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 1'
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 2'
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 3'
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'cid': 'Virtual Circuit 4',
|
||||
'provider_network': provider_network.pk,
|
||||
'provider_account': provider_account.pk,
|
||||
'status': CircuitStatusChoices.STATUS_PLANNED,
|
||||
},
|
||||
{
|
||||
'cid': 'Virtual Circuit 5',
|
||||
'provider_network': provider_network.pk,
|
||||
'provider_account': provider_account.pk,
|
||||
'status': CircuitStatusChoices.STATUS_PLANNED,
|
||||
},
|
||||
{
|
||||
'cid': 'Virtual Circuit 6',
|
||||
'provider_network': provider_network.pk,
|
||||
'provider_account': provider_account.pk,
|
||||
'status': CircuitStatusChoices.STATUS_PLANNED,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class VirtualCircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VirtualCircuitTermination
|
||||
brief_fields = ['description', 'display', 'id', 'interface', 'role', 'url', 'virtual_circuit']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
|
||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
|
||||
devices = (
|
||||
Device(site=site, name='hub', device_type=device_type, role=device_role),
|
||||
Device(site=site, name='spoke1', device_type=device_type, role=device_role),
|
||||
Device(site=site, name='spoke2', device_type=device_type, role=device_role),
|
||||
Device(site=site, name='spoke3', device_type=device_type, role=device_role),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
physical_interfaces = (
|
||||
Interface(device=devices[0], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=devices[1], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=devices[2], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=devices[3], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
)
|
||||
Interface.objects.bulk_create(physical_interfaces)
|
||||
|
||||
virtual_interfaces = (
|
||||
# Point-to-point VCs
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.1',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.2',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.3',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[1],
|
||||
name='eth0.1',
|
||||
parent=physical_interfaces[1],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[2],
|
||||
name='eth0.1',
|
||||
parent=physical_interfaces[2],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
name='eth0.1',
|
||||
parent=physical_interfaces[3],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
|
||||
# Hub and spoke VCs
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.9',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[1],
|
||||
name='eth0.9',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[2],
|
||||
name='eth0.9',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
name='eth0.9',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
)
|
||||
Interface.objects.bulk_create(virtual_interfaces)
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
|
||||
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
|
||||
|
||||
virtual_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 1'
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 2'
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 3'
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 4'
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||
|
||||
virtual_circuit_terminations = (
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[0],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[0]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[0],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[3]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[1],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[1]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[1],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[4]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[2],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[2]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[2],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[5]
|
||||
),
|
||||
)
|
||||
VirtualCircuitTermination.objects.bulk_create(virtual_circuit_terminations)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'virtual_circuit': virtual_circuits[3].pk,
|
||||
'role': VirtualCircuitTerminationRoleChoices.ROLE_HUB,
|
||||
'interface': virtual_interfaces[6].pk
|
||||
},
|
||||
{
|
||||
'virtual_circuit': virtual_circuits[3].pk,
|
||||
'role': VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
|
||||
'interface': virtual_interfaces[7].pk
|
||||
},
|
||||
{
|
||||
'virtual_circuit': virtual_circuits[3].pk,
|
||||
'role': VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
|
||||
'interface': virtual_interfaces[8].pk
|
||||
},
|
||||
{
|
||||
'virtual_circuit': virtual_circuits[3].pk,
|
||||
'role': VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
|
||||
'interface': virtual_interfaces[9].pk
|
||||
},
|
||||
]
|
||||
|
@ -3,7 +3,8 @@ from django.test import TestCase
|
||||
from circuits.choices import *
|
||||
from circuits.filtersets import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Cable, Region, Site, SiteGroup
|
||||
from dcim.choices import InterfaceTypeChoices
|
||||
from dcim.models import Cable, Device, DeviceRole, DeviceType, Interface, Manufacturer, Region, Site, SiteGroup
|
||||
from ipam.models import ASN, RIR
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
@ -678,3 +679,293 @@ class ProviderAccountTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class VirtualCircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VirtualCircuit.objects.all()
|
||||
filterset = VirtualCircuitFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
tenant_groups = (
|
||||
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
||||
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
||||
TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
|
||||
)
|
||||
for tenantgroup in tenant_groups:
|
||||
tenantgroup.save()
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
|
||||
Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]),
|
||||
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
|
||||
)
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
provider_accounts = (
|
||||
ProviderAccount(name='Provider Account 1', provider=providers[0], account='A'),
|
||||
ProviderAccount(name='Provider Account 2', provider=providers[1], account='B'),
|
||||
ProviderAccount(name='Provider Account 3', provider=providers[2], account='C'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
|
||||
provider_networks = (
|
||||
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
|
||||
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
|
||||
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
virutal_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[0],
|
||||
provider_account=provider_accounts[0],
|
||||
tenant=tenants[0],
|
||||
cid='Virtual Circuit 1',
|
||||
status=CircuitStatusChoices.STATUS_PLANNED,
|
||||
description='virtualcircuit1',
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[1],
|
||||
provider_account=provider_accounts[1],
|
||||
tenant=tenants[1],
|
||||
cid='Virtual Circuit 2',
|
||||
status=CircuitStatusChoices.STATUS_ACTIVE,
|
||||
description='virtualcircuit2',
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[2],
|
||||
provider_account=provider_accounts[2],
|
||||
tenant=tenants[2],
|
||||
cid='Virtual Circuit 3',
|
||||
status=CircuitStatusChoices.STATUS_DEPROVISIONING,
|
||||
description='virtualcircuit3',
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virutal_circuits)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'virtualcircuit1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_cid(self):
|
||||
params = {'cid': ['Virtual Circuit 1', 'Virtual Circuit 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_provider(self):
|
||||
providers = Provider.objects.all()[:2]
|
||||
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_provider_account(self):
|
||||
provider_accounts = ProviderAccount.objects.all()[:2]
|
||||
params = {'provider_account_id': [provider_accounts[0].pk, provider_accounts[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_provider_network(self):
|
||||
provider_networks = ProviderNetwork.objects.all()[:2]
|
||||
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_status(self):
|
||||
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['virtualcircuit1', 'virtualcircuit2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_tenant(self):
|
||||
tenants = Tenant.objects.all()[:2]
|
||||
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_tenant_group(self):
|
||||
tenant_groups = TenantGroup.objects.all()[:2]
|
||||
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class VirtualCircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
filterset = VirtualCircuitTerminationFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
|
||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
|
||||
devices = (
|
||||
Device(site=site, name='Device 1', device_type=device_type, role=device_role),
|
||||
Device(site=site, name='Device 2', device_type=device_type, role=device_role),
|
||||
Device(site=site, name='Device 3', device_type=device_type, role=device_role),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
virtual_interfaces = (
|
||||
# Device 1
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.1',
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.2',
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
# Device 2
|
||||
Interface(
|
||||
device=devices[1],
|
||||
name='eth0.1',
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[1],
|
||||
name='eth0.2',
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
# Device 3
|
||||
Interface(
|
||||
device=devices[2],
|
||||
name='eth0.1',
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[2],
|
||||
name='eth0.2',
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
)
|
||||
Interface.objects.bulk_create(virtual_interfaces)
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
provider_networks = (
|
||||
ProviderNetwork(provider=providers[0], name='Provider Network 1'),
|
||||
ProviderNetwork(provider=providers[1], name='Provider Network 2'),
|
||||
ProviderNetwork(provider=providers[2], name='Provider Network 3'),
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
provider_accounts = (
|
||||
ProviderAccount(provider=providers[0], account='Provider Account 1'),
|
||||
ProviderAccount(provider=providers[1], account='Provider Account 2'),
|
||||
ProviderAccount(provider=providers[2], account='Provider Account 3'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
|
||||
virtual_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[0],
|
||||
provider_account=provider_accounts[0],
|
||||
cid='Virtual Circuit 1'
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[1],
|
||||
provider_account=provider_accounts[1],
|
||||
cid='Virtual Circuit 2'
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[2],
|
||||
provider_account=provider_accounts[2],
|
||||
cid='Virtual Circuit 3'
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||
|
||||
virtual_circuit_terminations = (
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[0],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_HUB,
|
||||
interface=virtual_interfaces[0],
|
||||
description='termination1'
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[0],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
|
||||
interface=virtual_interfaces[3],
|
||||
description='termination2'
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[1],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[1],
|
||||
description='termination3'
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[1],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[4],
|
||||
description='termination4'
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[2],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[2],
|
||||
description='termination5'
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[2],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[5],
|
||||
description='termination6'
|
||||
),
|
||||
)
|
||||
VirtualCircuitTermination.objects.bulk_create(virtual_circuit_terminations)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'termination1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['termination1', 'termination2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_virtual_circuit_id(self):
|
||||
virtual_circuits = VirtualCircuit.objects.filter()[:2]
|
||||
params = {'virtual_circuit_id': [virtual_circuits[0].pk, virtual_circuits[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_provider(self):
|
||||
providers = Provider.objects.all()[:2]
|
||||
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_provider_network(self):
|
||||
provider_networks = ProviderNetwork.objects.all()[:2]
|
||||
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_provider_account(self):
|
||||
provider_accounts = ProviderAccount.objects.all()[:2]
|
||||
params = {'provider_account_id': [provider_accounts[0].pk, provider_accounts[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'provider_account': [provider_accounts[0].account, provider_accounts[1].account]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_interface(self):
|
||||
interfaces = Interface.objects.all()[:2]
|
||||
params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
@ -7,7 +7,8 @@ from django.urls import reverse
|
||||
from circuits.choices import *
|
||||
from circuits.models import *
|
||||
from core.models import ObjectType
|
||||
from dcim.models import Cable, Interface, Site
|
||||
from dcim.choices import InterfaceTypeChoices
|
||||
from dcim.models import Cable, Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
|
||||
from ipam.models import ASN, RIR
|
||||
from netbox.choices import ImportFormatChoices
|
||||
from users.models import ObjectPermission
|
||||
@ -341,7 +342,7 @@ class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class TestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
class CircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = CircuitTermination
|
||||
|
||||
@classmethod
|
||||
@ -518,3 +519,319 @@ class CircuitGroupAssignmentTestCase(
|
||||
cls.bulk_edit_data = {
|
||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||
}
|
||||
|
||||
|
||||
class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VirtualCircuit
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.add_permissions(
|
||||
'circuits.add_virtualcircuittermination',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
provider_networks = (
|
||||
ProviderNetwork(provider=provider, name='Provider Network 1'),
|
||||
ProviderNetwork(provider=provider, name='Provider Network 2'),
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
provider_accounts = (
|
||||
ProviderAccount(provider=provider, account='Provider Account 1'),
|
||||
ProviderAccount(provider=provider, account='Provider Account 2'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
|
||||
virtual_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[0],
|
||||
provider_account=provider_accounts[0],
|
||||
cid='Virtual Circuit 1'
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[0],
|
||||
provider_account=provider_accounts[0],
|
||||
cid='Virtual Circuit 2'
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[0],
|
||||
provider_account=provider_accounts[0],
|
||||
cid='Virtual Circuit 3'
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||
|
||||
device = create_test_device('Device 1')
|
||||
interfaces = (
|
||||
Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||
Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||
Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||
)
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'cid': 'Virtual Circuit X',
|
||||
'provider_network': provider_networks[1].pk,
|
||||
'provider_account': provider_accounts[1].pk,
|
||||
'status': CircuitStatusChoices.STATUS_PLANNED,
|
||||
'description': 'A new virtual circuit',
|
||||
'comments': 'Some comments',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"cid,provider_network,provider_account,status",
|
||||
f"Virtual Circuit 4,Provider Network 1,Provider Account 1,{CircuitStatusChoices.STATUS_PLANNED}",
|
||||
f"Virtual Circuit 5,Provider Network 1,Provider Account 1,{CircuitStatusChoices.STATUS_PLANNED}",
|
||||
f"Virtual Circuit 6,Provider Network 1,Provider Account 1,{CircuitStatusChoices.STATUS_PLANNED}",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,cid,description,status",
|
||||
f"{virtual_circuits[0].pk},Virtual Circuit A,New description,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||
f"{virtual_circuits[1].pk},Virtual Circuit B,New description,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||
f"{virtual_circuits[2].pk},Virtual Circuit C,New description,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'provider_network': provider_networks[1].pk,
|
||||
'provider_account': provider_accounts[1].pk,
|
||||
'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
|
||||
'description': 'New description',
|
||||
'comments': 'New comments',
|
||||
}
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
|
||||
def test_bulk_import_objects_with_terminations(self):
|
||||
interfaces = Interface.objects.filter(type=InterfaceTypeChoices.TYPE_VIRTUAL)
|
||||
json_data = f"""
|
||||
[
|
||||
{{
|
||||
"cid": "Virtual Circuit 7",
|
||||
"provider_network": "Provider Network 1",
|
||||
"status": "active",
|
||||
"terminations": [
|
||||
{{
|
||||
"role": "hub",
|
||||
"interface": {interfaces[0].pk}
|
||||
}},
|
||||
{{
|
||||
"role": "spoke",
|
||||
"interface": {interfaces[1].pk}
|
||||
}},
|
||||
{{
|
||||
"role": "spoke",
|
||||
"interface": {interfaces[2].pk}
|
||||
}}
|
||||
]
|
||||
}}
|
||||
]
|
||||
"""
|
||||
|
||||
initial_count = self._get_queryset().count()
|
||||
data = {
|
||||
'data': json_data,
|
||||
'format': ImportFormatChoices.JSON,
|
||||
}
|
||||
|
||||
# Assign model-level permission
|
||||
obj_perm = ObjectPermission(
|
||||
name='Test permission',
|
||||
actions=['add']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
|
||||
|
||||
# Try GET with model-level permission
|
||||
self.assertHttpStatus(self.client.get(self._get_url('import')), 200)
|
||||
|
||||
# Test POST with permission
|
||||
self.assertHttpStatus(self.client.post(self._get_url('import'), data), 302)
|
||||
self.assertEqual(self._get_queryset().count(), initial_count + 1)
|
||||
|
||||
|
||||
class VirtualCircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VirtualCircuitTermination
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
|
||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
|
||||
devices = (
|
||||
Device(site=site, name='hub', device_type=device_type, role=device_role),
|
||||
Device(site=site, name='spoke1', device_type=device_type, role=device_role),
|
||||
Device(site=site, name='spoke2', device_type=device_type, role=device_role),
|
||||
Device(site=site, name='spoke3', device_type=device_type, role=device_role),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
physical_interfaces = (
|
||||
Interface(device=devices[0], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=devices[1], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=devices[2], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=devices[3], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
)
|
||||
Interface.objects.bulk_create(physical_interfaces)
|
||||
|
||||
virtual_interfaces = (
|
||||
# Point-to-point VCs
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.1',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.2',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.3',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[1],
|
||||
name='eth0.1',
|
||||
parent=physical_interfaces[1],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[2],
|
||||
name='eth0.1',
|
||||
parent=physical_interfaces[2],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
name='eth0.1',
|
||||
parent=physical_interfaces[3],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
|
||||
# Hub and spoke VCs
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.9',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[1],
|
||||
name='eth0.9',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[2],
|
||||
name='eth0.9',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
name='eth0.9',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
)
|
||||
Interface.objects.bulk_create(virtual_interfaces)
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
|
||||
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
|
||||
|
||||
virtual_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 1'
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 2'
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 3'
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 4'
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||
|
||||
virtual_circuit_terminations = (
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[0],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[0]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[0],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[3]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[1],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[1]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[1],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[4]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[2],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[2]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[2],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[5]
|
||||
),
|
||||
)
|
||||
VirtualCircuitTermination.objects.bulk_create(virtual_circuit_terminations)
|
||||
|
||||
cls.form_data = {
|
||||
'virtual_circuit': virtual_circuits[3].pk,
|
||||
'role': VirtualCircuitTerminationRoleChoices.ROLE_HUB,
|
||||
'interface': virtual_interfaces[6].pk
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"virtual_circuit,role,interface,description",
|
||||
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_HUB},{virtual_interfaces[6].pk},Hub",
|
||||
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},{virtual_interfaces[7].pk},Spoke 1",
|
||||
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},{virtual_interfaces[8].pk},Spoke 2",
|
||||
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},{virtual_interfaces[9].pk},Spoke 3",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,role,description",
|
||||
f"{virtual_circuit_terminations[0].pk},{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},New description",
|
||||
f"{virtual_circuit_terminations[1].pk},{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},New description",
|
||||
f"{virtual_circuit_terminations[2].pk},{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},New description",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
@ -70,4 +70,20 @@ urlpatterns = [
|
||||
path('circuit-group-assignments/edit/', views.CircuitGroupAssignmentBulkEditView.as_view(), name='circuitgroupassignment_bulk_edit'),
|
||||
path('circuit-group-assignments/delete/', views.CircuitGroupAssignmentBulkDeleteView.as_view(), name='circuitgroupassignment_bulk_delete'),
|
||||
path('circuit-group-assignments/<int:pk>/', include(get_model_urls('circuits', 'circuitgroupassignment'))),
|
||||
|
||||
# Virtual circuits
|
||||
path('virtual-circuits/', views.VirtualCircuitListView.as_view(), name='virtualcircuit_list'),
|
||||
path('virtual-circuits/add/', views.VirtualCircuitEditView.as_view(), name='virtualcircuit_add'),
|
||||
path('virtual-circuits/import/', views.VirtualCircuitBulkImportView.as_view(), name='virtualcircuit_import'),
|
||||
path('virtual-circuits/edit/', views.VirtualCircuitBulkEditView.as_view(), name='virtualcircuit_bulk_edit'),
|
||||
path('virtual-circuits/delete/', views.VirtualCircuitBulkDeleteView.as_view(), name='virtualcircuit_bulk_delete'),
|
||||
path('virtual-circuits/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuit'))),
|
||||
|
||||
# Virtual circuit terminations
|
||||
path('virtual-circuit-terminations/', views.VirtualCircuitTerminationListView.as_view(), name='virtualcircuittermination_list'),
|
||||
path('virtual-circuit-terminations/add/', views.VirtualCircuitTerminationEditView.as_view(), name='virtualcircuittermination_add'),
|
||||
path('virtual-circuit-terminations/import/', views.VirtualCircuitTerminationBulkImportView.as_view(), name='virtualcircuittermination_import'),
|
||||
path('virtual-circuit-terminations/edit/', views.VirtualCircuitTerminationBulkEditView.as_view(), name='virtualcircuittermination_bulk_edit'),
|
||||
path('virtual-circuit-terminations/delete/', views.VirtualCircuitTerminationBulkDeleteView.as_view(), name='virtualcircuittermination_bulk_delete'),
|
||||
path('virtual-circuit-terminations/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuittermination'))),
|
||||
]
|
||||
|
@ -537,3 +537,109 @@ class CircuitGroupAssignmentBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
filterset = filtersets.CircuitGroupAssignmentFilterSet
|
||||
table = tables.CircuitGroupAssignmentTable
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuits
|
||||
#
|
||||
|
||||
class VirtualCircuitListView(generic.ObjectListView):
|
||||
queryset = VirtualCircuit.objects.annotate(
|
||||
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
|
||||
)
|
||||
filterset = filtersets.VirtualCircuitFilterSet
|
||||
filterset_form = forms.VirtualCircuitFilterForm
|
||||
table = tables.VirtualCircuitTable
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuit)
|
||||
class VirtualCircuitView(generic.ObjectView):
|
||||
queryset = VirtualCircuit.objects.all()
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuit, 'edit')
|
||||
class VirtualCircuitEditView(generic.ObjectEditView):
|
||||
queryset = VirtualCircuit.objects.all()
|
||||
form = forms.VirtualCircuitForm
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuit, 'delete')
|
||||
class VirtualCircuitDeleteView(generic.ObjectDeleteView):
|
||||
queryset = VirtualCircuit.objects.all()
|
||||
|
||||
|
||||
class VirtualCircuitBulkImportView(generic.BulkImportView):
|
||||
queryset = VirtualCircuit.objects.all()
|
||||
model_form = forms.VirtualCircuitImportForm
|
||||
additional_permissions = [
|
||||
'circuits.add_virtualcircuittermination',
|
||||
]
|
||||
related_object_forms = {
|
||||
'terminations': forms.VirtualCircuitTerminationImportRelatedForm,
|
||||
}
|
||||
|
||||
def prep_related_object_data(self, parent, data):
|
||||
data.update({'virtual_circuit': parent})
|
||||
return data
|
||||
|
||||
|
||||
class VirtualCircuitBulkEditView(generic.BulkEditView):
|
||||
queryset = VirtualCircuit.objects.annotate(
|
||||
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
|
||||
)
|
||||
filterset = filtersets.VirtualCircuitFilterSet
|
||||
table = tables.VirtualCircuitTable
|
||||
form = forms.VirtualCircuitBulkEditForm
|
||||
|
||||
|
||||
class VirtualCircuitBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VirtualCircuit.objects.annotate(
|
||||
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
|
||||
)
|
||||
filterset = filtersets.VirtualCircuitFilterSet
|
||||
table = tables.VirtualCircuitTable
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuit terminations
|
||||
#
|
||||
|
||||
class VirtualCircuitTerminationListView(generic.ObjectListView):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
filterset = filtersets.VirtualCircuitTerminationFilterSet
|
||||
filterset_form = forms.VirtualCircuitTerminationFilterForm
|
||||
table = tables.VirtualCircuitTerminationTable
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitTermination)
|
||||
class VirtualCircuitTerminationView(generic.ObjectView):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitTermination, 'edit')
|
||||
class VirtualCircuitTerminationEditView(generic.ObjectEditView):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
form = forms.VirtualCircuitTerminationForm
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitTermination, 'delete')
|
||||
class VirtualCircuitTerminationDeleteView(generic.ObjectDeleteView):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
|
||||
|
||||
class VirtualCircuitTerminationBulkImportView(generic.BulkImportView):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
model_form = forms.VirtualCircuitTerminationImportForm
|
||||
|
||||
|
||||
class VirtualCircuitTerminationBulkEditView(generic.BulkEditView):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
filterset = filtersets.VirtualCircuitTerminationFilterSet
|
||||
table = tables.VirtualCircuitTerminationTable
|
||||
form = forms.VirtualCircuitTerminationBulkEditForm
|
||||
|
||||
|
||||
class VirtualCircuitTerminationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
filterset = filtersets.VirtualCircuitTerminationFilterSet
|
||||
table = tables.VirtualCircuitTerminationTable
|
||||
|
@ -4,7 +4,7 @@ from django.utils.translation import gettext as _
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from circuits.models import CircuitTermination, VirtualCircuit, VirtualCircuitTermination
|
||||
from extras.filtersets import LocalConfigContextFilterSet
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.filtersets import PrimaryIPFilterSet
|
||||
@ -1842,6 +1842,16 @@ class InterfaceFilterSet(
|
||||
queryset=WirelessLink.objects.all(),
|
||||
label=_('Wireless link')
|
||||
)
|
||||
virtual_circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit_termination__virtual_circuit',
|
||||
queryset=VirtualCircuit.objects.all(),
|
||||
label=_('Virtual circuit (ID)'),
|
||||
)
|
||||
virtual_circuit_termination_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit_termination',
|
||||
queryset=VirtualCircuitTermination.objects.all(),
|
||||
label=_('Virtual circuit termination (ID)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
|
@ -998,6 +998,14 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
||||
def l2vpn_termination(self):
|
||||
return self.l2vpn_terminations.first()
|
||||
|
||||
@cached_property
|
||||
def connected_endpoints(self):
|
||||
# If this is a virtual interface, return the remote endpoint of the connected
|
||||
# virtual circuit, if any.
|
||||
if self.is_virtual and hasattr(self, 'virtual_circuit_termination'):
|
||||
return self.virtual_circuit_termination.peer_terminations
|
||||
return super().connected_endpoints
|
||||
|
||||
|
||||
#
|
||||
# Pass-through ports
|
||||
|
@ -649,6 +649,14 @@ class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpoi
|
||||
url_name='dcim:interface_list'
|
||||
)
|
||||
|
||||
# Override PathEndpointTable.connection to accommodate virtual circuits
|
||||
connection = columns.TemplateColumn(
|
||||
accessor='_path__destinations',
|
||||
template_code=INTERFACE_LINKTERMINATION,
|
||||
verbose_name=_('Connection'),
|
||||
orderable=False
|
||||
)
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = models.Interface
|
||||
fields = (
|
||||
|
@ -10,6 +10,20 @@ LINKTERMINATION = """
|
||||
{% endfor %}
|
||||
"""
|
||||
|
||||
INTERFACE_LINKTERMINATION = """
|
||||
{% load i18n %}
|
||||
{% if record.is_virtual and record.virtual_circuit_termination %}
|
||||
{% for termination in record.connected_endpoints %}
|
||||
<a href="{{ termination.interface.parent_object.get_absolute_url }}">{{ termination.interface.parent_object }}</a>
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
<a href="{{ termination.interface.get_absolute_url }}">{{ termination.interface }}</a>
|
||||
{% trans "via" %}
|
||||
<a href="{{ termination.parent_object.get_absolute_url }}">{{ termination.parent_object }}</a>
|
||||
{% if not forloop.last %}<br />{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}""" + LINKTERMINATION + """{% endif %}
|
||||
"""
|
||||
|
||||
CABLE_LENGTH = """
|
||||
{% load helpers %}
|
||||
{% if record.length %}{{ record.length|floatformat:"-2" }} {{ record.length_unit }}{% endif %}
|
||||
|
@ -1168,6 +1168,8 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
'tunnelgroup',
|
||||
'tunneltermination',
|
||||
'virtualchassis',
|
||||
'virtualcircuit',
|
||||
'virtualcircuittermination',
|
||||
'virtualdevicecontext',
|
||||
'virtualdisk',
|
||||
'virtualmachine',
|
||||
|
@ -284,6 +284,13 @@ CIRCUITS_MENU = Menu(
|
||||
get_model_item('circuits', 'circuittermination', _('Circuit Terminations')),
|
||||
),
|
||||
),
|
||||
MenuGroup(
|
||||
label=_('Virtual Circuits'),
|
||||
items=(
|
||||
get_model_item('circuits', 'virtualcircuit', _('Virtual Circuits')),
|
||||
get_model_item('circuits', 'virtualcircuittermination', _('Virtual Circuit Terminations')),
|
||||
),
|
||||
),
|
||||
MenuGroup(
|
||||
label=_('Providers'),
|
||||
items=(
|
||||
|
@ -50,6 +50,19 @@
|
||||
<h2 class="card-header">{% trans "Circuits" %}</h2>
|
||||
{% htmx_table 'circuits:circuit_list' provider_network_id=object.pk %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Virtual Circuits" %}
|
||||
{% if perms.circuits.add_virtualcircuit %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'circuits:virtualcircuit_add' %}?provider_network={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Virtual Circuit" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'circuits:virtualcircuit_list' provider_network_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
84
netbox/templates/circuits/virtualcircuit.html
Normal file
84
netbox/templates/circuits/virtualcircuit.html
Normal file
@ -0,0 +1,84 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'circuits:virtualcircuit_list' %}?provider_id={{ object.provider.pk }}">{{ object.provider }}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'circuits:virtualcircuit_list' %}?provider_network_id={{ object.provider_network.pk }}">{{ object.provider_network }}</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Virtual circuit" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider" %}</th>
|
||||
<td>{{ object.provider|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider Network" %}</th>
|
||||
<td>{{ object.provider_network|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider account" %}</th>
|
||||
<td>{{ object.provider_account|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Circuit ID" %}</th>
|
||||
<td>{{ object.cid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Terminations" %}
|
||||
{% if perms.circuits.add_virtualcircuittermination %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'circuits:virtualcircuittermination_add' %}?virtual_circuit={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Termination" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'circuits:virtualcircuittermination_list' virtual_circuit_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
81
netbox/templates/circuits/virtualcircuittermination.html
Normal file
81
netbox/templates/circuits/virtualcircuittermination.html
Normal file
@ -0,0 +1,81 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'circuits:virtualcircuit_list' %}?provider_id={{ object.virtual_circuit.provider.pk }}">{{ object.virtual_circuit.provider }}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'circuits:virtualcircuit_list' %}?provider_network_id={{ object.virtual_circuit.provider_network.pk }}">{{ object.virtual_circuit.provider_network }}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'circuits:virtualcircuittermination_list' %}?virtual_circuit_id={{ object.virtual_circuit.pk }}">{{ object.virtual_circuit }}</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Virtual Circuit Termination" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider" %}</th>
|
||||
<td>{{ object.virtual_circuit.provider|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider Network" %}</th>
|
||||
<td>{{ object.virtual_circuit.provider_network|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider account" %}</th>
|
||||
<td>{{ object.virtual_circuit.provider_account|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Virtual circuit" %}</th>
|
||||
<td>{{ object.virtual_circuit|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>{% badge object.get_role_display bg_color=object.get_role_color %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Interface" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Device" %}</th>
|
||||
<td>{{ object.interface.device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Interface" %}</th>
|
||||
<td>{{ object.interface|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Type" %}</th>
|
||||
<td>{{ object.interface.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.interface.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -152,7 +152,41 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% if not object.is_virtual %}
|
||||
{% if object.is_virtual and object.virtual_circuit_termination %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Virtual Circuit" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider" %}</th>
|
||||
<td>{{ object.virtual_circuit_termination.virtual_circuit.provider|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider Network" %}</th>
|
||||
<td>{{ object.virtual_circuit_termination.virtual_circuit.provider_network|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Circuit ID" %}</th>
|
||||
<td>{{ object.virtual_circuit_termination.virtual_circuit|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>{{ object.virtual_circuit_termination.get_role_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Connections" %}</th>
|
||||
<td>
|
||||
{% for termination in object.virtual_circuit_termination.peer_terminations %}
|
||||
<a href="{{ termination.interface.parent_object.get_absolute_url }}">{{ termination.interface.parent_object }}</a>
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
<a href="{{ termination.interface.get_absolute_url }}">{{ termination.interface }}</a>
|
||||
({{ termination.get_role_display }})
|
||||
{% if not forloop.last %}<br />{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% elif not object.is_virtual %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Connection" %}</h2>
|
||||
{% if object.mark_connected %}
|
||||
|
Loading…
Reference in New Issue
Block a user