mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-17 04:32:51 -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: 'models/circuits/provider.md'
|
||||||
- Provider Account: 'models/circuits/provideraccount.md'
|
- Provider Account: 'models/circuits/provideraccount.md'
|
||||||
- Provider Network: 'models/circuits/providernetwork.md'
|
- Provider Network: 'models/circuits/providernetwork.md'
|
||||||
|
- Virtual Circuit: 'models/circuits/virtualcircuit.md'
|
||||||
|
- Virtual Circuit Termination: 'models/circuits/virtualcircuittermination.md'
|
||||||
- Core:
|
- Core:
|
||||||
- DataFile: 'models/core/datafile.md'
|
- DataFile: 'models/core/datafile.md'
|
||||||
- DataSource: 'models/core/datasource.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 drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
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.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 dcim.api.serializers_.cables import CabledObjectSerializer
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
|
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
|
||||||
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
||||||
@ -20,6 +24,8 @@ __all__ = (
|
|||||||
'CircuitGroupSerializer',
|
'CircuitGroupSerializer',
|
||||||
'CircuitTerminationSerializer',
|
'CircuitTerminationSerializer',
|
||||||
'CircuitTypeSerializer',
|
'CircuitTypeSerializer',
|
||||||
|
'VirtualCircuitSerializer',
|
||||||
|
'VirtualCircuitTerminationSerializer',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -156,3 +162,32 @@ class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
|
|||||||
'id', 'url', 'display_url', 'display', 'group', 'circuit', 'priority', 'tags', 'created', 'last_updated',
|
'id', 'url', 'display_url', 'display', 'group', 'circuit', 'priority', 'tags', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'group', 'circuit', 'priority')
|
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-groups', views.CircuitGroupViewSet)
|
||||||
router.register('circuit-group-assignments', views.CircuitGroupAssignmentViewSet)
|
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'
|
app_name = 'circuits-api'
|
||||||
urlpatterns = router.urls
|
urlpatterns = router.urls
|
||||||
|
@ -93,3 +93,23 @@ class ProviderNetworkViewSet(NetBoxModelViewSet):
|
|||||||
queryset = ProviderNetwork.objects.all()
|
queryset = ProviderNetwork.objects.all()
|
||||||
serializer_class = serializers.ProviderNetworkSerializer
|
serializer_class = serializers.ProviderNetworkSerializer
|
||||||
filterset_class = filtersets.ProviderNetworkFilterSet
|
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_TERTIARY, _('Tertiary')),
|
||||||
(PRIORITY_INACTIVE, _('Inactive')),
|
(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 django.utils.translation import gettext as _
|
||||||
|
|
||||||
from dcim.filtersets import CabledObjectFilterSet
|
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 ipam.models import ASN
|
||||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
|
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
|
||||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||||
@ -20,6 +20,8 @@ __all__ = (
|
|||||||
'ProviderNetworkFilterSet',
|
'ProviderNetworkFilterSet',
|
||||||
'ProviderAccountFilterSet',
|
'ProviderAccountFilterSet',
|
||||||
'ProviderFilterSet',
|
'ProviderFilterSet',
|
||||||
|
'VirtualCircuitFilterSet',
|
||||||
|
'VirtualCircuitTerminationFilterSet',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -404,3 +406,108 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
|||||||
Q(circuit__cid__icontains=value) |
|
Q(circuit__cid__icontains=value) |
|
||||||
Q(group__name__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.core.exceptions import ObjectDoesNotExist
|
||||||
from django.utils.translation import gettext_lazy as _
|
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.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
@ -28,6 +30,8 @@ __all__ = (
|
|||||||
'ProviderBulkEditForm',
|
'ProviderBulkEditForm',
|
||||||
'ProviderAccountBulkEditForm',
|
'ProviderAccountBulkEditForm',
|
||||||
'ProviderNetworkBulkEditForm',
|
'ProviderNetworkBulkEditForm',
|
||||||
|
'VirtualCircuitBulkEditForm',
|
||||||
|
'VirtualCircuitTerminationBulkEditForm',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -291,3 +295,62 @@ class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
FieldSet('circuit', 'priority'),
|
FieldSet('circuit', 'priority'),
|
||||||
)
|
)
|
||||||
nullable_fields = ('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.choices import *
|
||||||
from circuits.constants import *
|
from circuits.constants import *
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
|
from dcim.models import Interface
|
||||||
from netbox.choices import DistanceUnitChoices
|
from netbox.choices import DistanceUnitChoices
|
||||||
from netbox.forms import NetBoxModelImportForm
|
from netbox.forms import NetBoxModelImportForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
@ -20,6 +21,9 @@ __all__ = (
|
|||||||
'ProviderImportForm',
|
'ProviderImportForm',
|
||||||
'ProviderAccountImportForm',
|
'ProviderAccountImportForm',
|
||||||
'ProviderNetworkImportForm',
|
'ProviderNetworkImportForm',
|
||||||
|
'VirtualCircuitImportForm',
|
||||||
|
'VirtualCircuitTerminationImportForm',
|
||||||
|
'VirtualCircuitTerminationImportRelatedForm',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -179,3 +183,73 @@ class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitGroupAssignment
|
model = CircuitGroupAssignment
|
||||||
fields = ('circuit', 'group', 'priority')
|
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 import forms
|
||||||
from django.utils.translation import gettext as _
|
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 circuits.models import *
|
||||||
from dcim.models import Location, Region, Site, SiteGroup
|
from dcim.models import Location, Region, Site, SiteGroup
|
||||||
from ipam.models import ASN
|
from ipam.models import ASN
|
||||||
@ -22,6 +25,8 @@ __all__ = (
|
|||||||
'ProviderFilterForm',
|
'ProviderFilterForm',
|
||||||
'ProviderAccountFilterForm',
|
'ProviderAccountFilterForm',
|
||||||
'ProviderNetworkFilterForm',
|
'ProviderNetworkFilterForm',
|
||||||
|
'VirtualCircuitFilterForm',
|
||||||
|
'VirtualCircuitTerminationFilterForm',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -292,3 +297,74 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
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.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.utils.translation import gettext_lazy as _
|
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.constants import *
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from dcim.models import Site
|
from dcim.models import Interface, Site
|
||||||
from ipam.models import ASN
|
from ipam.models import ASN
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
from utilities.forms import get_field_value
|
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.rendering import FieldSet, InlineFields
|
||||||
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
|
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
|
||||||
from utilities.templatetags.builtins.filters import bettertitle
|
from utilities.templatetags.builtins.filters import bettertitle
|
||||||
@ -24,6 +29,8 @@ __all__ = (
|
|||||||
'ProviderForm',
|
'ProviderForm',
|
||||||
'ProviderAccountForm',
|
'ProviderAccountForm',
|
||||||
'ProviderNetworkForm',
|
'ProviderNetworkForm',
|
||||||
|
'VirtualCircuitForm',
|
||||||
|
'VirtualCircuitTerminationForm',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -255,3 +262,66 @@ class CircuitGroupAssignmentForm(NetBoxModelForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'group', 'circuit', 'priority', 'tags',
|
'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
|
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CircuitTerminationFilter',
|
|
||||||
'CircuitFilter',
|
'CircuitFilter',
|
||||||
'CircuitGroupAssignmentFilter',
|
'CircuitGroupAssignmentFilter',
|
||||||
'CircuitGroupFilter',
|
'CircuitGroupFilter',
|
||||||
|
'CircuitTerminationFilter',
|
||||||
'CircuitTypeFilter',
|
'CircuitTypeFilter',
|
||||||
'ProviderFilter',
|
'ProviderFilter',
|
||||||
'ProviderAccountFilter',
|
'ProviderAccountFilter',
|
||||||
'ProviderNetworkFilter',
|
'ProviderNetworkFilter',
|
||||||
|
'VirtualCircuitFilter',
|
||||||
|
'VirtualCircuitTerminationFilter',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -61,3 +63,15 @@ class ProviderAccountFilter(BaseFilterMixin):
|
|||||||
@autotype_decorator(filtersets.ProviderNetworkFilterSet)
|
@autotype_decorator(filtersets.ProviderNetworkFilterSet)
|
||||||
class ProviderNetworkFilter(BaseFilterMixin):
|
class ProviderNetworkFilter(BaseFilterMixin):
|
||||||
pass
|
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: ProviderNetworkType = strawberry_django.field()
|
||||||
provider_network_list: List[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',
|
'ProviderType',
|
||||||
'ProviderAccountType',
|
'ProviderAccountType',
|
||||||
'ProviderNetworkType',
|
'ProviderNetworkType',
|
||||||
|
'VirtualCircuitTerminationType',
|
||||||
|
'VirtualCircuitType',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -120,3 +122,32 @@ class CircuitGroupType(OrganizationalObjectType):
|
|||||||
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
|
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
|
||||||
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
|
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
|
||||||
circuit: Annotated["CircuitType", 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 .circuits import *
|
||||||
from .providers 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),
|
('comments', 5000),
|
||||||
)
|
)
|
||||||
display_attrs = ('provider', 'service_id', 'description')
|
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 .circuits import *
|
||||||
from .columns import *
|
from .columns import *
|
||||||
from .providers 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.choices import *
|
||||||
from circuits.models 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 ipam.models import ASN, RIR
|
||||||
from utilities.testing import APITestCase, APIViewTestCases
|
from utilities.testing import APITestCase, APIViewTestCases
|
||||||
|
|
||||||
@ -397,3 +398,240 @@ class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'provider': providers[1].pk,
|
'provider': providers[1].pk,
|
||||||
'description': 'New description',
|
'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.choices import *
|
||||||
from circuits.filtersets import *
|
from circuits.filtersets import *
|
||||||
from circuits.models 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 ipam.models import ASN, RIR
|
||||||
from netbox.choices import DistanceUnitChoices
|
from netbox.choices import DistanceUnitChoices
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
@ -678,3 +679,293 @@ class ProviderAccountTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
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.choices import *
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from core.models import ObjectType
|
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 ipam.models import ASN, RIR
|
||||||
from netbox.choices import ImportFormatChoices
|
from netbox.choices import ImportFormatChoices
|
||||||
from users.models import ObjectPermission
|
from users.models import ObjectPermission
|
||||||
@ -341,7 +342,7 @@ class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class CircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -518,3 +519,319 @@ class CircuitGroupAssignmentTestCase(
|
|||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
'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/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/delete/', views.CircuitGroupAssignmentBulkDeleteView.as_view(), name='circuitgroupassignment_bulk_delete'),
|
||||||
path('circuit-group-assignments/<int:pk>/', include(get_model_urls('circuits', 'circuitgroupassignment'))),
|
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()
|
queryset = CircuitGroupAssignment.objects.all()
|
||||||
filterset = filtersets.CircuitGroupAssignmentFilterSet
|
filterset = filtersets.CircuitGroupAssignmentFilterSet
|
||||||
table = tables.CircuitGroupAssignmentTable
|
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.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import extend_schema_field
|
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.filtersets import LocalConfigContextFilterSet
|
||||||
from extras.models import ConfigTemplate
|
from extras.models import ConfigTemplate
|
||||||
from ipam.filtersets import PrimaryIPFilterSet
|
from ipam.filtersets import PrimaryIPFilterSet
|
||||||
@ -1842,6 +1842,16 @@ class InterfaceFilterSet(
|
|||||||
queryset=WirelessLink.objects.all(),
|
queryset=WirelessLink.objects.all(),
|
||||||
label=_('Wireless link')
|
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:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
|
@ -998,6 +998,14 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
def l2vpn_termination(self):
|
def l2vpn_termination(self):
|
||||||
return self.l2vpn_terminations.first()
|
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
|
# Pass-through ports
|
||||||
|
@ -649,6 +649,14 @@ class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpoi
|
|||||||
url_name='dcim:interface_list'
|
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):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = models.Interface
|
model = models.Interface
|
||||||
fields = (
|
fields = (
|
||||||
|
@ -10,6 +10,20 @@ LINKTERMINATION = """
|
|||||||
{% endfor %}
|
{% 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 = """
|
CABLE_LENGTH = """
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% if record.length %}{{ record.length|floatformat:"-2" }} {{ record.length_unit }}{% endif %}
|
{% if record.length %}{{ record.length|floatformat:"-2" }} {{ record.length_unit }}{% endif %}
|
||||||
|
@ -1168,6 +1168,8 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
'tunnelgroup',
|
'tunnelgroup',
|
||||||
'tunneltermination',
|
'tunneltermination',
|
||||||
'virtualchassis',
|
'virtualchassis',
|
||||||
|
'virtualcircuit',
|
||||||
|
'virtualcircuittermination',
|
||||||
'virtualdevicecontext',
|
'virtualdevicecontext',
|
||||||
'virtualdisk',
|
'virtualdisk',
|
||||||
'virtualmachine',
|
'virtualmachine',
|
||||||
|
@ -284,6 +284,13 @@ CIRCUITS_MENU = Menu(
|
|||||||
get_model_item('circuits', 'circuittermination', _('Circuit Terminations')),
|
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(
|
MenuGroup(
|
||||||
label=_('Providers'),
|
label=_('Providers'),
|
||||||
items=(
|
items=(
|
||||||
|
@ -50,6 +50,19 @@
|
|||||||
<h2 class="card-header">{% trans "Circuits" %}</h2>
|
<h2 class="card-header">{% trans "Circuits" %}</h2>
|
||||||
{% htmx_table 'circuits:circuit_list' provider_network_id=object.pk %}
|
{% htmx_table 'circuits:circuit_list' provider_network_id=object.pk %}
|
||||||
</div>
|
</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 %}
|
{% plugin_full_width_page object %}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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">
|
<div class="card">
|
||||||
<h2 class="card-header">{% trans "Connection" %}</h2>
|
<h2 class="card-header">{% trans "Connection" %}</h2>
|
||||||
{% if object.mark_connected %}
|
{% if object.mark_connected %}
|
||||||
|
Loading…
Reference in New Issue
Block a user